diff --git a/README.md b/README.md index 6ffc057..54d9b89 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,34 @@ $response = $descopeSDK->auth->sso->exchangeToken("code"); print_r($response); ``` +### OTP (One-Time Password) + +The delivery method is one of `"email"`, `"sms"`, `"whatsapp"` or `"voice"`. + +```php +// Send a code to a new or existing user +$descopeSDK->otp->signUp("email", "loginId", ["email" => "user@example.com"]); +$descopeSDK->otp->signIn("email", "loginId"); +$descopeSDK->otp->signUpOrIn("email", "loginId"); + +// Verify the received code and get a session +$response = $descopeSDK->otp->verifyCode("email", "loginId", "123456"); +print_r($response); +``` + +### Magic Link + +```php +// Send a magic link containing the given redirect URI +$descopeSDK->magicLink->signUp("email", "loginId", "https://example.com/verify", ["email" => "user@example.com"]); +$descopeSDK->magicLink->signIn("email", "loginId", "https://example.com/verify"); +$descopeSDK->magicLink->signUpOrIn("email", "loginId", "https://example.com/verify"); + +// Verify the token extracted from the magic link and get a session +$response = $descopeSDK->magicLink->verify("token"); +print_r($response); +``` + ### Session Management 1. `DescopeSDK->verify($sessionToken)` - will validate the session token and return either **TRUE** or **FALSE**, depending on if the JWT is valid and expired. @@ -254,6 +282,9 @@ print_r($response); 6. `DescopeSDK->getClaims($sessionToken)` - will validate the JWT signature and return all of the verified claims in an array format. 7. `DescopeSDK->getUserDetails($refreshToken)` - will return all of the user information (email, phone, verification status, etc.) using a provided refresh token. +8. `DescopeSDK->selectTenant($tenantId, $refreshToken)` - will return a new set of tokens scoped to the selected tenant. +9. `DescopeSDK->exchangeAccessKey($accessKey, $loginOptions)` - will exchange an access key for a session JWT. +10. `DescopeSDK->history($refreshToken)` - will return the current user's authentication history. ### User Management Functions @@ -461,6 +492,133 @@ $descopeSDK->management->user->setTemporaryPassword("testuser1", new UserPasswor $descopeSDK->management->user->setActivePassword("testuser1", new UserPassword(cleartext: "activePassword123")); ``` +### Tenant Management Functions + +Manage tenants for multi-tenant applications. + +```php +// Create a tenant (id is optional; one is generated if omitted) +$response = $descopeSDK->management->tenant->create("My Tenant", null, ["example.com"], ["plan" => "pro"]); +$tenantId = $response["id"]; + +// Update a tenant (overwrites all fields) +$descopeSDK->management->tenant->update($tenantId, "My Renamed Tenant", ["example.com"]); + +// Load a single tenant / all tenants +$tenant = $descopeSDK->management->tenant->load($tenantId); +$all = $descopeSDK->management->tenant->loadAll(); + +// Search tenants +$found = $descopeSDK->management->tenant->searchAll([], ["My Renamed Tenant"]); + +// Delete a tenant (cascade removes its users/keys) +$descopeSDK->management->tenant->delete($tenantId, false); +``` + +### Role Management Functions + +```php +$descopeSDK->management->role->create("My Role", "role description", ["Read", "Write"]); +$descopeSDK->management->role->update("My Role", "My Renamed Role", "updated", ["Read"]); +$roles = $descopeSDK->management->role->loadAll(); +$matches = $descopeSDK->management->role->search([], ["My Renamed Role"]); +$descopeSDK->management->role->delete("My Renamed Role"); +``` + +### Permission Management Functions + +```php +$descopeSDK->management->permission->create("Read", "can read"); +$descopeSDK->management->permission->update("Read", "ReadOnly", "can read only"); +$permissions = $descopeSDK->management->permission->loadAll(); +$descopeSDK->management->permission->delete("ReadOnly"); +``` + +### Access Key Management Functions + +```php +// Create an access key; the cleartext is only returned once, on creation +$response = $descopeSDK->management->accessKey->create("My Key", 0, ["My Role"]); +$cleartext = $response["cleartext"]; +$keyId = $response["key"]["id"]; + +$descopeSDK->management->accessKey->load($keyId); +$descopeSDK->management->accessKey->searchAll(); +$descopeSDK->management->accessKey->update($keyId, "My Renamed Key"); +$descopeSDK->management->accessKey->deactivate($keyId); +$descopeSDK->management->accessKey->activate($keyId); +$descopeSDK->management->accessKey->delete($keyId); +``` + +### SSO Application Management Functions + +Manage SSO (IdP) applications your project exposes to relying parties. + +```php +// OIDC application +$response = $descopeSDK->management->ssoApplication->createOidcApplication("My OIDC App", "https://login.example.com"); +$appId = $response["id"]; +$descopeSDK->management->ssoApplication->updateOidcApplication($appId, "My OIDC App", "https://login.example.com"); + +// SAML application +$descopeSDK->management->ssoApplication->createSamlApplication("My SAML App", "https://login.example.com"); + +$descopeSDK->management->ssoApplication->load($appId); +$descopeSDK->management->ssoApplication->loadAll(); +$descopeSDK->management->ssoApplication->delete($appId); +``` + +### SSO Settings (Tenant SSO Configuration) + +Configure the SSO provider used by a tenant. `management->sso` is the tenant SSO configuration component (distinct from the auth-flow `$descopeSDK->sso`). + +```php +$settings = $descopeSDK->management->sso->loadSettings("tenantId1"); + +// Configure OIDC for a tenant +$descopeSDK->management->sso->configureOIDCSettings("tenantId1", [ + "name" => "MyOIDC", + "clientId" => "clientId", + "clientSecret" => "clientSecret", + "redirectUrl" => "https://example.com/callback", + "authUrl" => "https://idp.example.com/authorize", + "tokenUrl" => "https://idp.example.com/token", + "userDataUrl" => "https://idp.example.com/userinfo", + "scope" => ["openid", "email"], +], ["example.com"]); + +// Configure SAML for a tenant +$descopeSDK->management->sso->configureSAMLSettings("tenantId1", [ + "idpUrl" => "https://idp.example.com/sso", + "entityId" => "entityId", + "idpCert" => "-----BEGIN CERTIFICATE----- ...", +], "https://example.com/callback", ["example.com"]); + +$descopeSDK->management->sso->deleteSettings("tenantId1"); +``` + +### JWT Management Functions + +```php +// Update a JWT with custom claims +$newJwt = $descopeSDK->management->jwt->updateJWT($jwt, ["myClaim" => "value"]); + +// Impersonate a user (requires the impersonator to have permission) +$impersonatedJwt = $descopeSDK->management->jwt->impersonate("impersonatorId", "targetLoginId", true); +``` + +### Flow & Theme Management Functions + +```php +$flows = $descopeSDK->management->flow->listFlows(); +$exported = $descopeSDK->management->flow->exportFlow("sign-up-or-in"); +$descopeSDK->management->flow->importFlow("sign-up-or-in", $exported["flow"], $exported["screens"] ?? []); +$descopeSDK->management->flow->delete(["old-flow-id"]); + +$theme = $descopeSDK->management->flow->exportTheme(); +$descopeSDK->management->flow->importTheme($theme["theme"]); +``` + ## Password Management The SDK provides several classes for handling different types of passwords and password hashes. Here's how to use them: diff --git a/phpunit.xml b/phpunit.xml index 0e890f1..ec98877 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,6 +9,8 @@ src/tests/APIRetryTest.php src/tests/StaticStateIsolationTest.php src/tests/EndpointsTest.php + src/tests/Management/ManagementParityTest.php + src/tests/Auth/AuthParityTest.php \ No newline at end of file diff --git a/src/SDK/Auth/MagicLink.php b/src/SDK/Auth/MagicLink.php new file mode 100644 index 0000000..348768d --- /dev/null +++ b/src/SDK/Auth/MagicLink.php @@ -0,0 +1,166 @@ +api = $api; + } + + /** + * Sign up a new user and send them a magic link. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user being signed up. + * @param string $uri The redirect URI embedded in the magic link. + * @param array|null $user Optional user details (e.g. email, phone, name). + * @param array|null $signUpOptions Optional sign-up options (customClaims, templateOptions). + * @return string The masked address the magic link was sent to. + * @throws AuthException + */ + public function signUp(string $deliveryMethod, string $loginId, string $uri, ?array $user = null, ?array $signUpOptions = null): string + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + $body = ['loginId' => $loginId, 'uri' => $uri]; + if ($user !== null) { + $body['user'] = $user; + } + if ($signUpOptions !== null) { + $body['loginOptions'] = $signUpOptions; + } + + $endpoint = EndpointsV1::$SIGN_UP_AUTH_MAGICLINK_PATH . '/' . $method; + $response = $this->api->doPost($endpoint, $body, false); + + return $response[$method] ?? ''; + } + + /** + * Sign in an existing user and send them a magic link. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user signing in. + * @param string $uri The redirect URI embedded in the magic link. + * @param array|null $loginOptions Optional login options. + * @param string|null $refreshToken Optional refresh token for step-up/MFA. + * @return string The masked address the magic link was sent to. + * @throws AuthException + */ + public function signIn(string $deliveryMethod, string $loginId, string $uri, ?array $loginOptions = null, ?string $refreshToken = null): string + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + $body = ['loginId' => $loginId, 'uri' => $uri]; + if ($loginOptions !== null) { + $body['loginOptions'] = $loginOptions; + } + + $endpoint = EndpointsV1::$SIGN_IN_AUTH_MAGICLINK_PATH . '/' . $method; + $response = $this->api->doPost($endpoint, $body, false, $refreshToken); + + return $response[$method] ?? ''; + } + + /** + * Sign up or sign in a user (whichever applies) and send them a magic link. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user. + * @param string $uri The redirect URI embedded in the magic link. + * @param array|null $loginOptions Optional login options. + * @return string The masked address the magic link was sent to. + * @throws AuthException + */ + public function signUpOrIn(string $deliveryMethod, string $loginId, string $uri, ?array $loginOptions = null): string + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + $body = ['loginId' => $loginId, 'uri' => $uri]; + if ($loginOptions !== null) { + $body['loginOptions'] = $loginOptions; + } + + $endpoint = EndpointsV1::$SIGN_UP_OR_IN_AUTH_MAGICLINK_PATH . '/' . $method; + $response = $this->api->doPost($endpoint, $body, false); + + return $response[$method] ?? ''; + } + + /** + * Verify a magic-link token and complete the authentication. + * + * @param string $token The token extracted from the magic link. + * @return array JWT response array. + * @throws AuthException + */ + public function verify(string $token): array + { + if (empty($token)) { + throw new AuthException(400, 'invalid argument', 'token cannot be empty'); + } + + $response = $this->api->doPost(EndpointsV1::$VERIFY_MAGICLINK_AUTH_PATH, ['token' => $token], false); + + return $this->api->generateJwtResponse($response, $response['refreshJwt'] ?? null, null); + } + + /** + * Validates the delivery method and returns its normalized string form. + * + * @param string $deliveryMethod The requested delivery method. + * @return string The normalized (lowercase) delivery method. + * @throws AuthException + */ + private function validateMethod(string $deliveryMethod): string + { + $method = strtolower($deliveryMethod); + if (!in_array($method, self::METHODS, true)) { + throw new AuthException(400, 'invalid argument', "Unknown delivery method: $deliveryMethod"); + } + return $method; + } + + /** + * Validates the login ID. + * + * @param string $loginId The login ID. + * @throws AuthException + */ + private function validateLoginId(string $loginId): void + { + if (empty($loginId)) { + throw new AuthException(400, 'invalid argument', 'login_id cannot be empty'); + } + } +} diff --git a/src/SDK/Auth/OTP.php b/src/SDK/Auth/OTP.php new file mode 100644 index 0000000..8db3a22 --- /dev/null +++ b/src/SDK/Auth/OTP.php @@ -0,0 +1,168 @@ +api = $api; + } + + /** + * Sign up a new user and send them an OTP code. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user being signed up. + * @param array|null $user Optional user details (e.g. email, phone, name). + * @param array|null $signUpOptions Optional sign-up options (customClaims, templateOptions). + * @return string The masked address the OTP was sent to. + * @throws AuthException + */ + public function signUp(string $deliveryMethod, string $loginId, ?array $user = null, ?array $signUpOptions = null): string + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + $body = ['loginId' => $loginId]; + if ($user !== null) { + $body['user'] = $user; + } + if ($signUpOptions !== null) { + $body['loginOptions'] = $signUpOptions; + } + + $uri = EndpointsV1::$SIGN_UP_AUTH_OTP_PATH . '/' . $method; + $response = $this->api->doPost($uri, $body, false); + + return $response[$method] ?? ''; + } + + /** + * Sign in an existing user and send them an OTP code. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user signing in. + * @param array|null $loginOptions Optional login options. + * @param string|null $refreshToken Optional refresh token for step-up/MFA. + * @return string The masked address the OTP was sent to. + * @throws AuthException + */ + public function signIn(string $deliveryMethod, string $loginId, ?array $loginOptions = null, ?string $refreshToken = null): string + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + $body = ['loginId' => $loginId]; + if ($loginOptions !== null) { + $body['loginOptions'] = $loginOptions; + } + + $uri = EndpointsV1::$SIGN_IN_AUTH_OTP_PATH . '/' . $method; + $response = $this->api->doPost($uri, $body, false, $refreshToken); + + return $response[$method] ?? ''; + } + + /** + * Sign up or sign in a user (whichever applies) and send them an OTP code. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user. + * @param array|null $loginOptions Optional login options. + * @return string The masked address the OTP was sent to. + * @throws AuthException + */ + public function signUpOrIn(string $deliveryMethod, string $loginId, ?array $loginOptions = null): string + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + $body = ['loginId' => $loginId]; + if ($loginOptions !== null) { + $body['loginOptions'] = $loginOptions; + } + + $uri = EndpointsV1::$SIGN_UP_OR_IN_AUTH_OTP_PATH . '/' . $method; + $response = $this->api->doPost($uri, $body, false); + + return $response[$method] ?? ''; + } + + /** + * Verify an OTP code and complete the authentication. + * + * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice". + * @param string $loginId The login ID of the user. + * @param string $code The OTP code the user received. + * @return array JWT response array. + * @throws AuthException + */ + public function verifyCode(string $deliveryMethod, string $loginId, string $code): array + { + $method = $this->validateMethod($deliveryMethod); + $this->validateLoginId($loginId); + + if (empty($code)) { + throw new AuthException(400, 'invalid argument', 'code cannot be empty'); + } + + $uri = EndpointsV1::$VERIFY_CODE_AUTH_PATH . '/' . $method; + $response = $this->api->doPost($uri, ['loginId' => $loginId, 'code' => $code], false); + + return $this->api->generateJwtResponse($response, $response['refreshJwt'] ?? null, null); + } + + /** + * Validates the delivery method and returns its normalized string form. + * + * @param string $deliveryMethod The requested delivery method. + * @return string The normalized (lowercase) delivery method. + * @throws AuthException + */ + private function validateMethod(string $deliveryMethod): string + { + $method = strtolower($deliveryMethod); + if (!in_array($method, self::METHODS, true)) { + throw new AuthException(400, 'invalid argument', "Unknown delivery method: $deliveryMethod"); + } + return $method; + } + + /** + * Validates the login ID. + * + * @param string $loginId The login ID. + * @throws AuthException + */ + private function validateLoginId(string $loginId): void + { + if (empty($loginId)) { + throw new AuthException(400, 'invalid argument', 'login_id cannot be empty'); + } + } +} diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 85a7836..63c9ff5 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -9,6 +9,8 @@ use Descope\SDK\Auth\Password; use Descope\SDK\Auth\SSO; use Descope\SDK\Auth\OAuth; +use Descope\SDK\Auth\OTP; +use Descope\SDK\Auth\MagicLink; use Descope\SDK\Management\Management; use Descope\SDK\Auth\Management\User; use Descope\SDK\Auth\Management\Audit; @@ -26,6 +28,8 @@ class DescopeSDK public Password $password; public SSO $sso; public OAuth $oauth; + public OTP $otp; + public MagicLink $magicLink; public Management $management; public API $api; private Verifier $verifier; @@ -70,6 +74,8 @@ public function __construct(array $config) $this->password = new Password($this->api); $this->sso = new SSO($this->api); $this->oauth = new OAuth($this->api); + $this->otp = new OTP($this->api); + $this->magicLink = new MagicLink($this->api); } /** @@ -226,6 +232,86 @@ public function logoutAll(?string $refreshToken = null): void ); } + /** + * Select a tenant for the current session, returning a new set of tokens + * scoped to that tenant. + * + * @param string $tenantId The tenant to select. + * @param string|null $refreshToken The refresh token of the current session. + * @return array The new session information. + * @throws AuthException|RateLimitException + */ + public function selectTenant(string $tenantId, ?string $refreshToken = null): array + { + if (empty($tenantId)) { + throw new ValidationException('Tenant ID cannot be null or empty.'); + } + + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + + if (empty($refreshToken)) { + throw ValidationException::forMissingRefreshToken(); + } + + $response = $this->api->doPost( + EndpointsV1::$SELECT_TENANT_PATH, + ['tenant' => $tenantId], + false, + $refreshToken + ); + + return $this->api->generateJwtResponse($response, $response['refreshJwt'] ?? null, null); + } + + /** + * Exchange an access key for a session JWT. + * + * @param string $accessKey The access key to exchange. + * @param array|null $loginOptions Optional login options (e.g. customClaims). + * @return array The API response containing the session JWT. + * @throws AuthException|RateLimitException + */ + public function exchangeAccessKey(string $accessKey, ?array $loginOptions = null): array + { + if (empty($accessKey)) { + throw new ValidationException('Access key cannot be null or empty.'); + } + + $body = []; + if ($loginOptions !== null) { + $body['loginOptions'] = $loginOptions; + } + + return $this->api->doPost( + EndpointsV1::$EXCHANGE_AUTH_ACCESS_KEY_PATH, + $body, + false, + $accessKey + ); + } + + /** + * Retrieve the current user's authentication history. + * + * @param string|null $refreshToken The refresh token of the user. + * @return array The list of authentication history entries. + * @throws AuthException|RateLimitException + */ + public function history(?string $refreshToken = null): array + { + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + + if (!$refreshToken) { + throw ValidationException::forMissingRefreshToken(); + } + + return $this->api->doGet( + EndpointsV1::$HISTORY_PATH, + false, + $refreshToken + ); + } + /** * Get the Password component. * @@ -256,6 +342,26 @@ public function oauth(): OAuth return $this->oauth; } + /** + * Get the OTP component. + * + * @return OTP The OTP instance. + */ + public function otp(): OTP + { + return $this->otp; + } + + /** + * Get the MagicLink component. + * + * @return MagicLink The MagicLink instance. + */ + public function magicLink(): MagicLink + { + return $this->magicLink; + } + /** * Get the Management component. * diff --git a/src/SDK/Management/AccessKey.php b/src/SDK/Management/AccessKey.php new file mode 100644 index 0000000..bf60233 --- /dev/null +++ b/src/SDK/Management/AccessKey.php @@ -0,0 +1,198 @@ +api = $api; + } + + /** + * Create a new access key. + * + * @param string $name The name of the access key. + * @param int $expireTime Access key expiration time (in seconds since epoch). 0 means never expires. + * @param array $roleNames An optional list of the access key's roles without tenant association. These roles are supported for keys of a project-level access key. + * @param array $keyTenants An optional list of tenant associations, each an associative array like ['tenantId' => 't1', 'roleNames' => ['r1']]. + * @param string|null $userId An optional user ID to associate the access key with. The key will inherit the user's permissions and attributes. + * @param array $customClaims An optional dictionary of custom claims to be added to the access key's JWT. + * @param string|null $description An optional description for the access key. + * @param array $permittedIps An optional list of IP addresses or CIDR ranges that are allowed to use this access key. + * @return array The created access key response, containing 'key' and 'cleartext'. + * @throws AuthException If the request fails. + */ + public function create( + string $name, + int $expireTime = 0, + array $roleNames = [], + array $keyTenants = [], + ?string $userId = null, + array $customClaims = [], + ?string $description = null, + array $permittedIps = [] + ): array { + $body = [ + 'name' => $name, + 'expireTime' => $expireTime, + 'roleNames' => $roleNames, + 'keyTenants' => $keyTenants, + 'customClaims' => $customClaims, + 'permittedIps' => $permittedIps, + ]; + + if ($userId !== null) { + $body['userId'] = $userId; + } + if ($description !== null) { + $body['description'] = $description; + } + + return $this->api->doPost(MgmtV1::$ACCESS_KEY_CREATE_PATH, $body, true); + } + + /** + * Load an existing access key by its ID. + * + * @param string $id The ID of the access key to load. + * @return array The access key response, containing 'key'. + * @throws AuthException If the request fails. + */ + public function load(string $id): array + { + return $this->api->doGet( + MgmtV1::$ACCESS_KEY_LOAD_PATH . '?' . http_build_query(['id' => $id]), + true + ); + } + + /** + * Search all access keys, optionally filtering by tenant IDs. + * + * @param array $tenantIds An optional list of tenant IDs to filter the access keys by. + * @return array The search response, containing 'keys'. + * @throws AuthException If the request fails. + */ + public function searchAll(array $tenantIds = []): array + { + $body = [ + 'tenantIds' => $tenantIds, + ]; + + return $this->api->doPost(MgmtV1::$ACCESS_KEYS_SEARCH_PATH, $body, true); + } + + /** + * Update an existing access key. + * + * @param string $id The ID of the access key to update. + * @param string $name The updated name of the access key. + * @param array|null $customClaims An optional dictionary of custom claims to update on the access key. + * @param string|null $description An optional updated description for the access key. + * @param array|null $roleNames An optional updated list of the access key's roles without tenant association. + * @param array|null $keyTenants An optional updated list of tenant associations, each an associative array like ['tenantId' => 't1', 'roleNames' => ['r1']]. + * @return array The update response. + * @throws AuthException If the request fails. + */ + public function update( + string $id, + string $name, + ?array $customClaims = null, + ?string $description = null, + ?array $roleNames = null, + ?array $keyTenants = null + ): array { + $body = [ + 'id' => $id, + 'name' => $name, + ]; + + if ($customClaims !== null) { + $body['customClaims'] = $customClaims; + } + if ($description !== null) { + $body['description'] = $description; + } + if ($roleNames !== null) { + $body['roleNames'] = $roleNames; + } + if ($keyTenants !== null) { + $body['keyTenants'] = $keyTenants; + } + + return $this->api->doPost(MgmtV1::$ACCESS_KEY_UPDATE_PATH, $body, true); + } + + /** + * Deactivate an access key. + * + * Deactivated access keys cannot be used to authenticate but can be + * reactivated later using the activate method. + * + * @param string $id The ID of the access key to deactivate. + * @return void + * @throws AuthException If the request fails. + */ + public function deactivate(string $id): void + { + $body = [ + 'id' => $id, + ]; + + $this->api->doPost(MgmtV1::$ACCESS_KEY_DEACTIVATE_PATH, $body, true); + } + + /** + * Activate an access key. + * + * @param string $id The ID of the access key to activate. + * @return void + * @throws AuthException If the request fails. + */ + public function activate(string $id): void + { + $body = [ + 'id' => $id, + ]; + + $this->api->doPost(MgmtV1::$ACCESS_KEY_ACTIVATE_PATH, $body, true); + } + + /** + * Delete an access key. + * + * IMPORTANT: This action is irreversible. Once an access key is deleted + * it cannot be recovered. + * + * @param string $id The ID of the access key to delete. + * @return void + * @throws AuthException If the request fails. + */ + public function delete(string $id): void + { + $body = [ + 'id' => $id, + ]; + + $this->api->doPost(MgmtV1::$ACCESS_KEY_DELETE_PATH, $body, true); + } +} diff --git a/src/SDK/Management/Flow.php b/src/SDK/Management/Flow.php new file mode 100644 index 0000000..e855c10 --- /dev/null +++ b/src/SDK/Management/Flow.php @@ -0,0 +1,172 @@ +api = $api; + } + + /** + * List all project flows. + * + * This method retrieves the metadata for every flow defined in the project. + * + * @return array The response containing a 'flows' entry with the list of flows. + * + * @throws AuthException If the list operation fails. + */ + public function listFlows(): array + { + $body = []; + + return $this->api->doPost( + MgmtV1::$FLOW_LIST_PATH, + $body, + true + ); + } + + /** + * Delete flows by their IDs. + * + * This method removes all flows identified by the provided list of flow IDs. + * + * @param array $flowIds The list of flow IDs to delete. + * + * @return void + * + * @throws AuthException If the delete operation fails. + */ + public function delete(array $flowIds): void + { + $body = [ + 'ids' => $flowIds, + ]; + + $this->api->doPost( + MgmtV1::$FLOW_DELETE_PATH, + $body, + true + ); + } + + /** + * Export a single flow by its ID. + * + * This method exports the full definition of a flow, including its screens, + * so it can be backed up or imported into another project. + * + * @param string $flowId The ID of the flow to export. + * + * @return array The response containing the exported flow definition. + * + * @throws AuthException If the export operation fails. + */ + public function exportFlow(string $flowId): array + { + $body = [ + 'flowId' => $flowId, + ]; + + return $this->api->doPost( + MgmtV1::$FLOW_EXPORT_PATH, + $body, + true + ); + } + + /** + * Import a flow into the project. + * + * This method imports a flow definition, optionally including its associated + * screens, under the specified flow ID. + * + * @param string $flowId The ID under which the flow will be imported. + * @param array $flow The flow definition to import. + * @param array $screens Optional list of screens associated with the flow. + * + * @return array The response containing the imported flow definition. + * + * @throws AuthException If the import operation fails. + */ + public function importFlow(string $flowId, array $flow, array $screens = []): array + { + $body = [ + 'flowId' => $flowId, + 'flow' => $flow, + 'screens' => $screens, + ]; + + return $this->api->doPost( + MgmtV1::$FLOW_IMPORT_PATH, + $body, + true + ); + } + + /** + * Export the project theme. + * + * This method exports the current project theme so it can be backed up or + * imported into another project. + * + * @return array The response containing the exported theme definition. + * + * @throws AuthException If the export operation fails. + */ + public function exportTheme(): array + { + $body = []; + + return $this->api->doPost( + MgmtV1::$THEME_EXPORT_PATH, + $body, + true + ); + } + + /** + * Import a theme into the project. + * + * This method imports a theme definition, replacing the current project theme. + * + * @param array $theme The theme definition to import. + * + * @return array The response containing the imported theme definition. + * + * @throws AuthException If the import operation fails. + */ + public function importTheme(array $theme): array + { + $body = [ + 'theme' => $theme, + ]; + + return $this->api->doPost( + MgmtV1::$THEME_IMPORT_PATH, + $body, + true + ); + } +} diff --git a/src/SDK/Management/JWT.php b/src/SDK/Management/JWT.php new file mode 100644 index 0000000..08ea696 --- /dev/null +++ b/src/SDK/Management/JWT.php @@ -0,0 +1,101 @@ +api = $api; + } + + /** + * Update a valid JWT with the custom claims provided. + * + * The new JWT will be signed with the latest signing key. It is up to the + * caller to make sure the provided JWT is valid, as its claims will be used + * as the basis for the newly generated token. + * + * @param string $jwt The existing valid JWT to update. + * @param array $customClaims A map of custom claims to add to the JWT. + * @param int|null $refreshDuration Optional duration, in seconds, for the + * refreshed token. When null, it is omitted. + * @return string The updated, signed JWT. + * @throws AuthException If the update request fails. + */ + public function updateJWT(string $jwt, array $customClaims = [], ?int $refreshDuration = null): string + { + $body = [ + 'jwt' => $jwt, + 'customClaims' => $customClaims, + ]; + + if ($refreshDuration !== null) { + $body['refreshDuration'] = $refreshDuration; + } + + $response = $this->api->doPost(MgmtV1::$UPDATE_JWT_PATH, $body, true); + + return $response['jwt'] ?? ''; + } + + /** + * Generate a JWT for a given user, on behalf of an impersonator. + * + * The impersonator must have the required permissions in order to + * impersonate the target user. + * + * @param string $impersonatorId The ID of the user performing the impersonation. + * @param string $loginId The login ID of the user being impersonated. + * @param bool $validateConsent Whether to validate that consent to impersonate has been given. + * @param array $customClaims A map of custom claims to add to the JWT. + * @param string|null $selectedTenant Optional tenant to select for the impersonated session. When null, it is omitted. + * @param int|null $refreshDuration Optional duration, in seconds, for the refreshed token. When null, it is omitted. + * @return string The impersonated, signed JWT. + * @throws AuthException If the impersonation request fails. + */ + public function impersonate( + string $impersonatorId, + string $loginId, + bool $validateConsent = true, + array $customClaims = [], + ?string $selectedTenant = null, + ?int $refreshDuration = null + ): string { + $body = [ + 'impersonatorId' => $impersonatorId, + 'loginId' => $loginId, + 'validateConsent' => $validateConsent, + 'customClaims' => $customClaims, + ]; + + if ($selectedTenant !== null) { + $body['selectedTenant'] = $selectedTenant; + } + + if ($refreshDuration !== null) { + $body['refreshDuration'] = $refreshDuration; + } + + $response = $this->api->doPost(MgmtV1::$IMPERSONATE_PATH, $body, true); + + return $response['jwt'] ?? ''; + } +} diff --git a/src/SDK/Management/Management.php b/src/SDK/Management/Management.php index f9efc36..b46c958 100644 --- a/src/SDK/Management/Management.php +++ b/src/SDK/Management/Management.php @@ -8,16 +8,22 @@ * Class Management * * Represents the management functionality for Descope, providing access to - * user management capabilities. + * the full set of management components (users, tenants, roles, permissions, + * access keys, SSO, JWT, flows, audit and outbound apps). */ class Management { - /** - * @var User The User management component. - */ public User $user; public Audit $audit; public OutboundApps $outboundApps; + public Tenant $tenant; + public Role $role; + public Permission $permission; + public AccessKey $accessKey; + public SSOApplication $ssoApplication; + public SSOSettings $sso; + public JWT $jwt; + public Flow $flow; /** * Constructor for Management class. @@ -29,6 +35,14 @@ public function __construct(API $auth) $this->user = new User($auth); $this->audit = new Audit($auth); $this->outboundApps = new OutboundApps($auth); + $this->tenant = new Tenant($auth); + $this->role = new Role($auth); + $this->permission = new Permission($auth); + $this->accessKey = new AccessKey($auth); + $this->ssoApplication = new SSOApplication($auth); + $this->sso = new SSOSettings($auth); + $this->jwt = new JWT($auth); + $this->flow = new Flow($auth); } /** @@ -60,4 +74,84 @@ public function outboundApps(): OutboundApps { return $this->outboundApps; } + + /** + * Get the Tenant Management component. + * + * @return Tenant The Tenant management instance. + */ + public function tenant(): Tenant + { + return $this->tenant; + } + + /** + * Get the Role Management component. + * + * @return Role The Role management instance. + */ + public function role(): Role + { + return $this->role; + } + + /** + * Get the Permission Management component. + * + * @return Permission The Permission management instance. + */ + public function permission(): Permission + { + return $this->permission; + } + + /** + * Get the Access Key Management component. + * + * @return AccessKey The Access Key management instance. + */ + public function accessKey(): AccessKey + { + return $this->accessKey; + } + + /** + * Get the SSO Application Management component. + * + * @return SSOApplication The SSO Application management instance. + */ + public function ssoApplication(): SSOApplication + { + return $this->ssoApplication; + } + + /** + * Get the SSO Settings Management component. + * + * @return SSOSettings The SSO Settings management instance. + */ + public function sso(): SSOSettings + { + return $this->sso; + } + + /** + * Get the JWT Management component. + * + * @return JWT The JWT management instance. + */ + public function jwt(): JWT + { + return $this->jwt; + } + + /** + * Get the Flow Management component. + * + * @return Flow The Flow management instance. + */ + public function flow(): Flow + { + return $this->flow; + } } diff --git a/src/SDK/Management/MgmtV1.php b/src/SDK/Management/MgmtV1.php index 1811dda..11cd228 100644 --- a/src/SDK/Management/MgmtV1.php +++ b/src/SDK/Management/MgmtV1.php @@ -20,6 +20,9 @@ class MgmtV1 // Paths for various management operations public static string $TEMPLATE_EXPORT_PATH; public static string $TEMPLATE_IMPORT_PATH; + public static string $THEME_IMPORT_PATH; + public static string $THEME_EXPORT_PATH; + public static string $FLOW_IMPORT_PATH; public static string $FLOW_EXPORT_PATH; public static string $FLOW_DELETE_PATH; public static string $FLOW_LIST_PATH; @@ -235,6 +238,9 @@ private static function updatePaths(): void self::$FLOW_LIST_PATH = self::$baseUrl . "/v1/mgmt/flow/list"; self::$FLOW_DELETE_PATH = self::$baseUrl . "/v1/mgmt/flow/delete"; self::$FLOW_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/flow/export"; + self::$FLOW_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/flow/import"; + self::$THEME_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/theme/export"; + self::$THEME_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/theme/import"; self::$TEMPLATE_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/template/import"; self::$TEMPLATE_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/template/export"; diff --git a/src/SDK/Management/Permission.php b/src/SDK/Management/Permission.php new file mode 100644 index 0000000..ff25b52 --- /dev/null +++ b/src/SDK/Management/Permission.php @@ -0,0 +1,96 @@ +api = $api; + } + + /** + * Create a new permission. + * + * @param string $name The name of the permission to create. + * @param string|null $description Optional description to explain the purpose of the permission. + * @return void + * @throws AuthException If the creation request fails. + */ + public function create(string $name, ?string $description = null): void + { + $body = [ + 'name' => $name, + ]; + + if ($description !== null) { + $body['description'] = $description; + } + + $this->api->doPost(MgmtV1::$PERMISSION_CREATE_PATH, $body, true); + } + + /** + * Update an existing permission. + * + * @param string $name The name of the permission to update. + * @param string $newName The updated name of the permission. + * @param string|null $description Optional description to explain the purpose of the permission. + * @return void + * @throws AuthException If the update request fails. + */ + public function update(string $name, string $newName, ?string $description = null): void + { + $body = [ + 'name' => $name, + 'newName' => $newName, + ]; + + if ($description !== null) { + $body['description'] = $description; + } + + $this->api->doPost(MgmtV1::$PERMISSION_UPDATE_PATH, $body, true); + } + + /** + * Delete an existing permission. + * + * @param string $name The name of the permission to delete. + * @return void + * @throws AuthException If the deletion request fails. + */ + public function delete(string $name): void + { + $body = [ + 'name' => $name, + ]; + + $this->api->doPost(MgmtV1::$PERMISSION_DELETE_PATH, $body, true); + } + + /** + * Load all permissions. + * + * @return array The response containing the list of permissions under the 'permissions' key. + * @throws AuthException If the load request fails. + */ + public function loadAll(): array + { + return $this->api->doGet(MgmtV1::$PERMISSION_LOAD_ALL_PATH, true); + } +} diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php index 5e822be..e0227ac 100644 --- a/src/SDK/Management/Role.php +++ b/src/SDK/Management/Role.php @@ -20,6 +20,133 @@ public function __construct(API $api) $this->api = $api; } + /** + * Creates a new role. + * + * @param string $name The role name. + * @param string|null $description Optional role description. + * @param array $permissionNames Permission names to grant to the role. + * @param string|null $tenantId Optional tenant ID for a tenant-scoped role. + * @param bool $defaultRole Whether this role is a default role for new users. + * @return void + * @throws AuthException If the request fails. + */ + public function create( + string $name, + ?string $description = null, + array $permissionNames = [], + ?string $tenantId = null, + bool $defaultRole = false + ): void { + $body = [ + 'name' => $name, + 'permissionNames' => $permissionNames, + 'defaultRole' => $defaultRole, + ]; + if ($description !== null) { + $body['description'] = $description; + } + if ($tenantId !== null) { + $body['tenantId'] = $tenantId; + } + + $this->api->doPost(MgmtV1::$ROLE_CREATE_PATH, $body, true); + } + + /** + * Updates an existing role. All parameters are required except where noted; + * omitted optional values will be cleared on the role. + * + * @param string $name The current role name. + * @param string $newName The new role name. + * @param string|null $description Optional role description. + * @param array $permissionNames Permission names to grant to the role. + * @param string|null $tenantId Optional tenant ID for a tenant-scoped role. + * @param bool $defaultRole Whether this role is a default role for new users. + * @return void + * @throws AuthException If the request fails. + */ + public function update( + string $name, + string $newName, + ?string $description = null, + array $permissionNames = [], + ?string $tenantId = null, + bool $defaultRole = false + ): void { + $body = [ + 'name' => $name, + 'newName' => $newName, + 'permissionNames' => $permissionNames, + 'defaultRole' => $defaultRole, + ]; + if ($description !== null) { + $body['description'] = $description; + } + if ($tenantId !== null) { + $body['tenantId'] = $tenantId; + } + + $this->api->doPost(MgmtV1::$ROLE_UPDATE_PATH, $body, true); + } + + /** + * Deletes a role. + * + * @param string $name The role name. + * @param string|null $tenantId Optional tenant ID for a tenant-scoped role. + * @return void + * @throws AuthException If the request fails. + */ + public function delete(string $name, ?string $tenantId = null): void + { + $body = ['name' => $name]; + if ($tenantId !== null) { + $body['tenantId'] = $tenantId; + } + + $this->api->doPost(MgmtV1::$ROLE_DELETE_PATH, $body, true); + } + + /** + * Loads all roles in the project. + * + * @return array The response containing the list of roles. + * @throws AuthException If the request fails. + */ + public function loadAll(): array + { + return $this->api->doGet(MgmtV1::$ROLE_LOAD_ALL_PATH, true); + } + + /** + * Searches roles matching the given filters. + * + * @param array $tenantIds Filter by tenant IDs. + * @param array $roleNames Filter by role names. + * @param string|null $roleNameLike Filter by a case-insensitive partial role name. + * @param array $permissionNames Filter by permission names. + * @return array The response containing the matching roles. + * @throws AuthException If the request fails. + */ + public function search( + array $tenantIds = [], + array $roleNames = [], + ?string $roleNameLike = null, + array $permissionNames = [] + ): array { + $body = [ + 'tenantIds' => $tenantIds, + 'roleNames' => $roleNames, + 'permissionNames' => $permissionNames, + ]; + if ($roleNameLike !== null) { + $body['roleNameLike'] = $roleNameLike; + } + + return $this->api->doPost(MgmtV1::$ROLE_SEARCH_PATH, $body, true); + } + /** * Validates tenant permissions for a JWT response. * diff --git a/src/SDK/Management/SSOApplication.php b/src/SDK/Management/SSOApplication.php new file mode 100644 index 0000000..f069c5a --- /dev/null +++ b/src/SDK/Management/SSOApplication.php @@ -0,0 +1,384 @@ +api = $api; + } + + /** + * Create a new OIDC SSO application. + * + * @param string $name The name of the SSO application. + * @param string $loginPageUrl The URL of the login page for this application. + * @param string|null $id Optional custom ID for the SSO application. + * @param bool $enabled Whether the application is enabled. Defaults to true. + * @param string|null $description Optional description of the application. + * @param string|null $logo Optional logo (base64 encoded image) for the application. + * + * @return array The response containing the created application's 'id'. + * + * @throws AuthException If the create operation fails. + */ + public function createOidcApplication( + string $name, + string $loginPageUrl, + ?string $id = null, + bool $enabled = true, + ?string $description = null, + ?string $logo = null + ): array { + $body = [ + 'name' => $name, + 'loginPageUrl' => $loginPageUrl, + 'enabled' => $enabled, + ]; + + if ($id !== null) { + $body['id'] = $id; + } + + if ($description !== null) { + $body['description'] = $description; + } + + if ($logo !== null) { + $body['logo'] = $logo; + } + + return $this->api->doPost( + MgmtV1::$SSO_APPLICATION_OIDC_CREATE_PATH, + $body, + true + ); + } + + /** + * Create a new SAML SSO application. + * + * @param string $name The name of the SSO application. + * @param string $loginPageUrl The URL of the login page for this application. + * @param string|null $id Optional custom ID for the SSO application. + * @param bool $enabled Whether the application is enabled. Defaults to true. + * @param string|null $description Optional description of the application. + * @param string|null $logo Optional logo (base64 encoded image) for the application. + * @param bool $useMetadataInfo Whether to use metadata URL for configuration. Defaults to false. + * @param string|null $metadataUrl Optional SAML metadata URL (used when $useMetadataInfo is true). + * @param string|null $entityId Optional SAML entity ID (used when $useMetadataInfo is false). + * @param string|null $acsUrl Optional ACS URL (used when $useMetadataInfo is false). + * @param string|null $certificate Optional certificate (used when $useMetadataInfo is false). + * @param array $attributeMapping Attribute mapping between Descope and the application. + * @param array $groupsMapping Groups mapping between Descope and the application. + * @param array $acsAllowedCallbacks List of allowed ACS callback URLs. + * @param string|null $subjectNameIdType Optional subject name ID type. + * @param string|null $subjectNameIdFormat Optional subject name ID format. + * @param bool|null $defaultRelayState Optional default relay state. + * @param bool|null $forceAuthentication Optional flag to force authentication. + * + * @return array The response containing the created application's 'id'. + * + * @throws AuthException If the create operation fails. + */ + public function createSamlApplication( + string $name, + string $loginPageUrl, + ?string $id = null, + bool $enabled = true, + ?string $description = null, + ?string $logo = null, + bool $useMetadataInfo = false, + ?string $metadataUrl = null, + ?string $entityId = null, + ?string $acsUrl = null, + ?string $certificate = null, + array $attributeMapping = [], + array $groupsMapping = [], + array $acsAllowedCallbacks = [], + ?string $subjectNameIdType = null, + ?string $subjectNameIdFormat = null, + ?bool $defaultRelayState = null, + ?bool $forceAuthentication = null + ): array { + $body = [ + 'name' => $name, + 'loginPageUrl' => $loginPageUrl, + 'enabled' => $enabled, + 'useMetadataInfo' => $useMetadataInfo, + 'attributeMapping' => $attributeMapping, + 'groupsMapping' => $groupsMapping, + 'acsAllowedCallbacks' => $acsAllowedCallbacks, + ]; + + if ($id !== null) { + $body['id'] = $id; + } + + if ($description !== null) { + $body['description'] = $description; + } + + if ($logo !== null) { + $body['logo'] = $logo; + } + + if ($metadataUrl !== null) { + $body['metadataUrl'] = $metadataUrl; + } + + if ($entityId !== null) { + $body['entityId'] = $entityId; + } + + if ($acsUrl !== null) { + $body['acsUrl'] = $acsUrl; + } + + if ($certificate !== null) { + $body['certificate'] = $certificate; + } + + if ($subjectNameIdType !== null) { + $body['subjectNameIdType'] = $subjectNameIdType; + } + + if ($subjectNameIdFormat !== null) { + $body['subjectNameIdFormat'] = $subjectNameIdFormat; + } + + if ($defaultRelayState !== null) { + $body['defaultRelayState'] = $defaultRelayState; + } + + if ($forceAuthentication !== null) { + $body['forceAuthentication'] = $forceAuthentication; + } + + return $this->api->doPost( + MgmtV1::$SSO_APPLICATION_SAML_CREATE_PATH, + $body, + true + ); + } + + /** + * Update an existing OIDC SSO application. + * + * @param string $id The ID of the SSO application to update. + * @param string $name The name of the SSO application. + * @param string $loginPageUrl The URL of the login page for this application. + * @param bool $enabled Whether the application is enabled. Defaults to true. + * @param string|null $description Optional description of the application. + * @param string|null $logo Optional logo (base64 encoded image) for the application. + * + * @return void + * + * @throws AuthException If the update operation fails. + */ + public function updateOidcApplication( + string $id, + string $name, + string $loginPageUrl, + bool $enabled = true, + ?string $description = null, + ?string $logo = null + ): void { + $body = [ + 'id' => $id, + 'name' => $name, + 'loginPageUrl' => $loginPageUrl, + 'enabled' => $enabled, + ]; + + if ($description !== null) { + $body['description'] = $description; + } + + if ($logo !== null) { + $body['logo'] = $logo; + } + + $this->api->doPost( + MgmtV1::$SSO_APPLICATION_OIDC_UPDATE_PATH, + $body, + true + ); + } + + /** + * Update an existing SAML SSO application. + * + * @param string $id The ID of the SSO application to update. + * @param string $name The name of the SSO application. + * @param string $loginPageUrl The URL of the login page for this application. + * @param bool $enabled Whether the application is enabled. Defaults to true. + * @param string|null $description Optional description of the application. + * @param string|null $logo Optional logo (base64 encoded image) for the application. + * @param bool $useMetadataInfo Whether to use metadata URL for configuration. Defaults to false. + * @param string|null $metadataUrl Optional SAML metadata URL (used when $useMetadataInfo is true). + * @param string|null $entityId Optional SAML entity ID (used when $useMetadataInfo is false). + * @param string|null $acsUrl Optional ACS URL (used when $useMetadataInfo is false). + * @param string|null $certificate Optional certificate (used when $useMetadataInfo is false). + * @param array $attributeMapping Attribute mapping between Descope and the application. + * @param array $groupsMapping Groups mapping between Descope and the application. + * @param array $acsAllowedCallbacks List of allowed ACS callback URLs. + * @param string|null $subjectNameIdType Optional subject name ID type. + * @param string|null $subjectNameIdFormat Optional subject name ID format. + * @param bool|null $defaultRelayState Optional default relay state. + * @param bool|null $forceAuthentication Optional flag to force authentication. + * + * @return void + * + * @throws AuthException If the update operation fails. + */ + public function updateSamlApplication( + string $id, + string $name, + string $loginPageUrl, + bool $enabled = true, + ?string $description = null, + ?string $logo = null, + bool $useMetadataInfo = false, + ?string $metadataUrl = null, + ?string $entityId = null, + ?string $acsUrl = null, + ?string $certificate = null, + array $attributeMapping = [], + array $groupsMapping = [], + array $acsAllowedCallbacks = [], + ?string $subjectNameIdType = null, + ?string $subjectNameIdFormat = null, + ?bool $defaultRelayState = null, + ?bool $forceAuthentication = null + ): void { + $body = [ + 'id' => $id, + 'name' => $name, + 'loginPageUrl' => $loginPageUrl, + 'enabled' => $enabled, + 'useMetadataInfo' => $useMetadataInfo, + 'attributeMapping' => $attributeMapping, + 'groupsMapping' => $groupsMapping, + 'acsAllowedCallbacks' => $acsAllowedCallbacks, + ]; + + if ($description !== null) { + $body['description'] = $description; + } + + if ($logo !== null) { + $body['logo'] = $logo; + } + + if ($metadataUrl !== null) { + $body['metadataUrl'] = $metadataUrl; + } + + if ($entityId !== null) { + $body['entityId'] = $entityId; + } + + if ($acsUrl !== null) { + $body['acsUrl'] = $acsUrl; + } + + if ($certificate !== null) { + $body['certificate'] = $certificate; + } + + if ($subjectNameIdType !== null) { + $body['subjectNameIdType'] = $subjectNameIdType; + } + + if ($subjectNameIdFormat !== null) { + $body['subjectNameIdFormat'] = $subjectNameIdFormat; + } + + if ($defaultRelayState !== null) { + $body['defaultRelayState'] = $defaultRelayState; + } + + if ($forceAuthentication !== null) { + $body['forceAuthentication'] = $forceAuthentication; + } + + $this->api->doPost( + MgmtV1::$SSO_APPLICATION_SAML_UPDATE_PATH, + $body, + true + ); + } + + /** + * Delete an SSO application. + * + * @param string $id The ID of the SSO application to delete. + * + * @return void + * + * @throws AuthException If the delete operation fails. + */ + public function delete(string $id): void + { + $body = [ + 'id' => $id, + ]; + + $this->api->doPost( + MgmtV1::$SSO_APPLICATION_DELETE_PATH, + $body, + true + ); + } + + /** + * Load an SSO application by its ID. + * + * @param string $id The ID of the SSO application to load. + * + * @return array The SSO application details. + * + * @throws AuthException If the load operation fails. + */ + public function load(string $id): array + { + return $this->api->doGet( + MgmtV1::$SSO_APPLICATION_LOAD_PATH . '?' . http_build_query(['id' => $id]), + true + ); + } + + /** + * Load all SSO applications. + * + * @return array The response containing all SSO applications under the 'apps' key. + * + * @throws AuthException If the load operation fails. + */ + public function loadAll(): array + { + return $this->api->doGet( + MgmtV1::$SSO_APPLICATION_LOAD_ALL_PATH, + true + ); + } +} diff --git a/src/SDK/Management/SSOSettings.php b/src/SDK/Management/SSOSettings.php new file mode 100644 index 0000000..d7adf54 --- /dev/null +++ b/src/SDK/Management/SSOSettings.php @@ -0,0 +1,176 @@ +api = $api; + } + + /** + * Load the SSO settings for a tenant. + * + * This method retrieves the current SSO configuration for the given tenant. + * + * @param string $tenantId The ID of the tenant whose SSO settings are loaded. + * + * @return array The tenant's SSO settings. + * + * @throws AuthException If the load operation fails. + */ + public function loadSettings(string $tenantId): array + { + return $this->api->doGet( + MgmtV1::$SSO_LOAD_SETTINGS_PATH . '?' . http_build_query(['tenantId' => $tenantId]), + true + ); + } + + /** + * Configure the OIDC SSO settings for a tenant. + * + * This method overwrites the tenant's SSO configuration with the provided + * OIDC settings. + * + * @param string $tenantId The ID of the tenant to configure. + * @param array $settings The OIDC settings (assoc array with keys such as + * name, clientId, clientSecret, redirectUrl, authUrl, + * tokenUrl, userDataUrl, scope, etc.). Passed through as-is. + * @param array $domains Optional list of domains associated with the tenant. + * + * @return void + * + * @throws AuthException If the configure operation fails. + */ + public function configureOIDCSettings(string $tenantId, array $settings, array $domains = []): void + { + $body = [ + 'tenantId' => $tenantId, + 'settings' => $settings, + 'domains' => $domains, + ]; + + $this->api->doPost( + MgmtV1::$SSO_CONFIGURE_OIDC_SETTINGS, + $body, + true + ); + } + + /** + * Configure the SAML SSO settings for a tenant. + * + * This method overwrites the tenant's SSO configuration with the provided + * SAML settings. + * + * @param string $tenantId The ID of the tenant to configure. + * @param array $settings The SAML settings. Passed through as-is. + * @param string|null $redirectUrl Optional redirect URL for the SSO flow. + * @param array $domains Optional list of domains associated with the tenant. + * + * @return void + * + * @throws AuthException If the configure operation fails. + */ + public function configureSAMLSettings( + string $tenantId, + array $settings, + ?string $redirectUrl = null, + array $domains = [] + ): void { + $body = [ + 'tenantId' => $tenantId, + 'settings' => $settings, + 'domains' => $domains, + ]; + + if ($redirectUrl !== null) { + $body['redirectUrl'] = $redirectUrl; + } + + $this->api->doPost( + MgmtV1::$SSO_CONFIGURE_SAML_SETTINGS, + $body, + true + ); + } + + /** + * Configure the SAML SSO settings for a tenant using metadata. + * + * This method overwrites the tenant's SSO configuration with the provided + * SAML settings supplied via metadata. + * + * @param string $tenantId The ID of the tenant to configure. + * @param array $settings The SAML metadata settings. Passed through as-is. + * @param string|null $redirectUrl Optional redirect URL for the SSO flow. + * @param array $domains Optional list of domains associated with the tenant. + * + * @return void + * + * @throws AuthException If the configure operation fails. + */ + public function configureSAMLSettingsByMetadata( + string $tenantId, + array $settings, + ?string $redirectUrl = null, + array $domains = [] + ): void { + $body = [ + 'tenantId' => $tenantId, + 'settings' => $settings, + 'domains' => $domains, + ]; + + if ($redirectUrl !== null) { + $body['redirectUrl'] = $redirectUrl; + } + + $this->api->doPost( + MgmtV1::$SSO_CONFIGURE_SAML_BY_METADATA_SETTINGS, + $body, + true + ); + } + + /** + * Delete the SSO settings for a tenant. + * + * This method removes the SSO configuration for the given tenant. + * + * @param string $tenantId The ID of the tenant whose SSO settings are deleted. + * + * @return void + * + * @throws AuthException If the delete operation fails. + */ + public function deleteSettings(string $tenantId): void + { + $this->api->doDelete( + MgmtV1::$SSO_SETTINGS_PATH . '?' . http_build_query(['tenantId' => $tenantId]) + ); + } +} diff --git a/src/SDK/Management/Tenant.php b/src/SDK/Management/Tenant.php new file mode 100644 index 0000000..a99ff93 --- /dev/null +++ b/src/SDK/Management/Tenant.php @@ -0,0 +1,210 @@ +api = $api; + } + + /** + * Create a new tenant. + * + * This method creates a new tenant with the specified name and optional settings. + * If no ID is provided, one will be generated automatically. + * + * @param string $name The name of the tenant. + * @param string|null $id Optional custom ID for the tenant. + * @param array $selfProvisioningDomains Optional list of domains that can self-provision into the tenant. + * @param array|null $customAttributes Optional map of custom attributes for the tenant. + * + * @return array The create response containing the tenant 'id'. + * + * @throws AuthException If the create operation fails. + */ + public function create( + string $name, + ?string $id = null, + array $selfProvisioningDomains = [], + ?array $customAttributes = null + ): array { + $body = [ + 'name' => $name, + 'selfProvisioningDomains' => $selfProvisioningDomains, + ]; + + if ($id !== null) { + $body['id'] = $id; + } + + if ($customAttributes !== null) { + $body['customAttributes'] = $customAttributes; + } + + return $this->api->doPost( + MgmtV1::$TENANT_CREATE_PATH, + $body, + true + ); + } + + /** + * Update an existing tenant. + * + * This method overwrites the tenant's settings with the provided values. + * All fields will be updated, so unspecified optional values should be + * provided in full to avoid unintentionally clearing them. + * + * @param string $id The ID of the tenant to update. + * @param string $name The new name of the tenant. + * @param array $selfProvisioningDomains Optional list of domains that can self-provision into the tenant. + * @param array|null $customAttributes Optional map of custom attributes for the tenant. + * + * @return void + * + * @throws AuthException If the update operation fails. + */ + public function update( + string $id, + string $name, + array $selfProvisioningDomains = [], + ?array $customAttributes = null + ): void { + $body = [ + 'id' => $id, + 'name' => $name, + 'selfProvisioningDomains' => $selfProvisioningDomains, + ]; + + if ($customAttributes !== null) { + $body['customAttributes'] = $customAttributes; + } + + $this->api->doPost( + MgmtV1::$TENANT_UPDATE_PATH, + $body, + true + ); + } + + /** + * Delete a tenant. + * + * This method removes a tenant identified by its ID. When cascade is enabled, + * users and other entities associated only with this tenant are also deleted. + * + * @param string $id The ID of the tenant to delete. + * @param bool $cascade Whether to cascade the deletion to associated entities. + * + * @return void + * + * @throws AuthException If the delete operation fails. + */ + public function delete(string $id, bool $cascade = false): void + { + $body = [ + 'id' => $id, + 'cascade' => $cascade, + ]; + + $this->api->doPost( + MgmtV1::$TENANT_DELETE_PATH, + $body, + true + ); + } + + /** + * Load a single tenant by its ID. + * + * This method retrieves the details of a specific tenant. + * + * @param string $id The ID of the tenant to load. + * + * @return array The tenant details. + * + * @throws AuthException If the load operation fails. + */ + public function load(string $id): array + { + return $this->api->doGet( + MgmtV1::$TENANT_LOAD_PATH . '?' . http_build_query(['id' => $id]), + true + ); + } + + /** + * Load all tenants in the project. + * + * This method retrieves the details of every tenant configured in the project. + * + * @return array The response containing the list of 'tenants'. + * + * @throws AuthException If the load operation fails. + */ + public function loadAll(): array + { + return $this->api->doGet( + MgmtV1::$TENANT_LOAD_ALL_PATH, + true + ); + } + + /** + * Search for tenants matching the provided filters. + * + * This method searches all tenants, optionally filtering by IDs, names, + * self-provisioning domains, and custom attributes. + * + * @param array $ids Optional list of tenant IDs to filter by. + * @param array $names Optional list of tenant names to filter by. + * @param array $selfProvisioningDomains Optional list of self-provisioning domains to filter by. + * @param array|null $customAttributes Optional map of custom attributes to filter by. + * + * @return array The search response containing the matching tenants. + * + * @throws AuthException If the search operation fails. + */ + public function searchAll( + array $ids = [], + array $names = [], + array $selfProvisioningDomains = [], + ?array $customAttributes = null + ): array { + $body = [ + 'tenantIds' => $ids, + 'tenantNames' => $names, + 'tenantSelfProvisioningDomains' => $selfProvisioningDomains, + ]; + + if ($customAttributes !== null) { + $body['customAttributes'] = $customAttributes; + } + + return $this->api->doPost( + MgmtV1::$TENANT_SEARCH_ALL_PATH, + $body, + true + ); + } +} diff --git a/src/tests/Auth/AuthParityTest.php b/src/tests/Auth/AuthParityTest.php new file mode 100644 index 0000000..4d78361 --- /dev/null +++ b/src/tests/Auth/AuthParityTest.php @@ -0,0 +1,125 @@ +sdk = new DescopeSDK(['projectId' => 'Ptest']); + EndpointsV1::setBaseUrlFromString('https://api.descope.com'); + } + + public function testAuthComponentsAreWired(): void + { + $this->assertInstanceOf(OTP::class, $this->sdk->otp); + $this->assertInstanceOf(OTP::class, $this->sdk->otp()); + $this->assertInstanceOf(MagicLink::class, $this->sdk->magicLink); + $this->assertInstanceOf(MagicLink::class, $this->sdk->magicLink()); + } + + /** + * @dataProvider sessionMethodsProvider + */ + public function testSessionLifecycleMethodsExist(string $method): void + { + $this->assertTrue( + method_exists($this->sdk, $method), + sprintf('DescopeSDK is missing method %s()', $method) + ); + } + + public function sessionMethodsProvider(): array + { + return [ + ['logout'], + ['logoutAll'], + ['selectTenant'], + ['exchangeAccessKey'], + ['history'], + ['refreshSession'], + ]; + } + + public function testOtpSignInAppendsMethodAndSendsLoginId(): void + { + $api = $this->createMock(API::class); + $api->expects($this->once()) + ->method('doPost') + ->with( + $this->stringEndsWith('/v1/auth/otp/signin/email'), + ['loginId' => 'a@b.com'], + false, + null + ) + ->willReturn(['email' => 'a***@b.com']); + + $otp = new OTP($api); + $this->assertSame('a***@b.com', $otp->signIn('email', 'a@b.com')); + } + + public function testOtpVerifyReturnsJwtResponse(): void + { + $api = $this->createMock(API::class); + $api->expects($this->once()) + ->method('doPost') + ->with( + $this->stringEndsWith('/v1/auth/otp/verify/sms'), + ['loginId' => '+123', 'code' => '000000'], + false + ) + ->willReturn(['refreshJwt' => 'rjwt']); + $api->expects($this->once()) + ->method('generateJwtResponse') + ->willReturn(['sessionToken' => 'st']); + + $otp = new OTP($api); + $this->assertSame(['sessionToken' => 'st'], $otp->verifyCode('sms', '+123', '000000')); + } + + public function testOtpRejectsUnknownMethod(): void + { + $this->expectException(AuthException::class); + $this->expectExceptionMessage('Unknown delivery method'); + (new OTP($this->createMock(API::class)))->signIn('carrier-pigeon', 'a@b.com'); + } + + public function testMagicLinkSignUpIncludesUri(): void + { + $api = $this->createMock(API::class); + $api->expects($this->once()) + ->method('doPost') + ->with( + $this->stringEndsWith('/v1/auth/magiclink/signup/email'), + $this->callback(fn ($body) => $body['loginId'] === 'a@b.com' && $body['uri'] === 'https://app/verify'), + false + ) + ->willReturn(['email' => 'a***@b.com']); + + $ml = new MagicLink($api); + $this->assertSame('a***@b.com', $ml->signUp('email', 'a@b.com', 'https://app/verify')); + } + + public function testMagicLinkVerifyRejectsEmptyToken(): void + { + $this->expectException(AuthException::class); + $this->expectExceptionMessage('token cannot be empty'); + (new MagicLink($this->createMock(API::class)))->verify(''); + } +} diff --git a/src/tests/Management/AccessKeyTest.php b/src/tests/Management/AccessKeyTest.php new file mode 100644 index 0000000..32a3470 --- /dev/null +++ b/src/tests/Management/AccessKeyTest.php @@ -0,0 +1,56 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testCreate() + { + $result = $this->descopeSDK->management->accessKey->create('My Key'); + $this->assertIsArray($result); + } + + public function testSearchAll() + { + $result = $this->descopeSDK->management->accessKey->searchAll(); + $this->assertIsArray($result); + $this->assertArrayHasKey('keys', $result); + } + + public function testLoadUpdate() + { + $this->descopeSDK->management->accessKey->load('k1'); + $this->descopeSDK->management->accessKey->update('k1', 'Renamed'); + $this->assertTrue(true); + } + + public function testActivateDeactivateDelete() + { + $this->descopeSDK->management->accessKey->activate('k1'); + $this->descopeSDK->management->accessKey->deactivate('k1'); + $this->descopeSDK->management->accessKey->delete('k1'); + $this->assertTrue(true); + } +} diff --git a/src/tests/Management/FlowTest.php b/src/tests/Management/FlowTest.php new file mode 100644 index 0000000..775ca8d --- /dev/null +++ b/src/tests/Management/FlowTest.php @@ -0,0 +1,59 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testListFlows() + { + $result = $this->descopeSDK->management->flow->listFlows(); + $this->assertIsArray($result); + $this->assertArrayHasKey('flows', $result); + } + + public function testExportImportFlow() + { + $exported = $this->descopeSDK->management->flow->exportFlow('sign-up-or-in'); + $this->assertIsArray($exported); + + $imported = $this->descopeSDK->management->flow->importFlow('sign-up-or-in', []); + $this->assertIsArray($imported); + } + + public function testDelete() + { + $this->descopeSDK->management->flow->delete(['f1']); + $this->assertTrue(true); + } + + public function testExportImportTheme() + { + $exported = $this->descopeSDK->management->flow->exportTheme(); + $this->assertIsArray($exported); + + $imported = $this->descopeSDK->management->flow->importTheme([]); + $this->assertIsArray($imported); + } +} diff --git a/src/tests/Management/JWTTest.php b/src/tests/Management/JWTTest.php new file mode 100644 index 0000000..95cbbdf --- /dev/null +++ b/src/tests/Management/JWTTest.php @@ -0,0 +1,40 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testUpdateJWT() + { + $result = $this->descopeSDK->management->jwt->updateJWT('someJwt', ['k' => 'v']); + $this->assertIsString($result); + } + + public function testImpersonate() + { + $result = $this->descopeSDK->management->jwt->impersonate('imp1', 'login1'); + $this->assertIsString($result); + } +} diff --git a/src/tests/Management/ManagementParityTest.php b/src/tests/Management/ManagementParityTest.php new file mode 100644 index 0000000..084c278 --- /dev/null +++ b/src/tests/Management/ManagementParityTest.php @@ -0,0 +1,83 @@ + 'Ptest', + 'managementKey' => 'Ktest', + ]); + $this->management = $sdk->management; + } + + public function testAllComponentsAreWired(): void + { + $components = [ + 'user' => \Descope\SDK\Management\User::class, + 'audit' => \Descope\SDK\Management\Audit::class, + 'outboundApps' => \Descope\SDK\Management\OutboundApps::class, + 'tenant' => \Descope\SDK\Management\Tenant::class, + 'role' => \Descope\SDK\Management\Role::class, + 'permission' => \Descope\SDK\Management\Permission::class, + 'accessKey' => \Descope\SDK\Management\AccessKey::class, + 'ssoApplication' => \Descope\SDK\Management\SSOApplication::class, + 'sso' => \Descope\SDK\Management\SSOSettings::class, + 'jwt' => \Descope\SDK\Management\JWT::class, + 'flow' => \Descope\SDK\Management\Flow::class, + ]; + + foreach ($components as $property => $class) { + $this->assertInstanceOf($class, $this->management->$property); + $this->assertInstanceOf($class, $this->management->$property()); + } + } + + /** + * @dataProvider expectedMethodsProvider + */ + public function testComponentExposesMethods(string $property, array $methods): void + { + $component = $this->management->$property; + foreach ($methods as $method) { + $this->assertTrue( + method_exists($component, $method), + sprintf('%s is missing method %s()', get_class($component), $method) + ); + } + } + + public function expectedMethodsProvider(): array + { + return [ + 'tenant' => ['tenant', ['create', 'update', 'delete', 'load', 'loadAll', 'searchAll']], + 'role' => ['role', ['create', 'update', 'delete', 'loadAll', 'search']], + 'permission' => ['permission', ['create', 'update', 'delete', 'loadAll']], + 'accessKey' => ['accessKey', ['create', 'load', 'searchAll', 'update', 'activate', 'deactivate', 'delete']], + 'ssoApplication' => ['ssoApplication', [ + 'createOidcApplication', 'createSamlApplication', + 'updateOidcApplication', 'updateSamlApplication', + 'delete', 'load', 'loadAll', + ]], + 'sso' => ['sso', [ + 'loadSettings', 'configureOIDCSettings', 'configureSAMLSettings', + 'configureSAMLSettingsByMetadata', 'deleteSettings', + ]], + 'jwt' => ['jwt', ['updateJWT', 'impersonate']], + 'flow' => ['flow', ['listFlows', 'delete', 'exportFlow', 'importFlow', 'exportTheme', 'importTheme']], + ]; + } +} diff --git a/src/tests/Management/PermissionTest.php b/src/tests/Management/PermissionTest.php new file mode 100644 index 0000000..99f3abe --- /dev/null +++ b/src/tests/Management/PermissionTest.php @@ -0,0 +1,48 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testCreate() + { + $this->descopeSDK->management->permission->create('Read', 'desc'); + $this->assertTrue(true); + } + + public function testLoadAll() + { + $result = $this->descopeSDK->management->permission->loadAll(); + $this->assertIsArray($result); + $this->assertArrayHasKey('permissions', $result); + } + + public function testUpdateDelete() + { + $this->descopeSDK->management->permission->update('Read', 'ReadOnly'); + $this->descopeSDK->management->permission->delete('ReadOnly'); + $this->assertTrue(true); + } +} diff --git a/src/tests/Management/RoleTest.php b/src/tests/Management/RoleTest.php new file mode 100644 index 0000000..39ec7a6 --- /dev/null +++ b/src/tests/Management/RoleTest.php @@ -0,0 +1,53 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testCreate() + { + $this->descopeSDK->management->role->create('My Role', 'desc', ['Read']); + $this->assertTrue(true); + } + + public function testLoadAll() + { + $result = $this->descopeSDK->management->role->loadAll(); + $this->assertIsArray($result); + } + + public function testSearch() + { + $result = $this->descopeSDK->management->role->search(); + $this->assertIsArray($result); + } + + public function testUpdateDelete() + { + $this->descopeSDK->management->role->update('My Role', 'Renamed'); + $this->descopeSDK->management->role->delete('Renamed'); + $this->assertTrue(true); + } +} diff --git a/src/tests/Management/SSOApplicationTest.php b/src/tests/Management/SSOApplicationTest.php new file mode 100644 index 0000000..85a0571 --- /dev/null +++ b/src/tests/Management/SSOApplicationTest.php @@ -0,0 +1,63 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testCreateOidcApplication() + { + $result = $this->descopeSDK->management->ssoApplication->createOidcApplication( + 'My App', + 'https://login.example.com' + ); + + $this->assertIsArray($result); + $this->assertArrayHasKey('id', $result); + } + + public function testCreateSamlApplication() + { + $result = $this->descopeSDK->management->ssoApplication->createSamlApplication( + 'My SAML', + 'https://login.example.com' + ); + + $this->assertIsArray($result); + } + + public function testLoadAll() + { + $result = $this->descopeSDK->management->ssoApplication->loadAll(); + $this->assertIsArray($result); + $this->assertArrayHasKey('apps', $result); + } + + public function testLoadDelete() + { + $this->descopeSDK->management->ssoApplication->load('app1'); + $this->descopeSDK->management->ssoApplication->delete('app1'); + $this->assertTrue(true); + } +} diff --git a/src/tests/Management/SSOSettingsTest.php b/src/tests/Management/SSOSettingsTest.php new file mode 100644 index 0000000..9f3b19d --- /dev/null +++ b/src/tests/Management/SSOSettingsTest.php @@ -0,0 +1,60 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testLoadSettings() + { + $result = $this->descopeSDK->management->sso->loadSettings('t1'); + $this->assertIsArray($result); + } + + public function testConfigureOIDCSettings() + { + $this->descopeSDK->management->sso->configureOIDCSettings('t1', [ + 'name' => 'n', + 'clientId' => 'c', + 'clientSecret' => 's', + ]); + $this->assertTrue(true); + } + + public function testConfigureSAMLSettings() + { + $this->descopeSDK->management->sso->configureSAMLSettings('t1', [ + 'idpUrl' => 'u', + 'entityId' => 'e', + 'idpCert' => 'cert', + ]); + $this->assertTrue(true); + } + + public function testDeleteSettings() + { + $this->descopeSDK->management->sso->deleteSettings('t1'); + $this->assertTrue(true); + } +} diff --git a/src/tests/Management/TenantTest.php b/src/tests/Management/TenantTest.php new file mode 100644 index 0000000..9c78e23 --- /dev/null +++ b/src/tests/Management/TenantTest.php @@ -0,0 +1,56 @@ +markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.'); + } + + $config = [ + 'projectId' => $projectId, + 'managementKey' => $managementKey, + ]; + + $this->descopeSDK = new DescopeSDK($config); + } + + public function testCreate() + { + $result = $this->descopeSDK->management->tenant->create('My Tenant'); + + $this->assertIsArray($result); + $this->assertArrayHasKey('id', $result); + } + + public function testLoadAll() + { + $result = $this->descopeSDK->management->tenant->loadAll(); + $this->assertIsArray($result); + } + + public function testSearchAll() + { + $result = $this->descopeSDK->management->tenant->searchAll(); + $this->assertIsArray($result); + } + + public function testLoadUpdateDelete() + { + $this->descopeSDK->management->tenant->load('t1'); + $this->descopeSDK->management->tenant->update('t1', 'New'); + $this->descopeSDK->management->tenant->delete('t1'); + $this->assertTrue(true); + } +}