Skip to content

Commit

Permalink
refactor: PATs (#6101)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/SR-379/refactor-pats

This PR refactors PATs.

- Adds a new `createPatSchema`, which better aligns with
https://docs.getunleash.io/contributing/ADRs/overarching/separation-request-response-schemas
- Drops the model type and class in favor of using the schema types
directly, which is more consistent with the rest of the codebase and
easier to maintain
 - Misc scouting, improvement and fixes

This breaks Enterprise temporarily, but it's faster to move forward this
way.
  • Loading branch information
nunogois authored Feb 1, 2024
1 parent 28fc36a commit db0a0d7
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 158 deletions.
72 changes: 40 additions & 32 deletions src/lib/db/pat-store.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Logger, LogProvider } from '../logger';
import { IPatStore } from '../types/stores/pat-store';
import Pat, { IPat } from '../types/models/pat';
import NotFoundError from '../error/notfound-error';
import { Db } from './db';
import { CreatePatSchema, PatSchema } from '../openapi';

const TABLE = 'personal_access_tokens';

Expand All @@ -15,26 +15,25 @@ const PAT_PUBLIC_COLUMNS = [
'seen_at',
];

const fromRow = (row) => {
if (!row) {
throw new NotFoundError('No PAT found');
}
return new Pat({
id: row.id,
secret: row.secret,
userId: row.user_id,
description: row.description,
createdAt: row.created_at,
seenAt: row.seen_at,
expiresAt: row.expires_at,
});
};

