diff --git a/drupalorg/drupalorg.module b/drupalorg/drupalorg.module index 1ae939b4c18d328d3575b1e7a41ddfcc45327c06..38c8585f429126aa1dc9dd0cf7085e1b852194be 100644 --- a/drupalorg/drupalorg.module +++ b/drupalorg/drupalorg.module @@ -400,6 +400,13 @@ function drupalorg_menu() { 'type' => MENU_CALLBACK, 'file' => 'drupalorg.pages.inc', ]; + $items['drupalorg-keycloak-event/%/%'] = [ + 'access callback' => 'drupalorg_keycloak_relay_event_access', + 'page callback' => 'drupalorg_keycloak_event', + 'page arguments' => [1, 2], + 'type' => MENU_CALLBACK, + 'file' => 'drupalorg.pages.inc', + ]; // Queue in-place-update “quasi diff” generation. $items['in-place-updates/%project/%drupalorg_in_place_update'] = [ @@ -665,6 +672,22 @@ function drupalorg_gitlab_webhook_access() { return TRUE; } +/** + * Menu access callback, validate Keycloak event payload. + */ +function drupalorg_keycloak_relay_event_access() { + if (empty(variable_get('drupalorg_keycloak_event_token'))) { + return FALSE; + } + if (empty($_SERVER['HTTP_X_KEYCLOAK_TOKEN'])) { + return FALSE; + } + if ($_SERVER['HTTP_X_KEYCLOAK_TOKEN'] !== variable_get('drupalorg_keycloak_event_token')) { + return FALSE; + } + return TRUE; +} + /** * Menu title callback. Be more welcoming than core. */ @@ -6471,6 +6494,10 @@ function drupalorg_cron_queue_info() { 'worker callback' => 'drupalorg_keycloak_process', 'skip on cron' => TRUE, ], + 'drupalorg_keycloak_event' => [ + 'worker callback' => 'drupalorg_keycloak_event_process', + 'skip on cron' => TRUE, + ], 'drupalorg_commits_comments' => [ 'worker callback' => 'drupalorg_commits_comments_process', 'skip on cron' => TRUE, @@ -11095,3 +11122,42 @@ function drupalorg_get_gitlab_project_id($namespace, $name) { return $ids[$key]; } + +/** + * Queue worker callback for KC delete account event. + * + * @param array $item + * The queue item. An map with the following keys. + * - 'uid': Drupal user id. + * - 'keycloak_uuid': Keycloak user UUID. + */ +function drupalorg_keycloak_event_process($item) { + if (empty($item['keycloak_uuid']) || empty($item['uid'] || empty($item['event']))) { + watchdog('drupalorg_keycloak_event', 'Incomplete item on the queue: @item', [ + '@item' => print_r($item, 1), + ], WATCHDOG_ERROR); + return; + } + $keycloak_user = (new KeycloakIntegration())->getUser($item['keycloak_uuid']); + + switch ($item['event']) { + case 'delete': + if (!is_null($keycloak_user)) { + watchdog('drupalorg_keycloak_event', 'Keycloak user still exists, skip deleting it: @item', [ + '@item' => print_r($item, 1), + ], WATCHDOG_ERROR); + return; + } + watchdog('drupalorg_keycloak_events_user_delete_process', 'Deleting drupal user uid "@uid" following a Keycloak user uuid "@uuid" delete', [ + '@uid' => $item['uid'], + '@uuid' => $item['keycloak_uuid'], + ], WATCHDOG_INFO); + user_delete($item['uid']); + return; + + default: + watchdog('drupalorg_keycloak_event', 'Unrecognised event on item: @item', [ + '@item' => print_r($item, 1), + ], WATCHDOG_ERROR); + } +} diff --git a/drupalorg/drupalorg.pages.inc b/drupalorg/drupalorg.pages.inc index eea6aabee6cbb08254cb6b42a2331fd691f2fd34..e1bb19dfcaffc615531c1be7968633536638386f 100644 --- a/drupalorg/drupalorg.pages.inc +++ b/drupalorg/drupalorg.pages.inc @@ -1662,3 +1662,98 @@ function drupalorg_registration_validation_text_form_submit($form, &$form_state) drupal_set_message(t('Support request emailed to <a href="mailto:help@drupal.org">help@drupal.org</a>.')); } + +/** + * Menu callback, handle Keycloak relayed event. + * + * @param string $event_trigger + * Trigger that produced the event. + * Either 'user' or 'admin'. + * @param string $event_type + * Type of sent on the payload. + * + * Payload is JSON with the event data. + * E.g. + * @code + * { + * "event_trigger": "user", + * "event": { + * "id": "0c9061c8-8656-4b73-9302-9cf52e94922b", + * "time": "1725046809362", + * "type": "CODE_TO_TOKEN", + * "realm_id": "Main", + * "client_id": "account-console", + * "user_id": "80cefc10-1d71-4b5b-b079-39345ab5e65d", + * "session_id": "9ecd59b9-7c17-4fc2-adc6-422f4d2bdace", + * "ip_address": "192.168.1.101", + * "error": "" + * } + * } + * @endcode + */ +function drupalorg_keycloak_event($event_trigger, $event_type) { + if (empty($event_trigger) || empty($event_type)) { + // Missing arguments. + watchdog('drupalorg_keycloak_event', 'Keycloak sent an event with missing arguments, event trigger was "@trigger", and event type was "@type".', [ + '@trigger' => $event_trigger, + '@type' => $event_type, + ], WATCHDOG_ERROR); + drupal_add_http_header('Status', '400 Bad Request'); + print '{"error": "Empty event trigger or event type in request path"}'; + drupal_page_footer(); + exit(); + } + if ($event_trigger !== 'user') { + // Only support KC user events for now. + drupal_add_http_header('Status', '204 No Content'); + drupal_page_footer(); + exit(); + } + $user_event = json_decode(file_get_contents('php://input'), true); + switch ($event_type) { + case 'DELETE_ACCOUNT': + _drupalorg_keycloak_event_handle_delete_account($user_event); + break; + default: + // Unsupported event type. + drupal_add_http_header('Status', '204 No Content'); + } + drupal_add_http_header('Content-Type', 'text/plain'); + echo '👋'; + drupal_page_footer(); + exit(); +} + +/** + * Handles KC user delete event. + * + * @param array $user_event + * Parsed JSON payload on POST. See payload example on + * drupalorg_keycloak_event(). + */ +function _drupalorg_keycloak_event_handle_delete_account($user_event) { + if (empty($user_event['event']['user_id'])) { + // Cannot continue without a Keycloak UUID. + watchdog('drupalorg_keycloak_event', 'Keycloak sent a user event without event.user_id set: @payload', [ + '@payload' => print_r($user_event, 1), + ], WATCHDOG_ERROR); + return; + } + $event = $user_event['event']; + $uid = db_query("SELECT uid FROM {authmap} WHERE module = 'openid_connect_keycloak' AND authname = :authname", [ + ':authname' => $event['user_id'], + ])->fetchField(); + if (empty($uid)) { + // Cannot find a related user id. + watchdog('drupalorg_keycloak_event', 'Provided Keycloak UUID "@uuid" does not have a related entry on authmap: @payload', [ + '@uuid' => print_r($event['user_id'], TRUE), + '@payload' => print_r($event, TRUE), + ], WATCHDOG_ERROR); + return; + } + DrupalQueue::get('drupalorg_keycloak_event')->createItem([ + 'uid' => $uid, + 'keycloak_uuid' => $event['user_id'], + 'event' => 'delete', + ]); +}