0
Fork 0
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:
Darcy Ye 2023-05-26 17:38:09 +08:00 committed by GitHub
parent 7fb5374963
commit 8cbf87bb73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 292 additions and 82 deletions

View file

@ -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

View file

@ -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'
) )
)
)
); );
}; };

View file

@ -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'
) )
)
)
); );
}; };

View file

@ -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
), ),
], ],
[] []

View file

@ -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;

View file

@ -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[]]> => {

View file

@ -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",

View file

@ -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),
});

View 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[]>();
};

View file

@ -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 };
};

View file

@ -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();
});
});

View file

@ -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

View file

@ -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(' ')
); );
}); });

View file

@ -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 () => {

View file

@ -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;

View file

@ -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,

View file

@ -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(),