const toRow = (pat: IPat) => ({
secret: pat.secret,
description: pat.description,
user_id: pat.userId,
expires_at: pat.expiresAt,
const rowToPat = ({
id,
description,
expires_at,
user_id,
created_at,
seen_at,
}): PatSchema => ({
id,
description,
expiresAt: expires_at,
userId: user_id,
createdAt: created_at,
seenAt: seen_at,
});

const patToRow = ({ description, expiresAt }: CreatePatSchema) => ({
description,
expires_at: expiresAt,
});

export default class PatStore implements IPatStore {
Expand All @@ -47,9 +46,15 @@ export default class PatStore implements IPatStore {
this.logger = getLogger('pat-store.ts');
}

async create(token: IPat): Promise<IPat> {
const row = await this.db(TABLE).insert(toRow(token)).returning('*');
return fromRow(row[0]);
async create(
pat: CreatePatSchema,
secret: string,
userId: number,
): Promise<PatSchema> {
const rows = await this.db(TABLE)
.insert({ ...patToRow(pat), secret, user_id: userId })
.returning('*');
return rowToPat(rows[0]);
}

async delete(id: number): Promise<void> {
Expand Down Expand Up @@ -96,21 +101,24 @@ export default class PatStore implements IPatStore {
return count;
}

async get(id: number): Promise<Pat> {
async get(id: number): Promise<PatSchema> {
const row = await this.db(TABLE).where({ id }).first();
return fromRow(row);
if (!row) {
throw new NotFoundError('No PAT found.');
}
return rowToPat(row);
}

async getAll(): Promise<Pat[]> {
const groups = await this.db.select(PAT_PUBLIC_COLUMNS).from(TABLE);
return groups.map(fromRow);
async getAll(): Promise<PatSchema[]> {
const pats = await this.db.select(PAT_PUBLIC_COLUMNS).from(TABLE);
return pats.map(rowToPat);
}

async getAllByUser(userId: number): Promise<Pat[]> {
const groups = await this.db
async getAllByUser(userId: number): Promise<PatSchema[]> {
const pats = await this.db
.select(PAT_PUBLIC_COLUMNS)
.from(TABLE)
.where('user_id', userId);
return groups.map(fromRow);
return pats.map(rowToPat);
}
}
2 changes: 2 additions & 0 deletions src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
passwordSchema,
patchesSchema,
patchSchema,
createPatSchema,
patSchema,
patsSchema,
permissionSchema,
Expand Down Expand Up @@ -306,6 +307,7 @@ export const schemas: UnleashSchemas = {
passwordSchema,
patchesSchema,
patchSchema,
createPatSchema,
patSchema,
patsSchema,
permissionSchema,
Expand Down
25 changes: 25 additions & 0 deletions src/lib/openapi/spec/create-pat-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FromSchema } from 'json-schema-to-ts';

export const createPatSchema = {
$id: '#/components/schemas/createPatSchema',
description:
'Describes the properties required to create a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens), or PAT. PATs are automatically scoped to the authenticated user.',
type: 'object',
required: ['description', 'expiresAt'],
properties: {
description: {
type: 'string',
description: `The PAT's description.`,
example: 'user:xyzrandomstring',
},
expiresAt: {
type: 'string',
format: 'date-time',
description: `The PAT's expiration date.`,
example: '2023-04-19T08:15:14.000Z',
},
},
components: {},
} as const;

export type CreatePatSchema = FromSchema<typeof createPatSchema>;
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './id-schema';
export * from './me-schema';
export * from './create-pat-schema';
export * from './pat-schema';
export * from './tag-schema';
export * from './date-schema';
Expand Down
26 changes: 13 additions & 13 deletions src/lib/openapi/spec/pat-schema.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { FromSchema } from 'json-schema-to-ts';
import { createPatSchema } from './create-pat-schema';

export const patSchema = {
$id: '#/components/schemas/patSchema',
type: 'object',
description:
'An overview of a [Personal Access Token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens).',
'Describes a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens), or PAT. PATs are automatically scoped to the authenticated user.',
required: ['id', 'createdAt', ...createPatSchema.required],
properties: {
id: {
type: 'integer',
description:
'The unique identification number for this Personal Access Token. (This property is set by Unleash when the token is created and cannot be set manually: if you provide a value when creating a PAT, Unleash will ignore it.)',
description: `The PAT's ID. PAT IDs are incrementing integers. In other words, a more recently created PAT will always have a higher ID than an older one.`,
example: 1,
minimum: 1,
},
secret: {
type: 'string',
description:
'The token used for authentication. (This property is set by Unleash when the token is created and cannot be set manually: if you provide a value when creating a PAT, Unleash will ignore it.)',
'The token used for authentication. It is automatically generated by Unleash when the PAT is created and that is the only time this property is returned.',
example: 'user:xyzrandomstring',
},
expiresAt: {
type: 'string',
format: 'date-time',
description: `The token's expiration date.`,
example: '2023-04-19T08:15:14.000Z',
},
createdAt: {
type: 'string',
format: 'date-time',
example: '2023-04-19T08:15:14.000Z',
description:
'When the token was created. (This property is set by Unleash when the token is created and cannot be set manually: if you provide a value when creating a PAT, Unleash will ignore it.)',
description: 'The date and time of when the PAT was created.',
},
seenAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-04-19T08:15:14.000Z',
description:
'When the token was last seen/used to authenticate with. `null` if it has not been used yet. (This property is set by Unleash when the token is created and cannot be set manually: if you provide a value when creating a PAT, Unleash will ignore it.)',
'When the PAT was last seen/used to authenticate with. `null` if it has not been used yet.',
},
userId: {
type: 'integer',
description: 'The ID of the user this PAT belongs to.',
example: 1337,
},
...createPatSchema.properties,
},
components: {
schemas: {},
Expand Down
6 changes: 3 additions & 3 deletions src/lib/openapi/spec/pats-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ export const patsSchema = {
$id: '#/components/schemas/patsSchema',
type: 'object',
description:
'Contains a collection of [Personal Access Tokens](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens).',
'Contains a collection of [personal access tokens](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens), or PATs. PATs are automatically scoped to the authenticated user.',
properties: {
pats: {
type: 'array',
description: 'A collection of PATs.',
items: {
$ref: '#/components/schemas/patSchema',
$ref: patSchema.$id,
},
description: 'A collection of Personal Access Tokens',
},
},
components: {
Expand Down
47 changes: 32 additions & 15 deletions src/lib/routes/admin-api/user/pat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ import PatService from '../../../services/pat-service';
import { NONE } from '../../../types/permissions';
import { IAuthRequest } from '../../unleash-types';
import { serializeDates } from '../../../types/serialize-dates';
import { patSchema } from '../../../openapi/spec/pat-schema';
import { PatSchema, patSchema } from '../../../openapi/spec/pat-schema';
import { PatsSchema, patsSchema } from '../../../openapi/spec/pats-schema';
import {
CreatePatSchema,
createPatSchema,
} from '../../../openapi/spec/create-pat-schema';
import { ForbiddenError, NotFoundError } from '../../../error';

export default class PatController extends Controller {
private patService: PatService;
Expand Down Expand Up @@ -53,11 +58,11 @@ export default class PatController extends Controller {
tags: ['Personal access tokens'],
operationId: 'getPats',
summary:
'Get all Personal Access Tokens for the current user.',
'Get all personal access tokens (PATs) for the current user.',
description:
'Returns all of the [Personal Access Tokens](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) belonging to the current user.',
'Returns all of the [personal access tokens](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PATs) belonging to the current user.',
responses: {
200: createResponseSchema('patsSchema'),
200: createResponseSchema(patsSchema.$id),
...getStandardResponses(401, 403, 404),
},
}),
Expand All @@ -72,12 +77,13 @@ export default class PatController extends Controller {
openApiService.validPath({
tags: ['Personal access tokens'],
operationId: 'createPat',
summary: 'Create a new Personal Access Token.',
summary:
'Create a new personal access token (PAT) for the current user.',
description:
'Creates a new [Personal Access Token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) for the current user.',
requestBody: createRequestSchema('patSchema'),
'Creates a new [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PAT) belonging to the current user.',
requestBody: createRequestSchema(createPatSchema.$id),
responses: {
201: resourceCreatedResponseSchema('patSchema'),
201: resourceCreatedResponseSchema(patSchema.$id),
...getStandardResponses(401, 403, 404),
},
}),
Expand All @@ -94,9 +100,10 @@ export default class PatController extends Controller {
openApiService.validPath({
tags: ['Personal access tokens'],
operationId: 'deletePat',
summary: 'Delete a Personal Access Token.',
summary:
'Delete a personal access token (PAT) for the current user.',
description:
'This endpoint allows for deleting a [Personal Access Token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) belonging to the current user.',
'Deletes a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PAT) belonging to the current user.',
responses: {
200: emptyResponse,
...getStandardResponses(401, 403, 404),
Expand All @@ -106,10 +113,16 @@ export default class PatController extends Controller {
});
}

async createPat(req: IAuthRequest, res: Response): Promise<void> {
async createPat(
req: IAuthRequest<unknown, unknown, CreatePatSchema>,
res: Response<PatSchema>,
): Promise<void> {
if (this.flagResolver.isEnabled('personalAccessTokensKillSwitch')) {
res.status(404).send({ message: 'PAT is disabled' });
return;
throw new NotFoundError('PATs are disabled.');
}

if (!req.user.id) {
throw new ForbiddenError('PATs require an authenticated user.');
}

const pat = req.body;
Expand All @@ -128,9 +141,13 @@ export default class PatController extends Controller {

async getPats(req: IAuthRequest, res: Response<PatsSchema>): Promise<void> {
if (this.flagResolver.isEnabled('personalAccessTokensKillSwitch')) {
res.status(404).send({ message: 'PAT is disabled' });
return;
throw new NotFoundError('PATs are disabled.');
}

if (!req.user.id) {
throw new ForbiddenError('PATs require an authenticated user.');
}

const pats = await this.patService.getAll(req.user.id);
this.openApiService.respondWithValidation(200, res, patsSchema.$id, {
pats: serializeDates(pats),
Expand Down
Loading

0 comments on commit db0a0d7

Please sign in to comment.