From 2a02afe268ee04d45c2103a570d1eb5cdda4bdc1 Mon Sep 17 00:00:00 2001 From: Matt H Date: Mon, 26 Aug 2024 16:58:20 -0400 Subject: [PATCH 01/38] Add a way to run the personnel-sync locally, for dev and testing To do so... 1. Run `docker compose run --rm externalgroupssync bash` 2. In that, run `~/.aws-lambda-rie/aws-lambda-rie ./personnel-sync` 3. In other terminal (on your host), run another `docker compose exec externalgroupssync bash` 4. In that, run `curl -XPOST "http://localhost:8080/2015-03-31/functions/function/invocations" -d '{}'` each time you want to invoke the personnel-sync. --- .gitignore | 3 +++ docker-compose.yml | 6 ++++++ ext-groups-sync/Dockerfile | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 ext-groups-sync/Dockerfile diff --git a/.gitignore b/.gitignore index 543c5c16..9392ff9c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ serverless-mfa-api/override/*.go serverless-mfa-api/override/server/*.go u2f-simulator/override/*.go u2f-simulator/override/u2fserver/*.go + +# Other files to exclude: +*/config.json diff --git a/docker-compose.yml b/docker-compose.yml index fb10cd4b..ba688b0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -198,6 +198,12 @@ services: # docker compose run --rm test vendor/bin/behat --stop-on-failure features/user.feature # docker compose run --rm test vendor/bin/behat --stop-on-failure features/user.feature:306 + externalgroupssync: + build: ./ext-groups-sync + volumes: + - ./ext-groups-sync/config.json:/app/config.json + - ./ext-groups-sync/.env:/app/.env + phpmyadmin: image: phpmyadmin:5 ports: diff --git a/ext-groups-sync/Dockerfile b/ext-groups-sync/Dockerfile new file mode 100644 index 00000000..3fe6a88a --- /dev/null +++ b/ext-groups-sync/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y \ + curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +RUN mkdir -p ~/.aws-lambda-rie +RUN curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie +RUN chmod +x ~/.aws-lambda-rie/aws-lambda-rie + +RUN curl --location -o personnel-sync.tar.gz https://github.com/silinternational/personnel-sync/releases/download/v6.8.3/personnel-sync_6.8.3_linux_amd64.tar.gz +RUN tar -xzf personnel-sync.tar.gz +RUN chmod +x personnel-sync + +CMD ["~/.aws-lambda-rie/aws-lambda-rie", "./personnel-sync"] From c02e402accce1a6154eb031a71a784420d19e3f2 Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 27 Aug 2024 14:29:11 -0400 Subject: [PATCH 02/38] Put email and app-prefix in URL for update-external-groups API --- api.raml | 23 ++++++++++--------- .../GroupsExternalUpdatesContext.php | 10 +++++--- application/frontend/config/main.php | 14 +++++------ .../frontend/controllers/UserController.php | 12 ++++++++-- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/api.raml b/api.raml index ec99df8c..672bbf86 100644 --- a/api.raml +++ b/api.raml @@ -47,19 +47,12 @@ types: GroupsExternalUpdate: type: object properties: - email_address: string - app_prefix: - type: string - description: > - The app-specific prefix, without the trailing hyphen. groups: type: string[] description: > The desired list of groups, including the app-prefix. example: | { - "email_address": "john_smith@example.com", - "app_prefix": "myapp", "groups": ["myapp-users", "myapp-managers"] } UserResponse: @@ -436,12 +429,20 @@ types: description: A server-side error occurred. body: type: Error - /external-groups: + /external-groups/{email}?app_prefix={app_prefix}: + uriParameters: + email: + type: string + description: The URL-encoded email address. + app_prefix: + type: string + description: > + The app-specific prefix, without the trailing hyphen. put: description: > - Update a user's list of external groups for a given app-prefix to be - exactly the given list, leaving external groups with other prefixes - unchanged. + Update the specified user's list of external groups for the given + app-prefix, to make it match the given list, leaving external groups + with other prefixes unchanged. body: type: GroupsExternalUpdate responses: diff --git a/application/features/bootstrap/GroupsExternalUpdatesContext.php b/application/features/bootstrap/GroupsExternalUpdatesContext.php index 61018287..ec2a842a 100644 --- a/application/features/bootstrap/GroupsExternalUpdatesContext.php +++ b/application/features/bootstrap/GroupsExternalUpdatesContext.php @@ -19,11 +19,15 @@ public function iUpdateThatUsersListOfExternalGroupsToTheFollowing($appPrefix, T } $this->cleanRequestBody(); - $this->setRequestBody('email_address', $this->getUserEmailAddress()); - $this->setRequestBody('app_prefix', $appPrefix); $this->setRequestBody('groups', $externalGroups); - $this->iRequestTheResourceBe('/user/external-groups', 'updated'); + $urlPath = sprintf( + '/user/external-groups/%s?app_prefix=%s', + urlencode($this->getUserEmailAddress()), + urlencode($appPrefix), + ); + + $this->iRequestTheResourceBe($urlPath, 'updated'); } /** diff --git a/application/frontend/config/main.php b/application/frontend/config/main.php index ea56b04e..f86e6de6 100644 --- a/application/frontend/config/main.php +++ b/application/frontend/config/main.php @@ -45,13 +45,13 @@ /* * User routes */ - 'GET user' => 'user/index', - 'GET user/' => 'user/view', - 'POST user' => 'user/create', - 'PUT user/external-groups' => 'user/update-external-groups', - 'PUT user/' => 'user/update', - 'PUT user//password' => 'user/update-password', - 'PUT user//password/assess' => 'user/assess-password', + 'GET user' => 'user/index', + 'GET user/' => 'user/view', + 'POST user' => 'user/create', + 'PUT user/external-groups/' => 'user/update-external-groups', + 'PUT user/' => 'user/update', + 'PUT user//password' => 'user/update-password', + 'PUT user//password/assess' => 'user/assess-password', /* * Authentication routes diff --git a/application/frontend/controllers/UserController.php b/application/frontend/controllers/UserController.php index 6c413c64..bf8425d0 100644 --- a/application/frontend/controllers/UserController.php +++ b/application/frontend/controllers/UserController.php @@ -77,10 +77,18 @@ public function actionUpdate(string $employeeId) public function actionUpdateExternalGroups() { - $emailAddress = Yii::$app->request->getBodyParam('email_address'); - $appPrefix = Yii::$app->request->getBodyParam('app_prefix'); + $emailAddress = Yii::$app->request->getQueryParam('email'); + $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); $externalGroups = Yii::$app->request->getBodyParam('groups'); + if (empty($emailAddress)) { + throw new UnprocessableEntityHttpException('No email address provided.'); + } + + if (empty($appPrefix)) { + throw new UnprocessableEntityHttpException('No app prefix provided.'); + } + $user = User::findByEmail($emailAddress); if ($user === null) { Yii::$app->response->statusCode = 404; From 5cb92fc7ac5b92e74d7646ecd9f2b0afd411fc7c Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 27 Aug 2024 14:45:23 -0400 Subject: [PATCH 03/38] Correct the query parameter definition in api.raml --- api.raml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api.raml b/api.raml index 672bbf86..847c6b28 100644 --- a/api.raml +++ b/api.raml @@ -429,13 +429,16 @@ types: description: A server-side error occurred. body: type: Error - /external-groups/{email}?app_prefix={app_prefix}: + /external-groups/{email}: uriParameters: email: type: string - description: The URL-encoded email address. + required: true + description: The (URL-encoded) email address. + queryParameters: app_prefix: type: string + required: true description: > The app-specific prefix, without the trailing hyphen. put: From 2cf104bc7506b721a61aa8a1b87d5131c2b26a3c Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 27 Aug 2024 15:30:10 -0400 Subject: [PATCH 04/38] Add an API endpoint for getting users with groups with a specific prefix --- api.raml | 64 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/api.raml b/api.raml index 847c6b28..1f3e1f7d 100644 --- a/api.raml +++ b/api.raml @@ -55,6 +55,20 @@ types: { "groups": ["myapp-users", "myapp-managers"] } + UserGroupsExternalResponse: + type: object + properties: + email: + type: string + description: That user's email address. + groups: + type: string[] + description: That user's list of groups with the specified app-prefix. + example: | + { + "email": "john_smith@example.com", + "groups": ["myapp-users", "myapp-managers"] + } UserResponse: description: > Information on user record. Password is not included if a password @@ -429,36 +443,50 @@ types: description: A server-side error occurred. body: type: Error - /external-groups/{email}: - uriParameters: - email: - type: string - required: true - description: The (URL-encoded) email address. + /external-groups: queryParameters: app_prefix: type: string required: true description: > The app-specific prefix, without the trailing hyphen. - put: + get: description: > - Update the specified user's list of external groups for the given - app-prefix, to make it match the given list, leaving external groups - with other prefixes unchanged. - body: - type: GroupsExternalUpdate + Get the list of users (and their external groups with the given + app-prefix) who have at least one external group with the given + app-prefix (i.e. ignoring external groups with other prefixes). responses: - 204: - description: > - The user's external groups for that app-prefix were updated. - 404: - description: > - No such user found. + 200: + body: + type: UserGroupsExternalResponse[] 422: description: The given data does not satisfy some validation rule. body: type: Error + /{email}: + uriParameters: + email: + type: string + required: true + description: The (URL-encoded) email address. + put: + description: > + Update the specified user's list of external groups for the given + app-prefix, to make it match the given list, leaving external groups + with other prefixes unchanged. + body: + type: GroupsExternalUpdate + responses: + 204: + description: > + The user's external groups for that app-prefix were updated. + 404: + description: > + No such user found. + 422: + description: The given data does not satisfy some validation rule. + body: + type: Error /{employee_id}: get: description: Get information about a specific user. From 2afb7fe324349ca5927f8cb617e26c3379df84f8 Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 27 Aug 2024 16:33:29 -0400 Subject: [PATCH 05/38] Add test for listing users and external groups for a given app-prefix --- application/behat.yml | 4 ++++ .../features/groups-external-list.feature | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 application/features/groups-external-list.feature diff --git a/application/behat.yml b/application/behat.yml index 9b3cf8de..c7e28906 100644 --- a/application/behat.yml +++ b/application/behat.yml @@ -23,6 +23,10 @@ default: paths: - "features/groups-external.feature" contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalContext ] + groups_external_list_features: + paths: + - "features/groups-external-list.feature" + contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalListContext ] groups_external_updates_features: paths: - "features/groups-external-updates.feature" diff --git a/application/features/groups-external-list.feature b/application/features/groups-external-list.feature new file mode 100644 index 00000000..8a75e9ea --- /dev/null +++ b/application/features/groups-external-list.feature @@ -0,0 +1,19 @@ +Feature: Getting a list of Users with external groups with a given prefix + + Background: + Given the requester is authorized + + Scenario: Get the list of users (and their external groups) with a specific app prefix + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one,map-america | + | jane_doe@example.org | wiki-one,wiki-two | + | bob_mcmanager@example.org | map-america,map-europe | + When I get the list of users with "wiki" external groups + Then the response status code should be 200 + And the response body should contain only the following entries: + | email | groups | + | john_smith@example.org | wiki-one | + | jane_doe@example.org | wiki-one,wiki-two | + + # Scenario: Get the list of users (and their external groups) but provide no app prefix From 1991af66911e3fbf169efce3ffe398fafa573d43 Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 27 Aug 2024 16:33:58 -0400 Subject: [PATCH 06/38] Refactor GroupsExternalContext for reuse of some test methods --- .../bootstrap/GroupsExternalContext.php | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/application/features/bootstrap/GroupsExternalContext.php b/application/features/bootstrap/GroupsExternalContext.php index 4b270dca..510f4b5c 100644 --- a/application/features/bootstrap/GroupsExternalContext.php +++ b/application/features/bootstrap/GroupsExternalContext.php @@ -18,52 +18,68 @@ class GroupsExternalContext extends FeatureContext */ public function aUserExists() { - $this->deleteThatTestUser(); - $this->createTestUser(); - $this->setThatUsersPassword($this->userPassword); + $this->deleteTestUser($this->userEmailAddress); + + $this->user = $this->createTestUser( + $this->userEmailAddress, + '11111' + ); + + $this->setTestUsersPassword( + $this->user, + $this->userPassword + ); } - private function deleteThatTestUser() + protected function deleteTestUser(string $emailAddress) { - $user = User::findByEmail($this->userEmailAddress); + $user = User::findByEmail($emailAddress); if ($user !== null) { $didDeleteUser = $user->delete(); Assert::notFalse($didDeleteUser, sprintf( - 'Failed to delete existing test user: %s', + 'Failed to delete existing test user (%s): %s', + $emailAddress, join("\n", $user->getFirstErrors()) )); } } - private function createTestUser() - { + protected function createTestUser( + string $emailAddress, + string $employeeId, + string $externalGroups = '' + ): User { + list($username, ) = explode('@', $emailAddress); + list($lcFirstName, $lcLastName) = explode('_', $username); $user = new User([ - 'email' => $this->userEmailAddress, - 'employee_id' => '11111', - 'first_name' => 'John', - 'last_name' => 'Smith', - 'username' => 'john_smith', + 'email' => $emailAddress, + 'employee_id' => $employeeId, + 'first_name' => ucfirst($lcFirstName), + 'last_name' => ucfirst($lcLastName), + 'username' => $username, + 'groups_external' => $externalGroups, ]); $user->scenario = User::SCENARIO_NEW_USER; $createdNewUser = $user->save(); Assert::true($createdNewUser, sprintf( - 'Failed to create test user: %s', + 'Failed to create test user %s: %s', + json_encode($emailAddress), join("\n", $user->getFirstErrors()) )); $user->refresh(); - - $this->user = $user; + return $user; } - private function setThatUsersPassword(string $password) + private function setTestUsersPassword(User $user, string $password) { - $this->user->scenario = User::SCENARIO_UPDATE_PASSWORD; - $this->user->password = $password; + $user->scenario = User::SCENARIO_UPDATE_PASSWORD; + $user->password = $password; - Assert::true($this->user->save(), sprintf( - "Failed to set the test user's password: %s", - join("\n", $this->user->getFirstErrors()) + Assert::true($user->save(), sprintf( + "Failed to set the %s test user's password: %s", + json_encode($user->email), + join("\n", $user->getFirstErrors()) )); } From 81d6473ca1ca85d27af6d12626f936130379a3cc Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 27 Aug 2024 16:34:45 -0400 Subject: [PATCH 07/38] Begin implementing list-external-groups test --- .../bootstrap/GroupsExternalListContext.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 application/features/bootstrap/GroupsExternalListContext.php diff --git a/application/features/bootstrap/GroupsExternalListContext.php b/application/features/bootstrap/GroupsExternalListContext.php new file mode 100644 index 00000000..0c1f3a38 --- /dev/null +++ b/application/features/bootstrap/GroupsExternalListContext.php @@ -0,0 +1,52 @@ +deleteTestUser($emailAddress); + $this->createTestUser( + $emailAddress, + (string)$employeeId, + $groups + ); + } + } + + /** + * @When I get the list of users with :appPrefix external groups + */ + public function iGetTheListOfUsersWithExternalGroups($appPrefix) + { + $this->cleanRequestBody(); + + $urlPath = sprintf( + '/user/external-groups/?app_prefix=%s', + urlencode($appPrefix), + ); + + $this->iRequestTheResourceBe($urlPath, 'retrieved'); + } + + /** + * @Then the response body should contain only the following entries: + */ + public function theResponseBodyShouldContainOnlyTheFollowingEntries(TableNode $table) + { + throw new PendingException(); + } +} From bb033db5a7cf012d15d5b8d20f8c544770bd9a3d Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 09:36:30 -0400 Subject: [PATCH 08/38] Add endpoint for listing users with external-groups with specific prefix --- application/common/models/User.php | 28 +++++++++++++++++++ .../bootstrap/GroupsExternalListContext.php | 20 +++++++++---- .../features/groups-external-list.feature | 2 +- application/frontend/config/main.php | 1 + .../frontend/controllers/UserController.php | 11 ++++++++ 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 3f8c5760..9472d7c7 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -641,6 +641,34 @@ public function getNagState() return $this->nagState->getState(); } + public static function listExternalGroups($appPrefix): array + { + /** @var User[] $users */ + $users = User::find()->where( + ['like', 'groups_external', $appPrefix . '-'] + )->all(); + + $responseData = []; + foreach ($users as $user) { + $userIsMatch = false; + $externalGroups = explode(',', $user->groups_external); + $externalGroupsWithAppPrefix = []; + foreach ($externalGroups as $externalGroup) { + if (str_starts_with($externalGroup, $appPrefix . '-')) { + $userIsMatch = true; + $externalGroupsWithAppPrefix[] = $externalGroup; + } + } + if ($userIsMatch) { + $responseData[] = [ + 'email' => $user->email, + 'groups' => $externalGroupsWithAppPrefix, + ]; + } + } + return $responseData; + } + public function loadMfaData(string $rpOrigin = '') { $verifiedMfaOptions = $this->getVerifiedMfaOptions($rpOrigin); diff --git a/application/features/bootstrap/GroupsExternalListContext.php b/application/features/bootstrap/GroupsExternalListContext.php index 0c1f3a38..0caad006 100644 --- a/application/features/bootstrap/GroupsExternalListContext.php +++ b/application/features/bootstrap/GroupsExternalListContext.php @@ -2,8 +2,8 @@ namespace Sil\SilIdBroker\Behat\Context; -use Behat\Behat\Tester\Exception\PendingException; use Behat\Gherkin\Node\TableNode; +use Webmozart\Assert\Assert; class GroupsExternalListContext extends GroupsExternalContext { @@ -35,7 +35,7 @@ public function iGetTheListOfUsersWithExternalGroups($appPrefix) $this->cleanRequestBody(); $urlPath = sprintf( - '/user/external-groups/?app_prefix=%s', + '/user/external-groups?app_prefix=%s', urlencode($appPrefix), ); @@ -43,10 +43,20 @@ public function iGetTheListOfUsersWithExternalGroups($appPrefix) } /** - * @Then the response body should contain only the following entries: + * @Then the response should only include the following users and groups: */ - public function theResponseBodyShouldContainOnlyTheFollowingEntries(TableNode $table) + public function theResponseShouldOnlyIncludeTheFollowingUsersAndGroups(TableNode $table) { - throw new PendingException(); + $expected = []; + foreach ($table as $row) { + $expected[] = [ + 'email' => $row['email'], + 'groups' => explode(',', $row['groups']), + ]; + } + Assert::eq( + json_encode($this->getResponseBody(), JSON_PRETTY_PRINT), + json_encode($expected, JSON_PRETTY_PRINT) + ); } } diff --git a/application/features/groups-external-list.feature b/application/features/groups-external-list.feature index 8a75e9ea..782a9d90 100644 --- a/application/features/groups-external-list.feature +++ b/application/features/groups-external-list.feature @@ -11,7 +11,7 @@ Feature: Getting a list of Users with external groups with a given prefix | bob_mcmanager@example.org | map-america,map-europe | When I get the list of users with "wiki" external groups Then the response status code should be 200 - And the response body should contain only the following entries: + And the response should only include the following users and groups: | email | groups | | john_smith@example.org | wiki-one | | jane_doe@example.org | wiki-one,wiki-two | diff --git a/application/frontend/config/main.php b/application/frontend/config/main.php index f86e6de6..e66ed678 100644 --- a/application/frontend/config/main.php +++ b/application/frontend/config/main.php @@ -48,6 +48,7 @@ 'GET user' => 'user/index', 'GET user/' => 'user/view', 'POST user' => 'user/create', + 'GET user/external-groups' => 'user/list-external-groups', 'PUT user/external-groups/' => 'user/update-external-groups', 'PUT user/' => 'user/update', 'PUT user//password' => 'user/update-password', diff --git a/application/frontend/controllers/UserController.php b/application/frontend/controllers/UserController.php index bf8425d0..4672d4a7 100644 --- a/application/frontend/controllers/UserController.php +++ b/application/frontend/controllers/UserController.php @@ -56,6 +56,17 @@ public function actionCreate(): User return $user; } + public function actionListExternalGroups() + { + $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); + + if (empty($appPrefix)) { + throw new UnprocessableEntityHttpException('No app prefix provided.'); + } + + return User::listExternalGroups($appPrefix); + } + public function actionUpdate(string $employeeId) { $user = User::findOne(['employee_id' => $employeeId]); From 07cd6bb3819e8520d89edffb8014a63502b40de7 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 10:44:27 -0400 Subject: [PATCH 09/38] Use comma-delimited string for external groups in API endpoints As an example, rather than having `["prefix-group1", "prefix-group2"]`, it will now have `"prefix-group1,prefix-group2"` as the value for the external groups API endpoints (both incoming, such as for updates, and outgoing, such as in the list endpoint's response). This makes it easier to sync this data with a spreadsheet (e.g. a Google Sheet). --- api.raml | 14 ++++++++------ application/common/models/User.php | 5 +++-- .../bootstrap/GroupsExternalListContext.php | 5 +---- .../bootstrap/GroupsExternalUpdatesContext.php | 5 ++++- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/api.raml b/api.raml index 1f3e1f7d..6c954fba 100644 --- a/api.raml +++ b/api.raml @@ -48,12 +48,12 @@ types: type: object properties: groups: - type: string[] + type: string description: > - The desired list of groups, including the app-prefix. + The (comma-delimited) desired list of groups, including the app-prefix. example: | { - "groups": ["myapp-users", "myapp-managers"] + "groups": "myapp-users,myapp-managers" } UserGroupsExternalResponse: type: object @@ -62,12 +62,14 @@ types: type: string description: That user's email address. groups: - type: string[] - description: That user's list of groups with the specified app-prefix. + type: string + description: > + That user's comma-delimited list of groups with the specified + app-prefix. example: | { "email": "john_smith@example.com", - "groups": ["myapp-users", "myapp-managers"] + "groups": "myapp-users,myapp-managers" } UserResponse: description: > diff --git a/application/common/models/User.php b/application/common/models/User.php index 9472d7c7..7c56e44c 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -662,7 +662,7 @@ public static function listExternalGroups($appPrefix): array if ($userIsMatch) { $responseData[] = [ 'email' => $user->email, - 'groups' => $externalGroupsWithAppPrefix, + 'groups' => join(',', $externalGroupsWithAppPrefix), ]; } } @@ -1031,8 +1031,9 @@ public function isPromptForMfa(): bool return false; } - public function updateExternalGroups($appPrefix, $appExternalGroups): bool + public function updateExternalGroups(string $appPrefix, string $csvAppExternalGroups): bool { + $appExternalGroups = explode(',', $csvAppExternalGroups); foreach ($appExternalGroups as $appExternalGroup) { if (! str_starts_with($appExternalGroup, $appPrefix . '-')) { $this->addErrors([ diff --git a/application/features/bootstrap/GroupsExternalListContext.php b/application/features/bootstrap/GroupsExternalListContext.php index 0caad006..16b8fddf 100644 --- a/application/features/bootstrap/GroupsExternalListContext.php +++ b/application/features/bootstrap/GroupsExternalListContext.php @@ -49,10 +49,7 @@ public function theResponseShouldOnlyIncludeTheFollowingUsersAndGroups(TableNode { $expected = []; foreach ($table as $row) { - $expected[] = [ - 'email' => $row['email'], - 'groups' => explode(',', $row['groups']), - ]; + $expected[] = $row; } Assert::eq( json_encode($this->getResponseBody(), JSON_PRETTY_PRINT), diff --git a/application/features/bootstrap/GroupsExternalUpdatesContext.php b/application/features/bootstrap/GroupsExternalUpdatesContext.php index ec2a842a..056a9a78 100644 --- a/application/features/bootstrap/GroupsExternalUpdatesContext.php +++ b/application/features/bootstrap/GroupsExternalUpdatesContext.php @@ -19,7 +19,10 @@ public function iUpdateThatUsersListOfExternalGroupsToTheFollowing($appPrefix, T } $this->cleanRequestBody(); - $this->setRequestBody('groups', $externalGroups); + $this->setRequestBody( + 'groups', + join(',', $externalGroups) + ); $urlPath = sprintf( '/user/external-groups/%s?app_prefix=%s', From c8742bdbdf6dd0b117988c2876cffcb9b5d9213b Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 10:52:58 -0400 Subject: [PATCH 10/38] Include an `id` of the email address too, to bypass personnel-sync bug This is intended to be temporary. --- application/common/models/User.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/common/models/User.php b/application/common/models/User.php index 7c56e44c..7eb1b25e 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -661,6 +661,7 @@ public static function listExternalGroups($appPrefix): array } if ($userIsMatch) { $responseData[] = [ + 'id' => $user->email, // TODO: Fix personnel-sync to not need this. 'email' => $user->email, 'groups' => join(',', $externalGroupsWithAppPrefix), ]; From 806b4dedd8d4ced1a0b777e3af4e8b4dd87a092f Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 10:53:37 -0400 Subject: [PATCH 11/38] Fix groups-external-list test to ignore the temporary `id` attribute --- .../bootstrap/GroupsExternalListContext.php | 19 +++++++++++++------ .../features/groups-external-list.feature | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/application/features/bootstrap/GroupsExternalListContext.php b/application/features/bootstrap/GroupsExternalListContext.php index 16b8fddf..9128229d 100644 --- a/application/features/bootstrap/GroupsExternalListContext.php +++ b/application/features/bootstrap/GroupsExternalListContext.php @@ -43,17 +43,24 @@ public function iGetTheListOfUsersWithExternalGroups($appPrefix) } /** - * @Then the response should only include the following users and groups: + * @Then the response should only include the following groups for the following users: */ - public function theResponseShouldOnlyIncludeTheFollowingUsersAndGroups(TableNode $table) + public function theResponseShouldOnlyIncludeTheFollowingGroupsForTheFollowingUsers(TableNode $table) { - $expected = []; + $expectedGroupsByUser = []; foreach ($table as $row) { - $expected[] = $row; + $expectedGroupsByUser[$row['email']] = $row['groups']; } + + $actualGroupsByUser = []; + $responseBody = $this->getResponseBody(); + foreach ($responseBody as $responseEntry) { + $actualGroupsByUser[$responseEntry['email']] = $responseEntry['groups']; + } + Assert::eq( - json_encode($this->getResponseBody(), JSON_PRETTY_PRINT), - json_encode($expected, JSON_PRETTY_PRINT) + json_encode($actualGroupsByUser, JSON_PRETTY_PRINT), + json_encode($expectedGroupsByUser, JSON_PRETTY_PRINT) ); } } diff --git a/application/features/groups-external-list.feature b/application/features/groups-external-list.feature index 782a9d90..4e900b4d 100644 --- a/application/features/groups-external-list.feature +++ b/application/features/groups-external-list.feature @@ -11,7 +11,7 @@ Feature: Getting a list of Users with external groups with a given prefix | bob_mcmanager@example.org | map-america,map-europe | When I get the list of users with "wiki" external groups Then the response status code should be 200 - And the response should only include the following users and groups: + And the response should only include the following groups for the following users: | email | groups | | john_smith@example.org | wiki-one | | jane_doe@example.org | wiki-one,wiki-two | From 615e6e3ad3558591471089afe8479a138db68a23 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 11:24:58 -0400 Subject: [PATCH 12/38] Enable updating a user's external groups for an app to be an empty list --- application/common/models/User.php | 7 ++++++- application/features/groups-external-updates.feature | 8 ++++++++ application/frontend/controllers/UserController.php | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 7eb1b25e..9a03a668 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1034,7 +1034,12 @@ public function isPromptForMfa(): bool public function updateExternalGroups(string $appPrefix, string $csvAppExternalGroups): bool { - $appExternalGroups = explode(',', $csvAppExternalGroups); + if (empty($csvAppExternalGroups)) { + $appExternalGroups = []; + } else { + $appExternalGroups = explode(',', $csvAppExternalGroups); + } + foreach ($appExternalGroups as $appExternalGroup) { if (! str_starts_with($appExternalGroup, $appPrefix . '-')) { $this->addErrors([ diff --git a/application/features/groups-external-updates.feature b/application/features/groups-external-updates.feature index c4c358d8..fc1bfbb6 100644 --- a/application/features/groups-external-updates.feature +++ b/application/features/groups-external-updates.feature @@ -22,6 +22,14 @@ Feature: Updating a User's list of external groups Then the response status code should be 204 And that user's list of external groups should be "wiki-managers" + Scenario: Remove all external groups from a user's list for a particular app + Given a user exists + And that user's list of external groups is "wiki-users,wiki-managers" + When I update that user's list of "wiki" external groups to the following: + | externalGroup | + Then the response status code should be 204 + And that user's list of external groups should be "" + Scenario: Leave a user's external groups for a different app unchanged Given a user exists And that user's list of external groups is "wiki-users,map-europe" diff --git a/application/frontend/controllers/UserController.php b/application/frontend/controllers/UserController.php index 4672d4a7..9119f249 100644 --- a/application/frontend/controllers/UserController.php +++ b/application/frontend/controllers/UserController.php @@ -90,7 +90,7 @@ public function actionUpdateExternalGroups() { $emailAddress = Yii::$app->request->getQueryParam('email'); $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); - $externalGroups = Yii::$app->request->getBodyParam('groups'); + $externalGroups = Yii::$app->request->getBodyParam('groups', ''); if (empty($emailAddress)) { throw new UnprocessableEntityHttpException('No email address provided.'); From c35198dcd94be0a6569c8d9cb5aefff7039e4c78 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 11:51:07 -0400 Subject: [PATCH 13/38] Add separate API endpoint for creating a user's prefixed external groups This avoids the need to include the email address in the URL path, which personnel-sync does not currently support. --- api.raml | 35 +++++++++++++++++++ application/behat.yml | 4 +++ .../bootstrap/GroupsExternalCreateContext.php | 33 +++++++++++++++++ .../features/groups-external-create.feature | 23 ++++++++++++ application/frontend/config/main.php | 9 +++-- .../frontend/controllers/UserController.php | 29 +++++++++++++++ 6 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 application/features/bootstrap/GroupsExternalCreateContext.php create mode 100644 application/features/groups-external-create.feature diff --git a/api.raml b/api.raml index 6c954fba..ff18a364 100644 --- a/api.raml +++ b/api.raml @@ -44,6 +44,23 @@ types: "code": 0, "status": 400 } + GroupsExternalCreate: + type: object + properties: + email: + type: string + required: true + description: > + The email address of the user to add these external groups to. + groups: + type: string + description: > + The (comma-delimited) desired list of groups, including the app-prefix. + example: | + { + "email": "john_smith@example.com" + "groups": "myapp-users,myapp-managers" + } GroupsExternalUpdate: type: object properties: @@ -465,6 +482,24 @@ types: description: The given data does not satisfy some validation rule. body: type: Error + post: + description: > + Set the given external groups to the specified user's list of external + groups for the given app-prefix, leaving external groups with other + prefixes unchanged. + body: + type: GroupsExternalCreate + responses: + 204: + description: > + The user's external groups for that app-prefix were set. + 404: + description: > + No such user found. + 422: + description: The given data does not satisfy some validation rule. + body: + type: Error /{email}: uriParameters: email: diff --git a/application/behat.yml b/application/behat.yml index c7e28906..240af816 100644 --- a/application/behat.yml +++ b/application/behat.yml @@ -23,6 +23,10 @@ default: paths: - "features/groups-external.feature" contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalContext ] + groups_external_create_features: + paths: + - "features/groups-external-create.feature" + contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalCreateContext ] groups_external_list_features: paths: - "features/groups-external-list.feature" diff --git a/application/features/bootstrap/GroupsExternalCreateContext.php b/application/features/bootstrap/GroupsExternalCreateContext.php new file mode 100644 index 00000000..3dcd1112 --- /dev/null +++ b/application/features/bootstrap/GroupsExternalCreateContext.php @@ -0,0 +1,33 @@ +cleanRequestBody(); + $this->setRequestBody('email', $this->getUserEmailAddress()); + $this->setRequestBody( + 'groups', + join(',', $externalGroups) + ); + + $urlPath = sprintf( + '/user/external-groups?app_prefix=%s', + urlencode($appPrefix), + ); + + $this->iRequestTheResourceBe($urlPath, 'created'); + } +} diff --git a/application/features/groups-external-create.feature b/application/features/groups-external-create.feature new file mode 100644 index 00000000..0a8a6799 --- /dev/null +++ b/application/features/groups-external-create.feature @@ -0,0 +1,23 @@ +Feature: Setting a User's list of external groups when they have none (for that prefix) + + Background: + Given the requester is authorized + + Scenario: Set the list of external groups for a user with no external groups + Given a user exists + And that user's list of external groups is "" + When I set that user's list of "wiki" external groups to the following: + | externalGroup | + | wiki-users | + | wiki-managers | + Then the response status code should be 204 + And that user's list of external groups should be "wiki-users,wiki-managers" + + Scenario: Set the list of external groups for a user with only other-prefix external groups + Given a user exists + And that user's list of external groups is "map-america" + When I set that user's list of "wiki" external groups to the following: + | externalGroup | + | wiki-users | + Then the response status code should be 204 + And that user's list of external groups should be "map-america,wiki-users" diff --git a/application/frontend/config/main.php b/application/frontend/config/main.php index e66ed678..5a0725c5 100644 --- a/application/frontend/config/main.php +++ b/application/frontend/config/main.php @@ -42,14 +42,19 @@ // http://www.yiiframework.com/doc-2.0/guide-rest-routing.html // http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html#named-parameters 'rules' => [ + /* + * External Groups routes + */ + 'GET user/external-groups' => 'user/list-external-groups', + 'POST user/external-groups' => 'user/create-external-groups', + 'PUT user/external-groups/' => 'user/update-external-groups', + /* * User routes */ 'GET user' => 'user/index', 'GET user/' => 'user/view', 'POST user' => 'user/create', - 'GET user/external-groups' => 'user/list-external-groups', - 'PUT user/external-groups/' => 'user/update-external-groups', 'PUT user/' => 'user/update', 'PUT user//password' => 'user/update-password', 'PUT user//password/assess' => 'user/assess-password', diff --git a/application/frontend/controllers/UserController.php b/application/frontend/controllers/UserController.php index 9119f249..0abe4edb 100644 --- a/application/frontend/controllers/UserController.php +++ b/application/frontend/controllers/UserController.php @@ -56,6 +56,35 @@ public function actionCreate(): User return $user; } + public function actionCreateExternalGroups() + { + $emailAddress = Yii::$app->request->getBodyParam('email'); + $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); + $externalGroups = Yii::$app->request->getBodyParam('groups'); + + if (empty($emailAddress)) { + throw new UnprocessableEntityHttpException('No email address provided.'); + } + + if (empty($appPrefix)) { + throw new UnprocessableEntityHttpException('No app prefix provided.'); + } + + $user = User::findByEmail($emailAddress); + if ($user === null) { + Yii::$app->response->statusCode = 404; + return; + } + + if ($user->updateExternalGroups($appPrefix, $externalGroups)) { + Yii::$app->response->statusCode = 204; + return; + } + + $errors = join(', ', $user->getFirstErrors()); + throw new UnprocessableEntityHttpException($errors); + } + public function actionListExternalGroups() { $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); From 507ac900a4bd18d9563a0ace3703150c7668f0f8 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 11:58:11 -0400 Subject: [PATCH 14/38] Improve the RAML documentation for `POST /user/external-groups` --- api.raml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api.raml b/api.raml index ff18a364..ea07516f 100644 --- a/api.raml +++ b/api.raml @@ -58,7 +58,7 @@ types: The (comma-delimited) desired list of groups, including the app-prefix. example: | { - "email": "john_smith@example.com" + "email": "john_smith@example.com", "groups": "myapp-users,myapp-managers" } GroupsExternalUpdate: @@ -484,9 +484,8 @@ types: type: Error post: description: > - Set the given external groups to the specified user's list of external - groups for the given app-prefix, leaving external groups with other - prefixes unchanged. + Give the specified user these external groups (for the specified + app-prefix), leaving external groups with other prefixes unchanged. body: type: GroupsExternalCreate responses: From 5ba12baab2ce39839823327b07646ff2bb6b84b8 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 28 Aug 2024 11:58:32 -0400 Subject: [PATCH 15/38] Update the PR template to mention the `api.raml` file --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bcbbe5de..d5576fb6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,7 +26,7 @@ --- ### Feature PR Checklist -- [ ] Documentation (README, local.env.dist, etc.) +- [ ] Documentation (README, local.env.dist, api.raml, etc.) - [ ] Tests created or updated - [ ] Run `make composershow` - [ ] Run `make psr2` From ad29b9e143311973ce5563132b4558e83f4411b7 Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 3 Sep 2024 16:38:26 -0400 Subject: [PATCH 16/38] Set up simplified `external-groups-sync` feature test file --- application/behat.yml | 14 +-- .../bootstrap/GroupsExternalSyncContext.php | 87 +++++++++++++++++++ .../features/groups-external-sync.feature | 13 +++ 3 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 application/features/bootstrap/GroupsExternalSyncContext.php create mode 100644 application/features/groups-external-sync.feature diff --git a/application/behat.yml b/application/behat.yml index 240af816..8c5ceee4 100644 --- a/application/behat.yml +++ b/application/behat.yml @@ -23,18 +23,10 @@ default: paths: - "features/groups-external.feature" contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalContext ] - groups_external_create_features: + groups_external_sync_features: paths: - - "features/groups-external-create.feature" - contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalCreateContext ] - groups_external_list_features: - paths: - - "features/groups-external-list.feature" - contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalListContext ] - groups_external_updates_features: - paths: - - "features/groups-external-updates.feature" - contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalUpdatesContext ] + - "features/groups-external-sync.feature" + contexts: [ Sil\SilIdBroker\Behat\Context\GroupsExternalSyncContext ] hibp_unit_tests_features: paths: - "features/hibp-unit-tests.feature" diff --git a/application/features/bootstrap/GroupsExternalSyncContext.php b/application/features/bootstrap/GroupsExternalSyncContext.php new file mode 100644 index 00000000..2f786c2d --- /dev/null +++ b/application/features/bootstrap/GroupsExternalSyncContext.php @@ -0,0 +1,87 @@ + [ + * 'john_smith@example.org' => 'wiki-one,wiki-two', + * ], + * ] + * ``` + * @var array + */ + private array $externalGroupsLists = []; + + /** + * @Given the following users exist, with these external groups: + */ + public function theFollowingUsersExistWithTheseExternalGroups(TableNode $table) + { + $dummyEmployeeId = 11110; + foreach ($table as $row) { + $emailAddress = $row['email']; + $employeeId = ++$dummyEmployeeId; + $groups = $row['groups']; + + $this->deleteTestUser($emailAddress); + $this->createTestUser( + $emailAddress, + (string)$employeeId, + $groups + ); + } + } + + /** + * @Given the :appPrefix external groups list is the following: + */ + public function theExternalGroupsListIsTheFollowing(string $appPrefix, TableNode $table) + { + $userGroupsMap = []; + foreach ($table as $row) { + $emailAddress = $row['email']; + $externalGroupsCsv = $row['groups']; + $userGroupsMap[$emailAddress] = $externalGroupsCsv; + } + $this->externalGroupsLists[$appPrefix] = $userGroupsMap; + + //// TEMP + //echo json_encode($this->externalGroupsLists, JSON_PRETTY_PRINT) . PHP_EOL; + } + + /** + * @When I sync the list of :appPrefix external groups + */ + public function iSyncTheListOfExternalGroups($appPrefix) + { + User::syncExternalGroups( + $appPrefix, + $this->externalGroupsLists[$appPrefix] + ); + } + + /** + * @Then the following users should have the following external groups: + */ + public function theFollowingUsersShouldHaveTheFollowingExternalGroups(TableNode $table) + { + foreach ($table as $row) { + $emailAddress = $row['email']; + $expectedExternalGroups = $row['groups']; + + $user = User::findByEmail($emailAddress); + Assert::notNull($emailAddress, 'User not found: ' . $emailAddress); + Assert::same($user->groups_external, $expectedExternalGroups); + } + } +} diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature new file mode 100644 index 00000000..3ec7185d --- /dev/null +++ b/application/features/groups-external-sync.feature @@ -0,0 +1,13 @@ +Feature: Syncing a specific app-prefix of external groups with an external list + + Scenario: Add an external group to a user's list for a particular app + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one,map-america | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | wiki-one,wiki-two | + When I sync the list of "wiki" external groups + Then the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-one,map-america,wiki-two | From 9ce751c2c45e1ca1e0e1608f9a6727758e6dea2c Mon Sep 17 00:00:00 2001 From: Matt H Date: Tue, 3 Sep 2024 17:03:34 -0400 Subject: [PATCH 17/38] Implement a (passing) test for sync adding an external group to a user --- application/common/models/User.php | 53 +++++++++++++++++++ .../bootstrap/GroupsExternalSyncContext.php | 25 +++++++-- .../features/groups-external-sync.feature | 3 +- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 9a03a668..d2f0d051 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -670,6 +670,28 @@ public static function listExternalGroups($appPrefix): array return $responseData; } + public static function listUsersWithExternalGroupWith($appPrefix): array + { + $appPrefixWithHyphen = $appPrefix . '-'; + + /** @var User[] $users */ + $users = User::find()->where( + ['like', 'groups_external', $appPrefixWithHyphen] + )->all(); + + $emailAddresses = []; + foreach ($users as $user) { + $externalGroups = explode(',', $user->groups_external); + foreach ($externalGroups as $externalGroup) { + if (str_starts_with($externalGroup, $appPrefixWithHyphen)) { + $emailAddresses[] = $user->email; + break; + } + } + } + return $emailAddresses; + } + public function loadMfaData(string $rpOrigin = '') { $verifiedMfaOptions = $this->getVerifiedMfaOptions($rpOrigin); @@ -1032,6 +1054,37 @@ public function isPromptForMfa(): bool return false; } + public static function syncExternalGroups(string $appPrefix, array $desiredExternalGroupsByUserEmail): array + { + $errors = []; + $emailAddressesOfCurrentMatches = self::listUsersWithExternalGroupWith($appPrefix); + + // Indicate that users not in the "desired" list should not have any + // such external groups. + foreach ($emailAddressesOfCurrentMatches as $email) { + if (! array_key_exists($email, $desiredExternalGroupsByUserEmail)) { + $desiredExternalGroupsByUserEmail[$email] = []; + } + } + + foreach ($desiredExternalGroupsByUserEmail as $email => $groupsForPrefix) { + $user = User::findByEmail($email); + if ($user === null) { + $errors[] = 'No user found for ' . $email; + continue; + } + $successful = $user->updateExternalGroups($appPrefix, $groupsForPrefix); + if (! $successful) { + $errors[] = sprintf( + 'Failed to update external groups for %s: %s', + $email, + $user->getFirstErrors() + ); + } + } + return $errors; + } + public function updateExternalGroups(string $appPrefix, string $csvAppExternalGroups): bool { if (empty($csvAppExternalGroups)) { diff --git a/application/features/bootstrap/GroupsExternalSyncContext.php b/application/features/bootstrap/GroupsExternalSyncContext.php index 2f786c2d..d276d132 100644 --- a/application/features/bootstrap/GroupsExternalSyncContext.php +++ b/application/features/bootstrap/GroupsExternalSyncContext.php @@ -22,6 +22,9 @@ class GroupsExternalSyncContext extends GroupsExternalContext */ private array $externalGroupsLists = []; + /** @var string[] */ + private array $syncErrors; + /** * @Given the following users exist, with these external groups: */ @@ -64,12 +67,20 @@ public function theExternalGroupsListIsTheFollowing(string $appPrefix, TableNode */ public function iSyncTheListOfExternalGroups($appPrefix) { - User::syncExternalGroups( + $this->syncErrors = User::syncExternalGroups( $appPrefix, $this->externalGroupsLists[$appPrefix] ); } + /** + * @Then there should not have been any sync errors + */ + public function thereShouldNotHaveBeenAnySyncErrors() + { + Assert::isEmpty($this->syncErrors); + } + /** * @Then the following users should have the following external groups: */ @@ -77,11 +88,19 @@ public function theFollowingUsersShouldHaveTheFollowingExternalGroups(TableNode { foreach ($table as $row) { $emailAddress = $row['email']; - $expectedExternalGroups = $row['groups']; + $expectedExternalGroups = explode(',', $row['groups']); $user = User::findByEmail($emailAddress); Assert::notNull($emailAddress, 'User not found: ' . $emailAddress); - Assert::same($user->groups_external, $expectedExternalGroups); + $actualExternalGroups = explode(',', $user->groups_external); + + sort($actualExternalGroups); + sort($expectedExternalGroups); + + Assert::same( + json_encode($actualExternalGroups, JSON_PRETTY_PRINT), + json_encode($expectedExternalGroups, JSON_PRETTY_PRINT) + ); } } } diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature index 3ec7185d..0c39a1cc 100644 --- a/application/features/groups-external-sync.feature +++ b/application/features/groups-external-sync.feature @@ -8,6 +8,7 @@ Feature: Syncing a specific app-prefix of external groups with an external list | email | groups | | john_smith@example.org | wiki-one,wiki-two | When I sync the list of "wiki" external groups - Then the following users should have the following external groups: + Then there should not have been any sync errors + And the following users should have the following external groups: | email | groups | | john_smith@example.org | wiki-one,map-america,wiki-two | From bfca7dfb53918a4e6db3f4ceb4c49362abc18432 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:03:26 -0400 Subject: [PATCH 18/38] Add test for removing an external group from a user during a sync --- application/features/groups-external-sync.feature | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature index 0c39a1cc..aa0479ff 100644 --- a/application/features/groups-external-sync.feature +++ b/application/features/groups-external-sync.feature @@ -12,3 +12,16 @@ Feature: Syncing a specific app-prefix of external groups with an external list And the following users should have the following external groups: | email | groups | | john_smith@example.org | wiki-one,map-america,wiki-two | + + Scenario: Remove an external group from a user's list for a particular app + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-users,wiki-managers,map-asia | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | wiki-users | + When I sync the list of "wiki" external groups + Then there should not have been any sync errors + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-users,map-asia | From 88eec2296b60369351f03763d9bc9f245b5660f7 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:15:59 -0400 Subject: [PATCH 19/38] Fix bug in data type used when removing all groups for a user --- application/common/models/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index d2f0d051..402cdb34 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1063,7 +1063,7 @@ public static function syncExternalGroups(string $appPrefix, array $desiredExter // such external groups. foreach ($emailAddressesOfCurrentMatches as $email) { if (! array_key_exists($email, $desiredExternalGroupsByUserEmail)) { - $desiredExternalGroupsByUserEmail[$email] = []; + $desiredExternalGroupsByUserEmail[$email] = ''; } } From 52e4841195c31bd31f22a025d2dc51b466d0b306 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:38:17 -0400 Subject: [PATCH 20/38] Add to (and clean up) the external-group tests --- .../bootstrap/GroupsExternalSyncContext.php | 14 ++- .../features/groups-external-sync.feature | 86 ++++++++++++++++--- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/application/features/bootstrap/GroupsExternalSyncContext.php b/application/features/bootstrap/GroupsExternalSyncContext.php index d276d132..1d039b15 100644 --- a/application/features/bootstrap/GroupsExternalSyncContext.php +++ b/application/features/bootstrap/GroupsExternalSyncContext.php @@ -57,9 +57,6 @@ public function theExternalGroupsListIsTheFollowing(string $appPrefix, TableNode $userGroupsMap[$emailAddress] = $externalGroupsCsv; } $this->externalGroupsLists[$appPrefix] = $userGroupsMap; - - //// TEMP - //echo json_encode($this->externalGroupsLists, JSON_PRETTY_PRINT) . PHP_EOL; } /** @@ -103,4 +100,15 @@ public function theFollowingUsersShouldHaveTheFollowingExternalGroups(TableNode ); } } + + /** + * @Then there should have been a sync error + */ + public function thereShouldHaveBeenASyncError() + { + Assert::notEmpty($this->syncErrors); + foreach ($this->syncErrors as $syncError) { + echo $syncError . PHP_EOL; + } + } } diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature index aa0479ff..93c6968d 100644 --- a/application/features/groups-external-sync.feature +++ b/application/features/groups-external-sync.feature @@ -2,26 +2,92 @@ Feature: Syncing a specific app-prefix of external groups with an external list Scenario: Add an external group to a user's list for a particular app Given the following users exist, with these external groups: - | email | groups | - | john_smith@example.org | wiki-one,map-america | + | email | groups | + | john_smith@example.org | wiki-one | And the "wiki" external groups list is the following: | email | groups | | john_smith@example.org | wiki-one,wiki-two | When I sync the list of "wiki" external groups Then there should not have been any sync errors And the following users should have the following external groups: - | email | groups | - | john_smith@example.org | wiki-one,map-america,wiki-two | + | email | groups | + | john_smith@example.org | wiki-one,wiki-two | - Scenario: Remove an external group from a user's list for a particular app + Scenario: Change the external group in a user's list for a particular app Given the following users exist, with these external groups: - | email | groups | - | john_smith@example.org | wiki-users,wiki-managers,map-asia | + | email | groups | + | john_smith@example.org | wiki-one | And the "wiki" external groups list is the following: - | email | groups | - | john_smith@example.org | wiki-users | + | email | groups | + | john_smith@example.org | wiki-two | When I sync the list of "wiki" external groups Then there should not have been any sync errors And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-two | + + Scenario: Leave a user's external groups for a different app unchanged + Given the following users exist, with these external groups: | email | groups | - | john_smith@example.org | wiki-users,map-asia | + | john_smith@example.org | wiki-one,map-europe | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | wiki-two | + When I sync the list of "wiki" external groups + Then there should not have been any sync errors + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-two,map-europe | + + Scenario: Remove an external group from a user's list for a particular app + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one,wiki-two | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | wiki-one | + When I sync the list of "wiki" external groups + Then there should not have been any sync errors + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-one | + + Scenario: Remove all external groups from a user's list for a particular app (no entry in list) + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one,wiki-two,map-asia | + And the "wiki" external groups list is the following: + | email | groups | + When I sync the list of "wiki" external groups + Then there should not have been any sync errors + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | map-asia | + + Scenario: Remove all external groups from a user's list for a particular app (blank entry in list) + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one,wiki-two,map-asia | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | | + When I sync the list of "wiki" external groups + Then there should not have been any sync errors + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | map-asia | + + Scenario: Try to add an external group that does not match the given app-prefix + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | wiki-one,map-asia | + When I sync the list of "wiki" external groups + Then there should have been a sync error + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-one | + + # Scenario: Properly handle (trim) spaces around external groups From e6f55421d8c5ebf5882971a141b6c4da7d2b6d2f Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:44:53 -0400 Subject: [PATCH 21/38] Fix data type when showing error during test --- application/common/models/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 402cdb34..7f88f578 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1076,9 +1076,9 @@ public static function syncExternalGroups(string $appPrefix, array $desiredExter $successful = $user->updateExternalGroups($appPrefix, $groupsForPrefix); if (! $successful) { $errors[] = sprintf( - 'Failed to update external groups for %s: %s', + "Failed to update external groups for %s: \n%s", $email, - $user->getFirstErrors() + json_encode($user->getFirstErrors(), JSON_PRETTY_PRINT) ); } } From 735cd2acc7fc484148b1dea9ef2e87fe577fa225 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:45:29 -0400 Subject: [PATCH 22/38] Trim external groups before using them --- application/common/models/User.php | 3 ++- .../bootstrap/GroupsExternalSyncContext.php | 5 ++++- application/features/groups-external-sync.feature | 13 ++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 7f88f578..33b0f83b 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1090,7 +1090,8 @@ public function updateExternalGroups(string $appPrefix, string $csvAppExternalGr if (empty($csvAppExternalGroups)) { $appExternalGroups = []; } else { - $appExternalGroups = explode(',', $csvAppExternalGroups); + $untrimmedAppExternalGroups = explode(',', $csvAppExternalGroups); + $appExternalGroups = array_map('trim', $untrimmedAppExternalGroups); } foreach ($appExternalGroups as $appExternalGroup) { diff --git a/application/features/bootstrap/GroupsExternalSyncContext.php b/application/features/bootstrap/GroupsExternalSyncContext.php index 1d039b15..7c67f318 100644 --- a/application/features/bootstrap/GroupsExternalSyncContext.php +++ b/application/features/bootstrap/GroupsExternalSyncContext.php @@ -75,7 +75,10 @@ public function iSyncTheListOfExternalGroups($appPrefix) */ public function thereShouldNotHaveBeenAnySyncErrors() { - Assert::isEmpty($this->syncErrors); + Assert::isEmpty($this->syncErrors, sprintf( + "Unexpected sync error(s): \n%s", + implode("\n", $this->syncErrors) + )); } /** diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature index 93c6968d..eb8ca382 100644 --- a/application/features/groups-external-sync.feature +++ b/application/features/groups-external-sync.feature @@ -90,4 +90,15 @@ Feature: Syncing a specific app-prefix of external groups with an external list | email | groups | | john_smith@example.org | wiki-one | - # Scenario: Properly handle (trim) spaces around external groups + Scenario: Properly handle (trim) spaces around external groups + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one | + And the "wiki" external groups list is the following: + | email | groups | + | john_smith@example.org | wiki-one, wiki-two | + When I sync the list of "wiki" external groups + Then there should not have been any sync errors + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-one,wiki-two | From 5bc6d15c1f5bd0d86d43edac82a0655ff5a46765 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:52:27 -0400 Subject: [PATCH 23/38] Add test for an empty email address in the external-groups list --- application/common/models/User.php | 2 +- application/features/groups-external-sync.feature | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 33b0f83b..76c75d37 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1070,7 +1070,7 @@ public static function syncExternalGroups(string $appPrefix, array $desiredExter foreach ($desiredExternalGroupsByUserEmail as $email => $groupsForPrefix) { $user = User::findByEmail($email); if ($user === null) { - $errors[] = 'No user found for ' . $email; + $errors[] = 'No user found for email address ' . json_encode($email); continue; } $successful = $user->updateExternalGroups($appPrefix, $groupsForPrefix); diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature index eb8ca382..85d7da7a 100644 --- a/application/features/groups-external-sync.feature +++ b/application/features/groups-external-sync.feature @@ -102,3 +102,17 @@ Feature: Syncing a specific app-prefix of external groups with an external list And the following users should have the following external groups: | email | groups | | john_smith@example.org | wiki-one,wiki-two | + + Scenario: Properly handle an empty email address + Given the following users exist, with these external groups: + | email | groups | + | john_smith@example.org | wiki-one | + And the "wiki" external groups list is the following: + | email | groups | + | | wiki-one | + | john_smith@example.org | wiki-one,wiki-two | + When I sync the list of "wiki" external groups + Then there should have been a sync error + And the following users should have the following external groups: + | email | groups | + | john_smith@example.org | wiki-one,wiki-two | From d51ae2df1b8a587c080ae651987af47cecd2cde6 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:52:55 -0400 Subject: [PATCH 24/38] Add comment about another test to add --- application/features/groups-external-sync.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/features/groups-external-sync.feature b/application/features/groups-external-sync.feature index 85d7da7a..dbe39a0c 100644 --- a/application/features/groups-external-sync.feature +++ b/application/features/groups-external-sync.feature @@ -116,3 +116,5 @@ Feature: Syncing a specific app-prefix of external groups with an external list And the following users should have the following external groups: | email | groups | | john_smith@example.org | wiki-one,wiki-two | + + # Scenario: Send 1 notification email if sync finds group(s) for a user not in this IDP From 78c665d06666351ea58ad2e58ce7f03714e962f1 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:55:27 -0400 Subject: [PATCH 25/38] Remove unneeded entry from `.gitignore` file --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9392ff9c..543c5c16 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,3 @@ serverless-mfa-api/override/*.go serverless-mfa-api/override/server/*.go u2f-simulator/override/*.go u2f-simulator/override/u2fserver/*.go - -# Other files to exclude: -*/config.json From ec22e8da23bd679ce9c03049500f619a7b3e02f2 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 10:58:53 -0400 Subject: [PATCH 26/38] Remove "external groups" API endpoints --- api.raml | 105 ------------------ application/frontend/config/main.php | 7 -- .../frontend/controllers/UserController.php | 69 ------------ 3 files changed, 181 deletions(-) diff --git a/api.raml b/api.raml index ea07516f..83ce571d 100644 --- a/api.raml +++ b/api.raml @@ -44,50 +44,6 @@ types: "code": 0, "status": 400 } - GroupsExternalCreate: - type: object - properties: - email: - type: string - required: true - description: > - The email address of the user to add these external groups to. - groups: - type: string - description: > - The (comma-delimited) desired list of groups, including the app-prefix. - example: | - { - "email": "john_smith@example.com", - "groups": "myapp-users,myapp-managers" - } - GroupsExternalUpdate: - type: object - properties: - groups: - type: string - description: > - The (comma-delimited) desired list of groups, including the app-prefix. - example: | - { - "groups": "myapp-users,myapp-managers" - } - UserGroupsExternalResponse: - type: object - properties: - email: - type: string - description: That user's email address. - groups: - type: string - description: > - That user's comma-delimited list of groups with the specified - app-prefix. - example: | - { - "email": "john_smith@example.com", - "groups": "myapp-users,myapp-managers" - } UserResponse: description: > Information on user record. Password is not included if a password @@ -462,67 +418,6 @@ types: description: A server-side error occurred. body: type: Error - /external-groups: - queryParameters: - app_prefix: - type: string - required: true - description: > - The app-specific prefix, without the trailing hyphen. - get: - description: > - Get the list of users (and their external groups with the given - app-prefix) who have at least one external group with the given - app-prefix (i.e. ignoring external groups with other prefixes). - responses: - 200: - body: - type: UserGroupsExternalResponse[] - 422: - description: The given data does not satisfy some validation rule. - body: - type: Error - post: - description: > - Give the specified user these external groups (for the specified - app-prefix), leaving external groups with other prefixes unchanged. - body: - type: GroupsExternalCreate - responses: - 204: - description: > - The user's external groups for that app-prefix were set. - 404: - description: > - No such user found. - 422: - description: The given data does not satisfy some validation rule. - body: - type: Error - /{email}: - uriParameters: - email: - type: string - required: true - description: The (URL-encoded) email address. - put: - description: > - Update the specified user's list of external groups for the given - app-prefix, to make it match the given list, leaving external groups - with other prefixes unchanged. - body: - type: GroupsExternalUpdate - responses: - 204: - description: > - The user's external groups for that app-prefix were updated. - 404: - description: > - No such user found. - 422: - description: The given data does not satisfy some validation rule. - body: - type: Error /{employee_id}: get: description: Get information about a specific user. diff --git a/application/frontend/config/main.php b/application/frontend/config/main.php index 5a0725c5..e44728e3 100644 --- a/application/frontend/config/main.php +++ b/application/frontend/config/main.php @@ -42,13 +42,6 @@ // http://www.yiiframework.com/doc-2.0/guide-rest-routing.html // http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html#named-parameters 'rules' => [ - /* - * External Groups routes - */ - 'GET user/external-groups' => 'user/list-external-groups', - 'POST user/external-groups' => 'user/create-external-groups', - 'PUT user/external-groups/' => 'user/update-external-groups', - /* * User routes */ diff --git a/application/frontend/controllers/UserController.php b/application/frontend/controllers/UserController.php index 0abe4edb..696a4808 100644 --- a/application/frontend/controllers/UserController.php +++ b/application/frontend/controllers/UserController.php @@ -56,46 +56,6 @@ public function actionCreate(): User return $user; } - public function actionCreateExternalGroups() - { - $emailAddress = Yii::$app->request->getBodyParam('email'); - $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); - $externalGroups = Yii::$app->request->getBodyParam('groups'); - - if (empty($emailAddress)) { - throw new UnprocessableEntityHttpException('No email address provided.'); - } - - if (empty($appPrefix)) { - throw new UnprocessableEntityHttpException('No app prefix provided.'); - } - - $user = User::findByEmail($emailAddress); - if ($user === null) { - Yii::$app->response->statusCode = 404; - return; - } - - if ($user->updateExternalGroups($appPrefix, $externalGroups)) { - Yii::$app->response->statusCode = 204; - return; - } - - $errors = join(', ', $user->getFirstErrors()); - throw new UnprocessableEntityHttpException($errors); - } - - public function actionListExternalGroups() - { - $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); - - if (empty($appPrefix)) { - throw new UnprocessableEntityHttpException('No app prefix provided.'); - } - - return User::listExternalGroups($appPrefix); - } - public function actionUpdate(string $employeeId) { $user = User::findOne(['employee_id' => $employeeId]); @@ -115,35 +75,6 @@ public function actionUpdate(string $employeeId) return $user; } - public function actionUpdateExternalGroups() - { - $emailAddress = Yii::$app->request->getQueryParam('email'); - $appPrefix = Yii::$app->request->getQueryParam('app_prefix'); - $externalGroups = Yii::$app->request->getBodyParam('groups', ''); - - if (empty($emailAddress)) { - throw new UnprocessableEntityHttpException('No email address provided.'); - } - - if (empty($appPrefix)) { - throw new UnprocessableEntityHttpException('No app prefix provided.'); - } - - $user = User::findByEmail($emailAddress); - if ($user === null) { - Yii::$app->response->statusCode = 404; - return; - } - - if ($user->updateExternalGroups($appPrefix, $externalGroups)) { - Yii::$app->response->statusCode = 204; - return; - } - - $errors = join(', ', $user->getFirstErrors()); - throw new UnprocessableEntityHttpException($errors); - } - public function actionUpdatePassword(string $employeeId) { $user = User::findOne(['employee_id' => $employeeId]); From df08107a355ff21df17f5f679b69ae90afb41713 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:00:49 -0400 Subject: [PATCH 27/38] Remove unneeded `User::listExternalGroups()` method --- application/common/models/User.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 76c75d37..d0e3d8ba 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -641,35 +641,6 @@ public function getNagState() return $this->nagState->getState(); } - public static function listExternalGroups($appPrefix): array - { - /** @var User[] $users */ - $users = User::find()->where( - ['like', 'groups_external', $appPrefix . '-'] - )->all(); - - $responseData = []; - foreach ($users as $user) { - $userIsMatch = false; - $externalGroups = explode(',', $user->groups_external); - $externalGroupsWithAppPrefix = []; - foreach ($externalGroups as $externalGroup) { - if (str_starts_with($externalGroup, $appPrefix . '-')) { - $userIsMatch = true; - $externalGroupsWithAppPrefix[] = $externalGroup; - } - } - if ($userIsMatch) { - $responseData[] = [ - 'id' => $user->email, // TODO: Fix personnel-sync to not need this. - 'email' => $user->email, - 'groups' => join(',', $externalGroupsWithAppPrefix), - ]; - } - } - return $responseData; - } - public static function listUsersWithExternalGroupWith($appPrefix): array { $appPrefixWithHyphen = $appPrefix . '-'; From 208f3f949ca7ee36e5c043eaa978d7296b8a3e6e Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:01:54 -0400 Subject: [PATCH 28/38] Remove unneeded entry in docker-compose.yml re: external groups --- docker-compose.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ba688b0f..fb10cd4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -198,12 +198,6 @@ services: # docker compose run --rm test vendor/bin/behat --stop-on-failure features/user.feature # docker compose run --rm test vendor/bin/behat --stop-on-failure features/user.feature:306 - externalgroupssync: - build: ./ext-groups-sync - volumes: - - ./ext-groups-sync/config.json:/app/config.json - - ./ext-groups-sync/.env:/app/.env - phpmyadmin: image: phpmyadmin:5 ports: From 39c0d56bffb7a5b21ffccb6ef0d31f0c60247c64 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:04:27 -0400 Subject: [PATCH 29/38] Remove tests for removed create- and list-external-groups API endpoints --- .../bootstrap/GroupsExternalCreateContext.php | 33 ---------- .../bootstrap/GroupsExternalListContext.php | 66 ------------------- .../features/groups-external-create.feature | 23 ------- .../features/groups-external-list.feature | 19 ------ 4 files changed, 141 deletions(-) delete mode 100644 application/features/bootstrap/GroupsExternalCreateContext.php delete mode 100644 application/features/bootstrap/GroupsExternalListContext.php delete mode 100644 application/features/groups-external-create.feature delete mode 100644 application/features/groups-external-list.feature diff --git a/application/features/bootstrap/GroupsExternalCreateContext.php b/application/features/bootstrap/GroupsExternalCreateContext.php deleted file mode 100644 index 3dcd1112..00000000 --- a/application/features/bootstrap/GroupsExternalCreateContext.php +++ /dev/null @@ -1,33 +0,0 @@ -cleanRequestBody(); - $this->setRequestBody('email', $this->getUserEmailAddress()); - $this->setRequestBody( - 'groups', - join(',', $externalGroups) - ); - - $urlPath = sprintf( - '/user/external-groups?app_prefix=%s', - urlencode($appPrefix), - ); - - $this->iRequestTheResourceBe($urlPath, 'created'); - } -} diff --git a/application/features/bootstrap/GroupsExternalListContext.php b/application/features/bootstrap/GroupsExternalListContext.php deleted file mode 100644 index 9128229d..00000000 --- a/application/features/bootstrap/GroupsExternalListContext.php +++ /dev/null @@ -1,66 +0,0 @@ -deleteTestUser($emailAddress); - $this->createTestUser( - $emailAddress, - (string)$employeeId, - $groups - ); - } - } - - /** - * @When I get the list of users with :appPrefix external groups - */ - public function iGetTheListOfUsersWithExternalGroups($appPrefix) - { - $this->cleanRequestBody(); - - $urlPath = sprintf( - '/user/external-groups?app_prefix=%s', - urlencode($appPrefix), - ); - - $this->iRequestTheResourceBe($urlPath, 'retrieved'); - } - - /** - * @Then the response should only include the following groups for the following users: - */ - public function theResponseShouldOnlyIncludeTheFollowingGroupsForTheFollowingUsers(TableNode $table) - { - $expectedGroupsByUser = []; - foreach ($table as $row) { - $expectedGroupsByUser[$row['email']] = $row['groups']; - } - - $actualGroupsByUser = []; - $responseBody = $this->getResponseBody(); - foreach ($responseBody as $responseEntry) { - $actualGroupsByUser[$responseEntry['email']] = $responseEntry['groups']; - } - - Assert::eq( - json_encode($actualGroupsByUser, JSON_PRETTY_PRINT), - json_encode($expectedGroupsByUser, JSON_PRETTY_PRINT) - ); - } -} diff --git a/application/features/groups-external-create.feature b/application/features/groups-external-create.feature deleted file mode 100644 index 0a8a6799..00000000 --- a/application/features/groups-external-create.feature +++ /dev/null @@ -1,23 +0,0 @@ -Feature: Setting a User's list of external groups when they have none (for that prefix) - - Background: - Given the requester is authorized - - Scenario: Set the list of external groups for a user with no external groups - Given a user exists - And that user's list of external groups is "" - When I set that user's list of "wiki" external groups to the following: - | externalGroup | - | wiki-users | - | wiki-managers | - Then the response status code should be 204 - And that user's list of external groups should be "wiki-users,wiki-managers" - - Scenario: Set the list of external groups for a user with only other-prefix external groups - Given a user exists - And that user's list of external groups is "map-america" - When I set that user's list of "wiki" external groups to the following: - | externalGroup | - | wiki-users | - Then the response status code should be 204 - And that user's list of external groups should be "map-america,wiki-users" diff --git a/application/features/groups-external-list.feature b/application/features/groups-external-list.feature deleted file mode 100644 index 4e900b4d..00000000 --- a/application/features/groups-external-list.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Getting a list of Users with external groups with a given prefix - - Background: - Given the requester is authorized - - Scenario: Get the list of users (and their external groups) with a specific app prefix - Given the following users exist, with these external groups: - | email | groups | - | john_smith@example.org | wiki-one,map-america | - | jane_doe@example.org | wiki-one,wiki-two | - | bob_mcmanager@example.org | map-america,map-europe | - When I get the list of users with "wiki" external groups - Then the response status code should be 200 - And the response should only include the following groups for the following users: - | email | groups | - | john_smith@example.org | wiki-one | - | jane_doe@example.org | wiki-one,wiki-two | - - # Scenario: Get the list of users (and their external groups) but provide no app prefix From 42040712b7dd36a1d5163d6f8f7f93544a4fac45 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:06:33 -0400 Subject: [PATCH 30/38] Remove tests for removed update-external-groups API endpoint --- .../GroupsExternalUpdatesContext.php | 44 ---------------- .../features/groups-external-updates.feature | 50 ------------------- 2 files changed, 94 deletions(-) delete mode 100644 application/features/bootstrap/GroupsExternalUpdatesContext.php delete mode 100644 application/features/groups-external-updates.feature diff --git a/application/features/bootstrap/GroupsExternalUpdatesContext.php b/application/features/bootstrap/GroupsExternalUpdatesContext.php deleted file mode 100644 index 056a9a78..00000000 --- a/application/features/bootstrap/GroupsExternalUpdatesContext.php +++ /dev/null @@ -1,44 +0,0 @@ -cleanRequestBody(); - $this->setRequestBody( - 'groups', - join(',', $externalGroups) - ); - - $urlPath = sprintf( - '/user/external-groups/%s?app_prefix=%s', - urlencode($this->getUserEmailAddress()), - urlencode($appPrefix), - ); - - $this->iRequestTheResourceBe($urlPath, 'updated'); - } - - /** - * @Then that user's list of external groups should be :commaSeparatedExternalGroups - */ - public function thatUsersListOfExternalGroupsShouldBe($commaSeparatedExternalGroups) - { - $user = User::findByEmail($this->getUserEmailAddress()); - Assert::same($user->groups_external, $commaSeparatedExternalGroups); - } -} diff --git a/application/features/groups-external-updates.feature b/application/features/groups-external-updates.feature deleted file mode 100644 index fc1bfbb6..00000000 --- a/application/features/groups-external-updates.feature +++ /dev/null @@ -1,50 +0,0 @@ -Feature: Updating a User's list of external groups - - Background: - Given the requester is authorized - - Scenario: Add an external group to a user's list for a particular app - Given a user exists - And that user's list of external groups is "wiki-users" - When I update that user's list of "wiki" external groups to the following: - | externalGroup | - | wiki-users | - | wiki-managers | - Then the response status code should be 204 - And that user's list of external groups should be "wiki-users,wiki-managers" - - Scenario: Remove an external group from a user's list for a particular app - Given a user exists - And that user's list of external groups is "wiki-users,wiki-managers" - When I update that user's list of "wiki" external groups to the following: - | externalGroup | - | wiki-managers | - Then the response status code should be 204 - And that user's list of external groups should be "wiki-managers" - - Scenario: Remove all external groups from a user's list for a particular app - Given a user exists - And that user's list of external groups is "wiki-users,wiki-managers" - When I update that user's list of "wiki" external groups to the following: - | externalGroup | - Then the response status code should be 204 - And that user's list of external groups should be "" - - Scenario: Leave a user's external groups for a different app unchanged - Given a user exists - And that user's list of external groups is "wiki-users,map-europe" - When I update that user's list of "map" external groups to the following: - | externalGroup | - | map-america | - Then the response status code should be 204 - And that user's list of external groups should be "wiki-users,map-america" - - Scenario: Try to add an external group that does not match the given app-prefix - Given a user exists - And that user's list of external groups is "wiki-users" - When I update that user's list of "wiki" external groups to the following: - | externalGroup | - | map-america | - Then the response status code should be 422 - And the response body should contain "prefix" - And that user's list of external groups should be "wiki-users" From 9011fd1280c415c333cf8a19d87964b93addca3d Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:08:18 -0400 Subject: [PATCH 31/38] Reduce visibility of a function only used internally --- application/common/models/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index d0e3d8ba..a8ef977c 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -641,7 +641,7 @@ public function getNagState() return $this->nagState->getState(); } - public static function listUsersWithExternalGroupWith($appPrefix): array + private static function listUsersWithExternalGroupWith($appPrefix): array { $appPrefixWithHyphen = $appPrefix . '-'; From 73f2cdd0997aa11318c08c1224a0c14e91ad8dd2 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:09:20 -0400 Subject: [PATCH 32/38] Remove unneeded `use` line --- application/features/bootstrap/GroupsExternalSyncContext.php | 1 - 1 file changed, 1 deletion(-) diff --git a/application/features/bootstrap/GroupsExternalSyncContext.php b/application/features/bootstrap/GroupsExternalSyncContext.php index 7c67f318..82ce8af6 100644 --- a/application/features/bootstrap/GroupsExternalSyncContext.php +++ b/application/features/bootstrap/GroupsExternalSyncContext.php @@ -2,7 +2,6 @@ namespace Sil\SilIdBroker\Behat\Context; -use Behat\Behat\Tester\Exception\PendingException; use Behat\Gherkin\Node\TableNode; use common\models\User; use Webmozart\Assert\Assert; From 5b2e48b2d6f19a98225aa3e4157bfab33b4a8af5 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:09:48 -0400 Subject: [PATCH 33/38] Remove unneeded comments about other external-groups tests to write --- application/features/groups-external.feature | 6 ------ 1 file changed, 6 deletions(-) diff --git a/application/features/groups-external.feature b/application/features/groups-external.feature index 3b6ef113..d1bd9c71 100644 --- a/application/features/groups-external.feature +++ b/application/features/groups-external.feature @@ -39,9 +39,3 @@ Feature: Incorporating custom (external) groups in a User's `members` list | one | | two | | {idpName} | - -# # Scenarios that belong in the new "groups_external" sync: -# Scenario: Send 1 notification email if sync finds group(s) for a user not in this IDP -# Scenario: Add entries in the synced Google Sheet to the `groups_external` field -# Scenario: Remove entries not in the synced Google Sheet from the `groups_external` field -# Scenario: Only use entries from the synced Google Sheet that specify this IDP From 799cc614a15d1db416f49a3efefc93fe5627da54 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:11:18 -0400 Subject: [PATCH 34/38] Remove unneeded extra indentation --- application/frontend/config/main.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/frontend/config/main.php b/application/frontend/config/main.php index e44728e3..13818255 100644 --- a/application/frontend/config/main.php +++ b/application/frontend/config/main.php @@ -45,12 +45,12 @@ /* * User routes */ - 'GET user' => 'user/index', - 'GET user/' => 'user/view', - 'POST user' => 'user/create', - 'PUT user/' => 'user/update', - 'PUT user//password' => 'user/update-password', - 'PUT user//password/assess' => 'user/assess-password', + 'GET user' => 'user/index', + 'GET user/' => 'user/view', + 'POST user' => 'user/create', + 'PUT user/' => 'user/update', + 'PUT user//password' => 'user/update-password', + 'PUT user//password/assess' => 'user/assess-password', /* * Authentication routes From 99c8eff0734e07385d49bc512dd6a1aa8b691079 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 11:12:04 -0400 Subject: [PATCH 35/38] Remove unneeded Dockerfile (re: personnel-sync) --- ext-groups-sync/Dockerfile | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 ext-groups-sync/Dockerfile diff --git a/ext-groups-sync/Dockerfile b/ext-groups-sync/Dockerfile deleted file mode 100644 index 3fe6a88a..00000000 --- a/ext-groups-sync/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM ubuntu:24.04 - -RUN apt-get update && apt-get install -y \ - curl \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -RUN mkdir -p ~/.aws-lambda-rie -RUN curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie -RUN chmod +x ~/.aws-lambda-rie/aws-lambda-rie - -RUN curl --location -o personnel-sync.tar.gz https://github.com/silinternational/personnel-sync/releases/download/v6.8.3/personnel-sync_6.8.3_linux_amd64.tar.gz -RUN tar -xzf personnel-sync.tar.gz -RUN chmod +x personnel-sync - -CMD ["~/.aws-lambda-rie/aws-lambda-rie", "./personnel-sync"] From 763a85391ee3173f182d73c65ee60f4505a8d2c9 Mon Sep 17 00:00:00 2001 From: Matt H Date: Wed, 4 Sep 2024 15:15:46 -0400 Subject: [PATCH 36/38] Add documentation to `User::syncExternalGroups()` --- application/common/models/User.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/application/common/models/User.php b/application/common/models/User.php index a8ef977c..b9cb9e0f 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1025,6 +1025,19 @@ public function isPromptForMfa(): bool return false; } + /** + * Sync the given external-groups data (having the given app-prefix) with + * the current Users' external groups data. + * + * @param string $appPrefix -- Example: "wiki" + * @param array $desiredExternalGroupsByUserEmail -- The exhaustive list of + * external groups for the given app-prefix, where each key is a User's + * email address and each value is a comma-delimited string of which + * groups (with that app-prefix) that the user should have. Any other + * Users with external groups starting with the given app-prefix will + * have those external groups removed. + * @return string[] -- The resulting error messages. + */ public static function syncExternalGroups(string $appPrefix, array $desiredExternalGroupsByUserEmail): array { $errors = []; From 364edb4a7533c2c4579ca086b74277cfc2672569 Mon Sep 17 00:00:00 2001 From: Matt H Date: Thu, 5 Sep 2024 10:05:45 -0400 Subject: [PATCH 37/38] Improve the documentation of `User::syncExternalGroups()` --- application/common/models/User.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index b9cb9e0f..5eca7c10 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1026,16 +1026,16 @@ public function isPromptForMfa(): bool } /** - * Sync the given external-groups data (having the given app-prefix) with - * the current Users' external groups data. + * Update users' external-groups data using the given external-groups data. * * @param string $appPrefix -- Example: "wiki" - * @param array $desiredExternalGroupsByUserEmail -- The exhaustive list of - * external groups for the given app-prefix, where each key is a User's - * email address and each value is a comma-delimited string of which - * groups (with that app-prefix) that the user should have. Any other - * Users with external groups starting with the given app-prefix will - * have those external groups removed. + * @param array $desiredExternalGroupsByUserEmail -- The authoritative list + * of external groups for the given app-prefix, where each key is a + * User's email address and each value is a comma-delimited string of + * which groups (with that app-prefix) that the user should have. Any + * other Users with external groups starting with the given app-prefix + * will have those external groups removed. Any external groups starting + * with a different prefix will be left unchanged. * @return string[] -- The resulting error messages. */ public static function syncExternalGroups(string $appPrefix, array $desiredExternalGroupsByUserEmail): array From f179ccd18103d729292c54c9a0dcd2533126f392 Mon Sep 17 00:00:00 2001 From: Matt H Date: Thu, 5 Sep 2024 10:07:07 -0400 Subject: [PATCH 38/38] Rename `syncExternalGroups()` to `updateUsersExternalGroups()` It is not doing a sync with any external system. It is merely updating records based on the given data. --- application/common/models/User.php | 6 ++++-- .../features/bootstrap/GroupsExternalSyncContext.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/application/common/models/User.php b/application/common/models/User.php index 5eca7c10..5507f89c 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -1038,8 +1038,10 @@ public function isPromptForMfa(): bool * with a different prefix will be left unchanged. * @return string[] -- The resulting error messages. */ - public static function syncExternalGroups(string $appPrefix, array $desiredExternalGroupsByUserEmail): array - { + public static function updateUsersExternalGroups( + string $appPrefix, + array $desiredExternalGroupsByUserEmail + ): array { $errors = []; $emailAddressesOfCurrentMatches = self::listUsersWithExternalGroupWith($appPrefix); diff --git a/application/features/bootstrap/GroupsExternalSyncContext.php b/application/features/bootstrap/GroupsExternalSyncContext.php index 82ce8af6..06fae6d8 100644 --- a/application/features/bootstrap/GroupsExternalSyncContext.php +++ b/application/features/bootstrap/GroupsExternalSyncContext.php @@ -63,7 +63,7 @@ public function theExternalGroupsListIsTheFollowing(string $appPrefix, TableNode */ public function iSyncTheListOfExternalGroups($appPrefix) { - $this->syncErrors = User::syncExternalGroups( + $this->syncErrors = User::updateUsersExternalGroups( $appPrefix, $this->externalGroupsLists[$appPrefix] );