0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(core,schemas): remove cloudConnection call in koaTenantGuard (#5395)

remove cloudConnection dependency in the koaTenantGuard
This commit is contained in:
simeng-li 2024-02-19 10:45:57 +08:00 committed by GitHub
parent 935ee34e12
commit 52f4e578a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 97 additions and 56 deletions

View file

@ -1,35 +1,30 @@
import type router from '@logto/cloud/routes';
import Client from '@withtyped/client';
import Sinon from 'sinon';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { CloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
import { type LogtoConfigLibrary } from '#src/libraries/logto-config.js';
import { MockQueries } from '#src/test-utils/tenant.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
import koaTenantGuard from './koa-tenant-guard.js';
const { jest } = import.meta;
const logtoConfigs: LogtoConfigLibrary = {
getCloudConnectionData: jest.fn().mockResolvedValue({
appId: 'appId',
appSecret: 'appSecret',
resource: 'resource',
}),
getOidcConfigs: jest.fn(),
};
const mockFindTenantStatusById = jest.fn();
const queries = new MockQueries({
tenants: {
findTenantSuspendStatusById: mockFindTenantStatusById,
},
});
describe('koaTenantGuard middleware', () => {
const cloudConnection = new CloudConnectionLibrary(logtoConfigs);
const mockCloudClient = new Client<typeof router>({ baseUrl: 'http://localhost:3000' });
const getClientSpy = jest.spyOn(cloudConnection, 'getClient').mockResolvedValue(mockCloudClient);
const clientGetSpy = jest.spyOn(mockCloudClient, 'get');
const next = jest.fn();
const ctx = createContextWithRouteParameters();
const tenantId = 'tenant_id';
afterEach(() => {
jest.clearAllMocks();
});
it('should return directly if not in cloud', async () => {
const stub = Sinon.stub(EnvSet, 'values').value({
@ -37,48 +32,31 @@ describe('koaTenantGuard middleware', () => {
isCloud: false,
});
await expect(koaTenantGuard(cloudConnection)(ctx, next)).resolves.not.toThrow();
expect(clientGetSpy).not.toBeCalled();
expect(getClientSpy).not.toBeCalled();
await expect(koaTenantGuard(tenantId, queries)(ctx, next)).resolves.not.toThrow();
expect(mockFindTenantStatusById).not.toBeCalled();
stub.restore();
});
it('should reject if tenant is suspended', async () => {
const stub = Sinon.stub(EnvSet, 'values').value({
Sinon.stub(EnvSet, 'values').value({
...EnvSet.values,
isCloud: true,
});
// @ts-expect-error mock returning value
clientGetSpy.mockResolvedValue({
isSuspended: true,
});
mockFindTenantStatusById.mockResolvedValueOnce({ isSuspended: true });
await expect(koaTenantGuard(cloudConnection)(ctx, next)).rejects.toMatchError(
await expect(koaTenantGuard(tenantId, queries)(ctx, next)).rejects.toMatchError(
new RequestError('subscription.tenant_suspended', 403)
);
expect(clientGetSpy).toBeCalledWith('/api/my/tenant');
stub.restore();
});
it('should resolve if tenant is not suspended', async () => {
const stub = Sinon.stub(EnvSet, 'values').value({
Sinon.stub(EnvSet, 'values').value({
...EnvSet.values,
isCloud: true,
});
// @ts-expect-error mock returning value
clientGetSpy.mockResolvedValue({
isSuspended: false,
});
await expect(koaTenantGuard(cloudConnection)(ctx, next)).resolves.not.toThrow();
expect(clientGetSpy).toBeCalledWith('/api/my/tenant');
stub.restore();
mockFindTenantStatusById.mockResolvedValueOnce({ tenantId, isSuspended: false });
await expect(koaTenantGuard(tenantId, queries)(ctx, next)).resolves.not.toThrow();
});
});

View file

@ -3,17 +3,11 @@ import { type IRouterParamContext } from 'koa-router';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
const getAvailableTenant = async (cloudConnection: CloudConnectionLibrary) => {
const client = await cloudConnection.getClient();
const tenant = await client.get('/api/my/tenant');
return tenant;
};
import type Queries from '#src/tenants/Queries.js';
export default function koaTenantGuard<StateT, ContextT extends IRouterParamContext, BodyT>(
cloudConnection: CloudConnectionLibrary
tenantId: string,
{ tenants }: Queries
): Middleware<StateT, ContextT, BodyT> {
return async (ctx, next) => {
const { isCloud } = EnvSet.values;
@ -22,9 +16,9 @@ export default function koaTenantGuard<StateT, ContextT extends IRouterParamCont
return next();
}
const tenant = await getAvailableTenant(cloudConnection);
const { isSuspended } = await tenants.findTenantSuspendStatusById(tenantId);
if (tenant.isSuspended) {
if (isSuspended) {
throw new RequestError('subscription.tenant_suspended', 403);
}

View file

@ -0,0 +1,28 @@
import { Tenants, type TenantModel } from '@logto/schemas/models';
import { convertToIdentifiers } from '@logto/shared';
import { sql, type CommonQueryMethods } from 'slonik';
const createTenantQueries = (pool: CommonQueryMethods) => {
const { table, fields } = convertToIdentifiers({
table: Tenants.tableName,
fields: Tenants.rawKeys,
});
const findTenantSuspendStatusById = async (
id: string
): Promise<Pick<TenantModel, 'id' | 'isSuspended'>> => {
const result = await pool.one<Pick<TenantModel, 'id' | 'isSuspended'>>(sql`
select ${sql.join([fields.id, fields.isSuspended], sql`, `)}
from ${table}
where ${fields.id} = ${id}
`);
return result;
};
return {
findTenantSuspendStatusById,
};
};
export default createTenantQueries;

View file

@ -46,7 +46,7 @@ const createRouters = (tenant: TenantContext) => {
const managementRouter: AuthedRouter = new Router();
managementRouter.use(koaAuth(tenant.envSet, getManagementApiResourceIndicator(tenant.id)));
managementRouter.use(koaTenantGuard(tenant.cloudConnection));
managementRouter.use(koaTenantGuard(tenant.id, tenant.queries));
applicationRoutes(managementRouter, tenant);
applicationRoleRoutes(managementRouter, tenant);

View file

@ -21,6 +21,7 @@ import { createRolesQueries } from '#src/queries/roles.js';
import { createScopeQueries } from '#src/queries/scope.js';
import { createSignInExperienceQueries } from '#src/queries/sign-in-experience.js';
import SsoConnectorQueries from '#src/queries/sso-connectors.js';
import createTenantQueries from '#src/queries/tenant.js';
import UserSsoIdentityQueries from '#src/queries/user-sso-identities.js';
import { createUserQueries } from '#src/queries/user.js';
import { createUsersRolesQueries } from '#src/queries/users-roles.js';
@ -51,6 +52,7 @@ export default class Queries {
organizations = new OrganizationQueries(this.pool);
ssoConnectors = new SsoConnectorQueries(this.pool);
userSsoIdentities = new UserSsoIdentityQueries(this.pool);
tenants = createTenantQueries(this.pool);
constructor(
public readonly pool: CommonQueryMethods,

View file

@ -0,0 +1,39 @@
import { type CommonQueryMethods, sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const getDatabaseName = async (pool: CommonQueryMethods) => {
const { currentDatabase } = await pool.one<{ currentDatabase: string }>(sql`
select current_database();
`);
return currentDatabase.replaceAll('-', '_');
};
/**
* Grant read permission to the is_suspended column in the tenants table to the logto_tenant_<databaseName> role.
*/
const alteration: AlterationScript = {
up: async (pool) => {
const databaseName = await getDatabaseName(pool);
const baseRoleId = sql.identifier([`logto_tenant_${databaseName}`]);
await pool.query(sql`
grant select (is_suspended)
on table tenants
to ${baseRoleId}
`);
},
down: async (pool) => {
const databaseName = await getDatabaseName(pool);
const baseRoleId = sql.identifier([`logto_tenant_${databaseName}`]);
await pool.query(sql`
revoke select(is_suspended)
on table tenants
from ${baseRoleId}
`);
},
};
export default alteration;

View file

@ -13,7 +13,7 @@ revoke all privileges
from logto_tenant_${database};
-- Allow limited select to perform the RLS policy query in `after_each` (using select ... from tenants ...)
grant select (id, db_user)
grant select (id, db_user, is_suspended)
on table tenants
to logto_tenant_${database};