mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
fix: limit total of PATs a user can have (#2301)
* fix: limit total of PATs a user can have * increase PAT limit to 10 * Update src/lib/services/pat-service.ts Co-authored-by: Simon Hornby <liquidwicked64@gmail.com> * disable button on the front-end when PAT limit is reached * import from server instead of repeating ourselves Co-authored-by: Simon Hornby <liquidwicked64@gmail.com>
This commit is contained in:
parent
98cda9258d
commit
9fb431aab7
@ -19,6 +19,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { PAT_LIMIT } from '@server/util/constants';
|
||||||
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens';
|
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import {
|
import {
|
||||||
@ -250,6 +251,7 @@ export const PersonalAPITokensTab = ({ user }: IPersonalAPITokensTabProps) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={tokens.length >= PAT_LIMIT}
|
||||||
onClick={() => setCreateOpen(true)}
|
onClick={() => setCreateOpen(true)}
|
||||||
>
|
>
|
||||||
New token
|
New token
|
||||||
|
@ -2,11 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@ -19,10 +15,11 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"paths": {
|
||||||
|
"@server/*": ["./../../src/lib/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"],
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,15 @@ export default class PatStore implements IPatStore {
|
|||||||
return present;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async countByUser(userId: number): Promise<number> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT COUNT(*) AS count FROM ${TABLE} WHERE user_id = ?`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
const { count } = result.rows[0];
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
async get(id: number): Promise<Pat> {
|
async get(id: number): Promise<Pat> {
|
||||||
const row = await this.db(TABLE).where({ id }).first();
|
const row = await this.db(TABLE).where({ id }).first();
|
||||||
return fromRow(row);
|
return fromRow(row);
|
||||||
|
@ -8,6 +8,8 @@ import crypto from 'crypto';
|
|||||||
import User from '../types/user';
|
import User from '../types/user';
|
||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
import NameExistsError from '../error/name-exists-error';
|
import NameExistsError from '../error/name-exists-error';
|
||||||
|
import { OperationDeniedError } from '../error/operation-denied-error';
|
||||||
|
import { PAT_LIMIT } from '../util/constants';
|
||||||
|
|
||||||
export default class PatService {
|
export default class PatService {
|
||||||
private config: IUnleashConfig;
|
private config: IUnleashConfig;
|
||||||
@ -67,6 +69,12 @@ export default class PatService {
|
|||||||
throw new BadDataError('The expiry date should be in future.');
|
throw new BadDataError('The expiry date should be in future.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((await this.patStore.countByUser(userId)) >= PAT_LIMIT) {
|
||||||
|
throw new OperationDeniedError(
|
||||||
|
`Too many PATs (${PAT_LIMIT}) already exist for this user.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
await this.patStore.existsWithDescriptionByUser(description, userId)
|
await this.patStore.existsWithDescriptionByUser(description, userId)
|
||||||
) {
|
) {
|
||||||
|
@ -9,4 +9,5 @@ export interface IPatStore extends Store<IPat, number> {
|
|||||||
description: string,
|
description: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<boolean>;
|
): Promise<boolean>;
|
||||||
|
countByUser(userId: number): Promise<number>;
|
||||||
}
|
}
|
||||||
|
@ -55,3 +55,5 @@ export const STRING_OPERATORS = [
|
|||||||
export const NUM_OPERATORS = [NUM_EQ, NUM_GT, NUM_GTE, NUM_LT, NUM_LTE];
|
export const NUM_OPERATORS = [NUM_EQ, NUM_GT, NUM_GTE, NUM_LT, NUM_LTE];
|
||||||
export const DATE_OPERATORS = [DATE_AFTER, DATE_BEFORE];
|
export const DATE_OPERATORS = [DATE_AFTER, DATE_BEFORE];
|
||||||
export const SEMVER_OPERATORS = [SEMVER_EQ, SEMVER_GT, SEMVER_LT];
|
export const SEMVER_OPERATORS = [SEMVER_EQ, SEMVER_GT, SEMVER_LT];
|
||||||
|
|
||||||
|
export const PAT_LIMIT = 10;
|
||||||
|
@ -3,6 +3,7 @@ import dbInit, { ITestDb } from '../../../helpers/database-init';
|
|||||||
import getLogger from '../../../../fixtures/no-logger';
|
import getLogger from '../../../../fixtures/no-logger';
|
||||||
import { IPat } from '../../../../../lib/types/models/pat';
|
import { IPat } from '../../../../../lib/types/models/pat';
|
||||||
import { IPatStore } from '../../../../../lib/types/stores/pat-store';
|
import { IPatStore } from '../../../../../lib/types/stores/pat-store';
|
||||||
|
import { PAT_LIMIT } from '../../../../../lib/util/constants';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -269,3 +270,36 @@ test('should not get user with expired token', async () => {
|
|||||||
.set('Authorization', token.secret)
|
.set('Authorization', token.secret)
|
||||||
.expect(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should fail creation of PAT when PAT limit has been reached', async () => {
|
||||||
|
await app.request
|
||||||
|
.post(`/auth/demo/login`)
|
||||||
|
.send({
|
||||||
|
email: 'user-too-many-pats@getunleash.io',
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const tokenCreations = [];
|
||||||
|
for (let i = 0; i < PAT_LIMIT; i++) {
|
||||||
|
tokenCreations.push(
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/user/tokens')
|
||||||
|
.send({
|
||||||
|
description: `my pat ${i}`,
|
||||||
|
expiresAt: tomorrow,
|
||||||
|
} as IPat)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Promise.all(tokenCreations);
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/user/tokens')
|
||||||
|
.send({
|
||||||
|
description: `my pat ${PAT_LIMIT}`,
|
||||||
|
expiresAt: tomorrow,
|
||||||
|
} as IPat)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
4
src/test/fixtures/fake-pat-store.ts
vendored
4
src/test/fixtures/fake-pat-store.ts
vendored
@ -27,6 +27,10 @@ export default class FakePatStore implements IPatStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countByUser(userId: number): Promise<number> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
get(key: number): Promise<IPat> {
|
get(key: number): Promise<IPat> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user