Skip to content

Commit

Permalink
feat(SDK-4543): Support Organizations with Client Grants (#736)
Browse files Browse the repository at this point in the history
### Changes

This pull request adds functionality associated with Organizations and
Client Grants. It introduces support for:

- Retrieving Organizations associated with a Client Grant.
- Retrieving Client Grants associated with an Organization.
- Associating or disassociating client grants from organizations.

### References

Please review internal ticket SDK-4543.

### Testing

Tests have been added and updated to cover these changes. Coverage
remains at 100%.

### Contributor Checklist

- [x] I agree to adhere to the [Auth0 General Contribution
Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md).
- [x] I agree to uphold the [Auth0 Code of
Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
  • Loading branch information
evansims authored Nov 13, 2023
1 parent 6764f47 commit 9a01566
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 19 deletions.
66 changes: 54 additions & 12 deletions src/API/Management/ClientGrants.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public function create(
string $audience,
?array $scope = null,
?RequestOptions $options = null,
?string $organizationUsage = null,
?bool $allowAnyOrganization = null,
): ResponseInterface {
[$clientId, $audience] = Toolkit::filter([$clientId, $audience])->string()->trim();
[$scope] = Toolkit::filter([$scope])->array()->trim();
Expand All @@ -30,16 +32,24 @@ public function create(
[$audience, \Auth0\SDK\Exception\ArgumentException::missing('audience')],
])->isString();

$body = [
'client_id' => $clientId,
'audience' => $audience,
'scope' => $scope,
];

if (null !== $organizationUsage) {
$body['organization_usage'] = $organizationUsage;
}

if (null !== $allowAnyOrganization) {
$body['allow_any_organization'] = $allowAnyOrganization;
}

return $this->getHttpClient()
->method('post')
->addPath(['client-grants'])
->withBody(
(object) [
'client_id' => $clientId,
'audience' => $audience,
'scope' => $scope,
],
)
->withBody((object) $body)
->withOptions($options)
->call();
}
Expand Down Expand Up @@ -120,10 +130,34 @@ public function getAllByClientId(
return $this->getAll($params, $options);
}

public function getOrganizations(
string $grantId,
?array $parameters = null,
?RequestOptions $options = null,
): ResponseInterface {
[$grantId] = Toolkit::filter([$grantId])->string()->trim();
[$parameters] = Toolkit::filter([$parameters])->array()->trim();

Toolkit::assert([
[$grantId, \Auth0\SDK\Exception\ArgumentException::missing('grantId')],
])->isString();

/** @var array<null|int|string> $parameters */

return $this->getHttpClient()
->method('get')
->addPath(['client-grants', $grantId, 'organizations'])
->withParams($parameters)
->withOptions($options)
->call();
}

public function update(
string $grantId,
?array $scope = null,
?RequestOptions $options = null,
?string $organizationUsage = null,
?bool $allowAnyOrganization = null,
): ResponseInterface {
[$grantId] = Toolkit::filter([$grantId])->string()->trim();
[$scope] = Toolkit::filter([$scope])->array()->trim();
Expand All @@ -132,13 +166,21 @@ public function update(
[$grantId, \Auth0\SDK\Exception\ArgumentException::missing('grantId')],
])->isString();

$body = [
'scope' => $scope,
];

if (null !== $organizationUsage) {
$body['organization_usage'] = $organizationUsage;
}

if (null !== $allowAnyOrganization) {
$body['allow_any_organization'] = $allowAnyOrganization;
}

return $this->getHttpClient()
->method('patch')->addPath(['client-grants', $grantId])
->withBody(
(object) [
'scope' => $scope,
],
)
->withBody((object) $body)
->withOptions($options)
->call();
}
Expand Down
67 changes: 67 additions & 0 deletions src/API/Management/Organizations.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@
*/
final class Organizations extends ManagementEndpoint implements OrganizationsInterface
{
public function addClientGrant(
string $id,
string $grantId,
?array $parameters = null,
?RequestOptions $options = null,
): ResponseInterface {
[$id, $grantId] = Toolkit::filter([$id, $grantId])->string()->trim();
[$parameters] = Toolkit::filter([$parameters])->array()->trim();

Toolkit::assert([
[$id, \Auth0\SDK\Exception\ArgumentException::missing('id')],
[$grantId, \Auth0\SDK\Exception\ArgumentException::missing('grantId')],
])->isString();

$body = [
'grant_id' => $grantId,
];

/** @var array<null|int|string> $parameters */

return $this->getHttpClient()
->method('post')->addPath(['organizations', $id, 'client-grants'])
->withBody((object) $body)
->withParams($parameters)
->withOptions($options)
->call();
}

public function addEnabledConnection(
string $id,
string $connectionId,
Expand Down Expand Up @@ -259,6 +287,22 @@ public function getByName(
->call();
}

public function getClientGrants(
string $id,
?RequestOptions $options = null,
): ResponseInterface {
[$id] = Toolkit::filter([$id])->string()->trim();

Toolkit::assert([
[$id, \Auth0\SDK\Exception\ArgumentException::missing('id')],
])->isString();

return $this->getHttpClient()
->method('get')->addPath(['organizations', $id, 'client-grants'])
->withOptions($options)
->call();
}

public function getEnabledConnection(
string $id,
string $connectionId,
Expand Down Expand Up @@ -361,6 +405,29 @@ public function getMembers(
->call();
}

public function removeClientGrant(
string $id,
string $grantId,
?array $parameters = null,
?RequestOptions $options = null,
): ResponseInterface {
[$id, $grantId] = Toolkit::filter([$id, $grantId])->string()->trim();
[$parameters] = Toolkit::filter([$parameters])->array()->trim();

Toolkit::assert([
[$id, \Auth0\SDK\Exception\ArgumentException::missing('id')],
[$grantId, \Auth0\SDK\Exception\ArgumentException::missing('grantId')],
])->isString();

/** @var array<null|int|string> $parameters */

return $this->getHttpClient()
->method('delete')->addPath(['organizations', $id, 'client-grants', $grantId])
->withParams($parameters)
->withOptions($options)
->call();
}

public function removeEnabledConnection(
string $id,
string $connectionId,
Expand Down
38 changes: 31 additions & 7 deletions src/Contract/API/Management/ClientGrantsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ interface ClientGrantsInterface
* Create a new Client Grant.
* Required scope: `create:client_grants`.
*
* @param string $clientId client ID to receive the grant
* @param string $audience audience identifier for the API being granted
* @param null|array<string> $scope Optional. Scopes allowed for this client grant.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these. See @see for supported options.)
* @param string $clientId client ID to receive the grant
* @param string $audience audience identifier for the API being granted
* @param null|array<string> $scope Optional. Scopes allowed for this client grant.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these. See @see for supported options.)
* @param null|string $organizationUsage Optional. Defines whether organizations can be used with client credentials exchanges for this grant. Possible values are `deny`, `allow` or `require`.
* @param null|bool $allowAnyOrganization Optional. If enabled, any organization can be used with this grant. If disabled (default), the grant must be explicitly assigned to the desired organizations.
*
* @throws \Auth0\SDK\Exception\ArgumentException when an invalid `clientId` or `audience` are provided
* @throws \Auth0\SDK\Exception\NetworkException when the API request fails due to a network error
Expand All @@ -28,6 +30,8 @@ public function create(
string $audience,
?array $scope = null,
?RequestOptions $options = null,
?string $organizationUsage = null,
?bool $allowAnyOrganization = null,
): ResponseInterface;

/**
Expand Down Expand Up @@ -101,13 +105,31 @@ public function getAllByClientId(
?RequestOptions $options = null,
): ResponseInterface;

/**
* Retrieve a client grant's organizations.
* Required scope: `read:organization_client_grants`.
*
* @param string $grantId Grant (by it's ID) to update
* @param null|int[]|null[]|string[] $parameters Optional. Additional query parameters to pass with the API request. See @see for supported options.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these. See @see for supported options.)
*
* @throws \Auth0\SDK\Exception\NetworkException when the API request fails due to a network error
*/
public function getOrganizations(
string $grantId,
?array $parameters = null,
?RequestOptions $options = null,
): ResponseInterface;

/**
* Update an existing Client Grant.
* Required scope: `update:client_grants`.
*
* @param string $grantId grant (by it's ID) to update
* @param null|array<string> $scope Optional. Array of scopes to update; will replace existing scopes, not merge.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these. See @see for supported options.)
* @param string $grantId Grant (by it's ID) to update
* @param null|array<string> $scope Optional. Array of scopes to update; will replace existing scopes, not merge.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these. See @see for supported options.)
* @param null|string $organizationUsage Optional. Defines whether organizations can be used with client credentials exchanges for this grant. Possible values are `deny`, `allow` or `require`.
* @param null|bool $allowAnyOrganization Optional. If enabled, any organization can be used with this grant. If disabled (default), the grant must be explicitly assigned to the desired organizations.
*
* @throws \Auth0\SDK\Exception\ArgumentException when an invalid `grantId` is provided
* @throws \Auth0\SDK\Exception\NetworkException when the API request fails due to a network error
Expand All @@ -118,5 +140,7 @@ public function update(
string $grantId,
?array $scope = null,
?RequestOptions $options = null,
?string $organizationUsage = null,
?bool $allowAnyOrganization = null,
): ResponseInterface;
}
50 changes: 50 additions & 0 deletions src/Contract/API/Management/OrganizationsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@

interface OrganizationsInterface
{
/**
* Associate a client grant to an organization.
* Required scope: `create:organization_client_grants`.
*
* @param string $id Organization (by ID) to associate the client grant with.
* @param string $grantId Client Grant (by ID) to associate with the organization.
* @param null|array<mixed> $parameters Optional. Additional body content to send with the API request.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these.
*
* @throws \Auth0\SDK\Exception\ArgumentException When an invalid `id` or `connectionId` are provided
* @throws \Auth0\SDK\Exception\NetworkException When the API request fails due to a network error
*/
public function addClientGrant(
string $id,
string $grantId,
?array $parameters = null,
?RequestOptions $options = null,
): ResponseInterface;

/**
* Add a connection to an organization.
* Required scope: `create:organization_connections`.
Expand Down Expand Up @@ -206,6 +225,21 @@ public function getByName(
?RequestOptions $options = null,
): ResponseInterface;

/**
* Get client grants associated to an organization.
* Required scope: `read:organization_client_grants`.
*
* @param string $id Organization (by ID) that the connection is associated with
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these.)
*
* @throws \Auth0\SDK\Exception\ArgumentException when an invalid `id` or `connectionId` are provided
* @throws \Auth0\SDK\Exception\NetworkException when the API request fails due to a network error
*/
public function getClientGrants(
string $id,
?RequestOptions $options = null,
): ResponseInterface;

/**
* Get a connection (by ID) associated with an organization.
* Required scope: `read:organization_connections`.
Expand Down Expand Up @@ -314,6 +348,22 @@ public function getMembers(
?RequestOptions $options = null,
): ResponseInterface;

/**
* Remove a client grant from an organization.
* Required scope: `delete:organization_client_grants`.
*
* @param string $id Organization (by ID) to remove client grant from.
* @param string $grantId Client Grant (by ID) to remove from the organization.
* @param null|array<mixed> $parameters Optional. Additional body content to send with the API request.
* @param null|RequestOptions $options Optional. Additional request options to use, such as a field filtering or pagination. (Not all endpoints support these.)
*/
public function removeClientGrant(
string $id,
string $grantId,
?array $parameters = null,
?RequestOptions $options = null,
): ResponseInterface;

/**
* Remove a connection from an organization.
* Required scope: `delete:organization_connections`.
Expand Down
26 changes: 26 additions & 0 deletions tests/Unit/API/AuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,32 @@
expect($requestHeaders['header_testing'][0])->toEqual(123);
});

test('clientCredentials() includes organization in request when configured', function(): void {
$clientSecret = uniqid();

$this->configuration->setClientSecret($clientSecret);
$authentication = $this->sdk->authentication();
$authentication->getHttpClient()->mockResponses([HttpResponseGenerator::create()]);
$authentication->clientCredentials(['organization' => 'org_xyz'], ['header_testing' => 123]);

$request = $authentication->getHttpClient()->getLastRequest()->getLastRequest();
$requestUri = $request->getUri();
$requestBody = explode('&', $request->getBody()->__toString());
$requestHeaders = $request->getHeaders();

expect($requestUri->getHost())->toEqual($this->configuration->getDomain());
expect($requestUri->getPath())->toEqual('/oauth/token');

expect($requestBody)->toContain('grant_type=client_credentials');
expect($requestBody)->toContain('client_id=__test_client_id__');
expect($requestBody)->toContain('client_secret=' . $clientSecret);
expect($requestBody)->toContain('audience=aud1');
expect($requestBody)->toContain('organization=org_xyz');

$this->assertArrayHasKey('header_testing', $requestHeaders);
expect($requestHeaders['header_testing'][0])->toEqual(123);
});

test('refreshToken() is properly formatted', function(): void {
$clientSecret = uniqid();
$refreshToken = uniqid();
Expand Down
Loading

0 comments on commit 9a01566

Please sign in to comment.