1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

Change PAT primary key from string to number (#2163)

* Update primary key

* Fix tests
This commit is contained in:
sjaanus 2022-10-10 12:35:12 +02:00 committed by GitHub
parent 879e1358ef
commit c105ca02f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 108 additions and 26 deletions

View File

@ -6,8 +6,8 @@ import NotFoundError from '../error/notfound-error';
const TABLE = 'personal_access_tokens'; const TABLE = 'personal_access_tokens';
const PAT_COLUMNS = [ const PAT_PUBLIC_COLUMNS = [
'secret', 'id',
'description', 'description',
'user_id', 'user_id',
'expires_at', 'expires_at',
@ -20,6 +20,7 @@ const fromRow = (row) => {
throw new NotFoundError('No PAT found'); throw new NotFoundError('No PAT found');
} }
return new Pat({ return new Pat({
id: row.id,
secret: row.secret, secret: row.secret,
userId: row.user_id, userId: row.user_id,
description: row.description, description: row.description,
@ -51,8 +52,12 @@ export default class PatStore implements IPatStore {
return fromRow(row[0]); return fromRow(row[0]);
} }
async delete(secret: string): Promise<void> { async delete(id: number): Promise<void> {
return this.db(TABLE).where({ secret: secret }).del(); return this.db(TABLE).where({ id: id }).del();
}
async deleteForUser(id: number, userId: number): Promise<void> {
return this.db(TABLE).where({ id: id, user_id: userId }).del();
} }
async deleteAll(): Promise<void> { async deleteAll(): Promise<void> {
@ -61,28 +66,28 @@ export default class PatStore implements IPatStore {
destroy(): void {} destroy(): void {}
async exists(secret: string): Promise<boolean> { async exists(id: number): Promise<boolean> {
const result = await this.db.raw( const result = await this.db.raw(
`SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`, `SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`,
[secret], [id],
); );
const { present } = result.rows[0]; const { present } = result.rows[0];
return present; return present;
} }
async get(secret: string): Promise<Pat> { async get(id: number): Promise<Pat> {
const row = await this.db(TABLE).where({ secret }).first(); const row = await this.db(TABLE).where({ id }).first();
return fromRow(row); return fromRow(row);
} }
async getAll(): Promise<Pat[]> { async getAll(): Promise<Pat[]> {
const groups = await this.db.select(PAT_COLUMNS).from(TABLE); const groups = await this.db.select(PAT_PUBLIC_COLUMNS).from(TABLE);
return groups.map(fromRow); return groups.map(fromRow);
} }
async getAllByUser(userId: number): Promise<Pat[]> { async getAllByUser(userId: number): Promise<Pat[]> {
const groups = await this.db const groups = await this.db
.select(PAT_COLUMNS) .select(PAT_PUBLIC_COLUMNS)
.from(TABLE) .from(TABLE)
.where('user_id', userId); .where('user_id', userId);
return groups.map(fromRow); return groups.map(fromRow);

View File

@ -4,6 +4,9 @@ export const patSchema = {
$id: '#/components/schemas/patSchema', $id: '#/components/schemas/patSchema',
type: 'object', type: 'object',
properties: { properties: {
id: {
type: 'number',
},
secret: { secret: {
type: 'string', type: 'string',
}, },

View File

@ -62,7 +62,7 @@ export default class PatController extends Controller {
this.route({ this.route({
method: 'delete', method: 'delete',
path: '/:secret', path: '/:id',
acceptAnyContentType: true, acceptAnyContentType: true,
handler: this.deletePat, handler: this.deletePat,
permission: NONE, permission: NONE,
@ -95,11 +95,11 @@ export default class PatController extends Controller {
} }
async deletePat( async deletePat(
req: IAuthRequest<{ secret: string }>, req: IAuthRequest<{ id: number }>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const { secret } = req.params; const { id } = req.params;
await this.patService.deletePat(secret); await this.patService.deletePat(id, req.user.id);
res.status(200).end(); res.status(200).end();
} }
} }

View File

@ -37,6 +37,7 @@ export default class PatService {
pat.userId = user.id; pat.userId = user.id;
const newPat = await this.patStore.create(pat); const newPat = await this.patStore.create(pat);
pat.secret = '***';
await this.eventStore.store({ await this.eventStore.store({
type: PAT_CREATED, type: PAT_CREATED,
createdBy: user.email || user.username, createdBy: user.email || user.username,
@ -50,8 +51,8 @@ export default class PatService {
return this.patStore.getAllByUser(user.id); return this.patStore.getAllByUser(user.id);
} }
async deletePat(secret: string): Promise<void> { async deletePat(id: number, userId: number): Promise<void> {
return this.patStore.delete(secret); return this.patStore.deleteForUser(id, userId);
} }
private generateSecretKey() { private generateSecretKey() {

View File

@ -1,4 +1,5 @@
export interface IPat { export interface IPat {
id: number;
secret: string; secret: string;
description: string; description: string;
userId: number; userId: number;
@ -8,6 +9,8 @@ export interface IPat {
} }
export default class Pat implements IPat { export default class Pat implements IPat {
id: number;
secret: string; secret: string;
description: string; description: string;
@ -21,13 +24,15 @@ export default class Pat implements IPat {
createdAt: Date; createdAt: Date;
constructor({ constructor({
secret, id,
userId, userId,
expiresAt, expiresAt,
seenAt, seenAt,
createdAt, createdAt,
secret,
description, description,
}: IPat) { }: IPat) {
this.id = id;
this.secret = secret; this.secret = secret;
this.userId = userId; this.userId = userId;
this.expiresAt = expiresAt; this.expiresAt = expiresAt;

View File

@ -1,7 +1,8 @@
import { Store } from './store'; import { Store } from './store';
import { IPat } from '../models/pat'; import { IPat } from '../models/pat';
export interface IPatStore extends Store<IPat, string> { export interface IPatStore extends Store<IPat, number> {
create(group: IPat): Promise<IPat>; create(group: IPat): Promise<IPat>;
getAllByUser(userId: number): Promise<IPat[]>; getAllByUser(userId: number): Promise<IPat[]>;
deleteForUser(id: number, userId: number): Promise<void>;
} }

View File

@ -0,0 +1,28 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
alter table personal_access_tokens
drop constraint personal_access_tokens_pkey;
ALTER TABLE personal_access_tokens
add column id serial primary key;
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
alter table personal_access_tokens
drop constraint personal_access_tokens_pkey;
alter table personal_access_tokens
drop column id;
alter table personal_access_tokens
add primary key (secret);
`,
cb,
);
};

View File

@ -8,6 +8,7 @@ let db: ITestDb;
let tomorrow = new Date(); let tomorrow = new Date();
let firstSecret; let firstSecret;
let firstId;
tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setDate(tomorrow.getDate() + 1);
beforeAll(async () => { beforeAll(async () => {
@ -44,6 +45,7 @@ test('should create a PAT', async () => {
expect(new Date(body.expiresAt)).toEqual(tomorrow); expect(new Date(body.expiresAt)).toEqual(tomorrow);
expect(body.description).toEqual(description); expect(body.description).toEqual(description);
firstSecret = body.secret; firstSecret = body.secret;
firstId = body.id;
const response = await request const response = await request
.get('/api/admin/user/tokens') .get('/api/admin/user/tokens')
@ -64,9 +66,9 @@ test('should delete the PAT', async () => {
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201); .expect(201);
const createdSecret = body.secret; const createdId = body.id;
await request.delete(`/api/admin/user/tokens/${createdSecret}`).expect(200); await request.delete(`/api/admin/user/tokens/${createdId}`).expect(200);
}); });
test('should get all PATs', async () => { test('should get all PATs', async () => {
@ -78,6 +80,36 @@ test('should get all PATs', async () => {
.expect(200); .expect(200);
expect(body.pats).toHaveLength(1); expect(body.pats).toHaveLength(1);
expect(body.pats[0].secret).toBeUndefined();
expect(body.pats[0].id).toBeDefined();
});
test('should not allow deletion of other users PAT', async () => {
const { request } = app;
await app.request
.post(`/auth/demo/login`)
.send({
email: 'user-second@getunleash.io',
})
.expect(200);
await request.delete(`/api/admin/user/tokens/${firstId}`).expect(200);
await app.request
.post(`/auth/demo/login`)
.send({
email: 'user@getunleash.io',
})
.expect(200);
const { body } = await request
.get('/api/admin/user/tokens')
.expect('Content-Type', /json/)
.expect(200);
expect(body.pats).toHaveLength(1);
expect(body.pats[0].secret).toBeUndefined();
}); });
test('should get only current user PATs', async () => { test('should get only current user PATs', async () => {

View File

@ -1688,6 +1688,9 @@ exports[`should serve the OpenAPI spec 1`] = `
"nullable": true, "nullable": true,
"type": "string", "type": "string",
}, },
"id": {
"type": "number",
},
"secret": { "secret": {
"type": "string", "type": "string",
}, },
@ -7051,13 +7054,13 @@ If the provided project does not exist, the list of events will be empty.",
], ],
}, },
}, },
"/api/admin/user/tokens/{secret}": { "/api/admin/user/tokens/{id}": {
"delete": { "delete": {
"operationId": "deletePat", "operationId": "deletePat",
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
"name": "secret", "name": "id",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",

View File

@ -6,7 +6,7 @@ export default class FakePatStore implements IPatStore {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
delete(key: string): Promise<void> { delete(key: number): Promise<void> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@ -16,11 +16,11 @@ export default class FakePatStore implements IPatStore {
destroy(): void {} destroy(): void {}
exists(key: string): Promise<boolean> { exists(key: number): Promise<boolean> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
get(key: string): Promise<IPat> { get(key: number): Promise<IPat> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@ -31,4 +31,8 @@ export default class FakePatStore implements IPatStore {
getAllByUser(userId: number): Promise<IPat[]> { getAllByUser(userId: number): Promise<IPat[]> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
deleteForUser(id: number, userId: number): Promise<void> {
throw new Error('Method not implemented.');
}
} }