Skip to content

Commit c5a6916

Browse files
authored
Merge pull request #25 from PostHog/tom/stale-grants
Publish Cloudwatch metric when stale grants detected
2 parents 50db1ad + bf60f8e commit c5a6916

File tree

4 files changed

+63
-5
lines changed

4 files changed

+63
-5
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ output "api_endpoint_url" {
225225
| <a name="output_config_s3_bucket_arn"></a> [config\_s3\_bucket\_arn](#output\_config\_s3\_bucket\_arn) | The ARN of the S3 bucket for storing configuration and cache data. |
226226
| <a name="output_config_s3_bucket_name"></a> [config\_s3\_bucket\_name](#output\_config\_s3\_bucket\_name) | The name of the S3 bucket for storing configuration and cache data. |
227227
| <a name="output_requester_api_endpoint_url"></a> [requester\_api\_endpoint\_url](#output\_requester\_api\_endpoint\_url) | The full URL to invoke the API. Pass this URL into the Slack App manifest as the Request URL. |
228+
| <a name="output_revoker_lambda_name"></a> [revoker\_lambda\_name](#output\_revoker\_lambda\_name) | The name of the revoker Lambda function. |
229+
| <a name="output_schedule_group_name"></a> [schedule\_group\_name](#output\_schedule\_group\_name) | The name of the EventBridge Scheduler schedule group. |
228230
| <a name="output_sso_elevator_bucket_id"></a> [sso\_elevator\_bucket\_id](#output\_sso\_elevator\_bucket\_id) | The name of the SSO elevator bucket. |
229231
<!-- END_TF_DOCS -->
230232

outputs.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,14 @@ output "attribute_sync_schedule_rule_arn" {
3434
description = "The ARN of the EventBridge rule that triggers the attribute syncer."
3535
value = var.attribute_sync_enabled ? aws_cloudwatch_event_rule.attribute_sync_schedule[0].arn : null
3636
}
37+
38+
# Alerting-related outputs
39+
output "revoker_lambda_name" {
40+
description = "The name of the revoker Lambda function."
41+
value = module.access_revoker.lambda_function_name
42+
}
43+
44+
output "schedule_group_name" {
45+
description = "The name of the EventBridge Scheduler schedule group."
46+
value = aws_scheduler_schedule_group.one_time_schedule_group.name
47+
}

perm_revoker_lambda.tf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,19 @@ data "aws_iam_policy_document" "revoker" {
190190
"${module.config_bucket.s3_bucket_arn}/*"
191191
]
192192
}
193+
statement {
194+
sid = "AllowCloudWatchPutMetricData"
195+
effect = "Allow"
196+
actions = [
197+
"cloudwatch:PutMetricData"
198+
]
199+
resources = ["*"]
200+
condition {
201+
test = "StringEquals"
202+
variable = "cloudwatch:namespace"
203+
values = ["SSOElevator"]
204+
}
205+
}
193206

194207
}
195208

src/revoker.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime, timedelta, timezone
22

33
import boto3
44
import slack_sdk
5+
from mypy_boto3_cloudwatch import CloudWatchClient
56
from mypy_boto3_events import EventBridgeClient
67
from mypy_boto3_identitystore import IdentityStoreClient
78
from mypy_boto3_organizations import OrganizationsClient
@@ -38,9 +39,30 @@
3839
identitystore_client = boto3.client("identitystore") # type: ignore # noqa: PGH003
3940
scheduler_client = boto3.client("scheduler") # type: ignore # noqa: PGH003
4041
events_client = boto3.client("events") # type: ignore # noqa: PGH003
42+
cloudwatch_client: CloudWatchClient = boto3.client("cloudwatch") # type: ignore # noqa: PGH003
4143
slack_client = slack_sdk.WebClient(token=cfg.slack_bot_token)
4244

4345

46+
def publish_stale_grants_metric(count: int, cloudwatch: CloudWatchClient) -> None:
47+
"""Publish a CloudWatch metric for stale grants detected.
48+
49+
This metric can be used to trigger alarms when grants exist that should have been revoked.
50+
"""
51+
if count > 0:
52+
logger.info("Publishing stale grants metric", extra={"count": count})
53+
cloudwatch.put_metric_data(
54+
Namespace="SSOElevator",
55+
MetricData=[
56+
{
57+
"MetricName": "StaleGrantsDetected",
58+
"Value": count,
59+
"Unit": "Count",
60+
"Timestamp": datetime.now(timezone.utc),
61+
}
62+
],
63+
)
64+
65+
4466
def lambda_handler(event: dict, __) -> SlackResponse | None: # type: ignore # noqa: ANN001, PGH003
4567
try:
4668
parsed_event = Event.model_validate(event).root
@@ -80,15 +102,15 @@ def lambda_handler(event: dict, __) -> SlackResponse | None: # type: ignore # n
80102

81103
case CheckOnInconsistency():
82104
logger.info("Handling CheckOnInconsistency event", extra={"event": parsed_event})
83-
check_on_groups_inconsistency(
105+
stale_group_count = check_on_groups_inconsistency(
84106
identity_store_client=identitystore_client,
85107
sso_client=sso_client,
86108
scheduler_client=scheduler_client,
87109
events_client=events_client,
88110
cfg=cfg,
89111
slack_client=slack_client,
90112
)
91-
return handle_check_on_inconsistency(
113+
stale_account_count = handle_check_on_inconsistency(
92114
sso_client=sso_client,
93115
cfg=cfg,
94116
scheduler_client=scheduler_client,
@@ -97,6 +119,8 @@ def lambda_handler(event: dict, __) -> SlackResponse | None: # type: ignore # n
97119
identitystore_client=identitystore_client,
98120
events_client=events_client,
99121
)
122+
publish_stale_grants_metric(stale_group_count + stale_account_count, cloudwatch_client)
123+
return
100124

101125
case SSOElevatorScheduledRevocation():
102126
logger.info("Handling SSOElevatorScheduledRevocation event", extra={"event": parsed_event})
@@ -687,7 +711,8 @@ def handle_check_on_inconsistency( # noqa: PLR0913
687711
slack_client: slack_sdk.WebClient,
688712
identitystore_client: IdentityStoreClient,
689713
events_client: EventBridgeClient,
690-
) -> None:
714+
) -> int:
715+
"""Check for inconsistent account assignments and return the count of stale grants found."""
691716
account_assignments = sso.get_account_assignment_information(sso_client, cfg, org_client)
692717
scheduled_revoke_events = schedule.get_scheduled_events(scheduler_client)
693718
account_assignments_from_events = [
@@ -701,8 +726,10 @@ def handle_check_on_inconsistency( # noqa: PLR0913
701726
if isinstance(scheduled_event, ScheduledRevokeEvent)
702727
]
703728

729+
stale_count = 0
704730
for account_assignment in account_assignments:
705731
if account_assignment not in account_assignments_from_events:
732+
stale_count += 1
706733
try:
707734
account = organizations.describe_account(org_client, account_assignment.account_id)
708735
except Exception:
@@ -739,6 +766,7 @@ def handle_check_on_inconsistency( # noqa: PLR0913
739766
f"The unidentified assignment will be automatically revoked.{time_notice}"
740767
),
741768
)
769+
return stale_count
742770

743771

744772
def check_on_groups_inconsistency( # noqa: PLR0913
@@ -748,7 +776,8 @@ def check_on_groups_inconsistency( # noqa: PLR0913
748776
events_client: EventBridgeClient,
749777
cfg: config.Config,
750778
slack_client: slack_sdk.WebClient,
751-
) -> None:
779+
) -> int:
780+
"""Check for inconsistent group assignments and return the count of stale grants found."""
752781
identity_store_id = sso.get_identity_store_id(cfg, sso_client)
753782
scheduled_revoke_events = schedule.get_scheduled_events(scheduler_client)
754783
group_assignments = sso.get_group_assignments(identity_store_id, identity_store_client, cfg)
@@ -763,8 +792,10 @@ def check_on_groups_inconsistency( # noqa: PLR0913
763792
for scheduled_event in scheduled_revoke_events
764793
if isinstance(scheduled_event, ScheduledGroupRevokeEvent)
765794
]
795+
stale_count = 0
766796
for group_assignment in group_assignments:
767797
if group_assignment not in group_assignments_from_events:
798+
stale_count += 1
768799
logger.warning("Group assignment is not in the scheduled events", extra={"assignment": group_assignment})
769800
mention = slack_helpers.create_slack_mention_by_principal_id(
770801
sso_user_id=group_assignment.user_principal_id,
@@ -791,6 +822,7 @@ def check_on_groups_inconsistency( # noqa: PLR0913
791822
f"The unidentified assignment will be automatically revoked.{time_notice}"
792823
),
793824
)
825+
return stale_count
794826

795827

796828
def handle_sso_elevator_group_scheduled_revocation( # noqa: PLR0913

0 commit comments

Comments
 (0)