From e60d8c64f65a162abce7da45794a09943f75e240 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 29 Feb 2024 20:41:15 +0200 Subject: [PATCH] Cleanup. Add comments to API methods. --- Controller/GrouperGroupsController.php | 417 ++++++++++++------------- Lib/GrouperApiAccess.php | 168 ++++++---- Lib/GrouperHTTPWrapper.php | 1 + Lib/enum.php | 6 + Model/GrouperGroup.php | 145 ++++++--- View/Elements/empty | 0 View/Layouts/empty | 0 webroot/css/co-grouper-plugin.css | 4 +- 8 files changed, 397 insertions(+), 344 deletions(-) delete mode 100644 View/Elements/empty delete mode 100644 View/Layouts/empty diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 79d3515..9792088 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -55,6 +55,44 @@ class GrouperGroupsController extends GrouperLiteWidgetAppController public $name = 'GrouperGroups'; + /** + * Add a new member to a group + * Called from all pages via AJAX call + * + * @throws JsonException + * @throws Exception + */ + public function addSubscriber(): void + { + $this->layout = null; + $this->autoRender = false; + + $groupName = urldecode($this->request->query['group']); + $addUserId = urldecode($this->request->query['userId']); + + // Need to see if coming from AdHoc or from a WG (Working Group) + // todo: Investigate further + // XXX groupJoin is not using this formatted syntax??? +// $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' +// : $groupName; + + try { + if(!$this->GrouperGroup->addGroupMember($this->userId, + $groupName, + $addUserId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + throw $e; + } + + $this->restResponse(HttpStatusCodesEnum::HTTP_CREATED); + } + /** * Overrides parent beforeFilter to verify that Session contains the correct API settings. * @@ -114,12 +152,31 @@ public function beforeRender() { } /** - * No true Index page, so sent to default page of My Membership + * Perform a "keyword" search for CO People, sort of like the CO Dashboard + * cross controller search, but intended specifically for "people finder" + * search while you type API calls. * - * @return CakeResponse Redirect to MyMembership page + * @since COmanage Registry v3.3.0 */ - public function index(): void - {} + + public function findSubscriber(): void + { + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; + + // What search mode should we use? + if(empty($this->request->query['mode'])) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Mode Not Specified'); + return; + } + + $matches = $this->GrouperGroup->findForPicker($this->cur_co['Co']['id'], + $this->request->query['mode'], + $this->request->query['term'] ?? null); + + $this->set(compact('matches')); + $this->set('_serialize', 'matches'); + } /** * @return void @@ -156,13 +213,13 @@ public function groupSubscribers(): void $groupName = urldecode($this->request->query['groupname']); $subscribers = []; -// //Need to see if coming from AdHoc or from a WG (Working Group) -// $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' -// : $groupName; + //Need to see if coming from AdHoc or from a WG (Working Group) + $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' + : $groupName; try { $subscribers = $this->GrouperGroup->getGroupMembers($this->userId, - $groupName, + $groupNameFormatted, $this->CoGrouperLiteWidget->getConfig()); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); @@ -173,137 +230,37 @@ public function groupSubscribers(): void $this->set('_serialize', 'subscribers'); } - /** - * Add a new member to a group - * Called from all pages via AJAX call - * - * @throws JsonException - * @throws Exception - */ - public function addSubscriber(): void - { - $this->layout = null; - $this->autoRender = false; - - $groupName = urldecode($this->request->query['group']); - $addUserId = urldecode($this->request->query['userId']); - - // Need to see if coming from AdHoc or from a WG (Working Group) - // todo: Investigate further - // XXX groupJoin is not using this formatted syntax??? -// $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' -// : $groupName; - - try { - if(!$this->GrouperGroup->addGroupMember($this->userId, - $groupName, - $addUserId, - $this->CoGrouperLiteWidget->getConfig())) { - // The Request returned unsuccessful, but we have not more infomration. In this case we will just return - // forbidden since we do not actually now what happened - $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - throw $e; - } - - $this->restResponse(HttpStatusCodesEnum::HTTP_CREATED); - } - - /** - * Perform a "keyword" search for CO People, sort of like the CO Dashboard - * cross controller search, but intended specifically for "people finder" - * search while you type API calls. - * - * @since COmanage Registry v3.3.0 - */ - - public function findSubscriber(): void - { - $this->request->allowMethod('ajax'); - $this->layout = 'ajax'; - - // What search mode should we use? - if(empty($this->request->query['mode'])) { - $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Mode Not Specified'); - return; - } - - $matches = $this->GrouperGroup->findForPicker($this->cur_co['Co']['id'], - $this->request->query['mode'], - $this->request->query['term'] ?? null); - - $this->set(compact('matches')); - $this->set('_serialize', 'matches'); - } - - /** - * Remove a member from a group - * Called from all pages via AJAX call - * - * TODO: We need to appropriately handle Unathenticated call. We have to bubble up the response and do something. - * @throws JsonException - */ - public function removeSubscriber(): void - { - $this->layout = null; - $this->autoRender = false; - - $groupName = urldecode($this->request->query['group']); - $remUserId = urldecode($this->request->query['userId']); - - //Need to see if coming from AdHoc or from a WG (Working Group) -// $groupNameFormatted = (strpos($groupName, ':') === false) ? 'ref:incommon-collab:' . $groupName . ':users' -// : $groupName; - - try { - if(!$this->GrouperGroup->removeGroupMember($this->userId, - $groupName, - $remUserId, - $this->CoGrouperLiteWidget->getConfig())) { - // The Request returned unsuccessful, but we have not more infomration. In this case we will just return - // forbidden since we do not actually now what happened - $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - throw $e; - } - - $this->restResponse(HttpStatusCodesEnum::HTTP_OK); - } - /** * Listing of all Grouper Groups owned/admin by User Or search those Grouper Groups */ public function groupOwnerApi() { //Set initial setting - $scope = [ - 'userId' => $this->userId + $arguments = [ + 'userId' => $this->userId, + 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; if (isset($this->request->query['search'])) { $searchCriteria = urldecode($this->request->query['search']); $this->set('searchcriteria', $searchCriteria); + //Add settings for search Owned Groups - $scope['method'] = 'getSearchedGroups'; - $scope['searchcriteria'] = $searchCriteria; - $scope['searchpage'] = 'ownerGroups'; + $arguments['searchCriteria'] = $searchCriteria; + $arguments['searchPage'] = 'ownerGroups'; + + $func = 'getSearchedGroups'; $errorHint = 'Search'; } else { - $scope['method'] = 'getOwnedGroups'; + $func = 'getOwnedGroups'; $errorHint = ''; } try { - $func = $scope['method']; - $groupowners = $this->GrouperGroup->$func($scope, - $this->CoGrouperLiteWidget->getConfig()); + $groupowners = $this->GrouperGroup->$func(...$arguments); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); - $this->set('groupsowners', array()); + $this->set('groupsowners', []); $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); return; } @@ -319,8 +276,9 @@ public function groupOwnerApi() { */ public function groupMemberApi() { //Set initial setting - $scope = [ - 'userId' => $this->userId + $arguments = [ + 'userId' => $this->userId, + 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; if (isset($this->request->query['search'])) { @@ -328,22 +286,20 @@ public function groupMemberApi() { $this->set('searchcriteria', $searchCriteria); //Add settings for search Member Groups - $scope['method'] = 'getSearchedGroups'; - $scope['searchcriteria'] = $searchCriteria; - $scope['searchpage'] = 'filteredMemberOfGroups'; - $scope['ContainsWG'] = true; + $arguments['searchCriteria'] = $searchCriteria; + $arguments['searchPage'] = 'filteredMemberOfGroups'; + + $func = 'getSearchedGroups'; $errorHint = 'Search'; } else { //Add setting for Group Membership - $scope['method'] = 'filteredMemberOfGroups'; + $func = 'filteredMemberOfGroups'; $errorHint = ''; } try { - $func = $scope['method']; - $data = $this->GrouperGroup->$func($scope, - $this->CoGrouperLiteWidget->getConfig()); - $finalData = $this->breakoutGroups($data); + $data = $this->GrouperGroup->$func(...$arguments); + $finalData = $this->GrouperGroup->breakoutWrkFromAdHocGroups($data); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); @@ -364,31 +320,31 @@ public function groupMemberApi() { */ public function groupOptinApi() { //Set initial setting - $scope = [ - 'userId' => $this->userId + $arguments = [ + 'userId' => $this->userId, + 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; if (isset($this->request->query['search'])) { $searchCriteria = urldecode($this->request->query['search']); $this->set('searchcriteria', $searchCriteria); + //Add settings for search Optin's - $scope['method'] = 'getSearchedGroups'; - $scope['searchcriteria'] = $searchCriteria; - $scope['searchpage'] = 'optinGroups'; - $errorHint = 'Search'; + $arguments['searchCriteria'] = $searchCriteria; + $arguments['searchPage'] = 'optinGroups'; + $errorHint = 'Search'; + $func = 'getSearchedGroups'; } else { - $scope['method'] = 'optinGroups'; + $func = 'optinGroups'; $errorHint = ''; } try { - $func = $scope['method']; - $groupoptins = $this->GrouperGroup->$func($scope, - $this->CoGrouperLiteWidget->getConfig()); + $groupoptins = $this->GrouperGroup->$func(...$arguments); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); - $this->set('groupoptins', array()); + $this->set('groupoptins', []); $this->Flash->set('An error occurred with the Optin Groups, please try again later.', array('key' => 'error')); return; @@ -428,64 +384,13 @@ public function groupCreateTemplate() $this->set('title', _txt('pl.grouperlite.title.templatecreate')); } - /** - * Process to join a group displayed on the "Optin" page - * - * @throws Exception - */ - public function joinGroup(): void - { - $this->layout = null; - $this->autoRender = false; - // todo: add Subscriber and joinGroup should accept the same query parameters. Currently the join Group - // accepts a GroupName, while the addSubscriber accepts a group parameter - $groupName = urldecode($this->request->query['GroupName']); - - try { - // Add myself - if(!$this->GrouperGroup->addGroupMember($this->userId, - $groupName, - $this->userId, - $this->CoGrouperLiteWidget->getConfig())) { - // The Request returned unsuccessful, but we have not more infomration. In this case we will just return - // forbidden since we do not actually now what happened - $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - throw $e; - } - - $this->restResponse(HttpStatusCodesEnum::HTTP_CREATED); - } - - /** - * Process to leave a group displayed on the "Member Of" page + * No true Index page, so sent to default page of My Membership * + * @return CakeResponse Redirect to MyMembership page */ - public function leaveGroup(): void - { - $this->layout = null; - $this->autoRender = false; - $groupName = urldecode($this->request->query['GroupName']); - - try { - if(!$this->GrouperGroup->removeGroupMember($this->userId, - $groupName, - $this->userId, - $this->CoGrouperLiteWidget->getConfig())) { - // The Request returned unsuccessful, but we have not more infomration. In this case we will just return - // forbidden since we do not actually now what happened - $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - throw $e; - } - - $this->restResponse(HttpStatusCodesEnum::HTTP_OK); - } + public function index(): void + {} /** * NOTE: All permissions will be done on the Grouper side. All Authenticated users will be able to @@ -511,8 +416,8 @@ function isAuthorized(): array|bool $identifiers = $this->Identifier->find('first', $args); if(!empty($identifiers) - && is_array($identifiers) - && isset($identifiers['Identifier']['identifier']) + && is_array($identifiers) + && isset($identifiers['Identifier']['identifier']) ) { $this->userId = $identifiers['Identifier']['identifier']; } @@ -534,7 +439,7 @@ function isAuthorized(): array|bool $p['addSubscriber'] = true; $p['findSubscriber'] = true; $p['removeSubscriber'] = true; - + $p['groupCreate'] = true; $p['joinGroup'] = true; $p['leaveGroup'] = true; @@ -545,44 +450,64 @@ function isAuthorized(): array|bool return ($p[$this->action]); } + /** - * Breakout Working Groups from AdHoc Groups for display purposes in UI. - * - * @param array $recordSet - * @return array[] + * Process to join a group displayed on the "Optin" page * + * @throws Exception */ - private function breakoutGroups(array $recordSet) + public function joinGroup(): void { - $wgData = array(); - $notWGData = array(); - //Parse out the Working Groups from the Ad-hoc groups - foreach ($recordSet as $record) { - if (isset($record['WGName'])) { - $wgData[] = $record; - } else { - $notWGData[] = $record; + $this->layout = null; + $this->autoRender = false; + // todo: add Subscriber and joinGroup should accept the same query parameters. Currently the join Group + // accepts a GroupName, while the addSubscriber accepts a group parameter + $groupName = urldecode($this->request->query['GroupName']); + + try { + // Add myself + if(!$this->GrouperGroup->addGroupMember($this->userId, + $groupName, + $this->userId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + throw $e; } - return array( - 'adhoc' => $notWGData, - 'working' => $wgData - ); + $this->restResponse(HttpStatusCodesEnum::HTTP_CREATED); } /** - * Override the default sanity check performed in AppController + * Process to leave a group displayed on the "Member Of" page * - * @since COmanage Registry v4.3.0 - * @return Boolean True if sanity check is successful */ - - public function verifyRequestedId(): bool + public function leaveGroup(): void { - return true; - } + $this->layout = null; + $this->autoRender = false; + $groupName = urldecode($this->request->query['GroupName']); + + try { + if(!$this->GrouperGroup->removeGroupMember($this->userId, + $groupName, + $this->userId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + throw $e; + } + $this->restResponse(HttpStatusCodesEnum::HTTP_OK); + } /** * For Models that accept a CO ID, find the provided CO ID. @@ -609,6 +534,40 @@ public function parseCOID($data = null): int { return -1; } + /** + * Remove a member from a group + * + * @throws JsonException + */ + public function removeSubscriber(): void + { + $this->layout = null; + $this->autoRender = false; + + $groupName = urldecode($this->request->query['group']); + $remUserId = urldecode($this->request->query['userId']); + + //Need to see if coming from AdHoc or from a WG (Working Group) + $groupNameFormatted = (strpos($groupName, ':') === false) ? 'ref:incommon-collab:' . $groupName . ':users' + : $groupName; + + try { + if(!$this->GrouperGroup->removeGroupMember($this->userId, + $groupNameFormatted, + $remUserId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + throw $e; + } + + $this->restResponse(HttpStatusCodesEnum::HTTP_OK); + } + /** * * @param integer $status HTTP result code @@ -635,4 +594,16 @@ public function restResponse(int $status, $this->response->body(json_encode($body, JSON_THROW_ON_ERROR)); $this->response->send(); } + + /** + * Override the default sanity check performed in AppController + * + * @since COmanage Registry v4.3.0 + * @return Boolean True if sanity check is successful + */ + + public function verifyRequestedId(): bool + { + return true; + } } \ No newline at end of file diff --git a/Lib/GrouperApiAccess.php b/Lib/GrouperApiAccess.php index abf1726..f1793e5 100644 --- a/Lib/GrouperApiAccess.php +++ b/Lib/GrouperApiAccess.php @@ -77,6 +77,7 @@ public function __construct(array $cfg) * @param string $groupName * @param string $addUserId * + * @example https://github.com/Internet2/grouper/blob/b1c7b14e30b3f73f58768fc75cf845ee9f1594ef/grouper-ws/grouper-ws/doc/samples/addMember/WsSampleAddMemberRest2_xml.txt * @return bool Requests success or not * @throws GrouperLiteWidgetException|JsonException */ @@ -202,7 +203,6 @@ public function createGroupWithTemplate(array $queryData): array } /** - * * For creating/updating Ad-Hoc groups not using the Grouper Template format * * Create or Update a Group where User is Admin/Owner @@ -215,6 +215,8 @@ public function createGroupWithTemplate(array $queryData): array * @return bool True if added or updated successful * @throws GrouperLiteWidgetException * @throws JsonException + * + * @example https://github.com/Internet2/grouper/blob/master/grouper-ws/grouper-ws/doc/samples/groupSave/WsSampleGroupSaveRestLite_xml.txt */ public function createUpdateGroup(string $actAsUserId, string $groupName, string $stem, string $groupDescription ): bool { @@ -238,7 +240,7 @@ public function createUpdateGroup(string $actAsUserId, string $groupName, string ] ] ]; - $actionEndpoint = "/groups"; + $actionEndpoint = '/groups'; try { $results = $this->http->sendRequest('POST', @@ -249,30 +251,37 @@ public function createUpdateGroup(string $actAsUserId, string $groupName, string throw $e; } - return isset($results['WsGroupSaveResults']['results']['resultMetadata']['resultCode']) - && $results['WsGroupSaveResults']['results']['resultMetadata']['resultCode'] === GrouperResultCodesEnum::SUCCESS; + if(isset($results['error']) && $results['error']) { + $cakeExceptionClass = $results['cakeException']; + throw new $cakeExceptionClass($results['message']); + } + + // XXX This is not required, since by now i know that the request has succeeded, + // but i am leaving it here for the time being + return isset($results['WsGroupSaveResults']['resultMetadata']['resultCode']) + && in_array($results['WsGroupSaveResults']['resultMetadata']['resultCode'], + [GrouperResultCodesEnum::SUCCESS, GrouperResultCodesEnum::SUCCESS_UPDATED], + true); } /** * * Method used to DELETE a Group in Grouper via the Template method. * - * @param array $queryData Array of conditions and data adding new Grouper Group + * @param string $actAsUserId + * @param string $workingGroupExt * * @return bool True if deleted successfully - * @throws GrouperLiteWidgetException|JsonException - * + * @throws GrouperLiteWidgetException + * @throws JsonException */ - public function deleteGroupWithTemplate(array $queryData): bool + public function deleteGroupWithTemplate(string $actAsUserId, string $workingGroupExt): bool { - $workingGroupExt = $queryData['workingGroupExt']; - $userId = $queryData['userId']; - $groupToDelete = [ 'WsRestGshTemplateExecRequest' => [ 'gshTemplateActAsSubjectLookup' => [ 'subjectSourceId' => 'ldap', - 'subjectId' => $userId + 'subjectId' => $actAsUserId ], 'ownerStemLookup' => [ 'stemName' => 'ref:incommon-collab' @@ -288,7 +297,7 @@ public function deleteGroupWithTemplate(array $queryData): bool ] ]; - $actionEndpoint = "/gshTemplateExec"; + $actionEndpoint = '/gshTemplateExec'; try { $results = $this->http->sendRequest('POST', @@ -337,7 +346,7 @@ public function getGroupInfo(string $groupName): array ] ] ]; - $actionEndpoint = "/attributeAssignments"; + $actionEndpoint = '/attributeAssignments'; try { $results = $this->http->sendRequest('POST', @@ -360,7 +369,7 @@ public function getGroupInfo(string $groupName): array } /** - * Get Groups that User is a member of from Grouper. + * Returns all the groups the active user is a member of, that they are allowed to see. * * Note: Params added at end make sure that the groups returned can only be viewed by the member logged into * Grouper Lite @@ -392,19 +401,45 @@ public function getUserGroupMemberships(string $actorUserId, string $userId): ar } /** + * Returns either the groups the user is able to Opt into or can manage the memberships of. * Used for requests made to Membership endpoint in Grouper WS * * @param string $userId + * @param string $actAsUserId * @param string $groupType * * @return array Group records associated to calling method * @throws GrouperLiteWidgetException - * @see getOwnedStems() - * @see getOptinGroups() - * @see getOptOutGroups() - * @see getOwnedGroups() + * + * @example https://github.com/Internet2/grouper/blob/b1c7b14e30b3f73f58768fc75cf845ee9f1594ef/grouper-ws/grouper-ws/doc/samples/getMemberships/WsSampleGetMembershipsRest2_xml.txt#L20 + * + * $: > grouperClientAlias --debug --operation=getMembershipsWs --subjectIds=john.b.doe@at.internet2.edu,subjectAll --actAsSubjectId=john.b.doe@at.internet2.edu + * + * POST /grouper-ws/servicesRest/5.8.1/memberships HTTP/1.1 + * Connection: close + * Authorization: Basic xxxxxxxxxxxxxxxx + * User-Agent: Jakarta Commons-HttpClient/3.1 + * Host: grouper.dev.at.internet2.edu:-1 + * Content-Length: 208 + * Content-Type: application/json; charset=UTF-8 + * + * { + * "WsRestGetMembershipsRequest":{ + * "actAsSubjectLookup":{ + * "subjectId":"john.b.doe@at.internet2.edu" + * }, + * "wsSubjectLookups":[ + * { + * "subjectId":"john.b.doe@at.internet2.edu" + * }, + * { + * "subjectId":"subjectAll" + * } + * ] + * } + * } */ - public function getGrouperUserMemberships(string $userId, string $groupType): array + public function getGrouperUserMemberships(string $userId, string $actAsUserId, string $groupType): array { if(!in_array($groupType, [ GrouperGroupTypeEnum::OPTINS, @@ -423,9 +458,17 @@ public function getGrouperUserMemberships(string $userId, string $groupType): ar true); // Build request logic - $data = []; - $data['WsRestGetMembershipsRequest']['fieldName'] = $groupType; - $data['WsRestGetMembershipsRequest']['wsSubjectLookups'][0]['subjectId'] = $isOptinsOrOptouts ? GrouperConfigEnums::ALL : $userId; + $data = [ + 'WsRestGetMembershipsRequest' => [ + 'fieldName' => $groupType, + 'actAsSubjectLookup' => [ + 'subjectId' => true ? '': $actAsUserId + ], + 'wsSubjectLookups' => [ + ['subjectId' => $isOptinsOrOptouts ? GrouperConfigEnums::ALL : $userId] + ], + ] + ]; if ($isOptinsOrOptouts) { // Build request logic, 2 subjectId's, second is for when user in "Secret" Optin/Optout Group @@ -443,7 +486,7 @@ public function getGrouperUserMemberships(string $userId, string $groupType): ar throw $e; } - return $results; + return $results['WsGetMembershipsResults']['wsGroups'] ?? []; } /** @@ -454,6 +497,36 @@ public function getGrouperUserMemberships(string $userId, string $groupType): ar * * @return array Listing of Members belonging to Grouper Group * @throws GrouperLiteWidgetException|JsonException|NotFoundException + * @example https://github.com/Internet2/grouper/blob/master/grouper-ws/grouper-ws/doc/samples/getGroups/WsSampleGetGroupsRest_json.txt + * + * grouperClientAlias --debug=true --operation=getMembersWs --actAsSubjectId=john.b.doe@at.internet2.edu --subjectAttributeNames=name --groupNames=ref:incommon-collab:co:member + * WebService: connecting to URL: 'https://grouper.dev.at.internet2.edu/grouper-ws/servicesRest/5.8.1/groups' + * + * ################ REQUEST START (indented) ############### + * + * POST /grouper-ws/servicesRest/5.8.1/groups HTTP/1.1 + * Connection: close + * Authorization: Basic xxxxxxxxxxxxxxxx + * User-Agent: Jakarta Commons-HttpClient/3.1 + * Host: grouper.dev.at.internet2.edu:-1 + * Content-Length: 201 + * Content-Type: application/json; charset=UTF-8 + * + * { + * "WsRestGetMembersRequest":{ + * "wsGroupLookups":[ + * { + * "groupName":"ref:incommon-collab:co:member" + * } + * ], + * actAsSubjectLookup":{ + * "subjectId":"john.b.doe@at.internet2.edu" + * }, + * "subjectAttributeNames":[ + * "name" + * ] + * } + * } */ public function getGroupMembers(string $actorUserId, string $groupName): array { @@ -489,49 +562,6 @@ public function getGroupMembers(string $actorUserId, string $groupName): array return $results['WsGetMembersResults']['results'][0]['wsSubjects'] ?? []; } - /** - * Gets all available Optin/OptOut groups in Grouper - * - * Returns Optin/OptOut groups that can be joined/left - * - * @param string $userId - * @param string $groupType - * - * @return array Optin groups from Grouper - * @throws GrouperLiteWidgetException - */ - public function getOptionalGroups(string $userId, string $groupType): array - { - try { - $results = $this->getGrouperUserMemberships($userId, $groupType); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - return $results['WsGetMembershipsResults']['wsGroups'] ?? []; - } - - /** - * Potential use was for creating adhoc group by a user, not associated to WG. - * - * Gets all Stems/Folders where User is admin/owner - * - * @param string $userId - * - * @return array Array of Stems/Folders from Grouper - * @throws GrouperLiteWidgetException - */ - public function getOwnedStems(string $userId): array - { - try { - $results = $this->getGrouperUserMemberships($userId, GrouperGroupTypeEnum::ADMIN); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - return $results['WsGetMembershipsResults']['wsStems'] ?? []; - } - /** * Is the user member of the Group * @@ -559,8 +589,8 @@ public function isMemberOfGroup(string $groupName, string $userId): bool throw $e; } - return isset($results['WsHasMemberLiteResul']['resultMetadata']['resultCode']) - && $results['WsHasMemberLiteResul']['resultMetadata']['resultCode'] === GrouperResultCodesEnum::IS_MEMBER; + return isset($results['WsHasMemberLiteResult']['resultMetadata']['resultCode']) + && $results['WsHasMemberLiteResult']['resultMetadata']['resultCode'] === GrouperResultCodesEnum::IS_MEMBER; } /** diff --git a/Lib/GrouperHTTPWrapper.php b/Lib/GrouperHTTPWrapper.php index 8475837..115d8cb 100644 --- a/Lib/GrouperHTTPWrapper.php +++ b/Lib/GrouperHTTPWrapper.php @@ -186,6 +186,7 @@ public function sendRequest(string $method, string $endPoint, string $body = '') $this->_request['body'] = $body; CakeLog::write('debug', __METHOD__ . '::connection url: ' . $uri); + CakeLog::write('debug', __METHOD__ . '::data: ' . $body); try { $apiResults = $this->request($this->_request); } catch (Exception $e) { diff --git a/Lib/enum.php b/Lib/enum.php index 963dae1..c496330 100644 --- a/Lib/enum.php +++ b/Lib/enum.php @@ -19,12 +19,16 @@ class GrouperSpecialGroups { const GROUPER_EMAIL_STEM = 'app:sympa'; } +// https://software.internet2.edu/grouper/doc/master/grouper-ws-parent/grouper-ws/apidocs/edu/internet2/middleware/grouper/ws/rest/WsRestResultProblem.html +// https://software.internet2.edu/grouper/doc/master/grouper-ws-parent/grouper-ws/apidocs/edu/internet2/middleware/grouper/ws/coresoap/WsAddMemberResult.WsAddMemberResultCode.html# class GrouperResultCodesEnum { const SUCCESS_ALREADY_EXISTED = 'SUCCESS_ALREADY_EXISTED'; const EXECUTE_FAILED = 'EXECUTE_FAILED'; const GROUP_NOT_FOUND = 'GROUP_NOT_FOUND'; const IS_MEMBER = 'IS_MEMBER'; const IS_NOT_MEMBER = 'IS_NOT_MEMBER'; + const INSUFFICIENT_PRIVILEGES = 'INSUFFICIENT_PRIVILEGES'; + const INVALID_QUERY = 'INVALID_QUERY'; const NO_SUCH_OBJECT = 'NO_SUCH_OBJECT'; const PROBLEM_WITH_ASSIGNMENT = 'PROBLEM_WITH_ASSIGNMENT'; const PROBLEM_GETTING_MEMBERS = 'PROBLEM_GETTING_MEMBERS'; @@ -39,6 +43,8 @@ class GrouperResultCodesEnum { class GrouperCodesToExceptionClassEnum { const EXECUTE_FAILED = 'BadRequestException'; const GROUP_NOT_FOUND = 'NotFoundException'; + const INSUFFICIENT_PRIVILEGES = 'UnauthorizedException'; + const INVALID_QUERY = 'BadRequestException'; const NO_SUCH_OBJECT = 'NotFoundException'; const PROBLEM_WITH_ASSIGNMENT = 'BadRequestException'; const SUBJECT_NOT_FOUND = 'NotFoundException'; diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index 1a1f8b6..97c22de 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -79,8 +79,8 @@ public function isUserGroupOwner(string $userId, array $cfg): bool } try { - $resultsAdmin = $this->grouperAPI->getGrouperUserMemberships($userId, GrouperGroupTypeEnum::ADMIN); - $resultsUpdate = $this->grouperAPI->getGrouperUserMemberships($userId, GrouperGroupTypeEnum::UPDATE); + $resultsAdmin = $this->grouperAPI->getGrouperUserMemberships($userId, $userId, GrouperGroupTypeEnum::ADMIN); + $resultsUpdate = $this->grouperAPI->getGrouperUserMemberships($userId, $userId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; @@ -130,20 +130,21 @@ private function initApi(array $cfg) { * Return all Groups that a User belongs to in Grouper. * Will also add OptOut Groups and flag them as joined so can display Optout option in UI. * - * @param array $conditions Listing of conditions for display of records, including UserId + * @param string $userId + * @param array $cfg + * * @return array Records of Groups from Grouper that the User belongs to * @throws GrouperLiteWidgetException - * * @since COmanage Registry v4.4.0 */ - public function filteredMemberOfGroups(array $conditions, array $cfg) + public function filteredMemberOfGroups(string $userId, array $cfg): array { $this->initApi($cfg); try { - $memberOfGroups = $this->memberOfGroups($conditions['userId'], $conditions['userId'], $cfg); + $memberOfGroups = $this->memberOfGroups($userId, $userId, $cfg); // Determine which groups can be left by user, if wanted. - $optOutGroups = $this->grouperAPI->getOptionalGroups($conditions['userId'], GrouperGroupTypeEnum::OPTOUTS); + $optOutGroups = $this->grouperAPI->getGrouperUserMemberships($userId, $userId, GrouperGroupTypeEnum::OPTOUTS); $optOutGroupsNames = Hash::combine($optOutGroups, '{n}.name', '{n}.displayExtension'); foreach ($memberOfGroups as &$memberOfGroup) { @@ -291,31 +292,50 @@ private function memberOfGroups(string $actorUserId, string $userId, array $cfg) /** * Return all Grouper Groups that the User has a role of owner/admin * - * @param array $conditions Listing of conditions for display of records, including UserId - * @param array $cfg + * @param string $userId + * @param array $cfg * * @return array * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - public function getOwnedGroups(array $conditions, array $cfg): array + public function getOwnedGroups(string $userId, array $cfg): array { - if(empty($conditions['userId'])) { + if(empty($userId)) { return false; } $this->initApi($cfg); try { - $resultsAdmin = $this->grouperAPI->getGrouperUserMemberships($conditions['userId'], GrouperGroupTypeEnum::ADMIN); - $resultsUpdate = $this->grouperAPI->getGrouperUserMemberships($conditions['userId'], GrouperGroupTypeEnum::UPDATE); + $resultsAdmin = $this->grouperAPI->getGrouperUserMemberships($userId, $userId, GrouperGroupTypeEnum::ADMIN); + $resultsUpdate = $this->grouperAPI->getGrouperUserMemberships($userId, $userId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return $this->removeDuplicates($resultsAdmin['WsGetMembershipsResults']['wsGroups'] ?? [], - $resultsUpdate['WsGetMembershipsResults']['wsGroups'] ?? []); + return $this->removeDuplicates($resultsAdmin, $resultsUpdate); + } + + /** + * Potential use was for creating adhoc group by a user, not associated to WG. + * + * Gets all Stems/Folders where User is admin/owner + * + * @param string $userId + * + * @return array Array of Stems/Folders from Grouper + * @throws GrouperLiteWidgetException + */ + public function getOwnedStems(string $userId): array + { + try { + return $this->grouperAPI->getGrouperUserMemberships($userId, $userId, GrouperGroupTypeEnum::ADMIN); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } } /** @@ -406,29 +426,35 @@ public function removeGroupMember(string $userId, * Get all Groups with Optin attribute set and display ones User can join. * Will Match up with Groups User is already a member of to determine which Optin groups to not display * - * @param array $conditions Listing of conditions for display of records, including UserId + * @param string $userId + * @param array $cfg + * * @return array Listing of Optin groups available in Grouper * @throws GrouperLiteWidgetException Captured in Controller - * * @since COmanage Registry v4.4.0 */ - public function optinGroups(array $conditions, array $cfg): array + public function optinGroups(string $userId, array $cfg): array { $this->initApi($cfg); try { // Groups the user can join or leave - $joinOrLeave = $this->grouperAPI->getOptionalGroups($conditions['userId'], - GrouperGroupTypeEnum::OPTINS); + $joinOrLeave = $this->grouperAPI->getGrouperUserMemberships($userId, + $userId, + GrouperGroupTypeEnum::OPTINS); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } + + try { + // Groups the user is a member of + $userGroups = $this->grouperAPI->getUserGroupMemberships($userId, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - // Groups the user is a member of - $userGroups = $this->memberOfGroups($conditions['userId'], - $conditions['userId'], - $cfg); // Extract the names of the Groups the user is a member of $userGroupsNames = Hash::extract($userGroups, '{n}.name'); // Return the groups the user can join and is not a member of @@ -508,17 +534,15 @@ public function createGroupWithTemplate(string $userId, array $groupData, array * @since COmanage Registry v4.4.0 */ - public function getSearchedGroups(array $conditions, array $cfg) + public function getSearchedGroups(string $userId, string $searchCriteria, string $searchPage, array $cfg) { $this->initApi($cfg); try { - //Breakout page where search was called and forward to appropriate method for processing - $page = $conditions['searchpage']; - $pageResults = $this->$page($conditions); + // Breakout page where search was called and forward to appropriate method for processing + $pageResults = isset($searchPage) ? $this->$searchPage($userId, $cfg) : []; - $returnResults = array(); - $searchCriteria = $conditions['searchcriteria']; + $returnResults = []; foreach ($pageResults as $result) { $compare = $result; @@ -533,11 +557,8 @@ public function getSearchedGroups(array $conditions, array $cfg) } } - if(isset($conditions['']) && $conditions['getSearchedGroups']){ - return $this->getFriendlyWorkingGroupName($returnResults, 'member'); - } else { - return $returnResults; - } + return $searchCriteria == 'getSearchedGroups' ? $this->getFriendlyWorkingGroupName($returnResults, 'member') + : $returnResults; } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -692,25 +713,49 @@ private function getFriendlyWorkingGroupName(array $groups, string $method) { */ public function removeDuplicates(array $arrOne, array $arrTwo) { - //Determine which array is bigger and use as base - $countOne = count($arrOne); - $countTwo = count($arrTwo); - if ($countOne >= $countTwo) { - $arrL = $arrOne; - $arrS = $arrTwo; - } else { - $arrL = $arrTwo; - $arrS = $arrOne; + // If one of the arrays is empty then return the other one as is + if(empty($arrOne) && empty($arrTwo)) { + return []; + } else if(empty($arrOne)) { + return $arrTwo; + } else if(empty($arrTwo)) { + return $arrOne; } - foreach ($arrL as $large) { - foreach ($arrS as $key => $val) { - if ($large['uuid'] == $val['uuid']) { - unset($arrS[$key]); - } + $uniqueArr = Hash::combine($arrOne, '{n}.uuid', '{n}'); + foreach($arrTwo as $data) { + if(!isset($uniqueArr[ $data['uuid'] ])) { + $uniqueArr[ $data['uuid'] ] = $data; + } + } + + return $uniqueArr; + } + + + /** + * Breakout Working Groups from AdHoc Groups. + * + * @param array $recordSet + * @return array[] + * + */ + public function breakoutWrkFromAdHocGroups(array $recordSet): array + { + $wgData = []; + $notWGData = []; + //Parse out the Working Groups from the Ad-hoc groups + foreach ($recordSet as $record) { + if (isset($record['WGName'])) { + $wgData[] = $record; + } else { + $notWGData[] = $record; } } - return array_merge_recursive($arrL, $arrS); + return [ + 'adhoc' => $notWGData, + 'working' => $wgData + ]; } } diff --git a/View/Elements/empty b/View/Elements/empty deleted file mode 100644 index e69de29..0000000 diff --git a/View/Layouts/empty b/View/Layouts/empty deleted file mode 100644 index e69de29..0000000 diff --git a/webroot/css/co-grouper-plugin.css b/webroot/css/co-grouper-plugin.css index 4d6ff8c..4736499 100644 --- a/webroot/css/co-grouper-plugin.css +++ b/webroot/css/co-grouper-plugin.css @@ -317,11 +317,11 @@ a.list-group-item-action:hover .fa { margin-bottom: 1px; } -.co-loading-mini-input-container { +.grouper_groups .co-loading-mini-input-container { flex-grow: 1; } -.co-loading-mini-input-container .co-loading-mini { +.grouper_groups .co-loading-mini-input-container .co-loading-mini { display: none; position: absolute; right: 38px;