diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index d0a0fec0a6..6752972244 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -85,6 +85,32 @@ export class DatabaseTaskStore implements TaskStore { this.db = options.database; } + async list(options: Partial): Promise { + const results = await this.db('tasks') + .where({ + created_by: options.createdBy, + }) + .orderBy('created_at', 'desc') + .select(); + + return results.map(result => ({ + id: result.id, + spec: JSON.parse(result.spec), + status: result.status, + createdBy: result.created_by, + lastHeartbeatAt: + typeof result.last_heartbeat_at === 'string' + ? DateTime.fromSQL(result.last_heartbeat_at, { + zone: 'UTC', + }).toISO() + : result.last_heartbeat_at, + createdAt: + typeof result.created_at === 'string' + ? DateTime.fromSQL(result.created_at, { zone: 'UTC' }).toISO() + : result.created_at, + })); + } + async getTask(taskId: string): Promise { const [result] = await this.db('tasks') .where({ id: taskId }) @@ -99,8 +125,16 @@ export class DatabaseTaskStore implements TaskStore { id: result.id, spec, status: result.status, - lastHeartbeatAt: result.last_heartbeat_at, - createdAt: result.created_at, + lastHeartbeatAt: + typeof result.last_heartbeat_at === 'string' + ? DateTime.fromSQL(result.last_heartbeat_at, { + zone: 'UTC', + }).toISO() + : result.last_heartbeat_at, + createdAt: + typeof result.created_at === 'string' + ? DateTime.fromSQL(result.created_at, { zone: 'UTC' }).toISO() + : result.created_at, createdBy: result.created_by ?? undefined, secrets, }; @@ -119,6 +153,7 @@ export class DatabaseTaskStore implements TaskStore { secrets: options.secrets ? JSON.stringify(options.secrets) : undefined, created_by: options.createdBy ?? null, status: 'open', + created_by: ('createdBy' in spec && spec.createdBy) || null, }); return { taskId }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 17f07f702d..927bbf6842 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -150,6 +150,15 @@ export class StorageTaskBroker implements TaskBroker { private readonly storage: TaskStore, private readonly logger: Logger, ) {} + + async list(options?: Partial): Promise { + if (!options || !options.createdBy) { + throw new Error('Method not implemented.'); + } + + return await this.storage.list({ createdBy: options.createdBy }); + } + private deferredDispatch = defer(); /** diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index a3a9ef84d1..1fc7670e79 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -49,6 +49,7 @@ export type SerializedTask = { lastHeartbeatAt?: string; createdBy?: string; secrets?: TaskSecrets; + createdBy: string | null; }; /** @@ -132,6 +133,7 @@ export interface TaskBroker { after: number | undefined; }): Observable<{ events: SerializedTaskEvent[] }>; get(taskId: string): Promise; + list(options?: Partial): Promise; } /** @@ -192,6 +194,7 @@ export interface TaskStore { listStaleTasks(options: { timeoutS: number }): Promise<{ tasks: { taskId: string }[]; }>; + list(options: Partial): Promise; emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; listEvents({ diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5453dcd234..58bb2e831a 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -129,6 +129,19 @@ export async function createRouter( actionsToRegister.forEach(action => actionRegistry.register(action)); workers.forEach(worker => worker.start()); + const getUserEntityRefFromToken = (backstageToken: string) => { + try { + const [_header, payload, _signature] = backstageToken.split('.'); + const parsedToken = JSON.parse(Buffer.from(payload, 'base64').toString()); + + return parsedToken.sub; + } catch (e) { + logger.warn('Could not parse token from request to create template'); + logger.debug(e); + return null; + } + }; + router .get( '/v2/templates/:namespace/:kind/:name/parameter-schema', @@ -207,6 +220,8 @@ export async function createRouter( const baseUrl = getEntityBaseUrl(template); + const createdBy = token && getUserEntityRefFromToken(token); + const taskSpec: TaskSpec = { apiVersion: template.apiVersion, steps: template.spec.steps.map((step, index) => ({ @@ -228,6 +243,7 @@ export async function createRouter( }), baseUrl, }, + createdBy, }; const result = await taskBroker.dispatch({ @@ -241,6 +257,22 @@ export async function createRouter( res.status(201).json({ id: result.taskId }); }) + .get('/v2/tasks', async (req, res) => { + const token = getBearerToken(req.headers.authorization); + const userEntityRef = token && getUserEntityRefFromToken(token); + + if (!userEntityRef) { + throw new InputError( + 'Could not find a valid user entity ref in the request', + ); + } + + const tasks = await taskBroker.list({ + createdBy: userEntityRef, + }); + + res.status(200).json(tasks); + }) .get('/v2/tasks/:taskId', async (req, res) => { const { taskId } = req.params; const task = await taskBroker.get(taskId);