From 360c825ec33853620a3b7286dbd07e2956f246af Mon Sep 17 00:00:00 2001 From: Bar Saar Date: Sun, 11 Feb 2024 10:17:04 +0200 Subject: [PATCH] added tenant get/set settings (#334) added password tenant get/set settings added to readme added tests --- README.md | 45 +++++++++++- examples/managementCli/package-lock.json | 42 +++++------ examples/managementCli/src/index.ts | 20 ++++++ lib/management/index.ts | 2 + lib/management/password.test.ts | 91 ++++++++++++++++++++++++ lib/management/password.ts | 27 +++++++ lib/management/paths.ts | 4 ++ lib/management/tenant.test.ts | 78 +++++++++++++++++++- lib/management/tenant.ts | 20 +++++- lib/management/types.ts | 38 ++++++++++ 10 files changed, 343 insertions(+), 24 deletions(-) create mode 100644 lib/management/password.test.ts create mode 100644 lib/management/password.ts diff --git a/README.md b/README.md index 1ee665ac..5ab80654 100644 --- a/README.md +++ b/README.md @@ -524,7 +524,7 @@ const descopeClient = DescopeClient({ ### Manage Tenants -You can create, update, delete or load tenants: +You can create, update, delete or load tenants, as well as read and update tenant settings: ```typescript // The self provisioning domains or optional. If given they'll be used to associate @@ -563,6 +563,49 @@ const searchRes = await descopeClient.management.tenant.searchAll(['id']); searchRes.data.forEach((tenant) => { // do something }); + +// Load tenant settings by id +const tenantSettings = await descopeClient.management.tenant.getSettings('my-tenant-id'); + +// Update will override all fields as is. Use carefully. +await descopeClient.management.tenant.configureSettings('my-tenant-id', { + domains: ['domain1.com'], + selfProvisioningDomains: ['domain1.com'], + sessionSettingsEnabled: true, + refreshTokenExpiration: 12, + refreshTokenExpirationUnit: 'days', + sessionTokenExpiration: 10, + sessionTokenExpirationUnit: 'minutes', + enableInactivity: true, + JITDisabled: false, + InactivityTime: 10, + InactivityTimeUnit: 'minutes', +}); +``` + +### Manage Password + +You can read and update any tenant password settings and policy: + +```typescript +// Load tenant password settings by id +const passwordSettings = await descopeClient.management.password.getSettings('my-tenant-id'); + +// Update will override all fields as is. Use carefully. +await descopeClient.management.password.configureSettings('my-tenant-id', { + enabled: true, + minLength: 8, + expiration: true, + expirationWeeks: 4, + lock: true, + lockAttempts: 5, + reuse: true, + reuseAmount: 6, + lowercase: true, + uppercase: false, + number: true, + nonAlphaNumeric: false, +}); ``` ### Manage SSO applications diff --git a/examples/managementCli/package-lock.json b/examples/managementCli/package-lock.json index 2eae28b5..0ba11c07 100644 --- a/examples/managementCli/package-lock.json +++ b/examples/managementCli/package-lock.json @@ -24,12 +24,12 @@ }, "../..": { "name": "@descope/node-sdk", - "version": "1.5.7", + "version": "1.6.3", "license": "MIT", "dependencies": { - "@descope/core-js-sdk": "1.6.4", - "jose": "4.15.1", - "node-fetch-commonjs": "3.3.2", + "@descope/core-js-sdk": "2.9.0", + "cross-fetch": "^4.0.0", + "jose": "4.15.4", "tslib": "^1.14.1" }, "devDependencies": { @@ -38,20 +38,20 @@ "@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-replace": "^5.0.0", "@rollup/plugin-typescript": "^8.3.0", - "@size-limit/preset-small-lib": "^8.0.0", + "@size-limit/preset-small-lib": "^11.0.0", "@types/jest": "^29.0.0", "@types/jsonwebtoken": "^9.0.0", - "@types/node": "^15.14.9", + "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^5.25.0", "@typescript-eslint/parser": "^5.27.0", "eslint": "^8.15.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^27.0.0", - "eslint-plugin-jest-dom": "^4.0.2", + "eslint-plugin-jest-dom": "^5.0.0", "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-no-only-tests": "^3.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", @@ -59,9 +59,9 @@ "husky": "^8.0.1", "jest": "^29.0.0", "jsdoc": "^4.0.0", - "lint-staged": "^13.0.3", + "lint-staged": "^15.0.0", "nock": "^13.2.4", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "pretty-quick": "^3.1.3", "rollup": "^2.62.0", "rollup-plugin-auto-external": "^2.0.0", @@ -69,7 +69,7 @@ "rollup-plugin-define": "^1.0.1", "rollup-plugin-delete": "^2.0.0", "rollup-plugin-dts": "^4.2.2", - "rollup-plugin-esbuild": "^5.0.0", + "rollup-plugin-esbuild": "^6.0.0", "rollup-plugin-inject-process-env": "^1.3.1", "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-terser": "^7.0.2", @@ -1116,38 +1116,38 @@ "@descope/node-sdk": { "version": "file:../..", "requires": { - "@descope/core-js-sdk": "1.6.4", + "@descope/core-js-sdk": "2.9.0", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-replace": "^5.0.0", "@rollup/plugin-typescript": "^8.3.0", - "@size-limit/preset-small-lib": "^8.0.0", + "@size-limit/preset-small-lib": "^11.0.0", "@types/jest": "^29.0.0", "@types/jsonwebtoken": "^9.0.0", - "@types/node": "^15.14.9", + "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^5.25.0", "@typescript-eslint/parser": "^5.27.0", + "cross-fetch": "^4.0.0", "eslint": "^8.15.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^27.0.0", - "eslint-plugin-jest-dom": "^4.0.2", + "eslint-plugin-jest-dom": "^5.0.0", "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-no-only-tests": "^3.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^4.0.0", "husky": "^8.0.1", "jest": "^29.0.0", - "jose": "4.15.1", + "jose": "4.15.4", "jsdoc": "^4.0.0", - "lint-staged": "^13.0.3", + "lint-staged": "^15.0.0", "nock": "^13.2.4", - "node-fetch-commonjs": "3.3.2", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "pretty-quick": "^3.1.3", "rollup": "^2.62.0", "rollup-plugin-auto-external": "^2.0.0", @@ -1155,7 +1155,7 @@ "rollup-plugin-define": "^1.0.1", "rollup-plugin-delete": "^2.0.0", "rollup-plugin-dts": "^4.2.2", - "rollup-plugin-esbuild": "^5.0.0", + "rollup-plugin-esbuild": "^6.0.0", "rollup-plugin-inject-process-env": "^1.3.1", "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-terser": "^7.0.2", diff --git a/examples/managementCli/src/index.ts b/examples/managementCli/src/index.ts index 605eff15..135d1f17 100644 --- a/examples/managementCli/src/index.ts +++ b/examples/managementCli/src/index.ts @@ -296,6 +296,26 @@ program handleSdkRes(await sdk.management.tenant.loadAll()); }); +// tenant-settings +program + .command('tenant-settings') + .description('Load tenant settings by id') + .argument('', 'Tenant ID') + .action(async (id) => { + handleSdkRes(await sdk.management.tenant.getSettings(id)); + }); + +// *** Password commands *** + +// password-settings +program + .command('password-settings') + .description('Load password settings by tenant id') + .argument('', 'Tenant ID') + .action(async (tenantId) => { + handleSdkRes(await sdk.management.password.getSettings(tenantId)); + }); + // *** SSO application commands *** // sso-application-create-oidc diff --git a/lib/management/index.ts b/lib/management/index.ts index 75cf62f4..881705cd 100644 --- a/lib/management/index.ts +++ b/lib/management/index.ts @@ -13,6 +13,7 @@ import WithTheme from './theme'; import WithAudit from './audit'; import WithAuthz from './authz'; import withSSOApplication from './ssoapplication'; +import withPassword from './password'; /** Constructs a higher level Management API that wraps the functions from code-js-sdk */ const withManagement = (sdk: CoreSdk, managementKey?: string) => ({ @@ -24,6 +25,7 @@ const withManagement = (sdk: CoreSdk, managementKey?: string) => ({ sso: withSSOSettings(sdk, managementKey), jwt: withJWT(sdk, managementKey), permission: withPermission(sdk, managementKey), + password: withPassword(sdk, managementKey), role: withRole(sdk, managementKey), group: withGroup(sdk, managementKey), flow: WithFlow(sdk, managementKey), diff --git a/lib/management/password.test.ts b/lib/management/password.test.ts new file mode 100644 index 00000000..9ca714e3 --- /dev/null +++ b/lib/management/password.test.ts @@ -0,0 +1,91 @@ +import { SdkResponse } from '@descope/core-js-sdk'; +import withManagement from '.'; +import apiPaths from './paths'; +import { PasswordSettings } from './types'; +import { mockCoreSdk, mockHttpClient } from './testutils'; + +const management = withManagement(mockCoreSdk, 'key'); + +const mockPasswordSettings: PasswordSettings = { + enabled: true, + minLength: 8, + expiration: true, + expirationWeeks: 4, + lock: true, + lockAttempts: 5, + reuse: true, + reuseAmount: 6, + lowercase: true, + uppercase: false, + number: true, + nonAlphaNumeric: false, +}; + +describe('Management Password', () => { + afterEach(() => { + jest.clearAllMocks(); + mockHttpClient.reset(); + }); + + describe('getSettings', () => { + it('should send the correct request and receive correct response', async () => { + const httpResponse = { + ok: true, + json: () => mockPasswordSettings, + clone: () => ({ + json: () => Promise.resolve(mockPasswordSettings), + }), + status: 200, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const resp: SdkResponse = await management.password.getSettings('test'); + + expect(mockHttpClient.get).toHaveBeenCalledWith(apiPaths.password.settings, { + queryParams: { tenantId: 'test' }, + token: 'key', + }); + + expect(resp).toEqual({ + code: 200, + data: mockPasswordSettings, + ok: true, + response: httpResponse, + }); + }); + }); + + describe('configureSettings', () => { + it('should send the correct request and receive correct response', async () => { + const httpResponse = { + ok: true, + json: () => {}, + clone: () => ({ + json: () => Promise.resolve(), + }), + status: 200, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const resp: SdkResponse = await management.password.configureSettings( + 'test', + mockPasswordSettings, + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + apiPaths.password.settings, + { ...mockPasswordSettings, tenantId: 'test' }, + { + token: 'key', + }, + ); + + expect(resp).toEqual({ + code: 200, + data: undefined, + ok: true, + response: httpResponse, + }); + }); + }); +}); diff --git a/lib/management/password.ts b/lib/management/password.ts new file mode 100644 index 00000000..35fb8389 --- /dev/null +++ b/lib/management/password.ts @@ -0,0 +1,27 @@ +import { SdkResponse, transformResponse } from '@descope/core-js-sdk'; +import { CoreSdk } from '../types'; +import apiPaths from './paths'; +import { PasswordSettings } from './types'; + +const withPassword = (sdk: CoreSdk, managementKey?: string) => ({ + getSettings: (tenantId: string): Promise> => + transformResponse( + sdk.httpClient.get(apiPaths.password.settings, { + queryParams: { tenantId }, + token: managementKey, + }), + (data) => data, + ), + configureSettings: (tenantId: string, settings: PasswordSettings): Promise> => + transformResponse( + sdk.httpClient.post( + apiPaths.password.settings, + { ...settings, tenantId }, + { + token: managementKey, + }, + ), + ), +}); + +export default withPassword; diff --git a/lib/management/paths.ts b/lib/management/paths.ts index 0035a77d..6abcec81 100644 --- a/lib/management/paths.ts +++ b/lib/management/paths.ts @@ -54,6 +54,7 @@ export default { update: '/v1/mgmt/tenant/update', delete: '/v1/mgmt/tenant/delete', load: '/v1/mgmt/tenant', + settings: '/v1/mgmt/tenant/settings', loadAll: '/v1/mgmt/tenant/all', searchAll: '/v1/mgmt/tenant/search', }, @@ -82,6 +83,9 @@ export default { jwt: { update: '/v1/mgmt/jwt/update', }, + password: { + settings: '/v1/mgmt/password/settings', + }, permission: { create: '/v1/mgmt/permission/create', update: '/v1/mgmt/permission/update', diff --git a/lib/management/tenant.test.ts b/lib/management/tenant.test.ts index d25c1105..30d58ebe 100644 --- a/lib/management/tenant.test.ts +++ b/lib/management/tenant.test.ts @@ -1,7 +1,7 @@ import { SdkResponse } from '@descope/core-js-sdk'; import withManagement from '.'; import apiPaths from './paths'; -import { CreateTenantResponse, Tenant } from './types'; +import { CreateTenantResponse, Tenant, TenantSettings } from './types'; import { mockCoreSdk, mockHttpClient } from './testutils'; const management = withManagement(mockCoreSdk, 'key'); @@ -16,6 +16,20 @@ const mockTenants = [ { id: 't3', name: 'name3', selfProvisioningDomains: ['domain3.com'] }, ]; +const mockSettings: TenantSettings = { + domains: ['domain1.com'], + selfProvisioningDomains: ['domain1.com'], + sessionSettingsEnabled: true, + refreshTokenExpiration: 12, + refreshTokenExpirationUnit: 'days', + sessionTokenExpiration: 10, + sessionTokenExpirationUnit: 'minutes', + enableInactivity: true, + JITDisabled: false, + InactivityTime: 10, + InactivityTimeUnit: 'minutes', +}; + const mockAllTenantsResponse = { tenants: mockTenants, }; @@ -202,4 +216,66 @@ describe('Management Tenant', () => { }); }); }); + + describe('getSettings', () => { + it('should send the correct request and receive correct response', async () => { + const httpResponse = { + ok: true, + json: () => mockSettings, + clone: () => ({ + json: () => Promise.resolve(mockSettings), + }), + status: 200, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const resp: SdkResponse = await management.tenant.getSettings('test'); + + expect(mockHttpClient.get).toHaveBeenCalledWith(apiPaths.tenant.settings, { + queryParams: { id: 'test' }, + token: 'key', + }); + + expect(resp).toEqual({ + code: 200, + data: mockSettings, + ok: true, + response: httpResponse, + }); + }); + }); + + describe('configureSettings', () => { + it('should send the correct request and receive correct response', async () => { + const httpResponse = { + ok: true, + json: () => {}, + clone: () => ({ + json: () => Promise.resolve(), + }), + status: 200, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const resp: SdkResponse = await management.tenant.configureSettings( + 'test', + mockSettings, + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + apiPaths.tenant.settings, + { ...mockSettings, tenantId: 'test' }, + { + token: 'key', + }, + ); + + expect(resp).toEqual({ + code: 200, + data: undefined, + ok: true, + response: httpResponse, + }); + }); + }); }); diff --git a/lib/management/tenant.ts b/lib/management/tenant.ts index a3b0fd41..fa9ef7d4 100644 --- a/lib/management/tenant.ts +++ b/lib/management/tenant.ts @@ -1,7 +1,7 @@ import { SdkResponse, transformResponse } from '@descope/core-js-sdk'; import { CoreSdk } from '../types'; import apiPaths from './paths'; -import { CreateTenantResponse, Tenant, AttributesTypes } from './types'; +import { CreateTenantResponse, Tenant, AttributesTypes, TenantSettings } from './types'; type MultipleTenantResponse = { tenants: Tenant[]; @@ -84,6 +84,24 @@ const withTenant = (sdk: CoreSdk, managementKey?: string) => ({ ), (data) => data.tenants, ), + getSettings: (tenantId: string): Promise> => + transformResponse( + sdk.httpClient.get(apiPaths.tenant.settings, { + queryParams: { id: tenantId }, + token: managementKey, + }), + (data) => data, + ), + configureSettings: (tenantId: string, settings: TenantSettings): Promise> => + transformResponse( + sdk.httpClient.post( + apiPaths.tenant.settings, + { ...settings, tenantId }, + { + token: managementKey, + }, + ), + ), }); export default withTenant; diff --git a/lib/management/types.ts b/lib/management/types.ts index 87317f12..e0ece9dd 100644 --- a/lib/management/types.ts +++ b/lib/management/types.ts @@ -1,5 +1,7 @@ import { UserResponse } from '@descope/core-js-sdk'; +export type ExpirationUnit = 'minutes' | 'hours' | 'days' | 'weeks'; + /** * Represents a tenant association for a User or Access Key. The tenantId is required to denote * which tenant the user or access key belongs to. The roleNames array is an optional list of @@ -140,6 +142,42 @@ export type Tenant = { authType?: 'none' | 'saml' | 'oidc'; }; +/** Represents settings of a tenant in a project. It has an id, a name and an array of + * self provisioning domains used to associate users with that tenant. + */ +export type TenantSettings = { + selfProvisioningDomains: string[]; + domains?: string[]; + authType?: 'none' | 'saml' | 'oidc'; + sessionSettingsEnabled?: boolean; + refreshTokenExpiration?: number; + refreshTokenExpirationUnit?: ExpirationUnit; + sessionTokenExpiration?: number; + sessionTokenExpirationUnit?: ExpirationUnit; + stepupTokenExpiration?: number; + stepupTokenExpirationUnit?: ExpirationUnit; + enableInactivity?: boolean; + InactivityTime?: number; + InactivityTimeUnit?: ExpirationUnit; + JITDisabled?: boolean; +}; + +/** Represents password settings of a tenant in a project. It has the password policy details. */ +export type PasswordSettings = { + enabled: boolean; + minLength: number; + lowercase: boolean; + uppercase: boolean; + number: boolean; + nonAlphaNumeric: boolean; + expiration: boolean; + expirationWeeks: number; + reuse: boolean; + reuseAmount: number; + lock: boolean; + lockAttempts: number; +}; + /** Represents OIDC settings of an SSO application in a project. */ export type SSOApplicationOIDCSettings = { loginPageUrl: string;