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:
parent
2d7c0aa3c3
commit
8f78515976
5 changed files with 69 additions and 10 deletions
|
@ -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 };
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue