From 3e8a45670ed142b20645326cb07223ed8dd118e3 Mon Sep 17 00:00:00 2001 From: Ami Mahloof Date: Mon, 12 Aug 2024 09:34:13 -0400 Subject: [PATCH 1/3] wip --- Gemfile.lock | 2 +- README.md | 691 ++-------------------------------- lib/descope/api/v1/session.rb | 16 + lib/descope/mixins/common.rb | 1 + 4 files changed, 40 insertions(+), 670 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2dd7dfa..289e709 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - descope (1.0.5) + descope (1.0.6) addressable (~> 2.8) jwt (~> 2.7) rest-client (~> 2.1) diff --git a/README.md b/README.md index 9b81180..cc77e40 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,14 @@ These sections show how to use the SDK to perform various authentication/authori 2. [Magic Link](#magic-link) 3. [Enchanted Link](#enchanted-link) 4. [OAuth](#oauth) -5. [SSO/SAML](#ssosaml) +5. [SSO (SAML / OIDC)](#sso-saml-oidc) 6. [TOTP Authentication](#totp-authentication) 7. [Passwords](#passwords) 8. [Session Validation](#session-validation) 9. [Roles & Permission Validation](#roles-permission-validation) 10. [Tenant selection](#tenant-selection) 11. [Signing Out](#signing-out) +12. [History](#history) ## API Management Function @@ -60,16 +61,18 @@ These sections show how to use the SDK to perform permission and user management 1. [Manage Tenants](#manage-tenants) 2. [Manage Users](#manage-users) 3. [Manage Access Keys](#manage-access-keys) -4. [Manage SSO Setting](#manage-sso-saml-settings) +4. [Manage SSO Setting](#manage-sso-setting) 5. [Manage Permissions](#manage-permissions) 6. [Manage Roles](#manage-roles) -7. [Search Roles](#search-roles) +7. [Query SSO Groups](#query-sso-groups) 8. [Manage Flows](#manage-flows-and-theme) 9. [Manage JWTs](#manage-jwts) -10. [Embedded links](#embedded-links) -11. [Audit](#audit) -12. [Manage ReBAC Authz](#manage-rebac-authz) -13. [Manage Project](#manage-project) +10. [Impersonate](#impersonate) +11. [Embedded links](#embedded-links) +12. [Audit](#audit) +13. [Manage ReBAC Authz](#manage-rebac-authz) +14. [Manage Project](#manage-project) +15. [Manage SSO Applications](#manage-sso-applications) If you wish to run any of our code examples and play with them, check out our [Code Examples](#code-examples) section. @@ -143,7 +146,7 @@ This method is similar to [Magic Link](#magic-link) but differs in two major way - This supports cross-device clicking, meaning the user can try to log in on one device, like a computer, while clicking the link on another device, for instance a mobile phone. -The Enchanted Link will redirect the user to page where the token needs to be verified. +The Enchanted Link will redirect the user to a page where the token needs to be verified. This redirection can be configured in code per request, or set globally in the [Descope Console](https://app.descope.com/settings/authentication/enchantedlink). The user can either `sign up`, `sign in` or `sign up or in` @@ -234,7 +237,7 @@ refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME The session and refresh JWTs should be returned to the caller, and passed with every request in the session. Read more on [session validation](#session-validation) -### SSO/SAML +### SSO (SAML / OIDC) Users can authenticate to a specific tenant using SAML or Single Sign On. Configure your SSO/SAML settings on the [Descope console](https://app.descope.com/settings/authentication/sso). To start a flow call: @@ -470,6 +473,16 @@ invalidate all user's refresh tokens. After calling this function, you must inva descope_client.sign_out_all('refresh_token') ``` +### History +You can get the current session user history. +The request requires a valid refresh token. + +```ruby +users_history_resp = descope_client.history(refresh_token) +for user_history in users_history_resp: + # Do something +``` + ## Management API It is very common for some form of management or automation to be required. These can be performed @@ -495,667 +508,7 @@ client = Descope::Client.new( ``` -### Manage Tenants - -You can create, update, delete or load tenants: - -```ruby -# You can optionally set your own ID when creating a tenant -descope_client.create_tenant( - name: 'My First Tenant', - id: 'my-custom-id', # This is optional. - self_provisioning_domains: ['domain.com'], - custom_attributes: { 'attribute-name': 'value' }, -) - -# Update will override all fields as is. Use carefully. -descope_client.update_tenant( - id: 'my-custom-id', - name: 'My First Tenant', - self_provisioning_domains: %w[domain.com another-domain.com], - custom_attributes: { 'attribute-name': 'value' }, -) - -# Tenant deletion cannot be undone. Use carefully. -descope_client.delete_tenant('my-custom-id') - -# Load tenant by id -tenant_resp = descope_client.load_tenant('my-custom-id') - -# Load all tenants -tenants_resp = descope_client.load_all_tenants -tenants = tenants_resp['tenants'] -tenants.each do |tenant| - # Do something -end - -# search all tenants -tenants_resp = descope_client.search_all_tenants(ids: ['id1'], names: ['name1'], custom_attributes: { 'k1': 'v1' }, self_provisioning_domains: ['spd1']) -tenants = tenants_resp['tenants'] -tenants.each do |tenant| - # Do something - tenant_id = tenant['id'] -end -``` - -### Manage Users - -You can create, update, delete or load users, as well as setting new password, expire password and search according to filters: - -```ruby -# A user must have a login ID, other fields are optional. -# Roles should be set directly if no tenants exist, otherwise set -# on a per-tenant basis. -descope_client.create_user( - login_id: 'desmond@descope.com', - email: 'desmond@descope.com', - display_name: 'Desmond Copeland', - user_tenants: [ - AssociatedTenant('my-tenant-id', ['role-name1']), - ], -) - -# Alternatively, a user can be created and invited via an email message. -# Make sure to configure the invite URL in the Descope console prior to using this function, -# and that an email address is provided in the information. -associated_tenants = [{ tenant_id: 'tenant_id1', role_names: %w[role_name1 role_name2] }] -descope_client.invite_user( - login_id: 'desmond@descope.com', - email: 'desmond@descope.com', - display_name: 'Desmond Copeland', - user_tenants: client.associated_tenants_to_hash_array(associated_tenants), -) - -# Update will override all fields as is. Use carefully. -descope_client.update_user( - login_id: 'desmond@descope.com', - email: 'desmond@descope.com', - display_name: 'Desmond Copeland', - user_tenants: client.associated_tenants_to_hash_array(associated_tenants) -) - -# Update explicit data for a user rather than overriding all fields -descope_client.update_login_id( - login_id: 'desmond@descope.com', - new_login_id: 'bane@descope.com' -) - -descope_client.update_phone( - login_id: 'desmond@descope.com', - phone: '+18005551234', - verified: true -) - -descope_client.user_remove_tenant_roles( - login_id: 'desmond@descope.com', - tenant_id: 'my-tenant-id', - role_names: ['role-name1'] -) - -# User deletion cannot be undone. Use carefully. -descope_client.delete_user('desmond@descope.com') - -# Load specific user -user_resp = descope_client.load_user('desmond@descope.com') -user = user_resp['user'] - -# If needed, users can be loaded using the user ID as well -user_resp = descope_client.load_by_user_id('') -user = user_resp['user'] - -# Logout user from all devices by login ID -descope_client.logout_user('') - -# Logout user from all devices by user ID -descope_client.logout_user_by_id('') - -# Search all users, optionally according to tenant and/or role filter -# results can be paginated using the limit and page parameters -users_resp = descope_client.search_all_users(tenant_ids = ['my-tenant-id']) -users = users_resp['users'] -users.each do |user| - # Do something -end -``` - -#### Set or Expire User Password - -You can set a new active password for a user, which they can then use to sign in. You can also set a temporary -password that the user will be forced to change on the next login. - -```ruby -# Set a user's temporary password -descope_client.set_temporary_password(login_id: '', password: ''); - -# Set a user's password -descope_client.set_active_password(login_id: '', password: ''); - -# Or alternatively, expire a user password -descope_client.expire_password('') -``` - -### Manage Access Keys - -You can create, update, delete or load access keys, as well as search according to filters: - -```ruby -# An access key must have a name and expiration, other fields are optional. -# Roles should be set directly if no tenants exist, otherwise set -# on a per-tenant basis. If custom_claims supplied they will be presented on the jwt. -# If customClaims is supplied, then those claims will be present in the JWT returned by calls to ExchangeAccessKey. -associated_tenants = [{ tenant_id: 'tenant_id1', role_names: %w[role_name1 role_name2] }] -create_resp = descope_client.create_access_key( - name: 'name', - expire_time: 1677844931, - key_tenants: associated_tenants, - custom_claims: {'k1': 'v1'} -) -key = create_resp['key'] -cleartext = create_resp['cleartext'] # make sure to save the returned cleartext securely. It will not be returned again. - -# Load a specific access key -access_key_resp = descope_client.load_access_key('key-id') -access_key = access_key_resp['key'] - -# Search all access keys, optionally according to a tenant filter -keys_resp = descope_client.search_all_access_keys(['my-tenant-id']) -keys = keys_resp['keys'] -keys.each do |key| - # Do something with key -end - -# Update will rename the access key -descope_client.update_access_key( - id: 'key-id', - name: 'new name' -) - -# Access keys can be deactivated to prevent usage. This can be undone using 'activate'. -descope_client.deactivate_access_key('key-id') - -# Disabled access keys can be activated once again. -descope_client.activate_access_key('key-id') - -# Access key deletion cannot be undone. Use carefully. -descope_client.delete_access_key('key-id') - -``` - -### Manage SSO SAML Settings - -You can manage SSO settings and map SSO group roles and user attributes. - -```ruby -# You can get SSO SAML settings for a tenant -sso_settings_res = descope_client.sso_get_settings('tenant-id') - -# You can configure SSO SAML settings manually by setting the required fields directly -descope_client.configure_sso_saml_metadata( - tenant_id: '123', # Which tenant this configuration is for - settings: { - name: 'test', - clientId: 'test', - scope: ['test'], - userAttrMapping: { - loginId: 'test', - username: 'test', - name: 'test' - }, - callbackDomain: 'test' - }, - redirect_url: 'https://your.domain.com', # Global redirection after successful authentication - domain: 'tenant-users.com' # Users authentication with this domain will be logged in to this tenant -) -``` - - -### Manage Permissions - -You can create, update, delete or load permissions: - -```ruby -# You can optionally set a description for a permission. -descope_client.create_permission( - name:'My Permission', - description:'Optional description to briefly explain what this permission allows.' -) - -# Update will override all fields as is. Use carefully. -descope_client.mgmt.update_permission( - name: 'My Permission', - new_name: 'My Updated Permission', - description: 'A revised description' -) - -# Permission deletion cannot be undone. Use carefully. -descope_client.mgmt.permission.delete('My Updated Permission') - -# Load all permissions -permissions_resp = descope_client.load_all_permissions -permissions = permissions_resp['permissions'] - permissions.each do |permission| - # Do something - end -``` - -### Manage Roles - -You can create, update, delete or load roles: - -```ruby -# You can optionally set a description and associated permission for a roles. -descope_client.create_role( - name: 'My Role', - description: 'Optional description to briefly explain what this role allows.', - permission_names: ['My Updated Permission'], - tenant_id: 'Optionally scope this role for this specific tenant. If left empty, the role will be available to all tenants.' -) - -# Update will override all fields as is. Use carefully. -descope_client.update_role( - name: 'My Role', - new_name: 'My Updated Role', - description: 'A revised description', - permission_names: ['My Updated Permission', 'Another Permission'], - tenant_id: 'The tenant ID to which this role is associated, leave empty, if role is a global one' -) - -# Role deletion cannot be undone. Use carefully. -descope_client.delete_role(name: 'My Updated Role', tenant_id: 'The tenant ID to which this role is associated, leave empty, if role is a global one') - -# Load all roles -roles_resp = descope_client.load_all_roles -roles = roles_resp['roles'] - roles.each do |role| - # Do something - end -# -``` - -# Search roles - -```ruby -roles_resp = descope_client.search_roles( - names: %w[role1 role2], # Search for roles with the names 'role1' and 'role2' - role_name_like: 'role', # Search for roles that contain the string 'role' - tenant_ids: %w[tenant1 tenant2], # Search for roles that are associated with the tenants 'tenant1' and 'tenant2' - permission_names: %w[permission1 permission2] # Search for roles that have the permissions 'permission1' and 'permission2' -) - -roles = roles_resp['roles'] -roles.each do |role| - # Do something -end -``` - -### Manage Flows and Theme - -You can list your flows and also import and export flows and screens, or the project theme: - -```ruby -# List all project flows -flows_resp = descope_client.list_or_search_flows() -puts("Total number of flows: #{flows_resp['total']}") -flows = flows_resp['flows'] -flows.each do |flow| - # Do something -end - -# Export a selected flow by id for the flow and matching screens. -exported_flow_and_screens = descope_client.export_flow('sign-up-or-in') - -# Import a given flow and screens to the flow matching the id provided. -imported_flow_and_screens = descope_client.import_flow( - flow_id: 'sign-up-or-in', - flow: {}, - screens: [] -) - -# Export your project theme. -exported_theme = descope_client.export_theme - -# Import a theme to your project. -imported_theme = descope_client.import_theme('theme') - -``` - -### Query SCIM Groups - -You can query SCIM groups: - -```ruby -# Load all groups for a given tenant id -groups_resp = descope_client.scim_search_groups( - group_id: 'group_id', - display_name: 'display_name', - members: ['members'], - external_id: 'external_id', - excluded_attributes: { abc: '123' } -) - -# Load SCIM group -group = descope_client.scim_load_group( - tenant_id: 'tenant-id', - group_id: 'group-id' -) - -# Load SCIM group members -group = descope_client.scim_create_group( - group_id: 'group_id', - display_name: 'display_name', - members: ['members'], - external_id: 'external_id', - excluded_attributes: { abc: '123' } -) -``` - -### Manage JWTs - -You can add custom claims to a valid JWT. - -```ruby -updated_jwt = descope_client.update_jwt( - jwt: 'original-jwt', - custom_claims: { - 'custom-key1': 'custom-value1', - 'custom-key2': 'custom-value2' - }, -) -``` - -# Note 1: The generate code/link functions, work only for test users, will not work for regular users. - -# Note 2: In case of testing sign-in / sign-up operations with test users, need to make sure to generate the code prior calling the sign-in / sign-up operations. - -### Embedded links - -Embedded links can be created to directly receive a verifiable token without sending it. - -This token can then be verified using the magic link 'verify' function, either directly or through a flow. - -```ruby -token = descope_client.generate_embedded_link(login_id: 'desmond@descope.com', custom_claims: {'key1':'value1'}) -``` - -### Audit - -You can perform an audit search for either specific values or full-text across the fields. Audit search is limited to the last 30 days. -Below are some examples. For a full list of available search criteria options, see the function documentation. - -```ruby -# Full text search on last 10 days -audits = descope_client.audit_search( - no_tenants: true, - actions: ['LoginSucceed'], - user_ids: %w[user1 user2], - exclude_actions: %w[exclude1 exclude2], - devices: %w[Bot Mobile Desktop Tablet Unknown], - methods: %w[otp totp magiclink oauth saml password], - geos: %w[US IL], - remote_addresses: %w[remote1 remote2], - login_ids: %w[login1 login2], - tenants: %w[tenant1 tenant2], - text: 'text123', - from_ts: time.now - 10 * 24 * 60 * 60, - to_ts: time.now - 1 * 24 * 60 * 60, -) - -# Search successful logins in the last 30 days -audits = descope_client.audit_search(actions: ['LoginSucceed']) -``` - -You can also create audit event with data -```ruby -descope_client.audit_create_event( - actor_id: "UXXX", # required, for example a user ID - tenant_id: "tenant-id", # required - action: "pencil.created", # required - type: "info", # either: info/warn/error # required - data: { - pencil_id: "PXXX", - pencil_name: "Pencil Name" - } # optional -) -``` - -### Manage ReBAC Authz - -Descope supports full relation based access control (ReBAC) using a [Google Zanzibar](https://research.google/pubs/pub48190/) like schema and operations. -A schema comprises namespaces (entities like documents, folders, orgs, etc.) and each namespace has relation definitions to define relations. -Each relation definition can be simple (either you have it or not) or complex (union of nodes). - -A simple example for a file system like schema would be: - -```yaml -# Example schema for the authz tests -name: Files -namespaces: - - name: org - relationDefinitions: - - name: parent - - name: member - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationLeft - relationDefinition: parent - relationDefinitionNamespace: org - targetRelationDefinition: member - targetRelationDefinitionNamespace: org - - name: folder - relationDefinitions: - - name: parent - - name: owner - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationRight - relationDefinition: parent - relationDefinitionNamespace: folder - targetRelationDefinition: owner - targetRelationDefinitionNamespace: folder - - name: editor - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationRight - relationDefinition: parent - relationDefinitionNamespace: folder - targetRelationDefinition: editor - targetRelationDefinitionNamespace: folder - - nType: child - expression: - neType: targetSet - targetRelationDefinition: owner - targetRelationDefinitionNamespace: folder - - name: viewer - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationRight - relationDefinition: parent - relationDefinitionNamespace: folder - targetRelationDefinition: viewer - targetRelationDefinitionNamespace: folder - - nType: child - expression: - neType: targetSet - targetRelationDefinition: editor - targetRelationDefinitionNamespace: folder - - name: doc - relationDefinitions: - - name: parent - - name: owner - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationRight - relationDefinition: parent - relationDefinitionNamespace: doc - targetRelationDefinition: owner - targetRelationDefinitionNamespace: folder - - name: editor - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationRight - relationDefinition: parent - relationDefinitionNamespace: doc - targetRelationDefinition: editor - targetRelationDefinitionNamespace: folder - - nType: child - expression: - neType: targetSet - targetRelationDefinition: owner - targetRelationDefinitionNamespace: doc - - name: viewer - complexDefinition: - nType: union - children: - - nType: child - expression: - neType: self - - nType: child - expression: - neType: relationRight - relationDefinition: parent - relationDefinitionNamespace: doc - targetRelationDefinition: viewer - targetRelationDefinitionNamespace: folder - - nType: child - expression: - neType: targetSet - targetRelationDefinition: editor - targetRelationDefinitionNamespace: doc -``` - -Descope SDK allows you to fully manage the schema and relations as well as perform simple (and not so simple) checks regarding the existence of relations. - -```ruby -# Load the existing schema -schema = descope_client.authz_load_schema - -# Save schema and make sure to remove all namespaces not listed -descope_client.authz_save_schema(schema: schema, upgrade: true) - -# Create a relation between a resource and user -descope_client.authz_create_relations( - [ - { - resource: 'some-doc', - relationDefinition: 'owner', - namespace: 'doc', - target: 'u1' - } - ] -) - -# Check if target has the relevant relation -# The answer should be true because an owner is also a viewer -relations = descope_client.authz_has_relations?( - [ - { - resource: 'some-doc', - relationDefinition: 'viewer', - namespace: 'doc', - target: 'u1' - } - ] -) -``` - -### Manage Project - -You can change the project name, as well as to clone the current project to a new one. - -```ruby - -# Change the project name -descope.client.rename_project('new-project-name') - -# Clone the current project, including its settings and configurations. -# Note that this action is supported only with a pro license or above. -# Users, tenants and access keys are not cloned. -clone_resp = descope.client.clone_project('new-project-name') -``` - -### Utils for your end to end (e2e) tests and integration tests - -To ease your e2e tests, we exposed dedicated management methods, -that way, you don't need to use 3rd party messaging services in order to receive sign-in/up Emails or SMS, and avoid the need of parsing the code and token from them. - -```ruby -# User for test can be created, this user will be able to generate code/link without -# the need of 3rd party messaging services. -# Test user must have a loginId, other fields are optional. -# Roles should be set directly if no tenants exist, otherwise set -# on a per-tenant basis. - -associated_tenants = [{ tenant_id: 'tenant_id1', role_names: %w[role_name1 role_name2] }] -descope_client.create_test_user( - login_id: 'desmond@descope.com', - email: 'desmond@descope.com', - display_name: 'Desmond Copeland', - user_tenants: client.associated_tenants_to_hash_array(associated_tenants) -) - -# Now test user got created, and this user will be available until you delete it, -# you can use any management operation for test user CRUD. -# You can also delete all test users. -descope_client.delete_all_test_users - -# OTP code can be generated for test user, for example: -resp = descope_client.generate_otp_for_test_user( - method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'login-id' -) -code = resp['code'] -# Now you can verify the code is valid (using descope_client.*.verify for example) - -# Same as OTP, magic link can be generated for test user, for example: -resp = descope_client.generate_magic_link_for_test_user( - method: Descope::Mixins::Common::DeliveryMethod::EMAIL, - login_id: 'login-id', -) -link = resp['link'] - -# Enchanted link can be generated for test user, for example: -resp = descope_client.generate_enchanted_link_for_test_user( - 'login-id', '' -) -link = resp['link'] -pending_ref = resp['pendingRef'] -``` ## API Rate Limits diff --git a/lib/descope/api/v1/session.rb b/lib/descope/api/v1/session.rb index 59045e9..5a2d0a1 100644 --- a/lib/descope/api/v1/session.rb +++ b/lib/descope/api/v1/session.rb @@ -78,6 +78,22 @@ def validate_and_refresh_session(session_token: nil, refresh_token: nil, audienc refresh_session(refresh_token:, audience:) end end + + def history(refresh_token = nil) + # Retrieve user authentication history for the refresh token + # Return List in the format + # [ + # { + # "userId": "User's ID", + # "loginTime": "User'sLogin time", + # "city": "User's city", + # "country": "User's country", + # "ip": User's IP + # } + # ] + validate_refresh_token_not_nil(refresh_token) + get(HISTORY_PATH, {}, {}, refresh_token) + end end end end diff --git a/lib/descope/mixins/common.rb b/lib/descope/mixins/common.rb index 127b14a..cbce7ad 100644 --- a/lib/descope/mixins/common.rb +++ b/lib/descope/mixins/common.rb @@ -52,6 +52,7 @@ module EndpointsV1 LOGOUT_ALL_PATH = '/v1/auth/logoutall' VALIDATE_SESSION_PATH = '/v1/auth/validate' ME_PATH = '/v1/auth/me' + HISTORY_PATH = '/v1/auth/me/history' # access key EXCHANGE_AUTH_ACCESS_KEY_PATH = '/v1/auth/accesskey/exchange' From d3e834014f7904492a8c97db59480b2843fd710a Mon Sep 17 00:00:00 2001 From: Ami Mahloof Date: Mon, 12 Aug 2024 18:04:37 -0400 Subject: [PATCH 2/3] SAML SSO Application API --- README.md | 74 ++++++ lib/descope/api/v1/management.rb | 2 + lib/descope/api/v1/management/common.rb | 21 +- .../api/v1/management/sso_application.rb | 234 ++++++++++++++++++ lib/descope/api/v1/management/sso_settings.rb | 26 +- .../api/v1/management/sso_application_spec.rb | 111 +++++++++ .../api/v1/management/sso_settings_spec.rb | 4 +- 7 files changed, 441 insertions(+), 31 deletions(-) create mode 100644 spec/lib.descope/api/v1/management/sso_application_spec.rb diff --git a/README.md b/README.md index 5d38b65..282938f 100644 --- a/README.md +++ b/README.md @@ -1180,6 +1180,80 @@ link = resp['link'] pending_ref = resp['pendingRef'] ``` +### Manage SSO Applications + +You can create, update, delete or load SSO applications: + +```ruby +descope_client.create_sso_oidc_app( + name: "My First sso app", + login_page_url: "https://dummy.com/login", + id: "my-custom-id", # this is optional +) + +# Create SAML sso application +descope_client.create_saml_application( + name: "My First sso app", + login_page_url: "https://dummy.com/login", + id: "my-custom-id", # this is optional + use_metadata_info: true, + metadata_url: "https://dummy.com/metadata", + default_relay_state: "relayState", + force_authentication: false, + logout_redirect_url: "https://dummy.com/logout", +) +``` + +# Update OIDC sso application +# Update will override all fields as is. Use carefully. + +```ruby +descope_client.update_sso_oidc_app( + id: "my-custom-id", + name: "My First sso app", + login_page_url: "https://dummy.com/login", +) +```` + +# Update SAML sso application +# Update will override all fields as is. Use carefully. + +```ruby +descope_client.update_saml_application( + id: "my-custom-id", + name: "My First sso app", + login_page_url: "https://dummy.com/login", + use_metadata_info: false, + entity_id: "ent1234", + acs_url: "https://dummy.com/acs", + certificate: "my cert" +) +``` + +# SSO application deletion cannot be undone. Use carefully. + +```ruby +descope_client.delete_sso_app('my-custom-id') +``` + +# Load SSO application by id + +```ruby +descope_client.load_sso_app('my-custom-id') +``` + +# Load all SSO applications + +```ruby +resp = descope_client.load_all_sso_apps +resp["apps"].each do |app| + # Do something +end +``` + +STOPPPED AT UTILS!!!!! + + ## API Rate Limits Handle API rate limits by comparing the exception to the APIRateLimitExceeded exception, which includes the RateLimitParameters map with the key 'Retry-After.' This key indicates how many seconds until the next valid API call can take place. diff --git a/lib/descope/api/v1/management.rb b/lib/descope/api/v1/management.rb index f6e057c..bbd15ac 100644 --- a/lib/descope/api/v1/management.rb +++ b/lib/descope/api/v1/management.rb @@ -10,6 +10,7 @@ require 'descope/api/v1/management/project' require 'descope/api/v1/management/authz' require 'descope/api/v1/management/audit' +require 'descope/api/v1/management/sso_application' require 'descope/api/v1/management/sso_settings' require 'descope/api/v1/management/scim' require 'descope/api/v1/management/password' @@ -29,6 +30,7 @@ module Management include Descope::Api::V1::Management::Project include Descope::Api::V1::Management::Authz include Descope::Api::V1::Management::Audit + include Descope::Api::V1::Management::SSOApplication include Descope::Api::V1::Management::SSOSettings include Descope::Api::V1::Management::SCIM include Descope::Api::V1::Management::Password diff --git a/lib/descope/api/v1/management/common.rb b/lib/descope/api/v1/management/common.rb index 9e1d1d6..04fe4de 100644 --- a/lib/descope/api/v1/management/common.rb +++ b/lib/descope/api/v1/management/common.rb @@ -55,13 +55,24 @@ module Common ACCESS_KEY_ACTIVATE_PATH = '/v1/mgmt/accesskey/activate' ACCESS_KEY_DELETE_PATH = '/v1/mgmt/accesskey/delete' - # sso + # sso application + SSO_APPLICATION_OIDC_CREATE_PATH = '/v1/mgmt/sso/idp/app/oidc/create' + SSO_APPLICATION_SAML_CREATE_PATH = '/v1/mgmt/sso/idp/app/saml/create' + SSO_APPLICATION_OIDC_UPDATE_PATH = '/v1/mgmt/sso/idp/app/oidc/update' + SSO_APPLICATION_SAML_UPDATE_PATH = '/v1/mgmt/sso/idp/app/saml/update' + SSO_APPLICATION_DELETE_PATH = '/v1/mgmt/sso/idp/app/delete' + SSO_APPLICATION_LOAD_PATH = '/v1/mgmt/sso/idp/app/load' + SSO_APPLICATION_LOAD_ALL_PATH = '/v1/mgmt/sso/idp/apps/load' + + # sso settings SSO_SETTINGS_PATH = '/v2/mgmt/sso/settings' + SSO_METADATA_PATH = '/v1/mgmt/sso/metadata' + SSO_MAPPING_PATH = '/v1/mgmt/sso/mapping' + SSO_LOAD_SETTINGS_PATH = '/v2/mgmt/sso/settings' # v2 only SSO_OIDC_PATH = '/v1/mgmt/sso/oidc' # configure ssp settings via oidc - SSO_OIDC_CREATE_APP_PATH = '/v1/mgmt/sso/idp/app/oidc/create' - SSO_OIDC_UPDATE_APP_PATH = '/v1/mgmt/sso/idp/app/oidc/create' - SSO_SAML_PATH = '/v1/mgmt/sso/saml' # configure ssp settings via saml - SSO_SAML_METADATA_PATH = '/v1/mgmt/sso/saml/metadata' # configure ssp settings via saml metadata + SSO_CONFIGURE_OIDC_SETTINGS_PATH = '/v1/mgmt/sso/oidc' + SSO_CONFIGURE_SAML_SETTINGS_PATH = '/v1/mgmt/sso/saml' + SSO_CONFIGURE_SAML_METADATA_PATH = '/v1/mgmt/sso/saml/metadata' # SCIM SCIM_GROUPS_PATH = '/scim/v2/Groups' diff --git a/lib/descope/api/v1/management/sso_application.rb b/lib/descope/api/v1/management/sso_application.rb index e69de29..24c3a5e 100644 --- a/lib/descope/api/v1/management/sso_application.rb +++ b/lib/descope/api/v1/management/sso_application.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +module Descope + module Api + module V1 + module Management + # Management API calls + module SSOApplication + include Descope::Api::V1::Management::Common + + def create_sso_oidc_app(id: nil, name: nil, description: nil, enabled: nil, logo: nil, login_page_url: nil) + # Create a new OIDC sso application with the given name. SSO application IDs are provisioned automatically, + # but can be provided explicitly if needed. Both the name and ID must be unique per project. + body = {} + body[:id] = id if id + body[:name] = name if name + body[:description] = description if description + body[:enabled] = enabled if enabled + body[:logo] = logo if logo + body[:loginPageUrl] = login_page_url if login_page_url + post(SSO_APPLICATION_OIDC_CREATE_PATH, body) + end + + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def create_saml_application( + name: nil, + login_page_url: nil, + id: nil, + description: nil, + logo: nil, + enabled: nil, + use_metadata_info: nil, + metadata_url: nil, + entity_id: nil, + acs_url: nil, + certificate: nil, + attribute_mapping: nil, + groups_mapping: nil, + acs_allowed_callbacks: nil, + subject_name_id_type: nil, + subject_name_id_format: nil, + default_relay_state: nil, + force_authentication: nil, + logout_redirect_url: nil + ) + # Create a new SAML sso application with the given name. SSO application IDs are provisioned automatically, + # but can be provided explicitly if needed. Both the name and ID must be unique per project. + + if use_metadata_info + raise 'metadata_url argument must be set' unless metadata_url + else + raise 'entity_id, acs_url, certificate arguments must be set' unless entity_id && acs_url && certificate + end + + attribute_mapping ||= [] + groups_mapping ||= [] + acs_allowed_callbacks ||= [] + body = compose_create_update_saml_body( + name:, + login_page_url:, + id:, + description:, + enabled:, + logo:, + use_metadata_info:, + metadata_url:, + entity_id:, + acs_url:, + certificate:, + attribute_mapping:, + groups_mapping:, + acs_allowed_callbacks:, + subject_name_id_type:, + subject_name_id_format:, + default_relay_state:, + force_authentication:, + logout_redirect_url: + ) + post(SSO_APPLICATION_SAML_CREATE_PATH, body) + end + + def update_sso_oidc_app(id: nil, name: nil, description: nil, enabled: nil, logo: nil, login_page_url: nil, force_authentication: nil) + # Update an existing OIDC sso application with the given parameters. IMPORTANT: All parameters are used as overrides + # to the existing sso application. Empty fields will override populated fields. Use carefully. + body = compose_create_update_oidc_body(name, login_page_url, id, description, enabled, logo, force_authentication) + post(SSO_APPLICATION_OIDC_UPDATE_PATH, body) + end + + def update_saml_application( + id: nil, + name: nil, + login_page_url: nil, + description: nil, + logo: nil, + enabled: nil, + use_metadata_info: nil, + metadata_url: nil, + entity_id: nil, + acs_url: nil, + certificate: nil, + attribute_mapping: nil, + groups_mapping: nil, + acs_allowed_callbacks: nil, + subject_name_id_type: nil, + subject_name_id_format: nil, + default_relay_state: nil, + force_authentication: nil, + logout_redirect_url: nil + ) + # Update an existing SAML sso application with the given parameters. IMPORTANT: All parameters are used as overrides + # to the existing sso application. Empty fields will override populated fields. Use carefully. + + if use_metadata_info + raise 'metadata_url argument must be set' unless metadata_url + else + raise 'entity_id, acs_url, certificate arguments must be set' unless entity_id && acs_url + end + + attribute_mapping ||= [] + groups_mapping ||= [] + acs_allowed_callbacks ||= [] + + body = compose_create_update_saml_body( + name:, + login_page_url:, + id:, + description:, + enabled:, + logo:, + use_metadata_info:, + metadata_url:, + entity_id:, + acs_url:, + certificate:, + attribute_mapping:, + groups_mapping:, + acs_allowed_callbacks:, + subject_name_id_type:, + subject_name_id_format:, + default_relay_state:, + force_authentication:, + logout_redirect_url: + ) + post(SSO_APPLICATION_SAML_UPDATE_PATH, body) + end + + def delete_sso_app(id) + # Delete an existing sso application. IMPORTANT: This operation is irreversible. Use carefully. + delete(SSO_APPLICATION_DELETE_PATH, { id: }) + end + + def load_sso_app(id) + # Load an existing sso application. + get(SSO_APPLICATION_LOAD_PATH, { id: }) + end + + def load_all_sso_apps + # Load all sso applications. + # + # Return value: + # { + # "apps": [ + # {"id":"app1","name":"","description":"","enabled":true,"logo":"","appType":"saml","samlSettings":{"loginPageUrl":"","idpCert":"","useMetadataInfo":true,"metadataUrl":"","entityId":"","acsUrl":"","certificate":"","attributeMapping":[{"name":"email","type":"","value":"attrVal1"}],"groupsMapping":[{"name":"grp1","type":"","filterType":"roles","value":"","roles":[{"id":"myRoleId","name":"myRole"}]}],"idpMetadataUrl":"","idpEntityId":"","idpSsoUrl":"","acsAllowedCallbacks":[],"subjectNameIdType":"","subjectNameIdFormat":"", "defaultRelayState":"", "forceAuthentication": false, "idpLogoutUrl": "", "logoutRedirectUrl": ""},"oidcSettings":{"loginPageUrl":"","issuer":"","discoveryUrl":"", "forceAuthentication":false}}, + # {"id":"app2","name":"","description":"","enabled":true,"logo":"","appType":"saml","samlSettings":{"loginPageUrl":"","idpCert":"","useMetadataInfo":true,"metadataUrl":"","entityId":"","acsUrl":"","certificate":"","attributeMapping":[{"name":"email","type":"","value":"attrVal1"}],"groupsMapping":[{"name":"grp1","type":"","filterType":"roles","value":"","roles":[{"id":"myRoleId","name":"myRole"}]}],"idpMetadataUrl":"","idpEntityId":"","idpSsoUrl":"","acsAllowedCallbacks":[],"subjectNameIdType":"","subjectNameIdFormat":"", "defaultRelayState":"", "forceAuthentication": false, "idpLogoutUrl": "", "logoutRedirectUrl": ""},"oidcSettings":{"loginPageUrl":"","issuer":"","discoveryUrl":"", "forceAuthentication":false}} + # ] + # } + # Containing the loaded sso applications information. + get(SSO_APPLICATION_LOAD_ALL_PATH, {}) + end + + private + + def compose_create_update_oidc_body(name, login_page_url, id, description, enabled, logo, force_authentication) + body = {} + body[:name] = name if name + body[:loginPageUrl] = login_page_url if login_page_url + body[:id] = id if id + body[:description] = description if description + body[:logo] = logo if logo + body[:enabled] = enabled if enabled + body[:force_authentication] = force_authentication if force_authentication + body + end + + # rubocop:disable Metrics/AbcSize + def compose_create_update_saml_body( + name: nil, + login_page_url: nil, + id: nil, + description: nil, + enabled: nil, + logo: nil, + use_metadata_info: nil, + metadata_url: nil, + entity_id: nil, + acs_url: nil, + certificate: nil, + attribute_mapping: nil, + groups_mapping: nil, + acs_allowed_callbacks: nil, + subject_name_id_type: nil, + subject_name_id_format: nil, + default_relay_state: nil, + force_authentication: nil, + logout_redirect_url: nil + ) + body = {} + body[:name] = name if name + body[:loginPageUrl] = login_page_url if login_page_url + body[:id] = id if id + body[:description] = description if description + body[:enabled] = enabled if enabled + body[:logo] = logo if logo + body[:useMetadataInfo] = use_metadata_info if use_metadata_info + body[:metadataUrl] = metadata_url if metadata_url + body[:entityId] = entity_id if entity_id + body[:acsUrl] = acs_url if acs_url + body[:certificate] = certificate if certificate + body[:attributeMapping] = attribute_mapping if attribute_mapping + body[:groupsMapping] = groups_mapping if groups_mapping + body[:acsAllowedCallbacks] = acs_allowed_callbacks if acs_allowed_callbacks + body[:subjectNameIdType] = subject_name_id_type if subject_name_id_type + body[:subjectNameIdFormat] = subject_name_id_format if subject_name_id_format + body[:defaultRelayState] = default_relay_state if default_relay_state + body[:forceAuthentication] = force_authentication if force_authentication + body[:logoutRedirectUrl] = logout_redirect_url if logout_redirect_url + puts "body: #{body}" + body + end + end + end + end + end +end diff --git a/lib/descope/api/v1/management/sso_settings.rb b/lib/descope/api/v1/management/sso_settings.rb index 299ec94..57f3549 100644 --- a/lib/descope/api/v1/management/sso_settings.rb +++ b/lib/descope/api/v1/management/sso_settings.rb @@ -18,28 +18,6 @@ def delete_sso_settings(tenant_id) delete(SSO_SETTINGS_PATH, { tenantId: tenant_id }) end - def create_sso_oidc_app(id: nil, name: nil, description: nil, enabled: nil, logo: nil, login_page_url: nil) - body = {} - body[:id] = id if id - body[:name] = name if name - body[:description] = description if description - body[:enabled] = enabled if enabled - body[:logo] = logo if logo - body[:loginPageUrl] = login_page_url if login_page_url - post(SSO_OIDC_CREATE_APP_PATH, body) - end - - def update_sso_oidc_app(id: nil, name: nil, description: nil, enabled: nil, logo: nil, login_page_url: nil) - body = {} - body[:id] = id if id - body[:name] = name if name - body[:description] = description if description - body[:enabled] = enabled if enabled - body[:logo] = logo if logo - body[:loginPageUrl] = login_page_url if login_page_url - put(SSO_OIDC_UPDATE_APP_PATH, body) - end - def configure_sso_oidc(tenant_id: nil, settings: nil, redirect_url: nil, domain: nil) raise Descope::ArgumentException.new('SSO settings must be a Hash', code: 400) unless settings.is_a?(Hash) @@ -63,12 +41,12 @@ def configure_sso_saml(tenant_id: nil, settings: nil, redirect_url: nil, domain: redirectUrl: redirect_url, domain: } - post(SSO_SAML_PATH, request_params) + post(SSO_SETTINGS_PATH, request_params) end def configure_sso_saml_metadata(tenant_id: nil, settings: nil, redirect_url: nil, domain: nil) # Configure tenant SSO SAML Metadata, using a valid management key. - post(SSO_SAML_METADATA_PATH, compose_metadata_body(tenant_id, settings, redirect_url, domain)) + post(SSO_METADATA_PATH, compose_metadata_body(tenant_id, settings, redirect_url, domain)) end private diff --git a/spec/lib.descope/api/v1/management/sso_application_spec.rb b/spec/lib.descope/api/v1/management/sso_application_spec.rb new file mode 100644 index 0000000..f0e4eb3 --- /dev/null +++ b/spec/lib.descope/api/v1/management/sso_application_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::SSOApplication do + before(:all) do + dummy_instance = DummyClass.new + dummy_instance.extend(Descope::Api::V1::Management::SSOApplication) + @instance = dummy_instance + end + + context('.create_sso_oidc_application') do + it 'should respond to .create_saml_application' do + expect(@instance).to respond_to :create_saml_application + end + + it 'is expected to create SAML application' do + expect(@instance).to receive(:post).with( + SSO_APPLICATION_OIDC_CREATE_PATH, { + id: 'tenant1', + name: 'test', + description: 'awesome tenant', + enabled: true, + logo: 'https://logo.com', + loginPageUrl: 'https://dummy.com/login' + } + ) + expect do + @instance.create_sso_oidc_app( + id: 'tenant1', + name: 'test', + description: 'awesome tenant', + enabled: true, + logo: 'https://logo.com', + login_page_url: 'https://dummy.com/login' + ) + end.not_to raise_error + end + end + + context('.create_saml_application') do + it 'should respond to .create_saml_application' do + expect(@instance).to respond_to :create_saml_application + end + + it 'is expected to create SAML application' do + expect(@instance).to receive(:post).with( + SSO_APPLICATION_SAML_CREATE_PATH, { + name: 'test', + description: 'awesome tenant', + id: 'tenant1', + loginPageUrl: 'https://dummy.com/login', + logo: 'https://logo.com', + enabled: true, + useMetadataInfo: true, + metadataUrl: 'https://dummy.com/metadata', + entityId: 'ent1234', + acsUrl: 'https://dummy.com/acs', + certificate: 'something', + attributeMapping: [ + { + 'abc': '123' + } + ], + groupsMapping: [ + { + 'abc': '123' + } + ], + acsAllowedCallbacks: true, + subjectNameIdType: 'test', + subjectNameIdFormat: 'test', + defaultRelayState: 'test', + forceAuthentication: true, + logoutRedirectUrl: 'https://dummy.com/logout' + } + ) + expect do + @instance.create_saml_application( + name: 'test', + login_page_url: 'https://dummy.com/login', + id: 'tenant1', + description: 'awesome tenant', + logo: 'https://logo.com', + enabled: true, + use_metadata_info: true, + metadata_url: 'https://dummy.com/metadata', + entity_id: 'ent1234', + acs_url: 'https://dummy.com/acs', + certificate: 'something', + attribute_mapping: [ + { + 'abc': '123' + } + ], + groups_mapping: [ + { + 'abc': '123' + } + ], + acs_allowed_callbacks: true, + subject_name_id_type: 'test', + subject_name_id_format: 'test', + default_relay_state: 'test', + force_authentication: true, + logout_redirect_url: 'https://dummy.com/logout' + ) + end.not_to raise_error + end + end +end diff --git a/spec/lib.descope/api/v1/management/sso_settings_spec.rb b/spec/lib.descope/api/v1/management/sso_settings_spec.rb index 84a2da3..dc95f09 100644 --- a/spec/lib.descope/api/v1/management/sso_settings_spec.rb +++ b/spec/lib.descope/api/v1/management/sso_settings_spec.rb @@ -87,7 +87,7 @@ it 'is expected to configure SSO settings' do expect(@instance).to receive(:post).with( - SSO_SAML_PATH, { + SSO_SETTINGS_PATH, { tenantId: '123', settings: { name: 'test', @@ -132,7 +132,7 @@ it 'is expected to configure SAML metadata' do expect(@instance).to receive(:post).with( - SSO_SAML_METADATA_PATH, { + SSO_METADATA_PATH, { tenantId: '123', settings: { name: 'test', From c9bdc9bd9adf64edab551262bf7d8160c5a4ea7d Mon Sep 17 00:00:00 2001 From: Ami Mahloof Date: Tue, 13 Aug 2024 09:25:26 -0400 Subject: [PATCH 3/3] rspec for sso apps --- README.md | 2 - .../api/v1/management/sso_application.rb | 6 +- .../api/v1/management/sso_application_spec.rb | 106 ++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 282938f..58d5dc5 100644 --- a/README.md +++ b/README.md @@ -1251,8 +1251,6 @@ resp["apps"].each do |app| end ``` -STOPPPED AT UTILS!!!!! - ## API Rate Limits diff --git a/lib/descope/api/v1/management/sso_application.rb b/lib/descope/api/v1/management/sso_application.rb index 24c3a5e..6119856 100644 --- a/lib/descope/api/v1/management/sso_application.rb +++ b/lib/descope/api/v1/management/sso_application.rb @@ -47,9 +47,11 @@ def create_saml_application( # but can be provided explicitly if needed. Both the name and ID must be unique per project. if use_metadata_info - raise 'metadata_url argument must be set' unless metadata_url + raise Descope::ArgumentException.new('metadata_url argument must be set', code: 400) unless metadata_url else - raise 'entity_id, acs_url, certificate arguments must be set' unless entity_id && acs_url && certificate + unless entity_id && acs_url && certificate + raise Descope::ArgumentException.new('entity_id, acs_url, certificate arguments must be set', code: 400) + end end attribute_mapping ||= [] diff --git a/spec/lib.descope/api/v1/management/sso_application_spec.rb b/spec/lib.descope/api/v1/management/sso_application_spec.rb index f0e4eb3..b61e709 100644 --- a/spec/lib.descope/api/v1/management/sso_application_spec.rb +++ b/spec/lib.descope/api/v1/management/sso_application_spec.rb @@ -107,5 +107,111 @@ ) end.not_to raise_error end + + it 'is expected to raise error if metadata_url is empty' do + expect do + @instance.create_saml_application( + name: 'test', + login_page_url: 'https://dummy.com/login', + id: 'tenant1', + description: 'awesome tenant', + logo: 'https://logo.com', + enabled: true, + use_metadata_info: true, + entity_id: 'ent1234', + acs_url: 'https://dummy.com/acs', + certificate: 'something', + attribute_mapping: [ + { + 'abc': '123' + } + ], + groups_mapping: [ + { + 'abc': '123' + } + ], + acs_allowed_callbacks: true, + subject_name_id_type: 'test', + subject_name_id_format: 'test', + default_relay_state: 'test', + force_authentication: true, + logout_redirect_url: 'https://dummy.com/logout' + ) + end.to raise_error(Descope::ArgumentException, 'metadata_url argument must be set') + end + end + + it 'is expected to raise error if entity_id acs_url and certificate arguments are missing' do + expect do + @instance.create_saml_application( + name: 'test', + login_page_url: 'https://dummy.com/login', + id: 'tenant1', + description: 'awesome tenant', + logo: 'https://logo.com', + enabled: true, + attribute_mapping: [ + { + 'abc': '123' + } + ], + groups_mapping: [ + { + 'abc': '123' + } + ], + acs_allowed_callbacks: true, + subject_name_id_type: 'test', + subject_name_id_format: 'test', + default_relay_state: 'test', + force_authentication: true, + logout_redirect_url: 'https://dummy.com/logout' + ) + end.to raise_error(Descope::ArgumentException, 'entity_id, acs_url, certificate arguments must be set') + end + + it 'is expected to update sso oidc application' do + expect(@instance).to receive(:post).with( + SSO_APPLICATION_OIDC_UPDATE_PATH, { + id: 'tenant1', + name: 'test', + description: 'awesome tenant', + enabled: true, + logo: 'https://logo.com', + loginPageUrl: 'https://dummy.com/login' + } + ) + expect do + @instance.update_sso_oidc_app( + id: 'tenant1', + name: 'test', + description: 'awesome tenant', + enabled: true, + logo: 'https://logo.com', + login_page_url: 'https://dummy.com/login' + ) + end.not_to raise_error + end + + it 'is expected to delete sso app' do + expect(@instance).to receive(:delete).with( + SSO_APPLICATION_DELETE_PATH, { id: 'tenant1' } + ) + expect { @instance.delete_sso_app('tenant1') }.not_to raise_error + end + + it 'is expected to load sso app' do + expect(@instance).to receive(:get).with( + SSO_APPLICATION_LOAD_PATH, { id: 'tenant1' } + ) + expect { @instance.load_sso_app('tenant1') }.not_to raise_error + end + + it 'is expected to load all sso apps' do + expect(@instance).to receive(:get).with( + SSO_APPLICATION_LOAD_ALL_PATH, {} + ) + expect { @instance.load_all_sso_apps }.not_to raise_error end end