From 9e1a8cea3801e0d09388bb78b6417e2d77e0b029 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 21 Jun 2023 16:25:19 +0530 Subject: [PATCH] fix: multitenancy recipe (#594) * fix: impl and test * fix: version * fix: impl * fix: pr comments * fix: version update * fix: pr comments --- lib/build/recipe/emailpassword/index.d.ts | 2 +- .../recipe/multitenancy/api/implementation.js | 3 +- lib/build/recipe/multitenancy/index.d.ts | 40 ++- lib/build/recipe/multitenancy/index.js | 31 +- .../multitenancy/recipeImplementation.d.ts | 2 +- .../multitenancy/recipeImplementation.js | 122 +++++-- lib/build/recipe/multitenancy/types.d.ts | 35 +- lib/build/recipe/passwordless/index.d.ts | 2 +- .../recipe/session/recipeImplementation.js | 1 + .../recipe/thirdparty/providers/apple.js | 2 +- .../thirdparty/providers/configUtils.d.ts | 1 - .../thirdparty/providers/configUtils.js | 3 +- .../recipe/thirdparty/providers/custom.js | 16 +- .../recipe/thirdparty/recipeImplementation.js | 3 +- lib/build/recipe/thirdparty/types.d.ts | 3 +- .../recipe/thirdpartyemailpassword/index.d.ts | 2 +- .../recipe/thirdpartypasswordless/index.d.ts | 2 +- lib/build/version.js | 2 +- .../recipe/multitenancy/api/implementation.ts | 8 +- lib/ts/recipe/multitenancy/index.ts | 59 ++- .../multitenancy/recipeImplementation.ts | 105 ++++-- lib/ts/recipe/multitenancy/types.ts | 33 +- lib/ts/recipe/session/recipeImplementation.ts | 1 + lib/ts/recipe/thirdparty/providers/apple.ts | 2 +- .../thirdparty/providers/configUtils.ts | 2 - lib/ts/recipe/thirdparty/providers/custom.ts | 16 +- .../recipe/thirdparty/recipeImplementation.ts | 3 +- lib/ts/recipe/thirdparty/types.ts | 3 +- lib/ts/version.ts | 2 +- test/multitenancy/tenants-crud.test.js | 336 ++++++++++++++++++ test/nextjs.test.js | 2 +- test/utils.js | 11 + test/with-typescript/index.ts | 14 +- 33 files changed, 710 insertions(+), 159 deletions(-) create mode 100644 test/multitenancy/tenants-crud.test.js diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts index 2097f1a0e..07edde3e8 100644 --- a/lib/build/recipe/emailpassword/index.d.ts +++ b/lib/build/recipe/emailpassword/index.d.ts @@ -66,7 +66,7 @@ export default class Wrapper { applyPasswordPolicy?: boolean; }): Promise< | { - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR"; + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; diff --git a/lib/build/recipe/multitenancy/api/implementation.js b/lib/build/recipe/multitenancy/api/implementation.js index a92ba5aa4..f8793cafe 100644 --- a/lib/build/recipe/multitenancy/api/implementation.js +++ b/lib/build/recipe/multitenancy/api/implementation.js @@ -36,14 +36,13 @@ function getAPIInterface() { return { loginMethodsGET: function ({ tenantId, clientType, options, userContext }) { return __awaiter(this, void 0, void 0, function* () { - const tenantConfigRes = yield options.recipeImplementation.getTenantConfig({ + const tenantConfigRes = yield options.recipeImplementation.getTenant({ tenantId, userContext, }); const providerInputsFromStatic = options.staticThirdPartyProviders; const providerConfigsFromCore = tenantConfigRes.thirdParty.providers; const mergedProviders = configUtils_1.mergeProvidersFromCoreAndStatic( - tenantId, providerConfigsFromCore, providerInputsFromStatic ); diff --git a/lib/build/recipe/multitenancy/index.d.ts b/lib/build/recipe/multitenancy/index.d.ts index cabb5677f..95478d47f 100644 --- a/lib/build/recipe/multitenancy/index.d.ts +++ b/lib/build/recipe/multitenancy/index.d.ts @@ -12,6 +12,9 @@ export default class Wrapper { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled: boolean; + coreConfig?: { + [key: string]: any; + }; }, userContext?: any ): Promise<{ @@ -23,9 +26,9 @@ export default class Wrapper { userContext?: any ): Promise<{ status: "OK"; - tenantExisted: boolean; + didExist: boolean; }>; - static getTenantConfig( + static getTenant( tenantId?: string, userContext?: any ): Promise<{ @@ -40,6 +43,9 @@ export default class Wrapper { enabled: boolean; providers: ProviderConfig[]; }; + coreConfig: { + [key: string]: any; + }; }>; static listAllTenants( userContext?: any @@ -48,6 +54,7 @@ export default class Wrapper { tenants: string[]; }>; static createOrUpdateThirdPartyConfig( + tenantId: string | undefined, config: ProviderConfig, skipValidation?: boolean, userContext?: any @@ -63,22 +70,41 @@ export default class Wrapper { status: "OK"; didConfigExist: boolean; }>; - static listThirdPartyConfigsForThirdPartyId( - thirdPartyId: string, + static associateUserToTenant( + tenantId: string | undefined, + userId: string, + userContext?: any + ): Promise< + | { + status: "OK"; + wasAlreadyAssociated: boolean; + } + | { + status: + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" + | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; + } + >; + static disassociateUserFromTenant( + tenantId: string | undefined, + userId: string, userContext?: any ): Promise<{ status: "OK"; - providers: ProviderConfig[]; + wasAssociated: boolean; }>; } export declare let init: typeof Recipe.init; export declare let createOrUpdateTenant: typeof Wrapper.createOrUpdateTenant; export declare let deleteTenant: typeof Wrapper.deleteTenant; -export declare let getTenantConfig: typeof Wrapper.getTenantConfig; +export declare let getTenant: typeof Wrapper.getTenant; export declare let listAllTenants: typeof Wrapper.listAllTenants; export declare let createOrUpdateThirdPartyConfig: typeof Wrapper.createOrUpdateThirdPartyConfig; export declare let deleteThirdPartyConfig: typeof Wrapper.deleteThirdPartyConfig; -export declare let listThirdPartyConfigsForThirdPartyId: typeof Wrapper.listThirdPartyConfigsForThirdPartyId; +export declare let associateUserToTenant: typeof Wrapper.associateUserToTenant; +export declare let disassociateUserFromTenant: typeof Wrapper.disassociateUserFromTenant; export { RecipeDisabledForTenantError, TenantDoesNotExistError }; export { AllowedDomainsClaim }; export type { RecipeInterface, APIOptions, APIInterface }; diff --git a/lib/build/recipe/multitenancy/index.js b/lib/build/recipe/multitenancy/index.js index 41da00ed9..66256d70c 100644 --- a/lib/build/recipe/multitenancy/index.js +++ b/lib/build/recipe/multitenancy/index.js @@ -50,7 +50,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.AllowedDomainsClaim = exports.TenantDoesNotExistError = exports.RecipeDisabledForTenantError = exports.listThirdPartyConfigsForThirdPartyId = exports.deleteThirdPartyConfig = exports.createOrUpdateThirdPartyConfig = exports.listAllTenants = exports.getTenantConfig = exports.deleteTenant = exports.createOrUpdateTenant = exports.init = void 0; +exports.AllowedDomainsClaim = exports.TenantDoesNotExistError = exports.RecipeDisabledForTenantError = exports.disassociateUserFromTenant = exports.associateUserToTenant = exports.deleteThirdPartyConfig = exports.createOrUpdateThirdPartyConfig = exports.listAllTenants = exports.getTenant = exports.deleteTenant = exports.createOrUpdateTenant = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = require("./error"); Object.defineProperty(exports, "RecipeDisabledForTenantError", { @@ -92,10 +92,10 @@ class Wrapper { }); }); } - static getTenantConfig(tenantId, userContext) { + static getTenant(tenantId, userContext) { return __awaiter(this, void 0, void 0, function* () { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.getTenantConfig({ + return recipeInstance.recipeInterfaceImpl.getTenant({ tenantId, userContext: userContext === undefined ? {} : userContext, }); @@ -109,10 +109,11 @@ class Wrapper { }); }); } - static createOrUpdateThirdPartyConfig(config, skipValidation, userContext) { + static createOrUpdateThirdPartyConfig(tenantId, config, skipValidation, userContext) { return __awaiter(this, void 0, void 0, function* () { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.createOrUpdateThirdPartyConfig({ + tenantId, config, skipValidation, userContext: userContext === undefined ? {} : userContext, @@ -129,11 +130,22 @@ class Wrapper { }); }); } - static listThirdPartyConfigsForThirdPartyId(thirdPartyId, userContext) { + static associateUserToTenant(tenantId, userId, userContext) { return __awaiter(this, void 0, void 0, function* () { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.listThirdPartyConfigsForThirdPartyId({ - thirdPartyId, + return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ + tenantId, + userId, + userContext: userContext === undefined ? {} : userContext, + }); + }); + } + static disassociateUserFromTenant(tenantId, userId, userContext) { + return __awaiter(this, void 0, void 0, function* () { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ + tenantId, + userId, userContext: userContext === undefined ? {} : userContext, }); }); @@ -144,8 +156,9 @@ Wrapper.init = recipe_1.default.init; exports.init = Wrapper.init; exports.createOrUpdateTenant = Wrapper.createOrUpdateTenant; exports.deleteTenant = Wrapper.deleteTenant; -exports.getTenantConfig = Wrapper.getTenantConfig; +exports.getTenant = Wrapper.getTenant; exports.listAllTenants = Wrapper.listAllTenants; exports.createOrUpdateThirdPartyConfig = Wrapper.createOrUpdateThirdPartyConfig; exports.deleteThirdPartyConfig = Wrapper.deleteThirdPartyConfig; -exports.listThirdPartyConfigsForThirdPartyId = Wrapper.listThirdPartyConfigsForThirdPartyId; +exports.associateUserToTenant = Wrapper.associateUserToTenant; +exports.disassociateUserFromTenant = Wrapper.disassociateUserFromTenant; diff --git a/lib/build/recipe/multitenancy/recipeImplementation.d.ts b/lib/build/recipe/multitenancy/recipeImplementation.d.ts index 2c970ac36..6a2182ed3 100644 --- a/lib/build/recipe/multitenancy/recipeImplementation.d.ts +++ b/lib/build/recipe/multitenancy/recipeImplementation.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck import { RecipeInterface } from "./"; import { Querier } from "../../querier"; -export default function getRecipeInterface(_: Querier): RecipeInterface; +export default function getRecipeInterface(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/multitenancy/recipeImplementation.js b/lib/build/recipe/multitenancy/recipeImplementation.js index 38514fa75..95fb3dc8a 100644 --- a/lib/build/recipe/multitenancy/recipeImplementation.js +++ b/lib/build/recipe/multitenancy/recipeImplementation.js @@ -30,66 +30,122 @@ var __awaiter = step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; Object.defineProperty(exports, "__esModule", { value: true }); -function getRecipeInterface(_) { +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const constants_1 = require("./constants"); +function getRecipeInterface(querier) { return { getTenantId: function ({ tenantIdFromFrontend }) { return __awaiter(this, void 0, void 0, function* () { - // TODO return tenantIdFromFrontend; }); }, - createOrUpdateTenant: function () { + createOrUpdateTenant: function ({ tenantId, config }) { return __awaiter(this, void 0, void 0, function* () { - // TODO - throw new Error("Not implemented"); + let response = yield querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/multitenancy/tenant`), + Object.assign({ tenantId }, config) + ); + return response; }); }, - deleteTenant: function () { + deleteTenant: function ({ tenantId }) { return __awaiter(this, void 0, void 0, function* () { - // TODO - throw new Error("Not implemented"); + let response = yield querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/remove`), + { + tenantId, + } + ); + return response; }); }, - getTenantConfig: function () { + getTenant: function ({ tenantId }) { return __awaiter(this, void 0, void 0, function* () { - return { - status: "OK", - emailPassword: { - enabled: true, - }, - passwordless: { - enabled: true, - }, - thirdParty: { - enabled: true, - providers: [], - }, - }; + let response = yield querier.sendGetRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/tenant` + ), + {} + ); + return response; }); }, listAllTenants: function () { return __awaiter(this, void 0, void 0, function* () { - // TODO - throw new Error("Not implemented"); + let response = yield querier.sendGetRequest( + new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/list`), + {} + ); + return response; + }); + }, + createOrUpdateThirdPartyConfig: function ({ tenantId, config, skipValidation }) { + return __awaiter(this, void 0, void 0, function* () { + let response = yield querier.sendPutRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/config/thirdparty` + ), + { + config, + skipValidation, + } + ); + return response; }); }, - createOrUpdateThirdPartyConfig: function () { + deleteThirdPartyConfig: function ({ tenantId, thirdPartyId }) { return __awaiter(this, void 0, void 0, function* () { - // TODO - throw new Error("Not implemented"); + let response = yield querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/config/thirdparty/remove` + ), + { + thirdPartyId, + } + ); + return response; }); }, - deleteThirdPartyConfig: function () { + associateUserToTenant: function ({ tenantId, userId }) { return __awaiter(this, void 0, void 0, function* () { - // TODO - throw new Error("Not implemented"); + let response = yield querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/tenant/user` + ), + { + userId, + } + ); + return response; }); }, - listThirdPartyConfigsForThirdPartyId: function () { + disassociateUserFromTenant: function ({ tenantId, userId }) { return __awaiter(this, void 0, void 0, function* () { - // TODO - throw new Error("Not implemented"); + let response = yield querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/tenant/user/remove` + ), + { + userId, + } + ); + return response; }); }, }; diff --git a/lib/build/recipe/multitenancy/types.d.ts b/lib/build/recipe/multitenancy/types.d.ts index f1046c903..18f2bc56c 100644 --- a/lib/build/recipe/multitenancy/types.d.ts +++ b/lib/build/recipe/multitenancy/types.d.ts @@ -59,6 +59,9 @@ export declare type RecipeInterface = { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled?: boolean; + coreConfig?: { + [key: string]: any; + }; }; userContext: any; }) => Promise<{ @@ -70,9 +73,9 @@ export declare type RecipeInterface = { userContext: any; }) => Promise<{ status: "OK"; - tenantExisted: boolean; + didExist: boolean; }>; - getTenantConfig: (input: { + getTenant: (input: { tenantId?: string; userContext: any; }) => Promise<{ @@ -87,6 +90,9 @@ export declare type RecipeInterface = { enabled: boolean; providers: ProviderConfig[]; }; + coreConfig: { + [key: string]: any; + }; }>; listAllTenants: (input: { userContext: any; @@ -95,6 +101,7 @@ export declare type RecipeInterface = { tenants: string[]; }>; createOrUpdateThirdPartyConfig: (input: { + tenantId?: string; config: ProviderConfig; skipValidation?: boolean; userContext: any; @@ -110,12 +117,30 @@ export declare type RecipeInterface = { status: "OK"; didConfigExist: boolean; }>; - listThirdPartyConfigsForThirdPartyId: (input: { - thirdPartyId: string; + associateUserToTenant: (input: { + tenantId?: string; + userId: string; + userContext: any; + }) => Promise< + | { + status: "OK"; + wasAlreadyAssociated: boolean; + } + | { + status: + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" + | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; + } + >; + disassociateUserFromTenant: (input: { + tenantId?: string; + userId: string; userContext: any; }) => Promise<{ status: "OK"; - providers: ProviderConfig[]; + wasAssociated: boolean; }>; }; export declare type APIOptions = { diff --git a/lib/build/recipe/passwordless/index.d.ts b/lib/build/recipe/passwordless/index.d.ts index f8966f784..080d1a770 100644 --- a/lib/build/recipe/passwordless/index.d.ts +++ b/lib/build/recipe/passwordless/index.d.ts @@ -90,7 +90,7 @@ export default class Wrapper { phoneNumber?: string | null; userContext?: any; }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; }>; static revokeAllCodes( input: diff --git a/lib/build/recipe/session/recipeImplementation.js b/lib/build/recipe/session/recipeImplementation.js index 293078de5..c4a1835cf 100644 --- a/lib/build/recipe/session/recipeImplementation.js +++ b/lib/build/recipe/session/recipeImplementation.js @@ -93,6 +93,7 @@ exports.protectedProps = [ "parentRefreshTokenHash1", "refreshTokenHash1", "antiCsrfToken", + "tId", ]; function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverrides) { const JWKS = querier.getAllCoreUrlsForPath("/.well-known/jwks.json").map((url) => diff --git a/lib/build/recipe/thirdparty/providers/apple.js b/lib/build/recipe/thirdparty/providers/apple.js index 7e260f305..b555e360e 100644 --- a/lib/build/recipe/thirdparty/providers/apple.js +++ b/lib/build/recipe/thirdparty/providers/apple.js @@ -128,7 +128,7 @@ function Apple(input) { ); } config.clientSecret = getClientSecret( - config.clientID, + config.clientId, config.additionalConfig.keyId, config.additionalConfig.teamId, config.additionalConfig.privateKey diff --git a/lib/build/recipe/thirdparty/providers/configUtils.d.ts b/lib/build/recipe/thirdparty/providers/configUtils.d.ts index ff78b0245..b4de2c594 100644 --- a/lib/build/recipe/thirdparty/providers/configUtils.d.ts +++ b/lib/build/recipe/thirdparty/providers/configUtils.d.ts @@ -18,7 +18,6 @@ export declare function findAndCreateProviderInstance( ): Promise; export declare function mergeConfig(staticConfig: ProviderConfig, coreConfig: ProviderConfig): ProviderConfig; export declare function mergeProvidersFromCoreAndStatic( - tenantId: string | undefined, providerConfigsFromCore: ProviderConfig[], providerInputsFromStatic: ProviderInput[] ): ProviderInput[]; diff --git a/lib/build/recipe/thirdparty/providers/configUtils.js b/lib/build/recipe/thirdparty/providers/configUtils.js index 72057cb8a..001f654aa 100644 --- a/lib/build/recipe/thirdparty/providers/configUtils.js +++ b/lib/build/recipe/thirdparty/providers/configUtils.js @@ -126,11 +126,10 @@ function mergeConfig(staticConfig, coreConfig) { return result; } exports.mergeConfig = mergeConfig; -function mergeProvidersFromCoreAndStatic(tenantId, providerConfigsFromCore, providerInputsFromStatic) { +function mergeProvidersFromCoreAndStatic(providerConfigsFromCore, providerInputsFromStatic) { const mergedProviders = []; if (providerConfigsFromCore.length === 0) { for (const config of providerInputsFromStatic) { - config.config.tenantId = tenantId; mergedProviders.push(config); } } else { diff --git a/lib/build/recipe/thirdparty/providers/custom.js b/lib/build/recipe/thirdparty/providers/custom.js index a7011d68b..329c06d85 100644 --- a/lib/build/recipe/thirdparty/providers/custom.js +++ b/lib/build/recipe/thirdparty/providers/custom.js @@ -182,7 +182,7 @@ function NewProvider(input) { } let impl = { id: input.config.thirdPartyId, - config: Object.assign(Object.assign({}, input.config), { clientID: "temp" }), + config: Object.assign(Object.assign({}, input.config), { clientId: "temp" }), getConfigForClientType: function ({ clientType }) { return __awaiter(this, void 0, void 0, function* () { if (clientType === undefined) { @@ -204,7 +204,7 @@ function NewProvider(input) { getAuthorisationRedirectURL: function ({ redirectURIOnProviderDashboard }) { return __awaiter(this, void 0, void 0, function* () { const queryParams = { - client_id: impl.config.clientID, + client_id: impl.config.clientId, redirect_uri: redirectURIOnProviderDashboard, response_type: "code", }; @@ -232,8 +232,8 @@ function NewProvider(input) { } let url = impl.config.authorizationEndpoint; /* Transformation needed for dev keys BEGIN */ - if (isUsingDevelopmentClientId(impl.config.clientID)) { - queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientID); + if (isUsingDevelopmentClientId(impl.config.clientId)) { + queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); queryParams["actual_redirect_uri"] = url; url = DEV_OAUTH_AUTHORIZATION_URL; } @@ -255,7 +255,7 @@ function NewProvider(input) { } const tokenAPIURL = impl.config.tokenEndpoint; const accessTokenAPIParams = { - client_id: impl.config.clientID, + client_id: impl.config.clientId, redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, code: redirectURIInfo.redirectURIQueryParams["code"], grant_type: "authorization_code", @@ -274,8 +274,8 @@ function NewProvider(input) { } } /* Transformation needed for dev keys BEGIN */ - if (isUsingDevelopmentClientId(impl.config.clientID)) { - accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientID); + if (isUsingDevelopmentClientId(impl.config.clientId)) { + accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); accessTokenAPIParams["redirect_uri"] = DEV_OAUTH_REDIRECT_URL; } /* Transformation needed for dev keys END */ @@ -295,7 +295,7 @@ function NewProvider(input) { idToken, impl.config.jwksURI, { - audience: getActualClientIdFromDevelopmentClientId(impl.config.clientID), + audience: getActualClientIdFromDevelopmentClientId(impl.config.clientId), } ); if (impl.config.validateIdTokenPayload !== undefined) { diff --git a/lib/build/recipe/thirdparty/recipeImplementation.js b/lib/build/recipe/thirdparty/recipeImplementation.js index 9371b047a..20fecde52 100644 --- a/lib/build/recipe/thirdparty/recipeImplementation.js +++ b/lib/build/recipe/thirdparty/recipeImplementation.js @@ -108,9 +108,8 @@ function getRecipeImplementation(querier, providers) { getProvider: function ({ thirdPartyId, tenantId, clientType, userContext }) { return __awaiter(this, void 0, void 0, function* () { const mtRecipe = recipe_1.default.getInstanceOrThrowError(); - const tenantConfig = yield mtRecipe.recipeInterfaceImpl.getTenantConfig({ tenantId, userContext }); + const tenantConfig = yield mtRecipe.recipeInterfaceImpl.getTenant({ tenantId, userContext }); const mergedProviders = configUtils_1.mergeProvidersFromCoreAndStatic( - tenantId, tenantConfig.thirdParty.providers, providers ); diff --git a/lib/build/recipe/thirdparty/types.d.ts b/lib/build/recipe/thirdparty/types.d.ts index 3ddf8afb8..c69f7fb87 100644 --- a/lib/build/recipe/thirdparty/types.d.ts +++ b/lib/build/recipe/thirdparty/types.d.ts @@ -33,7 +33,7 @@ export declare type UserInfoMap = { }; export declare type ProviderClientConfig = { clientType?: string; - clientID: string; + clientId: string; clientSecret?: string; scope?: string[]; forcePKCE?: boolean; @@ -43,7 +43,6 @@ export declare type ProviderClientConfig = { }; declare type CommonProviderConfig = { thirdPartyId: string; - tenantId?: string; name?: string; authorizationEndpoint?: string; authorizationEndpointQueryParams?: { diff --git a/lib/build/recipe/thirdpartyemailpassword/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/index.d.ts index faef9a877..cc03b0a58 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/index.d.ts @@ -93,7 +93,7 @@ export default class Wrapper { applyPasswordPolicy?: boolean; }): Promise< | { - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR"; + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; diff --git a/lib/build/recipe/thirdpartypasswordless/index.d.ts b/lib/build/recipe/thirdpartypasswordless/index.d.ts index fbbe2021f..29b963ec8 100644 --- a/lib/build/recipe/thirdpartypasswordless/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/index.d.ts @@ -117,7 +117,7 @@ export default class Wrapper { phoneNumber?: string | null; userContext?: any; }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; }>; static revokeAllCodes( input: diff --git a/lib/build/version.js b/lib/build/version.js index a0857cb64..ebcb41e61 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -16,6 +16,6 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * under the License. */ exports.version = "14.1.2"; -exports.cdiSupported = ["2.21"]; +exports.cdiSupported = ["3.0"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.6"; diff --git a/lib/ts/recipe/multitenancy/api/implementation.ts b/lib/ts/recipe/multitenancy/api/implementation.ts index 4ee4db57f..f96029366 100644 --- a/lib/ts/recipe/multitenancy/api/implementation.ts +++ b/lib/ts/recipe/multitenancy/api/implementation.ts @@ -4,7 +4,7 @@ import { findAndCreateProviderInstance, mergeProvidersFromCoreAndStatic } from " export default function getAPIInterface(): APIInterface { return { loginMethodsGET: async function ({ tenantId, clientType, options, userContext }) { - const tenantConfigRes = await options.recipeImplementation.getTenantConfig({ + const tenantConfigRes = await options.recipeImplementation.getTenant({ tenantId, userContext, }); @@ -12,11 +12,7 @@ export default function getAPIInterface(): APIInterface { const providerInputsFromStatic = options.staticThirdPartyProviders; const providerConfigsFromCore = tenantConfigRes.thirdParty.providers; - const mergedProviders = mergeProvidersFromCoreAndStatic( - tenantId, - providerConfigsFromCore, - providerInputsFromStatic - ); + const mergedProviders = mergeProvidersFromCoreAndStatic(providerConfigsFromCore, providerInputsFromStatic); const finalProviderList: { id: string; diff --git a/lib/ts/recipe/multitenancy/index.ts b/lib/ts/recipe/multitenancy/index.ts index 35b23eb6b..5fd46f3d3 100644 --- a/lib/ts/recipe/multitenancy/index.ts +++ b/lib/ts/recipe/multitenancy/index.ts @@ -24,7 +24,12 @@ export default class Wrapper { static async createOrUpdateTenant( tenantId?: string, - config?: { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled: boolean }, + config?: { + emailPasswordEnabled?: boolean; + passwordlessEnabled?: boolean; + thirdPartyEnabled: boolean; + coreConfig?: { [key: string]: any }; + }, userContext?: any ): Promise<{ status: "OK"; @@ -43,7 +48,7 @@ export default class Wrapper { userContext?: any ): Promise<{ status: "OK"; - tenantExisted: boolean; + didExist: boolean; }> { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.deleteTenant({ @@ -52,7 +57,7 @@ export default class Wrapper { }); } - static async getTenantConfig( + static async getTenant( tenantId?: string, userContext?: any ): Promise<{ @@ -67,9 +72,10 @@ export default class Wrapper { enabled: boolean; providers: ProviderConfig[]; }; + coreConfig: { [key: string]: any }; }> { const recipeInstance = Recipe.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.getTenantConfig({ + return recipeInstance.recipeInterfaceImpl.getTenant({ tenantId, userContext: userContext === undefined ? {} : userContext, }); @@ -88,6 +94,7 @@ export default class Wrapper { } static async createOrUpdateThirdPartyConfig( + tenantId: string | undefined, config: ProviderConfig, skipValidation?: boolean, userContext?: any @@ -97,6 +104,7 @@ export default class Wrapper { }> { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.createOrUpdateThirdPartyConfig({ + tenantId, config, skipValidation, userContext: userContext === undefined ? {} : userContext, @@ -119,16 +127,43 @@ export default class Wrapper { }); } - static async listThirdPartyConfigsForThirdPartyId( - thirdPartyId: string, + static async associateUserToTenant( + tenantId: string | undefined, + userId: string, + userContext?: any + ): Promise< + | { + status: "OK"; + wasAlreadyAssociated: boolean; + } + | { + status: + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" + | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; + } + > { + const recipeInstance = Recipe.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ + tenantId, + userId, + userContext: userContext === undefined ? {} : userContext, + }); + } + + static async disassociateUserFromTenant( + tenantId: string | undefined, + userId: string, userContext?: any ): Promise<{ status: "OK"; - providers: ProviderConfig[]; + wasAssociated: boolean; }> { const recipeInstance = Recipe.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.listThirdPartyConfigsForThirdPartyId({ - thirdPartyId, + return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ + tenantId, + userId, userContext: userContext === undefined ? {} : userContext, }); } @@ -138,12 +173,14 @@ export let init = Wrapper.init; export let createOrUpdateTenant = Wrapper.createOrUpdateTenant; export let deleteTenant = Wrapper.deleteTenant; -export let getTenantConfig = Wrapper.getTenantConfig; +export let getTenant = Wrapper.getTenant; export let listAllTenants = Wrapper.listAllTenants; export let createOrUpdateThirdPartyConfig = Wrapper.createOrUpdateThirdPartyConfig; export let deleteThirdPartyConfig = Wrapper.deleteThirdPartyConfig; -export let listThirdPartyConfigsForThirdPartyId = Wrapper.listThirdPartyConfigsForThirdPartyId; + +export let associateUserToTenant = Wrapper.associateUserToTenant; +export let disassociateUserFromTenant = Wrapper.disassociateUserFromTenant; export { RecipeDisabledForTenantError, TenantDoesNotExistError }; export { AllowedDomainsClaim }; diff --git a/lib/ts/recipe/multitenancy/recipeImplementation.ts b/lib/ts/recipe/multitenancy/recipeImplementation.ts index 1a6a683f9..ab3b8f6ca 100644 --- a/lib/ts/recipe/multitenancy/recipeImplementation.ts +++ b/lib/ts/recipe/multitenancy/recipeImplementation.ts @@ -1,57 +1,96 @@ import { RecipeInterface } from "./"; import { Querier } from "../../querier"; +import NormalisedURLPath from "../../normalisedURLPath"; +import { DEFAULT_TENANT_ID } from "./constants"; -export default function getRecipeInterface(_: Querier): RecipeInterface { +export default function getRecipeInterface(querier: Querier): RecipeInterface { return { getTenantId: async function ({ tenantIdFromFrontend }) { - // TODO return tenantIdFromFrontend; }, - createOrUpdateTenant: async function () { - // TODO - throw new Error("Not implemented"); + createOrUpdateTenant: async function ({ tenantId, config }) { + let response = await querier.sendPutRequest(new NormalisedURLPath(`/recipe/multitenancy/tenant`), { + tenantId, + ...config, + }); + + return response; }, - deleteTenant: async function () { - // TODO - throw new Error("Not implemented"); + deleteTenant: async function ({ tenantId }) { + let response = await querier.sendPostRequest(new NormalisedURLPath(`/recipe/multitenancy/tenant/remove`), { + tenantId, + }); + + return response; }, - getTenantConfig: async function () { - return { - status: "OK", - emailPassword: { - enabled: true, - }, - passwordless: { - enabled: true, - }, - thirdParty: { - enabled: true, - providers: [], - }, - }; + getTenant: async function ({ tenantId }) { + let response = await querier.sendGetRequest( + new NormalisedURLPath( + `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/tenant` + ), + {} + ); + + return response; }, listAllTenants: async function () { - // TODO - throw new Error("Not implemented"); + let response = await querier.sendGetRequest(new NormalisedURLPath(`/recipe/multitenancy/tenant/list`), {}); + return response; + }, + + createOrUpdateThirdPartyConfig: async function ({ tenantId, config, skipValidation }) { + let response = await querier.sendPutRequest( + new NormalisedURLPath( + `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/config/thirdparty` + ), + { + config, + skipValidation, + } + ); + return response; }, - createOrUpdateThirdPartyConfig: async function () { - // TODO - throw new Error("Not implemented"); + deleteThirdPartyConfig: async function ({ tenantId, thirdPartyId }) { + let response = await querier.sendPostRequest( + new NormalisedURLPath( + `/${ + tenantId === undefined ? DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/config/thirdparty/remove` + ), + { + thirdPartyId, + } + ); + return response; }, - deleteThirdPartyConfig: async function () { - // TODO - throw new Error("Not implemented"); + associateUserToTenant: async function ({ tenantId, userId }) { + let response = await querier.sendPostRequest( + new NormalisedURLPath( + `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/tenant/user` + ), + { + userId, + } + ); + return response; }, - listThirdPartyConfigsForThirdPartyId: async function () { - // TODO - throw new Error("Not implemented"); + disassociateUserFromTenant: async function ({ tenantId, userId }) { + let response = await querier.sendPostRequest( + new NormalisedURLPath( + `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/tenant/user/remove` + ), + { + userId, + } + ); + return response; }, }; } diff --git a/lib/ts/recipe/multitenancy/types.ts b/lib/ts/recipe/multitenancy/types.ts index b28756dff..fa29477a9 100644 --- a/lib/ts/recipe/multitenancy/types.ts +++ b/lib/ts/recipe/multitenancy/types.ts @@ -84,6 +84,7 @@ export type RecipeInterface = { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled?: boolean; + coreConfig?: { [key: string]: any }; }; userContext: any; }) => Promise<{ @@ -95,9 +96,9 @@ export type RecipeInterface = { userContext: any; }) => Promise<{ status: "OK"; - tenantExisted: boolean; + didExist: boolean; }>; - getTenantConfig: (input: { + getTenant: (input: { tenantId?: string; userContext: any; }) => Promise<{ @@ -112,6 +113,7 @@ export type RecipeInterface = { enabled: boolean; providers: ProviderConfig[]; }; + coreConfig: { [key: string]: any }; }>; listAllTenants: (input: { userContext: any; @@ -122,6 +124,7 @@ export type RecipeInterface = { // Third party provider management createOrUpdateThirdPartyConfig: (input: { + tenantId?: string; config: ProviderConfig; skipValidation?: boolean; userContext: any; @@ -137,12 +140,32 @@ export type RecipeInterface = { status: "OK"; didConfigExist: boolean; }>; - listThirdPartyConfigsForThirdPartyId: (input: { - thirdPartyId: string; + + // User tenant association + associateUserToTenant: (input: { + tenantId?: string; + userId: string; + userContext: any; + }) => Promise< + | { + status: "OK"; + wasAlreadyAssociated: boolean; + } + | { + status: + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" + | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; + } + >; + disassociateUserFromTenant: (input: { + tenantId?: string; + userId: string; userContext: any; }) => Promise<{ status: "OK"; - providers: ProviderConfig[]; + wasAssociated: boolean; }>; }; diff --git a/lib/ts/recipe/session/recipeImplementation.ts b/lib/ts/recipe/session/recipeImplementation.ts index 34fe6c714..a6f7d4133 100644 --- a/lib/ts/recipe/session/recipeImplementation.ts +++ b/lib/ts/recipe/session/recipeImplementation.ts @@ -40,6 +40,7 @@ export const protectedProps = [ "parentRefreshTokenHash1", "refreshTokenHash1", "antiCsrfToken", + "tId", ]; export default function getRecipeInterface( diff --git a/lib/ts/recipe/thirdparty/providers/apple.ts b/lib/ts/recipe/thirdparty/providers/apple.ts index 10fdee223..e6a7759f7 100644 --- a/lib/ts/recipe/thirdparty/providers/apple.ts +++ b/lib/ts/recipe/thirdparty/providers/apple.ts @@ -69,7 +69,7 @@ export default function Apple(input: ProviderInput): TypeProvider { } config.clientSecret = getClientSecret( - config.clientID, + config.clientId, config.additionalConfig.keyId, config.additionalConfig.teamId, config.additionalConfig.privateKey diff --git a/lib/ts/recipe/thirdparty/providers/configUtils.ts b/lib/ts/recipe/thirdparty/providers/configUtils.ts index 1b07c606e..8467b2e04 100644 --- a/lib/ts/recipe/thirdparty/providers/configUtils.ts +++ b/lib/ts/recipe/thirdparty/providers/configUtils.ts @@ -119,7 +119,6 @@ export function mergeConfig(staticConfig: ProviderConfig, coreConfig: ProviderCo } export function mergeProvidersFromCoreAndStatic( - tenantId: string | undefined, providerConfigsFromCore: ProviderConfig[], providerInputsFromStatic: ProviderInput[] ): ProviderInput[] { @@ -127,7 +126,6 @@ export function mergeProvidersFromCoreAndStatic( if (providerConfigsFromCore.length === 0) { for (const config of providerInputsFromStatic) { - config.config.tenantId = tenantId; mergedProviders.push(config); } } else { diff --git a/lib/ts/recipe/thirdparty/providers/custom.ts b/lib/ts/recipe/thirdparty/providers/custom.ts index 586ca80bf..c2d968771 100644 --- a/lib/ts/recipe/thirdparty/providers/custom.ts +++ b/lib/ts/recipe/thirdparty/providers/custom.ts @@ -146,7 +146,7 @@ export default function NewProvider(input: ProviderInput): TypeProvider { // setting this temporarily. it will be replaced with correct config // by the `fetchAndSetConfig` function ...input.config, - clientID: "temp", + clientId: "temp", }, getConfigForClientType: async function ({ clientType }) { @@ -171,7 +171,7 @@ export default function NewProvider(input: ProviderInput): TypeProvider { getAuthorisationRedirectURL: async function ({ redirectURIOnProviderDashboard }) { const queryParams: { [key: string]: string } = { - client_id: impl.config.clientID, + client_id: impl.config.clientId, redirect_uri: redirectURIOnProviderDashboard, response_type: "code", }; @@ -206,8 +206,8 @@ export default function NewProvider(input: ProviderInput): TypeProvider { let url: string = impl.config.authorizationEndpoint; /* Transformation needed for dev keys BEGIN */ - if (isUsingDevelopmentClientId(impl.config.clientID)) { - queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientID); + if (isUsingDevelopmentClientId(impl.config.clientId)) { + queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); queryParams["actual_redirect_uri"] = url; url = DEV_OAUTH_AUTHORIZATION_URL; } @@ -231,7 +231,7 @@ export default function NewProvider(input: ProviderInput): TypeProvider { } const tokenAPIURL = impl.config.tokenEndpoint; const accessTokenAPIParams: { [key: string]: string } = { - client_id: impl.config.clientID, + client_id: impl.config.clientId, redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, code: redirectURIInfo.redirectURIQueryParams["code"], grant_type: "authorization_code", @@ -252,8 +252,8 @@ export default function NewProvider(input: ProviderInput): TypeProvider { } /* Transformation needed for dev keys BEGIN */ - if (isUsingDevelopmentClientId(impl.config.clientID)) { - accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientID); + if (isUsingDevelopmentClientId(impl.config.clientId)) { + accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); accessTokenAPIParams["redirect_uri"] = DEV_OAUTH_REDIRECT_URL; } /* Transformation needed for dev keys END */ @@ -278,7 +278,7 @@ export default function NewProvider(input: ProviderInput): TypeProvider { idToken, impl.config.jwksURI, { - audience: getActualClientIdFromDevelopmentClientId(impl.config.clientID), + audience: getActualClientIdFromDevelopmentClientId(impl.config.clientId), } ); diff --git a/lib/ts/recipe/thirdparty/recipeImplementation.ts b/lib/ts/recipe/thirdparty/recipeImplementation.ts index 8cd798de6..f5475a76c 100644 --- a/lib/ts/recipe/thirdparty/recipeImplementation.ts +++ b/lib/ts/recipe/thirdparty/recipeImplementation.ts @@ -108,10 +108,9 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro getProvider: async function ({ thirdPartyId, tenantId, clientType, userContext }) { const mtRecipe = MultitenancyRecipe.getInstanceOrThrowError(); - const tenantConfig = await mtRecipe.recipeInterfaceImpl.getTenantConfig({ tenantId, userContext }); + const tenantConfig = await mtRecipe.recipeInterfaceImpl.getTenant({ tenantId, userContext }); const mergedProviders: ProviderInput[] = mergeProvidersFromCoreAndStatic( - tenantId, tenantConfig.thirdParty.providers, providers ); diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index 62cd04fb9..51e293822 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -40,7 +40,7 @@ export type UserInfoMap = { export type ProviderClientConfig = { clientType?: string; - clientID: string; + clientId: string; clientSecret?: string; scope?: string[]; forcePKCE?: boolean; @@ -49,7 +49,6 @@ export type ProviderClientConfig = { type CommonProviderConfig = { thirdPartyId: string; - tenantId?: string; name?: string; authorizationEndpoint?: string; diff --git a/lib/ts/version.ts b/lib/ts/version.ts index 8bff0f89c..eeec746a8 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -14,7 +14,7 @@ */ export const version = "14.1.2"; -export const cdiSupported = ["2.21"]; +export const cdiSupported = ["3.0"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} export const dashboardVersion = "0.6"; diff --git a/test/multitenancy/tenants-crud.test.js b/test/multitenancy/tenants-crud.test.js new file mode 100644 index 000000000..43fec31ee --- /dev/null +++ b/test/multitenancy/tenants-crud.test.js @@ -0,0 +1,336 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST } = require("../utils"); +let assert = require("assert"); +const express = require("express"); +const request = require("supertest"); +let { Querier } = require("../../lib/build/querier"); +let { ProcessState } = require("../../lib/build/processState"); +let SuperTokens = require("../../"); +let Multitenancy = require("../../recipe/multitenancy"); +let EmailPassword = require("../../recipe/emailpassword"); +let { middleware, errorHandler } = require("../../framework/express"); + +describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("test creation of tenants", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + await Multitenancy.createOrUpdateTenant("t2", { passwordlessEnabled: true }); + await Multitenancy.createOrUpdateTenant("t3", { thirdPartyEnabled: true }); + + const tenants = await Multitenancy.listAllTenants(); + assert(tenants.tenants.length === 4); // public + 3 tenants created above + }); + + it("test get tenant", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + await Multitenancy.createOrUpdateTenant("t2", { passwordlessEnabled: true }); + await Multitenancy.createOrUpdateTenant("t3", { thirdPartyEnabled: true }); + + let tenantConfig = await Multitenancy.getTenant("t1"); + assert(tenantConfig.emailPassword.enabled === true); + assert(tenantConfig.passwordless.enabled === false); + assert(tenantConfig.thirdParty.enabled === false); + assert(tenantConfig.coreConfig !== undefined); + + tenantConfig = await Multitenancy.getTenant("t2"); + assert(tenantConfig.passwordless.enabled === true); + assert(tenantConfig.emailPassword.enabled === false); + assert(tenantConfig.thirdParty.enabled === false); + assert(tenantConfig.coreConfig !== undefined); + + tenantConfig = await Multitenancy.getTenant("t3"); + assert(tenantConfig.thirdParty.enabled === true); + assert(tenantConfig.passwordless.enabled === false); + assert(tenantConfig.emailPassword.enabled === false); + assert(tenantConfig.coreConfig !== undefined); + }); + + it("test update tenant", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + + let tenantConfig = await Multitenancy.getTenant("t1"); + assert(tenantConfig.emailPassword.enabled === true); + assert(tenantConfig.passwordless.enabled === false); + assert(tenantConfig.thirdParty.enabled === false); + + await Multitenancy.createOrUpdateTenant("t1", { passwordlessEnabled: true }); + tenantConfig = await Multitenancy.getTenant("t1"); + assert(tenantConfig.emailPassword.enabled === true); + assert(tenantConfig.passwordless.enabled === true); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: false }); + tenantConfig = await Multitenancy.getTenant("t1"); + assert(tenantConfig.emailPassword.enabled === false); + assert(tenantConfig.passwordless.enabled === true); + }); + + it("test delete tenant", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + await Multitenancy.createOrUpdateTenant("t2", { passwordlessEnabled: true }); + await Multitenancy.createOrUpdateTenant("t3", { thirdPartyEnabled: true }); + + let tenants = await Multitenancy.listAllTenants(); + assert(tenants.tenants.length === 4); // public + 3 tenants created above + + let response = await Multitenancy.deleteTenant("t3"); + assert(response.didExist === true); + + tenants = await Multitenancy.listAllTenants(); + assert(tenants.tenants.length === 3); // public + 3 tenants created above + + response = await Multitenancy.deleteTenant("t3"); + assert(response.didExist === false); + }); + + it("test creation of thirdParty config", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + + await Multitenancy.createOrUpdateThirdPartyConfig("t1", { + thirdPartyId: "google", + clients: [{ clientId: "abcd" }], + }); + + const tenantConfig = await Multitenancy.getTenant("t1"); + + assert(tenantConfig.thirdParty.providers.length === 1); + assert(tenantConfig.thirdParty.providers[0].thirdPartyId === "google"); + assert(tenantConfig.thirdParty.providers[0].clients.length === 1); + assert(tenantConfig.thirdParty.providers[0].clients[0].clientId === "abcd"); + }); + + it("test deletion of thirdparty id", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + + await Multitenancy.createOrUpdateThirdPartyConfig("t1", { + thirdPartyId: "google", + clients: [{ clientId: "abcd" }], + }); + + let tenantConfig = await Multitenancy.getTenant("t1"); + + assert(tenantConfig.thirdParty.providers.length === 1); + + await Multitenancy.deleteThirdPartyConfig("t1", "google"); + + tenantConfig = await Multitenancy.getTenant("t1"); + assert(tenantConfig.thirdParty.providers.length === 0); + }); + + it("test updation of thirdparty provider", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + + await Multitenancy.createOrUpdateThirdPartyConfig("t1", { + thirdPartyId: "google", + clients: [{ clientId: "abcd" }], + }); + + let tenantConfig = await Multitenancy.getTenant("t1"); + + assert(tenantConfig.thirdParty.providers.length === 1); + + await Multitenancy.createOrUpdateThirdPartyConfig("t1", { + thirdPartyId: "google", + name: "Custom name", + clients: [{ clientId: "efgh" }], + }); + + tenantConfig = await Multitenancy.getTenant("t1"); + assert(tenantConfig.thirdParty.providers.length === 1); + assert(tenantConfig.thirdParty.providers[0].thirdPartyId === "google"); + assert(tenantConfig.thirdParty.providers[0].name === "Custom name"); + assert(tenantConfig.thirdParty.providers[0].clients.length === 1); + assert(tenantConfig.thirdParty.providers[0].clients[0].clientId === "efgh"); + }); + + it("test user association and disassociation with tenants", async function () { + await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Multitenancy.init(), EmailPassword.init()], + }); + + const app = express(); + + app.use(middleware()); + app.use(errorHandler()); + + await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true }); + await Multitenancy.createOrUpdateTenant("t2", { emailPasswordEnabled: true }); + await Multitenancy.createOrUpdateTenant("t3", { emailPasswordEnabled: true }); + + const user = await EmailPassword.signUp("test@example.com", "password1"); + const userId = user.user.id; + + await Multitenancy.associateUserToTenant("t1", userId); + await Multitenancy.associateUserToTenant("t2", userId); + await Multitenancy.associateUserToTenant("t3", userId); + + let newUser = await EmailPassword.getUserById(userId); + + assert(newUser.tenantIds.length === 4); // public + 3 tenants created above + + await Multitenancy.disassociateUserFromTenant("t1", userId); + await Multitenancy.disassociateUserFromTenant("t2", userId); + await Multitenancy.disassociateUserFromTenant("t3", userId); + + newUser = await EmailPassword.getUserById(userId); + + assert(newUser.tenantIds.length === 1); // only public + }); +}); diff --git a/test/nextjs.test.js b/test/nextjs.test.js index 61fb92dc2..8917aac41 100644 --- a/test/nextjs.test.js +++ b/test/nextjs.test.js @@ -440,7 +440,7 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi thirdPartyId: "google", clients: [ { - clientID: "", + clientId: "", clientSecret: "", }, ], diff --git a/test/utils.js b/test/utils.js index b08f836ac..22c2f2187 100644 --- a/test/utils.js +++ b/test/utils.js @@ -36,6 +36,7 @@ let { maxVersion } = require("../lib/build/utils"); const { default: OpenIDRecipe } = require("../lib/build/recipe/openid/recipe"); const { wrapRequest } = require("../framework/express"); const { join } = require("path"); +const axios = require("axios"); const users = require("./users.json"); @@ -306,6 +307,16 @@ module.exports.startST = async function (host = "localhost", port = 8080) { }); }; +module.exports.startSTWithMultitenancy = async function (host = "localhost", port = 8080) { + await module.exports.startST(host, port); + const OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = + "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; + + await axios.put(`http://${host}:${port}/ee/license`, { + licenseKey: OPAQUE_KEY_WITH_MULTITENANCY_FEATURE, + }); +}; + async function getListOfPids() { let installationPath = process.env.INSTALL_PATH; let currList; diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index d299e3885..dcb7bb910 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -125,7 +125,7 @@ ThirdPartyPasswordless.init({ { config: { thirdPartyId: "google", - clients: [{ clientID: "" }], + clients: [{ clientId: "" }], }, }, ], @@ -315,7 +315,7 @@ ThirdPartyPasswordless.init({ { config: { thirdPartyId: "google", - clients: [{ clientID: "" }], + clients: [{ clientId: "" }], }, }, ], @@ -381,7 +381,7 @@ ThirdPartyPasswordless.init({ { config: { thirdPartyId: "google", - clients: [{ clientID: "" }], + clients: [{ clientId: "" }], }, }, ], @@ -898,8 +898,8 @@ Multitenancy.init({ return await oI.deleteTenant({ tenantId, userContext }); }, - getTenantConfig: async function ({ tenantId, userContext }) { - return await oI.getTenantConfig({ tenantId, userContext }); + getTenant: async function ({ tenantId, userContext }) { + return await oI.getTenant({ tenantId, userContext }); }, listAllTenants: async function ({ userContext }) { @@ -913,10 +913,6 @@ Multitenancy.init({ deleteThirdPartyConfig: async function ({ tenantId, thirdPartyId, userContext }) { return await oI.deleteThirdPartyConfig({ tenantId, thirdPartyId, userContext }); }, - - listThirdPartyConfigsForThirdPartyId: async function ({ thirdPartyId, userContext }) { - return await oI.listThirdPartyConfigsForThirdPartyId({ thirdPartyId, userContext }); - }, }; }, },