From 0481a450be109de5462f4bf842531aa0bdc69c02 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sat, 11 Feb 2023 14:38:16 +0800 Subject: [PATCH] refactor: decouple admin tenant and user tenant --- packages/console/src/pages/GetStarted/hook.ts | 6 ++- packages/core/jest.setup.js | 3 ++ .../index.test.ts} | 42 +++++++++------ .../{koa-auth.ts => koa-auth/index.ts} | 18 +++---- .../core/src/middleware/koa-auth/utils.ts | 54 +++++++++++++++++++ packages/core/src/routes-me/init.ts | 9 ++-- packages/core/src/routes/authn.ts | 2 +- packages/core/src/routes/init.ts | 8 ++- .../actions/submit-interaction.test.ts | 10 ++-- .../interaction/actions/submit-interaction.ts | 2 +- packages/core/src/routes/types.ts | 2 +- packages/core/src/routes/well-known.test.ts | 3 +- packages/core/src/tenants/Tenant.ts | 22 ++++---- .../core/src/tenants/TenantPoolContext.ts | 5 ++ packages/core/src/tenants/index.ts | 2 +- packages/integration-tests/src/api/index.ts | 1 - .../src/tests/api/admin-user.roles.test.ts | 8 +-- .../src/tests/api/get-access-token.test.ts | 29 +++++----- .../src/tests/api/resource.scope.test.ts | 6 +-- .../src/tests/api/resource.test.ts | 6 +-- packages/schemas/tables/users.sql | 14 +++-- 21 files changed, 163 insertions(+), 89 deletions(-) rename packages/core/src/middleware/{koa-auth.test.ts => koa-auth/index.test.ts} (79%) rename packages/core/src/middleware/{koa-auth.ts => koa-auth/index.ts} (88%) create mode 100644 packages/core/src/middleware/koa-auth/utils.ts create mode 100644 packages/core/src/tenants/TenantPoolContext.ts diff --git a/packages/console/src/pages/GetStarted/hook.ts b/packages/console/src/pages/GetStarted/hook.ts index 0b3b0f732..4c2dd05ea 100644 --- a/packages/console/src/pages/GetStarted/hook.ts +++ b/packages/console/src/pages/GetStarted/hook.ts @@ -1,7 +1,7 @@ import type { AdminConsoleKey } from '@logto/phrases'; import type { Application } from '@logto/schemas'; import { AppearanceMode, demoAppApplicationId } from '@logto/schemas'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import useSWR from 'swr'; @@ -18,6 +18,7 @@ import Passwordless from '@/assets/images/passwordless.svg'; import SocialDark from '@/assets/images/social-dark.svg'; import Social from '@/assets/images/social.svg'; import { ConnectorsTabs } from '@/consts/page-tabs'; +import { AppEndpointsContext } from '@/containers/AppEndpointsProvider'; import { RequestError } from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; import useDocumentationUrl from '@/hooks/use-documentation-url'; @@ -37,6 +38,7 @@ type GetStartedMetadata = { const useGetStartedMetadata = () => { const { getDocumentationUrl } = useDocumentationUrl(); const { configs, updateConfigs } = useConfigs(); + const { app } = useContext(AppEndpointsContext); const theme = useTheme(); const isLightMode = theme === AppearanceMode.LightMode; const { data: demoApp, error } = useSWR( @@ -67,7 +69,7 @@ const useGetStartedMetadata = () => { isHidden: hideDemo, onClick: async () => { void updateConfigs({ demoChecked: true }); - window.open('/demo-app', '_blank'); + window.open(new URL('/demo-app', app), '_blank'); }, }, { diff --git a/packages/core/jest.setup.js b/packages/core/jest.setup.js index 0cd06ef71..62647ef99 100644 --- a/packages/core/jest.setup.js +++ b/packages/core/jest.setup.js @@ -23,6 +23,9 @@ mockEsm('#src/env-set/check-alteration-state.js', () => ({ // eslint-disable-next-line unicorn/consistent-function-scoping mockEsmDefault('#src/env-set/oidc.js', () => () => ({ issuer: 'https://logto.test/oidc', + cookieKeys: [], + privateJwks: [], + publicJwks: [], })); /* End */ diff --git a/packages/core/src/middleware/koa-auth.test.ts b/packages/core/src/middleware/koa-auth/index.test.ts similarity index 79% rename from packages/core/src/middleware/koa-auth.test.ts rename to packages/core/src/middleware/koa-auth/index.test.ts index 4726f582c..83768b9d8 100644 --- a/packages/core/src/middleware/koa-auth.test.ts +++ b/packages/core/src/middleware/koa-auth/index.test.ts @@ -9,18 +9,24 @@ import RequestError from '#src/errors/RequestError/index.js'; import { mockEnvSet } from '#src/test-utils/env-set.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import type { WithAuthContext } from './koa-auth.js'; +import type { WithAuthContext } from './index.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); +mockEsm('./utils.js', () => ({ + getAdminTenantTokenValidationSet: jest.fn().mockResolvedValue({ keys: [], issuer: [] }), +})); + const { jwtVerify } = mockEsm('jose', () => ({ + createLocalJWKSet: jest.fn(), jwtVerify: jest .fn() .mockReturnValue({ payload: { sub: 'fooUser', scope: defaultManagementApi.scope.name } }), })); -const koaAuth = await pickDefault(import('./koa-auth.js')); +const audience = defaultManagementApi.resource.indicator; +const koaAuth = await pickDefault(import('./index.js')); describe('koaAuth middleware', () => { const baseCtx = createContextWithRouteParameters(); @@ -64,7 +70,7 @@ describe('koaAuth middleware', () => { developmentUserId: 'foo', }); - await koaAuth(mockEnvSet)(ctx, next); + await koaAuth(mockEnvSet, audience)(ctx, next); expect(ctx.auth).toEqual({ type: 'user', id: 'foo' }); stub.restore(); @@ -79,7 +85,7 @@ describe('koaAuth middleware', () => { }, }; - await koaAuth(mockEnvSet)(mockCtx, next); + await koaAuth(mockEnvSet, audience)(mockCtx, next); expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' }); }); @@ -91,7 +97,7 @@ describe('koaAuth middleware', () => { isIntegrationTest: true, }); - await koaAuth(mockEnvSet)(ctx, next); + await koaAuth(mockEnvSet, audience)(ctx, next); expect(ctx.auth).toEqual({ type: 'user', id: 'foo' }); stub.restore(); @@ -112,7 +118,7 @@ describe('koaAuth middleware', () => { }, }; - await koaAuth(mockEnvSet)(mockCtx, next); + await koaAuth(mockEnvSet, audience)(mockCtx, next); expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' }); stub.restore(); @@ -125,12 +131,14 @@ describe('koaAuth middleware', () => { authorization: 'Bearer access_token', }, }; - await koaAuth(mockEnvSet)(ctx, next); + await koaAuth(mockEnvSet, audience)(ctx, next); expect(ctx.auth).toEqual({ type: 'user', id: 'fooUser' }); }); it('expect to throw if authorization header is missing', async () => { - await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(authHeaderMissingError); + await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError( + authHeaderMissingError + ); }); it('expect to throw if authorization header token type not recognized ', async () => { @@ -141,7 +149,9 @@ describe('koaAuth middleware', () => { }, }; - await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(tokenNotSupportedError); + await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError( + tokenNotSupportedError + ); }); it('expect to throw if jwt sub is missing', async () => { @@ -154,11 +164,13 @@ describe('koaAuth middleware', () => { }, }; - await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(jwtSubMissingError); + await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(jwtSubMissingError); }); it('expect to have `client` type per jwt verify result', async () => { - jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'bar', client_id: 'bar' } })); + jwtVerify.mockImplementationOnce(() => ({ + payload: { sub: 'bar', client_id: 'bar', scope: 'all' }, + })); ctx.request = { ...ctx.request, @@ -167,7 +179,7 @@ describe('koaAuth middleware', () => { }, }; - await koaAuth(mockEnvSet)(ctx, next); + await koaAuth(mockEnvSet, audience)(ctx, next); expect(ctx.auth).toEqual({ type: 'app', id: 'bar' }); }); @@ -181,7 +193,7 @@ describe('koaAuth middleware', () => { }, }; - await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(forbiddenError); + await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(forbiddenError); }); it('expect to throw if jwt scope does not include management resource scope', async () => { @@ -196,7 +208,7 @@ describe('koaAuth middleware', () => { }, }; - await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(forbiddenError); + await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError(forbiddenError); }); it('expect to throw unauthorized error if unknown error occurs', async () => { @@ -210,7 +222,7 @@ describe('koaAuth middleware', () => { }, }; - await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError( + await expect(koaAuth(mockEnvSet, audience)(ctx, next)).rejects.toMatchError( new RequestError({ code: 'auth.unauthorized', status: 401 }, new Error('unknown error')) ); }); diff --git a/packages/core/src/middleware/koa-auth.ts b/packages/core/src/middleware/koa-auth/index.ts similarity index 88% rename from packages/core/src/middleware/koa-auth.ts rename to packages/core/src/middleware/koa-auth/index.ts index d61423c85..18959004a 100644 --- a/packages/core/src/middleware/koa-auth.ts +++ b/packages/core/src/middleware/koa-auth/index.ts @@ -1,6 +1,6 @@ import type { IncomingHttpHeaders } from 'http'; -import { adminTenantId, defaultManagementApi } from '@logto/schemas'; +import { adminTenantId, defaultManagementApi, PredefinedScope } from '@logto/schemas'; import type { Optional } from '@silverhand/essentials'; import type { JWK } from 'jose'; import { createLocalJWKSet, jwtVerify } from 'jose'; @@ -10,9 +10,10 @@ import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { tenantPool } from '#src/tenants/index.js'; import assertThat from '#src/utils/assert-that.js'; +import { getAdminTenantTokenValidationSet } from './utils.js'; + export type Auth = { type: 'user' | 'app'; id: string; @@ -68,13 +69,11 @@ export const verifyBearerTokenFromRequest = async ( return [publicJwks, [issuer]]; } - const { - envSet: { oidc: adminOidc }, - } = await tenantPool.get(adminTenantId); + const adminSet = await getAdminTenantTokenValidationSet(); return [ - [...publicJwks, ...adminOidc.publicJwks], - [issuer, adminOidc.issuer], + [...publicJwks, ...adminSet.keys], + [issuer, ...adminSet.issuer], ]; }; @@ -105,8 +104,7 @@ export const verifyBearerTokenFromRequest = async ( export default function koaAuth( envSet: EnvSet, - audience: string, - expectScopes = [defaultManagementApi.scope.name] + audience: string ): MiddlewareType, ResponseBodyT> { return async (ctx, next) => { const { sub, clientId, scopes } = await verifyBearerTokenFromRequest( @@ -116,7 +114,7 @@ export default function koaAuth scopes.includes(scope)), + scopes.includes(PredefinedScope.All), new RequestError({ code: 'auth.forbidden', status: 403 }) ); diff --git a/packages/core/src/middleware/koa-auth/utils.ts b/packages/core/src/middleware/koa-auth/utils.ts new file mode 100644 index 000000000..198d5cc57 --- /dev/null +++ b/packages/core/src/middleware/koa-auth/utils.ts @@ -0,0 +1,54 @@ +import crypto from 'crypto'; + +import type { LogtoConfig } from '@logto/schemas'; +import { + logtoOidcConfigGuard, + adminTenantId, + LogtoOidcConfigKey, + LogtoConfigs, +} from '@logto/schemas'; +import { convertToIdentifiers } from '@logto/shared'; +import type { JWK } from 'jose'; +import { sql } from 'slonik'; + +import { EnvSet } from '#src/env-set/index.js'; +import { exportJWK } from '#src/utils/jwks.js'; + +const { table, fields } = convertToIdentifiers(LogtoConfigs); + +/** + * This function is to fetch OIDC public signing keys and the issuer from the admin tenant + * in order to let user tenants recognize Access Tokens issued by the admin tenant. + * + * Usually you don't mean to call this function. + */ +export const getAdminTenantTokenValidationSet = async (): Promise<{ + keys: JWK[]; + issuer: string[]; +}> => { + const { isDomainBasedMultiTenancy, urlSet, adminUrlSet } = EnvSet.values; + + if (!isDomainBasedMultiTenancy && adminUrlSet.deduplicated().length === 0) { + return { keys: [], issuer: [] }; + } + + const pool = await EnvSet.pool; + const { value } = await pool.one(sql` + select ${sql.join([fields.key, fields.value], sql`,`)} from ${table} + where ${fields.tenantId} = ${adminTenantId} + and ${fields.key} = ${LogtoOidcConfigKey.PrivateKeys} + `); + const privateKeys = logtoOidcConfigGuard['oidc.privateKeys'] + .parse(value) + .map((key) => crypto.createPrivateKey(key)); + const publicKeys = privateKeys.map((key) => crypto.createPublicKey(key)); + + return { + keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))), + issuer: [ + (isDomainBasedMultiTenancy + ? urlSet.endpoint.replace('*', adminTenantId) + : adminUrlSet.endpoint) + '/oidc', + ], + }; +}; diff --git a/packages/core/src/routes-me/init.ts b/packages/core/src/routes-me/init.ts index 650adfbc3..fa26ef050 100644 --- a/packages/core/src/routes-me/init.ts +++ b/packages/core/src/routes-me/init.ts @@ -2,14 +2,13 @@ import { adminTenantId, arbitraryObjectGuard, getManagementApiResourceIndicator, - PredefinedScope, } from '@logto/schemas'; import Koa from 'koa'; import Router from 'koa-router'; import RequestError from '#src/errors/RequestError/index.js'; -import type { WithAuthContext } from '#src/middleware/koa-auth.js'; -import koaAuth from '#src/middleware/koa-auth.js'; +import type { WithAuthContext } from '#src/middleware/koa-auth/index.js'; +import koaAuth from '#src/middleware/koa-auth/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; @@ -25,9 +24,7 @@ export default function initMeApis(tenant: TenantContext): Koa { console.log('????', getManagementApiResourceIndicator(adminTenantId, 'me')); meRouter.use( - koaAuth(tenant.envSet, getManagementApiResourceIndicator(adminTenantId, 'me'), [ - PredefinedScope.All, - ]), + koaAuth(tenant.envSet, getManagementApiResourceIndicator(adminTenantId, 'me')), async (ctx, next) => { assertThat( ctx.auth.type === 'user', diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index 147d78a0e..71604094b 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; -import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth.js'; +import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import assertThat from '#src/utils/assert-that.js'; diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index cce59f553..c6135995a 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -1,12 +1,12 @@ import cors from '@koa/cors'; -import { getManagementApiResourceIndicator, PredefinedScope } from '@logto/schemas'; +import { getManagementApiResourceIndicator } from '@logto/schemas'; import Koa from 'koa'; import Router from 'koa-router'; import { EnvSet } from '#src/env-set/index.js'; import type TenantContext from '#src/tenants/TenantContext.js'; -import koaAuth from '../middleware/koa-auth.js'; +import koaAuth from '../middleware/koa-auth/index.js'; import adminUserRoleRoutes from './admin-user-role.js'; import adminUserRoutes from './admin-user.js'; import applicationRoutes from './application.js'; @@ -35,9 +35,7 @@ const createRouters = (tenant: TenantContext) => { interactionRoutes(interactionRouter, tenant); const managementRouter: AuthedRouter = new Router(); - managementRouter.use( - koaAuth(tenant.envSet, getManagementApiResourceIndicator(tenant.id), [PredefinedScope.All]) - ); + managementRouter.use(koaAuth(tenant.envSet, getManagementApiResourceIndicator(tenant.id))); applicationRoutes(managementRouter, tenant); logtoConfigRoutes(managementRouter, tenant); connectorRoutes(managementRouter, tenant); diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index 5bb2c1f9c..4422a8439 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -1,4 +1,4 @@ -import { InteractionEvent, adminConsoleApplicationId } from '@logto/schemas'; +import { InteractionEvent, adminConsoleApplicationId, adminTenantId } from '@logto/schemas'; import { createMockUtils, pickDefault } from '@logto/shared/esm'; import type Provider from 'oidc-provider'; @@ -31,6 +31,10 @@ const { encryptUserPassword } = mockEsm('#src/libraries/user.js', () => ({ }), })); +mockEsm('#src/utils/tenant.js', () => ({ + getTenantId: () => adminTenantId, +})); + const userQueries = { findUserById: jest .fn() @@ -115,7 +119,7 @@ describe('submit action', () => { id: 'uid', ...upsertProfile, }, - false + [] ); expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, { login: { accountId: 'uid' }, @@ -153,7 +157,7 @@ describe('submit action', () => { id: 'uid', ...upsertProfile, }, - true + ['user', 'default:admin'] ); expect(assignInteractionResults).toBeCalledWith(adminConsoleCtx, tenant.provider, { login: { accountId: 'uid' }, diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 6afe041af..111a76bf4 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -174,7 +174,7 @@ export default async function submitInteraction( id, ...upsertProfile, }, - createAdminUser ? [getManagementApiAdminName(defaultTenantId), UserRole.User] : [] + createAdminUser ? [UserRole.User, getManagementApiAdminName(defaultTenantId)] : [] ); await assignInteractionResults(ctx, provider, { login: { accountId: id } }); diff --git a/packages/core/src/routes/types.ts b/packages/core/src/routes/types.ts index f65388c96..52e40e852 100644 --- a/packages/core/src/routes/types.ts +++ b/packages/core/src/routes/types.ts @@ -2,7 +2,7 @@ import type { ExtendableContext } from 'koa'; import type Router from 'koa-router'; import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; -import type { WithAuthContext } from '#src/middleware/koa-auth.js'; +import type { WithAuthContext } from '#src/middleware/koa-auth/index.js'; import type { WithI18nContext } from '#src/middleware/koa-i18next.js'; import type TenantContext from '#src/tenants/TenantContext.js'; diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 525c4570d..594544d03 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -111,11 +111,12 @@ describe('GET /.well-known/sign-in-exp', () => { expect(response.body).toMatchObject({ ...adminConsoleSignInExperience, + tenantId: 'admin', branding: { ...adminConsoleSignInExperience.branding, slogan: 'admin_console.welcome.title', }, - termsOfUseUrl: mockSignInExperience.termsOfUseUrl, + termsOfUseUrl: null, languageInfo: mockSignInExperience.languageInfo, socialConnectors: [], signInMode: SignInMode.SignIn, diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index a96684716..a6627618b 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -79,20 +79,20 @@ export default class Tenant implements TenantContext { // Mount APIs app.use(mount('/api', initApis(tenantContext))); - // Mount `/me` APIs for admin tenant + // Mount admin tenant APIs and app if (id === adminTenantId) { - console.log('111111111111122221'); + // Mount `/me` APIs for admin tenant app.use(mount('/me', initMeApis(tenantContext))); - } - // Mount Admin Console - app.use(koaConsoleRedirectProxy(queries)); - app.use( - mount( - '/' + UserApps.Console, - koaSpaProxy(mountedApps, UserApps.Console, 5002, UserApps.Console) - ) - ); + // Mount Admin Console + app.use(koaConsoleRedirectProxy(queries)); + app.use( + mount( + '/' + UserApps.Console, + koaSpaProxy(mountedApps, UserApps.Console, 5002, UserApps.Console) + ) + ); + } // Mount demo app app.use( diff --git a/packages/core/src/tenants/TenantPoolContext.ts b/packages/core/src/tenants/TenantPoolContext.ts new file mode 100644 index 000000000..399abdb7b --- /dev/null +++ b/packages/core/src/tenants/TenantPoolContext.ts @@ -0,0 +1,5 @@ +import type TenantContext from './TenantContext.js'; + +export default abstract class TenantPoolContext { + public abstract get(tenantId: string): Promise; +} diff --git a/packages/core/src/tenants/index.ts b/packages/core/src/tenants/index.ts index 365acc433..836235aec 100644 --- a/packages/core/src/tenants/index.ts +++ b/packages/core/src/tenants/index.ts @@ -2,7 +2,7 @@ import LRUCache from 'lru-cache'; import Tenant from './Tenant.js'; -class TenantPool { +export class TenantPool { protected cache = new LRUCache({ max: 500 }); async get(tenantId: string): Promise { diff --git a/packages/integration-tests/src/api/index.ts b/packages/integration-tests/src/api/index.ts index 7faf8bd7b..932d4916d 100644 --- a/packages/integration-tests/src/api/index.ts +++ b/packages/integration-tests/src/api/index.ts @@ -5,7 +5,6 @@ export * from './sign-in-experience.js'; export * from './admin-user.js'; export * from './logs.js'; export * from './dashboard.js'; -export * from './me.js'; export * from './wellknown.js'; export * from './interaction.js'; diff --git a/packages/integration-tests/src/tests/api/admin-user.roles.test.ts b/packages/integration-tests/src/tests/api/admin-user.roles.test.ts index 2b4f584d9..2c55157df 100644 --- a/packages/integration-tests/src/tests/api/admin-user.roles.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.roles.test.ts @@ -1,4 +1,4 @@ -import { adminRoleId } from '@logto/schemas'; +import { defaultManagementApi } from '@logto/schemas'; import { HTTPError } from 'got'; import { assignRolesToUser, getUserRoles, deleteRoleFromUser } from '#src/api/index.js'; @@ -25,8 +25,8 @@ describe('admin console user management (roles)', () => { it('should delete role from user successfully', async () => { const user = await createUserByAdmin(); - await assignRolesToUser(user.id, [adminRoleId]); - await deleteRoleFromUser(user.id, adminRoleId); + await assignRolesToUser(user.id, [defaultManagementApi.role.id]); + await deleteRoleFromUser(user.id, defaultManagementApi.role.id); const roles = await getUserRoles(user.id); expect(roles.length).toBe(0); @@ -35,7 +35,7 @@ describe('admin console user management (roles)', () => { it('should delete non-exist-role from user failed', async () => { const user = await createUserByAdmin(); - const response = await deleteRoleFromUser(user.id, adminRoleId).catch( + const response = await deleteRoleFromUser(user.id, defaultManagementApi.role.id).catch( (error: unknown) => error ); expect(response instanceof HTTPError && response.response.statusCode === 404).toBe(true); diff --git a/packages/integration-tests/src/tests/api/get-access-token.test.ts b/packages/integration-tests/src/tests/api/get-access-token.test.ts index 65d0b17a8..4f2341fdc 100644 --- a/packages/integration-tests/src/tests/api/get-access-token.test.ts +++ b/packages/integration-tests/src/tests/api/get-access-token.test.ts @@ -1,12 +1,7 @@ import path from 'path'; import { fetchTokenByRefreshToken } from '@logto/js'; -import { - managementResource, - InteractionEvent, - adminRoleId, - managementResourceScope, -} from '@logto/schemas'; +import { defaultManagementApi, InteractionEvent } from '@logto/schemas'; import { assert } from '@silverhand/essentials'; import fetch from 'node-fetch'; @@ -27,14 +22,14 @@ describe('get access token', () => { beforeAll(async () => { await createUserByAdmin(guestUsername, password); const user = await createUserByAdmin(username, password); - await assignUsersToRole([user.id], adminRoleId); + await assignUsersToRole([user.id], defaultManagementApi.role.id); await enableAllPasswordSignInMethods(); }); it('sign-in and getAccessToken with admin user', async () => { const client = new MockClient({ - resources: [managementResource.indicator], - scopes: [managementResourceScope.name], + resources: [defaultManagementApi.resource.indicator], + scopes: [defaultManagementApi.scope.name], }); await client.initSession(); await client.successSend(putInteraction, { @@ -43,11 +38,11 @@ describe('get access token', () => { }); const { redirectTo } = await client.submitInteraction(); await processSession(client, redirectTo); - const accessToken = await client.getAccessToken(managementResource.indicator); + const accessToken = await client.getAccessToken(defaultManagementApi.resource.indicator); expect(accessToken).not.toBeNull(); expect(getAccessTokenPayload(accessToken)).toHaveProperty( 'scope', - managementResourceScope.name + defaultManagementApi.scope.name ); // Request for invalid resource should throw @@ -56,8 +51,8 @@ describe('get access token', () => { it('sign-in and getAccessToken with guest user', async () => { const client = new MockClient({ - resources: [managementResource.indicator], - scopes: [managementResourceScope.name], + resources: [defaultManagementApi.resource.indicator], + scopes: [defaultManagementApi.scope.name], }); await client.initSession(); await client.successSend(putInteraction, { @@ -66,16 +61,16 @@ describe('get access token', () => { }); const { redirectTo } = await client.submitInteraction(); await processSession(client, redirectTo); - const accessToken = await client.getAccessToken(managementResource.indicator); + const accessToken = await client.getAccessToken(defaultManagementApi.resource.indicator); expect(getAccessTokenPayload(accessToken)).not.toHaveProperty( 'scope', - managementResourceScope.name + defaultManagementApi.scope.name ); }); it('sign-in and get multiple Access Token by the same Refresh Token within refreshTokenReuseInterval', async () => { - const client = new MockClient({ resources: [managementResource.indicator] }); + const client = new MockClient({ resources: [defaultManagementApi.resource.indicator] }); await client.initSession(); @@ -98,7 +93,7 @@ describe('get access token', () => { clientId: defaultConfig.appId, tokenEndpoint: path.join(logtoUrl, '/oidc/token'), refreshToken, - resource: managementResource.indicator, + resource: defaultManagementApi.resource.indicator, }, async (...args: Parameters): Promise => { const response = await fetch(...args); diff --git a/packages/integration-tests/src/tests/api/resource.scope.test.ts b/packages/integration-tests/src/tests/api/resource.scope.test.ts index 79b0a2c50..8c8aebbf7 100644 --- a/packages/integration-tests/src/tests/api/resource.scope.test.ts +++ b/packages/integration-tests/src/tests/api/resource.scope.test.ts @@ -1,4 +1,4 @@ -import { managementResource, managementResourceScope } from '@logto/schemas'; +import { defaultManagementApi } from '@logto/schemas'; import { HTTPError } from 'got'; import { createResource } from '#src/api/index.js'; @@ -7,9 +7,9 @@ import { generateScopeName } from '#src/utils.js'; describe('scopes', () => { it('should get management api resource scopes successfully', async () => { - const scopes = await getScopes(managementResource.id); + const scopes = await getScopes(defaultManagementApi.resource.id); - expect(scopes[0]).toMatchObject(managementResourceScope); + expect(scopes[0]).toMatchObject(defaultManagementApi.scope); }); it('should create scope successfully', async () => { diff --git a/packages/integration-tests/src/tests/api/resource.test.ts b/packages/integration-tests/src/tests/api/resource.test.ts index 12744b33e..8f50b6600 100644 --- a/packages/integration-tests/src/tests/api/resource.test.ts +++ b/packages/integration-tests/src/tests/api/resource.test.ts @@ -1,4 +1,4 @@ -import { managementResource } from '@logto/schemas'; +import { defaultManagementApi } from '@logto/schemas'; import { HTTPError } from 'got'; import { createResource, getResource, updateResource, deleteResource } from '#src/api/index.js'; @@ -6,9 +6,9 @@ import { generateResourceIndicator, generateResourceName } from '#src/utils.js'; describe('admin console api resources', () => { it('should get management api resource details successfully', async () => { - const fetchedManagementApiResource = await getResource(managementResource.id); + const fetchedManagementApiResource = await getResource(defaultManagementApi.resource.id); - expect(fetchedManagementApiResource).toMatchObject(managementResource); + expect(fetchedManagementApiResource).toMatchObject(defaultManagementApi.resource); }); it('should create api resource successfully', async () => { diff --git a/packages/schemas/tables/users.sql b/packages/schemas/tables/users.sql index 37db1f92f..94b1629a9 100644 --- a/packages/schemas/tables/users.sql +++ b/packages/schemas/tables/users.sql @@ -6,9 +6,9 @@ create table users ( tenant_id varchar(21) not null references tenants (id) on update cascade on delete cascade, id varchar(12) not null, - username varchar(128) unique, - primary_email varchar(128) unique, - primary_phone varchar(128) unique, + username varchar(128), + primary_email varchar(128), + primary_phone varchar(128), password_encrypted varchar(128), password_encryption_method users_password_encryption_method, name varchar(128), @@ -19,7 +19,13 @@ create table users ( is_suspended boolean not null default false, last_sign_in_at timestamptz, created_at timestamptz not null default (now()), - primary key (id) + primary key (id), + constraint users__username + unique (tenant_id, username), + constraint users__primary_email + unique (tenant_id, primary_email), + constraint users__primary_phone + unique (tenant_id, primary_phone) ); create index users__id