0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

fix(cloud,test): should block when users trying to create too many tenants (#4048)

This commit is contained in:
Darcy Ye 2023-06-19 14:45:33 +08:00 committed by GitHub
parent 2d7c0aa3c3
commit 8f78515976
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 69 additions and 10 deletions

View file

@ -49,6 +49,26 @@ describe('POST /api/tenants', () => {
).rejects.toMatchObject({ status: 403 });
});
it('should throw 403 when trying to create more than 3 tenants with `CreateTenant` scope', async () => {
const tenant: TenantInfo = {
id: 'tenant',
name: 'tenant',
tag: TenantTag.Development,
indicator: 'https://tenant.foo.bar',
};
library.getAvailableTenants.mockResolvedValueOnce([tenant, tenant, tenant]);
await expect(
router.routes()(
buildRequestAuthContext('POST /tenants', {
body: { name: 'tenant', tag: TenantTag.Development },
})([CloudScope.CreateTenant]),
noop,
createHttpContext()
)
).rejects.toMatchObject({ status: 403 });
});
it('should be able to create a new tenant', async () => {
const tenant: TenantInfo = {
id: 'tenant_a',
@ -56,6 +76,7 @@ describe('POST /api/tenants', () => {
tag: TenantTag.Development,
indicator: 'https://foo.bar',
};
library.getAvailableTenants.mockResolvedValueOnce([]);
library.createNewTenant.mockImplementationOnce(async (_, payload) => {
return { ...tenant, ...payload };
});

View file

@ -68,6 +68,18 @@ export const tenantsRoutes = (library: TenantsLibrary) =>
throw new RequestError('Forbidden due to lack of permission.', 403);
}
/**
* Should throw 403 when users with `CreateTenant` scope are attempting to create more than 3 tenants.
* This does not apply to users with `ManageTenant` scope.
*/
if (context.auth.scopes.includes(CloudScope.CreateTenant)) {
const availableTenants = await library.getAvailableTenants(context.auth.id);
assert(
availableTenants.length < 3,
new RequestError(`Can not have more than 3 tenants.`, 403)
);
}
return next({
...context,
json: await library.createNewTenant(context.auth.id, context.guarded.body),

View file

@ -119,9 +119,24 @@
cursor: pointer;
}
.icon {
> svg {
width: 20px;
height: 20px;
color: var(--color-neutral-50);
}
&.disabled {
&:hover {
background: transparent;
}
&:not(:disabled) {
cursor: not-allowed;
}
> div,
> svg {
color: var(--color-placeholder);
}
}
}

View file

@ -1,4 +1,4 @@
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import { type TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames';
import { useRef, useState } from 'react';
import { toast } from 'react-hot-toast';
@ -35,10 +35,12 @@ function TenantSelector() {
return <AppError errorMessage={error.message} callStack={error.stack} />;
}
if (!tenants?.length) {
if (!tenants?.length || !currentTenantInfo) {
return null;
}
const isCreateButtonDisabled = tenants.length >= 3;
return (
<>
<div
@ -53,11 +55,8 @@ function TenantSelector() {
setShowDropdown(true);
}}
>
<div className={styles.name}>{currentTenantInfo?.name ?? 'My project'}</div>
<TenantEnvTag
className={styles.tag}
tag={currentTenantInfo?.tag ?? TenantTag.Development}
/>
<div className={styles.name}>{currentTenantInfo.name}</div>
<TenantEnvTag className={styles.tag} tag={currentTenantInfo.tag} />
<KeyboardArrowDown className={styles.arrowIcon} />
</div>
<Dropdown
@ -89,16 +88,25 @@ function TenantSelector() {
<div
role="button"
tabIndex={0}
className={styles.createTenantButton}
className={classNames(
isCreateButtonDisabled && styles.disabled,
styles.createTenantButton
)}
onClick={() => {
if (isCreateButtonDisabled) {
return;
}
setShowCreateTenantModal(true);
}}
onKeyDown={onKeyDownHandler(() => {
if (isCreateButtonDisabled) {
return;
}
setShowCreateTenantModal(true);
})}
>
<div>{t('cloud.tenant.create_tenant')}</div>
<PlusSign className={styles.icon} />
<PlusSign />
</div>
</Dropdown>
<CreateTenantModal

View file

@ -85,6 +85,9 @@ describe('Tenant APIs', () => {
expect(tenant).toHaveProperty('tag', payload.tag);
expect(tenant).toHaveProperty('name', payload.name);
}
await expect(
createTenant(accessToken, { name: 'tenant4', tag: TenantTag.Staging })
).rejects.toThrow();
await deleteTenant(accessToken, tenant3.id);
const resources = await authedAdminTenantApi.get('resources').json<Resource[]>();
expect(