Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,9 @@ Plugins in Access follow the conventions defined by the [Python pluggy framework

An example implementation of a notification plugin is included in [examples/plugins/notifications](https://github.com/discord/access/tree/main/examples/plugins/notifications), which can be extended to send messages using custom Python code. It implements the `NotificationPluginSpec` found in [notifications.py](https://github.com/discord/access/blob/main/api/plugins/notifications.py)

There's also an example implementation of a conditional access plugin in [examples/plugins/conditional_access](https://github.com/discord/access/tree/main/examples/plugins/conditional_access), which can be extended to conditionally approve or deny requests. It implements the `ConditionalAccessPluginSpec` found in [requests.py](https://github.com/discord/access/blob/main/api/plugins/conditional_access.py).
There's also an example implementation of a conditional access plugin in [examples/plugins/conditional_access](https://github.com/discord/access/tree/main/examples/plugins/conditional_access), which can be extended to conditionally approve or deny requests. It implements the `ConditionalAccessPluginSpec` found in [conditional_access.py](https://github.com/discord/access/blob/main/api/plugins/conditional_access.py).

Audit events plugins can be created to stream access operations to external systems (SIEM, logging platforms, analytics). These plugins implement the `AuditEventsPluginSpec` found in [audit_events.py](https://github.com/discord/access/blob/main/api/plugins/audit_events.py).

### Installing a Plugin in the Docker Container

Expand Down
12 changes: 6 additions & 6 deletions api/operations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from api.operations.approve_access_request import ApproveAccessRequest
from api.operations.create_access_request import CreateAccessRequest
from api.operations.reject_access_request import RejectAccessRequest
from api.operations.approve_role_request import ApproveRoleRequest
from api.operations.create_role_request import CreateRoleRequest
from api.operations.reject_role_request import RejectRoleRequest
from api.operations.create_group import CreateGroup
from api.operations.create_access_request import CreateAccessRequest
from api.operations.create_app import CreateApp
from api.operations.create_group import CreateGroup
from api.operations.create_role_request import CreateRoleRequest
from api.operations.create_tag import CreateTag
from api.operations.delete_app import DeleteApp
from api.operations.delete_group import DeleteGroup
from api.operations.delete_tag import DeleteTag
from api.operations.delete_user import DeleteUser
from api.operations.modify_groups_time_limit import ModifyGroupsTimeLimit
from api.operations.modify_app_tags import ModifyAppTags
from api.operations.modify_group_tags import ModifyGroupTags
from api.operations.modify_group_type import ModifyGroupType
from api.operations.modify_group_users import ModifyGroupUsers
from api.operations.modify_groups_time_limit import ModifyGroupsTimeLimit
from api.operations.modify_role_groups import ModifyRoleGroups
from api.operations.reject_access_request import RejectAccessRequest
from api.operations.reject_role_request import RejectRoleRequest
from api.operations.unmanage_group import UnmanageGroup

__all__ = [
Expand Down
56 changes: 48 additions & 8 deletions api/operations/approve_access_request.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional

from flask import current_app, has_request_context, request
from sqlalchemy.orm import joinedload, selectin_polymorphic
from uuid import uuid4

from api.extensions import db
from api.models import AccessRequest, AccessRequestStatus, AppGroup, OktaGroup, OktaUser, RoleGroup
from api.operations.constraints import CheckForReason
from api.operations.modify_group_users import ModifyGroupUsers
from api.plugins import get_notification_hook
from api.plugins import get_audit_events_hook, get_notification_hook
from api.plugins.audit_events import AuditEventEnvelope
from api.views.schemas import AuditLogSchema, EventType
from flask import current_app, has_request_context, request
from sqlalchemy.orm import joinedload, selectin_polymorphic


class ApproveAccessRequest:
Expand Down Expand Up @@ -100,9 +101,11 @@ def execute(self) -> AccessRequest:
{
"event_type": EventType.access_approve,
"user_agent": request.headers.get("User-Agent") if context else None,
"ip": request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None,
"ip": (
request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None
),
"current_user_id": self.approver_id,
"current_user_email": self.approver_email,
"group": group,
Expand Down Expand Up @@ -131,6 +134,43 @@ def execute(self) -> AccessRequest:
notify=self.notify,
).execute()

# Emit audit event to plugins (after DB commit)
try:
requester = db.session.get(OktaUser, self.access_request.requester_user_id)
audit_hook = get_audit_events_hook()
envelope = AuditEventEnvelope(
id=uuid4(),
event_type="access_approve",
timestamp=datetime.now(timezone.utc),
actor_id=self.approver_id or "system",
actor_email=self.approver_email,
target_type="access_request",
target_id=str(self.access_request.id),
target_name=f"Access request for {group.name}" if group else "Unknown group",
action="approved",
reason=self.approval_reason,
payload={
"access_request_id": str(self.access_request.id),
"requester_user_id": self.access_request.requester_user_id,
"requester_email": requester.email if requester else None,
"requested_group_id": self.access_request.requested_group_id,
"requested_group_name": group.name if group else None,
"request_ownership": self.access_request.request_ownership,
"ending_at": self.ending_at.isoformat() if self.ending_at else None,
},
metadata={
"user_agent": request.headers.get("User-Agent") if context else None,
"ip_address": (
request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None
),
},
)
audit_hook.audit_event_logged(envelope=envelope)
except Exception as e:
current_app.logger.error(f"Failed to emit audit event: {e}", exc_info=True)

# Now handled inside ModifyGroupUsers
# self.access_request.approved_membership_id = (
# OktaUserGroupMember.query.filter(
Expand Down
57 changes: 49 additions & 8 deletions api/operations/approve_role_request.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional

from flask import current_app, has_request_context, request
from sqlalchemy.orm import joinedload, selectin_polymorphic
from uuid import uuid4

from api.extensions import db
from api.models import AccessRequestStatus, AppGroup, OktaGroup, OktaUser, RoleGroup, RoleRequest
from api.operations.constraints import CheckForReason
from api.operations.modify_role_groups import ModifyRoleGroups
from api.plugins import get_notification_hook
from api.plugins import get_audit_events_hook, get_notification_hook
from api.plugins.audit_events import AuditEventEnvelope
from api.views.schemas import AuditLogSchema, EventType
from flask import current_app, has_request_context, request
from sqlalchemy.orm import joinedload, selectin_polymorphic


class ApproveRoleRequest:
Expand Down Expand Up @@ -97,9 +98,11 @@ def execute(self) -> RoleRequest:
{
"event_type": EventType.role_request_approve,
"user_agent": request.headers.get("User-Agent") if context else None,
"ip": request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None,
"ip": (
request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None
),
"current_user_id": self.approver_id,
"current_user_email": self.approver_email,
"group": group,
Expand All @@ -109,6 +112,44 @@ def execute(self) -> RoleRequest:
)
)

# Emit audit event to plugins (after DB commit)
try:
requester = db.session.get(OktaUser, self.role_request.requester_user_id)
audit_hook = get_audit_events_hook()
envelope = AuditEventEnvelope(
id=uuid4(),
event_type="role_request_approve",
timestamp=datetime.now(timezone.utc),
actor_id=self.approver_id or "system",
actor_email=self.approver_email,
target_type="role_request",
target_id=str(self.role_request.id),
target_name=f"Role request for {group.name}" if group else "Unknown group",
action="approved",
reason=self.approval_reason,
payload={
"role_request_id": str(self.role_request.id),
"requester_user_id": self.role_request.requester_user_id,
"requester_email": requester.email if requester else None,
"requester_role_id": self.role_request.requester_role_id,
"requested_group_id": self.role_request.requested_group_id,
"requested_group_name": group.name if group else None,
"request_ownership": self.role_request.request_ownership,
"ending_at": self.ending_at.isoformat() if self.ending_at else None,
},
metadata={
"user_agent": request.headers.get("User-Agent") if context else None,
"ip_address": (
request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None
),
},
)
audit_hook.audit_event_logged(envelope=envelope)
except Exception as e:
current_app.logger.error(f"Failed to emit audit event: {e}", exc_info=True)

if self.role_request.request_ownership:
ModifyRoleGroups(
role_group=self.role_request.requester_role,
Expand Down
6 changes: 1 addition & 5 deletions api/operations/constraints/check_for_reason.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
from typing import Optional, Tuple

from sqlalchemy.orm import (
selectin_polymorphic,
selectinload,
)

from api.extensions import db
from api.models import AppGroup, OktaGroup, OktaGroupTagMap, RoleGroup, RoleGroupMap, Tag
from api.models.tag import coalesce_constraints
from sqlalchemy.orm import selectin_polymorphic, selectinload


class CheckForReason:
Expand Down
6 changes: 1 addition & 5 deletions api/operations/constraints/check_for_self_add.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
from typing import Optional, Tuple

from sqlalchemy.orm import (
selectin_polymorphic,
selectinload,
)

from api.authorization import AuthorizationHelpers
from api.extensions import db
from api.models import AppGroup, OktaGroup, OktaGroupTagMap, OktaUser, OktaUserGroupMember, RoleGroup, RoleGroupMap, Tag
from api.models.tag import coalesce_constraints
from sqlalchemy.orm import selectin_polymorphic, selectinload


class CheckForSelfAdd:
Expand Down
65 changes: 48 additions & 17 deletions api/operations/create_access_request.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import random
import string
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional

from flask import current_app, has_request_context, request
from sqlalchemy.orm import joinedload, selectin_polymorphic, selectinload
from uuid import uuid4

from api.extensions import db
from api.models import (
AccessRequest,
AccessRequestStatus,
AppGroup,
OktaGroup,
OktaGroupTagMap,
OktaUser,
RoleGroup,
)
from api.models import AccessRequest, AccessRequestStatus, AppGroup, OktaGroup, OktaGroupTagMap, OktaUser, RoleGroup
from api.models.app_group import get_access_owners, get_app_managers
from api.models.okta_group import get_group_managers
from api.operations.approve_access_request import ApproveAccessRequest
from api.operations.reject_access_request import RejectAccessRequest
from api.plugins import get_conditional_access_hook, get_notification_hook
from api.plugins import get_audit_events_hook, get_conditional_access_hook, get_notification_hook
from api.plugins.audit_events import AuditEventEnvelope
from api.views.schemas import AuditLogSchema, EventType
from flask import current_app, has_request_context, request
from sqlalchemy.orm import joinedload, selectin_polymorphic, selectinload


class CreateAccessRequest:
Expand Down Expand Up @@ -114,9 +107,11 @@ def execute(self) -> Optional[AccessRequest]:
{
"event_type": EventType.access_create,
"user_agent": request.headers.get("User-Agent") if context else None,
"ip": request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None,
"ip": (
request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None
),
"current_user_id": self.requester.id,
"current_user_email": self.requester.email,
"group": group,
Expand All @@ -127,6 +122,42 @@ def execute(self) -> Optional[AccessRequest]:
)
)

# Emit audit event to plugins (after DB commit)
try:
audit_hook = get_audit_events_hook()
envelope = AuditEventEnvelope(
id=uuid4(),
event_type="access_create",
timestamp=datetime.now(timezone.utc),
actor_id=self.requester.id,
actor_email=self.requester.email,
target_type="access_request",
target_id=str(access_request.id),
target_name=f"Access request for {group.name}" if group else "Unknown group",
action="created",
reason=self.request_reason,
payload={
"access_request_id": str(access_request.id),
"requester_user_id": self.requester.id,
"requester_email": self.requester.email,
"requested_group_id": self.requested_group.id,
"requested_group_name": group.name if group else None,
"request_ownership": self.request_ownership,
"request_ending_at": self.request_ending_at.isoformat() if self.request_ending_at else None,
},
metadata={
"user_agent": request.headers.get("User-Agent") if context else None,
"ip_address": (
request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", request.remote_addr))
if context
else None
),
},
)
audit_hook.audit_event_logged(envelope=envelope)
except Exception as e:
current_app.logger.error(f"Failed to emit audit event: {e}", exc_info=True)

conditional_access_responses = self.conditional_access_hook.access_request_created(
access_request=access_request,
group=group,
Expand Down
Loading
Loading