mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
feat(console,core)!: use rbac scope to control management resource (#2942)
This commit is contained in:
parent
9292bc086d
commit
9ed66a8593
9 changed files with 59 additions and 57 deletions
|
@ -23,7 +23,7 @@
|
|||
"@logto/language-kit": "workspace:*",
|
||||
"@logto/phrases": "workspace:*",
|
||||
"@logto/phrases-ui": "workspace:*",
|
||||
"@logto/react": "1.0.0-beta.14",
|
||||
"@logto/react": "1.0.0-beta.15",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@parcel/core": "2.8.2",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import { LogtoProvider } from '@logto/react';
|
||||
import { adminConsoleApplicationId, managementResource } from '@logto/schemas';
|
||||
import {
|
||||
adminConsoleApplicationId,
|
||||
managementResource,
|
||||
managementResourceScope,
|
||||
} from '@logto/schemas';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
|
@ -139,7 +143,7 @@ const App = () => (
|
|||
endpoint: window.location.origin,
|
||||
appId: adminConsoleApplicationId,
|
||||
resources: [managementResource.indicator],
|
||||
scopes: [UserScope.Identities, UserScope.CustomData],
|
||||
scopes: [UserScope.Identities, UserScope.CustomData, managementResourceScope.name],
|
||||
}}
|
||||
>
|
||||
<Main />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UserRole } from '@logto/schemas';
|
||||
import { managementResourceScope } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
import type { Context } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
@ -15,7 +15,9 @@ const { jest } = import.meta;
|
|||
const { mockEsm } = createMockUtils(jest);
|
||||
|
||||
const { jwtVerify } = mockEsm('jose', () => ({
|
||||
jwtVerify: jest.fn().mockReturnValue({ payload: { sub: 'fooUser', role_names: ['admin'] } }),
|
||||
jwtVerify: jest
|
||||
.fn()
|
||||
.mockReturnValue({ payload: { sub: 'fooUser', scope: managementResourceScope.name } }),
|
||||
}));
|
||||
|
||||
const koaAuth = await pickDefault(import('./koa-auth.js'));
|
||||
|
@ -169,7 +171,7 @@ describe('koaAuth middleware', () => {
|
|||
expect(ctx.auth).toEqual({ type: 'app', id: 'bar' });
|
||||
});
|
||||
|
||||
it('expect to throw if jwt role_names is missing', async () => {
|
||||
it('expect to throw if jwt scope is missing', async () => {
|
||||
jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'fooUser' } }));
|
||||
|
||||
ctx.request = {
|
||||
|
@ -179,14 +181,14 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(envSetForTest, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
||||
forbiddenError
|
||||
);
|
||||
await expect(
|
||||
koaAuth(envSetForTest, managementResourceScope.name)(ctx, next)
|
||||
).rejects.toMatchError(forbiddenError);
|
||||
});
|
||||
|
||||
it('expect to throw if jwt role_names does not include admin', async () => {
|
||||
it('expect to throw if jwt scope does not include management resource scope', async () => {
|
||||
jwtVerify.mockImplementationOnce(() => ({
|
||||
payload: { sub: 'fooUser', role_names: ['foo'] },
|
||||
payload: { sub: 'fooUser', scope: 'foo' },
|
||||
}));
|
||||
|
||||
ctx.request = {
|
||||
|
@ -196,9 +198,9 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(envSetForTest, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
||||
forbiddenError
|
||||
);
|
||||
await expect(
|
||||
koaAuth(envSetForTest, managementResourceScope.name)(ctx, next)
|
||||
).rejects.toMatchError(forbiddenError);
|
||||
});
|
||||
|
||||
it('expect to throw unauthorized error if unknown error occurs', async () => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import { UserRole, managementResource } from '@logto/schemas';
|
||||
import { managementResource, managementResourceScope } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { jwtVerify } from 'jose';
|
||||
import type { MiddlewareType, Request } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
@ -42,6 +42,7 @@ const extractBearerTokenFromHeaders = ({ authorization }: IncomingHttpHeaders) =
|
|||
type TokenInfo = {
|
||||
sub: string;
|
||||
clientId: unknown;
|
||||
scopes: string[];
|
||||
roleNames?: string[];
|
||||
};
|
||||
|
||||
|
@ -54,13 +55,13 @@ export const verifyBearerTokenFromRequest = async (
|
|||
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
||||
|
||||
if ((!isProduction || isIntegrationTest) && userId) {
|
||||
return { sub: userId, clientId: undefined, roleNames: [UserRole.Admin] };
|
||||
return { sub: userId, clientId: undefined, scopes: [managementResourceScope.name] };
|
||||
}
|
||||
|
||||
try {
|
||||
const { localJWKSet, issuer } = envSet.oidc;
|
||||
const {
|
||||
payload: { sub, client_id: clientId, role_names: roleNames },
|
||||
payload: { sub, client_id: clientId, scope = '' },
|
||||
} = await jwtVerify(extractBearerTokenFromHeaders(request.headers), localJWKSet, {
|
||||
issuer,
|
||||
audience: resourceIndicator,
|
||||
|
@ -68,7 +69,7 @@ export const verifyBearerTokenFromRequest = async (
|
|||
|
||||
assertThat(sub, new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }));
|
||||
|
||||
return { sub, clientId, roleNames: conditional(Array.isArray(roleNames) && roleNames) };
|
||||
return { sub, clientId, scopes: z.string().parse(scope).split(' ') };
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RequestError) {
|
||||
throw error;
|
||||
|
@ -80,18 +81,18 @@ export const verifyBearerTokenFromRequest = async (
|
|||
|
||||
export default function koaAuth<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
|
||||
envSet: EnvSet,
|
||||
forRole?: UserRole
|
||||
forScope?: string
|
||||
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const { sub, clientId, roleNames } = await verifyBearerTokenFromRequest(
|
||||
const { sub, clientId, scopes } = await verifyBearerTokenFromRequest(
|
||||
envSet,
|
||||
ctx.request,
|
||||
managementResource.indicator
|
||||
);
|
||||
|
||||
if (forRole) {
|
||||
if (forScope) {
|
||||
assertThat(
|
||||
roleNames?.includes(forRole),
|
||||
scopes.includes(forScope),
|
||||
new RequestError({ code: 'auth.forbidden', status: 403 })
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UserRole } from '@logto/schemas';
|
||||
import { managementResourceScope } from '@logto/schemas';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
|
||||
|
@ -32,7 +32,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
interactionRoutes(interactionRouter, tenant);
|
||||
|
||||
const managementRouter: AuthedRouter = new Router();
|
||||
managementRouter.use(koaAuth(tenant.envSet, UserRole.Admin));
|
||||
managementRouter.use(koaAuth(tenant.envSet, managementResourceScope.name));
|
||||
applicationRoutes(managementRouter, tenant);
|
||||
settingRoutes(managementRouter, tenant);
|
||||
connectorRoutes(managementRouter, tenant);
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { adminConsoleApplicationId, UserRole } from '@logto/schemas';
|
||||
import {
|
||||
adminConsoleApplicationId,
|
||||
managementResourceId,
|
||||
managementResourceScope,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
@ -29,12 +33,15 @@ export default function consentRoutes<T>(
|
|||
|
||||
const { accountId } = session;
|
||||
|
||||
// Temp solution before migrating to RBAC. Block non-admin user from consenting to admin console
|
||||
// Block non-admin user from consenting to admin console
|
||||
if (String(client_id) === adminConsoleApplicationId) {
|
||||
const { roleNames } = await libraries.users.findUserByIdWithRoles(accountId);
|
||||
const scopes = await libraries.users.findUserScopesForResourceId(
|
||||
accountId,
|
||||
managementResourceId
|
||||
);
|
||||
|
||||
assertThat(
|
||||
roleNames.includes(UserRole.Admin),
|
||||
scopes.some(({ name }) => name === managementResourceScope.name),
|
||||
new RequestError({ code: 'auth.forbidden', status: 401 })
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@logto/phrases": "workspace:*",
|
||||
"@logto/react": "1.0.0-beta.14",
|
||||
"@logto/react": "1.0.0-beta.15",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@parcel/core": "2.8.2",
|
||||
"@parcel/transformer-sass": "2.8.2",
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"@jest/types": "^29.1.2",
|
||||
"@logto/connector-kit": "workspace:*",
|
||||
"@logto/js": "1.0.0-beta.14",
|
||||
"@logto/node": "1.0.0-beta.14",
|
||||
"@logto/node": "1.0.0-beta.15",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "1.3.0",
|
||||
|
|
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
|
@ -109,7 +109,7 @@ importers:
|
|||
'@logto/language-kit': workspace:*
|
||||
'@logto/phrases': workspace:*
|
||||
'@logto/phrases-ui': workspace:*
|
||||
'@logto/react': 1.0.0-beta.14
|
||||
'@logto/react': 1.0.0-beta.15
|
||||
'@logto/schemas': workspace:*
|
||||
'@mdx-js/react': ^1.6.22
|
||||
'@parcel/core': 2.8.2
|
||||
|
@ -178,7 +178,7 @@ importers:
|
|||
'@logto/language-kit': link:../toolkit/language-kit
|
||||
'@logto/phrases': link:../phrases
|
||||
'@logto/phrases-ui': link:../phrases-ui
|
||||
'@logto/react': 1.0.0-beta.14_react@18.2.0
|
||||
'@logto/react': 1.0.0-beta.15_react@18.2.0
|
||||
'@logto/schemas': link:../schemas
|
||||
'@mdx-js/react': 1.6.22_react@18.2.0
|
||||
'@parcel/core': 2.8.2
|
||||
|
@ -408,7 +408,7 @@ importers:
|
|||
'@logto/core-kit': workspace:*
|
||||
'@logto/language-kit': workspace:*
|
||||
'@logto/phrases': workspace:*
|
||||
'@logto/react': 1.0.0-beta.14
|
||||
'@logto/react': 1.0.0-beta.15
|
||||
'@logto/schemas': workspace:*
|
||||
'@parcel/core': 2.8.2
|
||||
'@parcel/transformer-sass': 2.8.2
|
||||
|
@ -436,7 +436,7 @@ importers:
|
|||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
'@logto/language-kit': link:../toolkit/language-kit
|
||||
'@logto/phrases': link:../phrases
|
||||
'@logto/react': 1.0.0-beta.14_react@18.2.0
|
||||
'@logto/react': 1.0.0-beta.15_react@18.2.0
|
||||
'@logto/schemas': link:../schemas
|
||||
'@parcel/core': 2.8.2
|
||||
'@parcel/transformer-sass': 2.8.2_@parcel+core@2.8.2
|
||||
|
@ -466,7 +466,7 @@ importers:
|
|||
'@jest/types': ^29.1.2
|
||||
'@logto/connector-kit': workspace:*
|
||||
'@logto/js': 1.0.0-beta.14
|
||||
'@logto/node': 1.0.0-beta.14
|
||||
'@logto/node': 1.0.0-beta.15
|
||||
'@logto/schemas': workspace:*
|
||||
'@peculiar/webcrypto': ^1.3.3
|
||||
'@silverhand/eslint-config': 1.3.0
|
||||
|
@ -494,7 +494,7 @@ importers:
|
|||
'@jest/types': 29.1.2
|
||||
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||
'@logto/js': 1.0.0-beta.14
|
||||
'@logto/node': 1.0.0-beta.14
|
||||
'@logto/node': 1.0.0-beta.15
|
||||
'@logto/schemas': link:../schemas
|
||||
'@peculiar/webcrypto': 1.3.3
|
||||
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
|
||||
|
@ -2310,26 +2310,14 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/@logto/browser/1.0.0-beta.14:
|
||||
resolution: {integrity: sha512-yjD1qtRXbX2E5Jgr5F1BK4SRwNhIlbJZK1yZLZNvOltEG76NhfoqvCI8P5PGIiPwvunB2lqPNJFsNOSI3k0Q+w==}
|
||||
/@logto/browser/1.0.0-beta.15:
|
||||
resolution: {integrity: sha512-AbIvIAO9GYXa2G2Komx0+pQ/PrSIdXCpEVZUtjEhQXQ07gmkeq4TRv3Q68H7HyHTVS1S2Rd5eZpWcqTcrj7DhQ==}
|
||||
dependencies:
|
||||
'@logto/client': 1.0.0-beta.14
|
||||
'@logto/client': 1.0.0-beta.15
|
||||
'@silverhand/essentials': 1.3.0
|
||||
js-base64: 3.7.3
|
||||
dev: true
|
||||
|
||||
/@logto/client/1.0.0-beta.14:
|
||||
resolution: {integrity: sha512-quhQJ4rjb1Djhspeq2F5pFxXdgjN5UaWei6nnbUfp12CDhRojKrLJIGl+FDx/HSWuG0b93nwxKnJeJsiX/8E3Q==}
|
||||
dependencies:
|
||||
'@logto/core-kit': 1.0.0-beta.20
|
||||
'@logto/js': 1.0.0-beta.14
|
||||
'@silverhand/essentials': 1.3.0
|
||||
camelcase-keys: 7.0.2
|
||||
jose: 4.11.1
|
||||
lodash.get: 4.4.2
|
||||
lodash.once: 4.1.1
|
||||
dev: true
|
||||
|
||||
/@logto/client/1.0.0-beta.15:
|
||||
resolution: {integrity: sha512-+CrgyUcBcTILpfMPtwIEwBD60XgXUCdu7MpnvNZjd0sNaUpAoyFbUiRKzvbFeF7w9Nc4zO/kgAwbk36kqTXsvw==}
|
||||
dependencies:
|
||||
|
@ -2371,8 +2359,8 @@ packages:
|
|||
zod: 3.20.2
|
||||
dev: true
|
||||
|
||||
/@logto/node/1.0.0-beta.14:
|
||||
resolution: {integrity: sha512-+0S6lBBcG3pOmjEMRQnD+6X0MJ3V3E/4In59ckl/uVr/UgIufvOKWJwWCfsVKyguaO3QweJn19x7YkF8FyO31g==}
|
||||
/@logto/node/1.0.0-beta.15:
|
||||
resolution: {integrity: sha512-ELTnVZqKwRH7NIa3C2EWEmWGvocPexpcdpWE9GHFt4N7nYB1mcFoeB5yni6uLdkUNpoW1cKfoGg8ZXEPlaodGQ==}
|
||||
dependencies:
|
||||
'@logto/client': 1.0.0-beta.15
|
||||
'@silverhand/essentials': 1.3.0
|
||||
|
@ -2382,12 +2370,12 @@ packages:
|
|||
- encoding
|
||||
dev: true
|
||||
|
||||
/@logto/react/1.0.0-beta.14_react@18.2.0:
|
||||
resolution: {integrity: sha512-lHuwpHzJkIbHj/VvhzxmL7hWkyDYA8rInv0sm0M21br43OotgP7fMc62Wj78ty+QIj+or5UGwCcULBa8HySQcQ==}
|
||||
/@logto/react/1.0.0-beta.15_react@18.2.0:
|
||||
resolution: {integrity: sha512-G/GZFtPifv9Ln+iL8ka+LhfnawyC2UUFzcEIomO5qP5fN11+eYES80O6a0jNGPm3+8t6otefYPO8skyByohm/w==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0 || ^18.0.0'
|
||||
dependencies:
|
||||
'@logto/browser': 1.0.0-beta.14
|
||||
'@logto/browser': 1.0.0-beta.15
|
||||
'@silverhand/essentials': 1.3.0
|
||||
react: 18.2.0
|
||||
dev: true
|
||||
|
|
Loading…
Add table
Reference in a new issue