mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
feat(schemas,cli,core,cloud): add manage tenant self scope (#3865)
This commit is contained in:
parent
7fb5374963
commit
8cbf87bb73
17 changed files with 292 additions and 82 deletions
2
.github/workflows/integration-test.yml
vendored
2
.github/workflows/integration-test.yml
vendored
|
@ -54,7 +54,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
target: [api, ui, ui-cloud]
|
target: [api, api-cloud, ui, ui-cloud]
|
||||||
|
|
||||||
needs: package
|
needs: package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -33,10 +33,12 @@ export const seedAdminData = async (
|
||||||
data: AdminData | UpdateAdminData,
|
data: AdminData | UpdateAdminData,
|
||||||
...additionalScopes: CreateScope[]
|
...additionalScopes: CreateScope[]
|
||||||
) => {
|
) => {
|
||||||
const { resource, scope, role } = data;
|
const { resource, scopes, role } = data;
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId,
|
scopes.every(
|
||||||
|
(scope) => resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId
|
||||||
|
),
|
||||||
new Error('All data should have the same tenant ID')
|
new Error('All data should have the same tenant ID')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -58,20 +60,25 @@ export const seedAdminData = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
await pool.query(insertInto(resource, 'resources'));
|
await pool.query(insertInto(resource, 'resources'));
|
||||||
await pool.query(insertInto(scope, 'scopes'));
|
await Promise.all(
|
||||||
await Promise.all(additionalScopes.map(async (scope) => pool.query(insertInto(scope, 'scopes'))));
|
[...scopes, ...additionalScopes].map(async (scope) => pool.query(insertInto(scope, 'scopes')))
|
||||||
|
);
|
||||||
|
|
||||||
const roleId = await processRole();
|
const roleId = await processRole();
|
||||||
await pool.query(
|
await Promise.all(
|
||||||
|
scopes.map(async ({ id }) =>
|
||||||
|
pool.query(
|
||||||
insertInto(
|
insertInto(
|
||||||
{
|
{
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
roleId,
|
roleId,
|
||||||
scopeId: scope.id,
|
scopeId: id,
|
||||||
tenantId: resource.tenantId,
|
tenantId: resource.tenantId,
|
||||||
} satisfies CreateRolesScope,
|
} satisfies CreateRolesScope,
|
||||||
'roles_scopes'
|
'roles_scopes'
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -44,31 +44,39 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const insertAdminData = async (data: AdminData) => {
|
const insertAdminData = async (data: AdminData) => {
|
||||||
const { resource, scope, role } = data;
|
const { resource, scopes, role } = data;
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
resource.tenantId && scope.tenantId && role.tenantId,
|
resource.tenantId && scopes.every(({ tenantId }) => tenantId) && role.tenantId,
|
||||||
new Error('Tenant ID cannot be empty.')
|
new Error('Tenant ID cannot be empty.')
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId,
|
scopes.every(
|
||||||
|
(scope) => resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId
|
||||||
|
),
|
||||||
new Error('All data should have the same tenant ID.')
|
new Error('All data should have the same tenant ID.')
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.query(insertInto(resource, 'resources'));
|
await client.query(insertInto(resource, 'resources'));
|
||||||
await client.query(insertInto(scope, 'scopes'));
|
await Promise.all(scopes.map(async (scope) => client.query(insertInto(scope, 'scopes'))));
|
||||||
await client.query(insertInto(role, 'roles'));
|
await client.query(insertInto(role, 'roles'));
|
||||||
await client.query(
|
|
||||||
|
const { tenantId } = resource;
|
||||||
|
await Promise.all(
|
||||||
|
scopes.map(async ({ id }) =>
|
||||||
|
client.query(
|
||||||
insertInto(
|
insertInto(
|
||||||
{
|
{
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
scopeId: scope.id,
|
scopeId: id,
|
||||||
tenantId: resource.tenantId,
|
tenantId,
|
||||||
} satisfies CreateRolesScope,
|
} satisfies CreateRolesScope,
|
||||||
'roles_scopes'
|
'roles_scopes'
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,8 @@ function Content() {
|
||||||
PredefinedScope.All,
|
PredefinedScope.All,
|
||||||
...conditionalArray(
|
...conditionalArray(
|
||||||
isCloud && cloudApi.scopes.CreateTenant,
|
isCloud && cloudApi.scopes.CreateTenant,
|
||||||
isCloud && cloudApi.scopes.ManageTenant
|
isCloud && cloudApi.scopes.ManageTenant,
|
||||||
|
isCloud && cloudApi.scopes.ManageTenantSelf
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -20,9 +20,12 @@ mockEsm('./utils.js', () => ({
|
||||||
|
|
||||||
const { jwtVerify } = mockEsm('jose', () => ({
|
const { jwtVerify } = mockEsm('jose', () => ({
|
||||||
createLocalJWKSet: jest.fn(),
|
createLocalJWKSet: jest.fn(),
|
||||||
jwtVerify: jest
|
jwtVerify: jest.fn().mockReturnValue({
|
||||||
.fn()
|
payload: {
|
||||||
.mockReturnValue({ payload: { sub: 'fooUser', scope: defaultManagementApi.scope.name } }),
|
sub: 'fooUser',
|
||||||
|
scope: defaultManagementApi.scopes.map((scope) => scope.name).join(' '),
|
||||||
|
},
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const audience = defaultManagementApi.resource.indicator;
|
const audience = defaultManagementApi.resource.indicator;
|
||||||
|
|
|
@ -60,7 +60,11 @@ export const verifyBearerTokenFromRequest = async (
|
||||||
if ((!isProduction || isIntegrationTest) && userId) {
|
if ((!isProduction || isIntegrationTest) && userId) {
|
||||||
consoleLog.warn(`Found dev user ID ${userId}, skip token validation.`);
|
consoleLog.warn(`Found dev user ID ${userId}, skip token validation.`);
|
||||||
|
|
||||||
return { sub: userId, clientId: undefined, scopes: [defaultManagementApi.scope.name] };
|
return {
|
||||||
|
sub: userId,
|
||||||
|
clientId: undefined,
|
||||||
|
scopes: defaultManagementApi.scopes.map(({ name }) => name),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getKeysAndIssuer = async (): Promise<[JWK[], string[]]> => {
|
const getKeysAndIssuer = async (): Promise<[JWK[], string[]]> => {
|
||||||
|
|
|
@ -12,8 +12,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap",
|
"build": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap",
|
||||||
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
|
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
|
||||||
"test": "pnpm build && pnpm test:api && pnpm test:ui",
|
"test": "pnpm build && pnpm test:api && pnpm test:api-cloud && pnpm test:ui",
|
||||||
"test:api": "pnpm test:only -i ./lib/tests/api/",
|
"test:api": "pnpm test:only -i ./lib/tests/api/",
|
||||||
|
"test:api-cloud": "pnpm test:only -i ./lib/tests/api-cloud/",
|
||||||
"test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui/",
|
"test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui/",
|
||||||
"test:ui-cloud": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui-cloud/",
|
"test:ui-cloud": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui-cloud/",
|
||||||
"lint": "eslint --ext .ts src",
|
"lint": "eslint --ext .ts src",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { got } from 'got';
|
import { got } from 'got';
|
||||||
|
|
||||||
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
import { logtoConsoleUrl, logtoUrl, logtoCloudUrl } from '#src/constants.js';
|
||||||
|
|
||||||
const api = got.extend({
|
const api = got.extend({
|
||||||
prefixUrl: new URL('/api', logtoUrl),
|
prefixUrl: new URL('/api', logtoUrl),
|
||||||
|
@ -24,3 +24,7 @@ export const authedAdminTenantApi = adminTenantApi.extend({
|
||||||
'development-user-id': 'integration-test-admin-user',
|
'development-user-id': 'integration-test-admin-user',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const cloudApi = got.extend({
|
||||||
|
prefixUrl: new URL('/api', logtoCloudUrl),
|
||||||
|
});
|
||||||
|
|
21
packages/integration-tests/src/api/tenant.ts
Normal file
21
packages/integration-tests/src/api/tenant.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import type { TenantInfo } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { cloudApi } from './api.js';
|
||||||
|
|
||||||
|
export const createTenant = async (accessToken: string) => {
|
||||||
|
return cloudApi
|
||||||
|
.extend({
|
||||||
|
headers: { authorization: `Bearer ${accessToken}` },
|
||||||
|
})
|
||||||
|
.post('tenants')
|
||||||
|
.json<TenantInfo>();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTenants = async (accessToken: string) => {
|
||||||
|
return cloudApi
|
||||||
|
.extend({
|
||||||
|
headers: { authorization: `Bearer ${accessToken}` },
|
||||||
|
})
|
||||||
|
.get('tenants')
|
||||||
|
.json<TenantInfo[]>();
|
||||||
|
};
|
|
@ -2,15 +2,21 @@
|
||||||
// Since they are just different in URLs
|
// Since they are just different in URLs
|
||||||
|
|
||||||
import type { LogtoConfig } from '@logto/node';
|
import type { LogtoConfig } from '@logto/node';
|
||||||
import type { Role, User } from '@logto/schemas';
|
|
||||||
import {
|
import {
|
||||||
|
cloudApiIndicator,
|
||||||
|
CloudScope,
|
||||||
PredefinedScope,
|
PredefinedScope,
|
||||||
adminTenantId,
|
adminTenantId,
|
||||||
defaultTenantId,
|
defaultTenantId,
|
||||||
getManagementApiResourceIndicator,
|
getManagementApiResourceIndicator,
|
||||||
|
getManagementApiAdminName,
|
||||||
adminConsoleApplicationId,
|
adminConsoleApplicationId,
|
||||||
InteractionEvent,
|
InteractionEvent,
|
||||||
|
AdminTenantRole,
|
||||||
|
type Role,
|
||||||
|
type User,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
import { conditionalArray } from '@silverhand/essentials';
|
||||||
|
|
||||||
import { authedAdminTenantApi as api, adminTenantApi } from '#src/api/api.js';
|
import { authedAdminTenantApi as api, adminTenantApi } from '#src/api/api.js';
|
||||||
import type { InteractionPayload } from '#src/api/interaction.js';
|
import type { InteractionPayload } from '#src/api/interaction.js';
|
||||||
|
@ -25,7 +31,7 @@ export const createResponseWithCode = (statusCode: number) => ({
|
||||||
response: { statusCode },
|
response: { statusCode },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createUserWithAllRoles = async () => {
|
const createUserWithRoles = async (roleNames: string[]) => {
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
const user = await api
|
const user = await api
|
||||||
|
@ -37,7 +43,9 @@ export const createUserWithAllRoles = async () => {
|
||||||
// Should have roles for default tenant Management API and admin tenant Me API
|
// Should have roles for default tenant Management API and admin tenant Me API
|
||||||
const roles = await api.get('roles').json<Role[]>();
|
const roles = await api.get('roles').json<Role[]>();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
roles.map(async ({ id }) =>
|
roles
|
||||||
|
.filter(({ name }) => roleNames.includes(name))
|
||||||
|
.map(async ({ id }) =>
|
||||||
api.post(`roles/${id}/users`, {
|
api.post(`roles/${id}/users`, {
|
||||||
json: { userIds: [user.id] },
|
json: { userIds: [user.id] },
|
||||||
})
|
})
|
||||||
|
@ -47,6 +55,12 @@ export const createUserWithAllRoles = async () => {
|
||||||
return [user, { username, password }] as const;
|
return [user, { username, password }] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createUserWithAllRoles = async () => {
|
||||||
|
const allRoles = await api.get('roles').json<Role[]>();
|
||||||
|
const allRoleNames = allRoles.map(({ name }) => name);
|
||||||
|
return createUserWithRoles(allRoleNames);
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteUser = async (id: string) => {
|
export const deleteUser = async (id: string) => {
|
||||||
await api.delete(`users/${id}`);
|
await api.delete(`users/${id}`);
|
||||||
};
|
};
|
||||||
|
@ -86,7 +100,7 @@ export const initClientAndSignIn = async (
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createUserAndSignInWithClient = async () => {
|
export const createUserWithAllRolesAndSignInToClient = async () => {
|
||||||
const [{ id }, { username, password }] = await createUserWithAllRoles();
|
const [{ id }, { username, password }] = await createUserWithAllRoles();
|
||||||
const client = await initClientAndSignIn(username, password, {
|
const client = await initClientAndSignIn(username, password, {
|
||||||
resources: [resourceDefault, resourceMe],
|
resources: [resourceDefault, resourceMe],
|
||||||
|
@ -95,3 +109,20 @@ export const createUserAndSignInWithClient = async () => {
|
||||||
|
|
||||||
return { id, client };
|
return { id, client };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createUserAndSignInToCloudClient = async (
|
||||||
|
userRoleType: AdminTenantRole.User | AdminTenantRole.Admin
|
||||||
|
) => {
|
||||||
|
const [{ id }, { username, password }] = await createUserWithRoles(
|
||||||
|
conditionalArray<string>(
|
||||||
|
AdminTenantRole.User,
|
||||||
|
userRoleType === AdminTenantRole.Admin && getManagementApiAdminName(adminTenantId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const client = await initClientAndSignIn(username, password, {
|
||||||
|
resources: [cloudApiIndicator],
|
||||||
|
scopes: Object.values(CloudScope),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id, client };
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import {
|
||||||
|
cloudApiIndicator,
|
||||||
|
CloudScope,
|
||||||
|
AdminTenantRole,
|
||||||
|
type Resource,
|
||||||
|
type Scope,
|
||||||
|
type Role,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
|
||||||
|
import { authedAdminTenantApi } from '#src/api/api.js';
|
||||||
|
import { createTenant, getTenants } from '#src/api/tenant.js';
|
||||||
|
import { createUserAndSignInToCloudClient } from '#src/helpers/admin-tenant.js';
|
||||||
|
|
||||||
|
describe('Tenant APIs', () => {
|
||||||
|
it('should be able to create multiple tenants for `admin` role', async () => {
|
||||||
|
const { client } = await createUserAndSignInToCloudClient(AdminTenantRole.Admin);
|
||||||
|
const accessToken = await client.getAccessToken(cloudApiIndicator);
|
||||||
|
const tenant1 = await createTenant(accessToken);
|
||||||
|
const tenant2 = await createTenant(accessToken);
|
||||||
|
expect(tenant1).toHaveProperty('id');
|
||||||
|
expect(tenant2).toHaveProperty('id');
|
||||||
|
const tenants = await getTenants(accessToken);
|
||||||
|
expect(tenants.length).toBeGreaterThan(2);
|
||||||
|
expect(tenants.find((tenant) => tenant.id === tenant1.id)).toBeDefined();
|
||||||
|
expect(tenants.find((tenant) => tenant.id === tenant2.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create only one tenant for `user` role', async () => {
|
||||||
|
const { client } = await createUserAndSignInToCloudClient(AdminTenantRole.User);
|
||||||
|
const accessToken = await client.getAccessToken(cloudApiIndicator);
|
||||||
|
const tenant1 = await createTenant(accessToken);
|
||||||
|
await expect(createTenant(accessToken)).rejects.toThrow();
|
||||||
|
expect(tenant1).toHaveProperty('id');
|
||||||
|
const tenants = await getTenants(accessToken);
|
||||||
|
expect(tenants.length).toEqual(1);
|
||||||
|
expect(tenants.find((tenant) => tenant.id === tenant1.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`user` role should have `CloudScope.ManageTenantSelf` scope', async () => {
|
||||||
|
const resources = await authedAdminTenantApi.get('resources').json<Resource[]>();
|
||||||
|
const cloudApiResource = resources.find(({ indicator }) => indicator === cloudApiIndicator);
|
||||||
|
expect(cloudApiResource).toBeDefined();
|
||||||
|
const scopes = await authedAdminTenantApi
|
||||||
|
.get(`resources/${cloudApiResource!.id}/scopes`)
|
||||||
|
.json<Scope[]>();
|
||||||
|
const manageOwnTenantScope = scopes.find((scope) => scope.name === CloudScope.ManageTenantSelf);
|
||||||
|
expect(manageOwnTenantScope).toBeDefined();
|
||||||
|
const roles = await authedAdminTenantApi.get('roles').json<Role[]>();
|
||||||
|
const userRole = roles.find(({ name }) => name === 'user');
|
||||||
|
expect(userRole).toBeDefined();
|
||||||
|
const roleScopes = await authedAdminTenantApi
|
||||||
|
.get(`roles/${userRole!.id}/scopes`)
|
||||||
|
.json<Scope[]>();
|
||||||
|
expect(roleScopes.find(({ id }) => id === manageOwnTenantScope!.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,7 +3,7 @@ import { got } from 'got';
|
||||||
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
||||||
import {
|
import {
|
||||||
createResponseWithCode,
|
createResponseWithCode,
|
||||||
createUserAndSignInWithClient,
|
createUserWithAllRolesAndSignInToClient,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
resourceDefault,
|
resourceDefault,
|
||||||
resourceMe,
|
resourceMe,
|
||||||
|
@ -22,7 +22,7 @@ describe('me', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only recognize the access token with correct resource and scope', async () => {
|
it('should only recognize the access token with correct resource and scope', async () => {
|
||||||
const { id, client } = await createUserAndSignInWithClient();
|
const { id, client } = await createUserWithAllRolesAndSignInToClient();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
got.get(logtoConsoleUrl + '/me/custom-data', {
|
got.get(logtoConsoleUrl + '/me/custom-data', {
|
||||||
|
@ -40,7 +40,7 @@ describe('me', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to update custom data', async () => {
|
it('should be able to update custom data', async () => {
|
||||||
const { id, client } = await createUserAndSignInWithClient();
|
const { id, client } = await createUserWithAllRolesAndSignInToClient();
|
||||||
const headers = { authorization: `Bearer ${await client.getAccessToken(resourceMe)}` };
|
const headers = { authorization: `Bearer ${await client.getAccessToken(resourceMe)}` };
|
||||||
|
|
||||||
const data = await got
|
const data = await got
|
||||||
|
|
|
@ -29,7 +29,7 @@ describe('get access token', () => {
|
||||||
it('can sign in and getAccessToken with admin user', async () => {
|
it('can sign in and getAccessToken with admin user', async () => {
|
||||||
const client = new MockClient({
|
const client = new MockClient({
|
||||||
resources: [defaultManagementApi.resource.indicator],
|
resources: [defaultManagementApi.resource.indicator],
|
||||||
scopes: [defaultManagementApi.scope.name],
|
scopes: defaultManagementApi.scopes.map(({ name }) => name),
|
||||||
});
|
});
|
||||||
await client.initSession();
|
await client.initSession();
|
||||||
await client.successSend(putInteraction, {
|
await client.successSend(putInteraction, {
|
||||||
|
@ -42,7 +42,7 @@ describe('get access token', () => {
|
||||||
expect(accessToken).not.toBeNull();
|
expect(accessToken).not.toBeNull();
|
||||||
expect(getAccessTokenPayload(accessToken)).toHaveProperty(
|
expect(getAccessTokenPayload(accessToken)).toHaveProperty(
|
||||||
'scope',
|
'scope',
|
||||||
defaultManagementApi.scope.name
|
defaultManagementApi.scopes.map(({ name }) => name).join(' ')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Request for invalid resource should throw
|
// Request for invalid resource should throw
|
||||||
|
@ -52,7 +52,7 @@ describe('get access token', () => {
|
||||||
it('can sign in and getAccessToken with guest user', async () => {
|
it('can sign in and getAccessToken with guest user', async () => {
|
||||||
const client = new MockClient({
|
const client = new MockClient({
|
||||||
resources: [defaultManagementApi.resource.indicator],
|
resources: [defaultManagementApi.resource.indicator],
|
||||||
scopes: [defaultManagementApi.scope.name],
|
scopes: defaultManagementApi.scopes.map(({ name }) => name),
|
||||||
});
|
});
|
||||||
await client.initSession();
|
await client.initSession();
|
||||||
await client.successSend(putInteraction, {
|
await client.successSend(putInteraction, {
|
||||||
|
@ -65,7 +65,7 @@ describe('get access token', () => {
|
||||||
|
|
||||||
expect(getAccessTokenPayload(accessToken)).not.toHaveProperty(
|
expect(getAccessTokenPayload(accessToken)).not.toHaveProperty(
|
||||||
'scope',
|
'scope',
|
||||||
defaultManagementApi.scope.name
|
defaultManagementApi.scopes.map(({ name }) => name).join(' ')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ describe('scopes', () => {
|
||||||
it('should get management api resource scopes successfully', async () => {
|
it('should get management api resource scopes successfully', async () => {
|
||||||
const scopes = await getScopes(defaultManagementApi.resource.id);
|
const scopes = await getScopes(defaultManagementApi.resource.id);
|
||||||
|
|
||||||
expect(scopes[0]).toMatchObject(defaultManagementApi.scope);
|
expect(scopes[0]).toMatchObject(expect.objectContaining(defaultManagementApi.scopes[0]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create scope successfully', async () => {
|
it('should create scope successfully', async () => {
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { generateStandardId } from '@logto/shared/universal';
|
||||||
|
import { sql } from 'slonik';
|
||||||
|
|
||||||
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||||
|
|
||||||
|
const adminTenantId = 'admin';
|
||||||
|
|
||||||
|
const alteration: AlterationScript = {
|
||||||
|
up: async (pool) => {
|
||||||
|
// Get `resourceId` of the admin tenant's resource whose indicator is `https://cloud.logto.io/api`.
|
||||||
|
const { id: resourceId } = await pool.one<{ id: string }>(sql`
|
||||||
|
select id from resources
|
||||||
|
where tenant_id = ${adminTenantId}
|
||||||
|
and indicator = 'https://cloud.logto.io/api'
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get `roleId` of the admin tenant's role whose name is `user`.
|
||||||
|
const { id: roleId } = await pool.one<{ id: string }>(sql`
|
||||||
|
select id from roles
|
||||||
|
where tenant_id = ${adminTenantId}
|
||||||
|
and name = 'user';
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Insert `manage:tenant:self` scope.
|
||||||
|
const scopeId = generateStandardId();
|
||||||
|
await pool.query(sql`
|
||||||
|
insert into scopes (tenant_id, id, name, description, resource_id)
|
||||||
|
values (
|
||||||
|
${adminTenantId},
|
||||||
|
${scopeId},
|
||||||
|
'manage:tenant:self',
|
||||||
|
'Allow managing tenant itself, including update and delete.',
|
||||||
|
${resourceId}
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
// Assign `manage:tenant:self` scope to `user` role.
|
||||||
|
await pool.query(sql`
|
||||||
|
insert into roles_scopes (tenant_id, id, role_id, scope_id)
|
||||||
|
values (
|
||||||
|
${adminTenantId},
|
||||||
|
${generateStandardId()},
|
||||||
|
${roleId},
|
||||||
|
${scopeId}
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
down: async (pool) => {
|
||||||
|
// Delete `manage:tenant:self` scope.
|
||||||
|
// No need to delete `roles_scopes` because it will be cascade deleted.
|
||||||
|
await pool.query(sql`
|
||||||
|
delete from scopes
|
||||||
|
where tenant_id = ${adminTenantId} and name = 'manage:tenant:self';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default alteration;
|
|
@ -10,8 +10,12 @@ import { adminTenantId } from './tenant.js';
|
||||||
export const cloudApiIndicator = 'https://cloud.logto.io/api';
|
export const cloudApiIndicator = 'https://cloud.logto.io/api';
|
||||||
|
|
||||||
export enum CloudScope {
|
export enum CloudScope {
|
||||||
|
/** The user can create a user tenant. */
|
||||||
CreateTenant = 'create:tenant',
|
CreateTenant = 'create:tenant',
|
||||||
|
/** The user can perform arbitrary operations on any tenant. */
|
||||||
ManageTenant = 'manage:tenant',
|
ManageTenant = 'manage:tenant',
|
||||||
|
/** The user can update or delete its own tenants. */
|
||||||
|
ManageTenantSelf = 'manage:tenant:self',
|
||||||
SendSms = 'send:sms',
|
SendSms = 'send:sms',
|
||||||
SendEmail = 'send:email',
|
SendEmail = 'send:email',
|
||||||
}
|
}
|
||||||
|
@ -34,7 +38,13 @@ export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]>
|
||||||
indicator: cloudApiIndicator,
|
indicator: cloudApiIndicator,
|
||||||
name: `Logto Cloud API`,
|
name: `Logto Cloud API`,
|
||||||
},
|
},
|
||||||
scope: buildScope(CloudScope.CreateTenant, 'Allow creating new tenants.'),
|
scopes: [
|
||||||
|
buildScope(CloudScope.CreateTenant, 'Allow creating new tenants.'),
|
||||||
|
buildScope(
|
||||||
|
CloudScope.ManageTenantSelf,
|
||||||
|
'Allow managing tenant itself, including update and delete.'
|
||||||
|
),
|
||||||
|
],
|
||||||
role: {
|
role: {
|
||||||
tenantId: adminTenantId,
|
tenantId: adminTenantId,
|
||||||
name: AdminTenantRole.User,
|
name: AdminTenantRole.User,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { adminTenantId, defaultTenantId } from './tenant.js';
|
||||||
|
|
||||||
export type AdminData = {
|
export type AdminData = {
|
||||||
resource: CreateResource;
|
resource: CreateResource;
|
||||||
scope: CreateScope;
|
scopes: CreateScope[];
|
||||||
role: CreateRole;
|
role: CreateRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,7 +35,8 @@ export const defaultManagementApi = Object.freeze({
|
||||||
indicator: `https://${defaultTenantId}.logto.app/api`,
|
indicator: `https://${defaultTenantId}.logto.app/api`,
|
||||||
name: 'Logto Management API',
|
name: 'Logto Management API',
|
||||||
},
|
},
|
||||||
scope: {
|
scopes: [
|
||||||
|
{
|
||||||
tenantId: defaultTenantId,
|
tenantId: defaultTenantId,
|
||||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||||
id: defaultScopeAllId,
|
id: defaultScopeAllId,
|
||||||
|
@ -44,6 +45,7 @@ export const defaultManagementApi = Object.freeze({
|
||||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||||
resourceId: defaultResourceId,
|
resourceId: defaultResourceId,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
role: {
|
role: {
|
||||||
tenantId: defaultTenantId,
|
tenantId: defaultTenantId,
|
||||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||||
|
@ -79,13 +81,15 @@ export const createAdminData = (tenantId: string): AdminData => {
|
||||||
indicator: getManagementApiResourceIndicator(tenantId),
|
indicator: getManagementApiResourceIndicator(tenantId),
|
||||||
name: `Logto Management API`,
|
name: `Logto Management API`,
|
||||||
},
|
},
|
||||||
scope: {
|
scopes: [
|
||||||
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
name: PredefinedScope.All,
|
name: PredefinedScope.All,
|
||||||
description: 'Default scope for Management API, allows all permissions.',
|
description: 'Default scope for Management API, allows all permissions.',
|
||||||
resourceId,
|
resourceId,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
role: {
|
role: {
|
||||||
tenantId,
|
tenantId,
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
|
@ -106,13 +110,15 @@ export const createAdminDataInAdminTenant = (tenantId: string): AdminData => {
|
||||||
indicator: getManagementApiResourceIndicator(tenantId),
|
indicator: getManagementApiResourceIndicator(tenantId),
|
||||||
name: `Logto Management API for tenant ${tenantId}`,
|
name: `Logto Management API for tenant ${tenantId}`,
|
||||||
},
|
},
|
||||||
scope: {
|
scopes: [
|
||||||
|
{
|
||||||
tenantId: adminTenantId,
|
tenantId: adminTenantId,
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
name: PredefinedScope.All,
|
name: PredefinedScope.All,
|
||||||
description: 'Default scope for Management API, allows all permissions.',
|
description: 'Default scope for Management API, allows all permissions.',
|
||||||
resourceId,
|
resourceId,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
role: {
|
role: {
|
||||||
tenantId: adminTenantId,
|
tenantId: adminTenantId,
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
|
@ -132,13 +138,15 @@ export const createMeApiInAdminTenant = (): AdminData => {
|
||||||
indicator: getManagementApiResourceIndicator(adminTenantId, 'me'),
|
indicator: getManagementApiResourceIndicator(adminTenantId, 'me'),
|
||||||
name: `Logto Me API`,
|
name: `Logto Me API`,
|
||||||
},
|
},
|
||||||
scope: {
|
scopes: [
|
||||||
|
{
|
||||||
tenantId: adminTenantId,
|
tenantId: adminTenantId,
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
name: PredefinedScope.All,
|
name: PredefinedScope.All,
|
||||||
description: 'Default scope for Me API, allows all permissions.',
|
description: 'Default scope for Me API, allows all permissions.',
|
||||||
resourceId,
|
resourceId,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
role: {
|
role: {
|
||||||
tenantId: adminTenantId,
|
tenantId: adminTenantId,
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue