11import { Inject , Injectable , Scope } from 'graphql-modules' ;
22import { sql , type CommonQueryMethods , type DatabasePool } from 'slonik' ;
33import z from 'zod' ;
4+ import { cache } from '@hive/api/shared/helpers' ;
45import {
56 decodeCreatedAtAndUUIDIdBasedCursor ,
67 encodeCreatedAtAndUUIDIdBasedCursor ,
@@ -38,7 +39,6 @@ import {
3839} )
3940export class PersonalAccessTokens {
4041 logger : Logger ;
41- private findById : ReturnType < typeof PersonalAccessTokens . findById > ;
4242
4343 constructor (
4444 @Inject ( PG_POOL_CONFIG ) private pool : DatabasePool ,
@@ -54,7 +54,6 @@ export class PersonalAccessTokens {
5454 this . logger = logger . child ( {
5555 source : 'OrganizationAccessTokens' ,
5656 } ) ;
57- this . findById = PersonalAccessTokens . findById ( { logger : this . logger , pool } ) ;
5857 }
5958
6059 async create ( args : {
@@ -183,18 +182,25 @@ export class PersonalAccessTokens {
183182 async delete ( args : { personalAccessTokenId : string } ) {
184183 const viewer = await this . session . getViewer ( ) ;
185184
186- const record = await this . findById ( args . personalAccessTokenId ) ;
185+ const record = await PersonalAccessTokens . findById ( {
186+ pool : this . pool ,
187+ logger : this . logger ,
188+ } ) ( args . personalAccessTokenId ) ;
187189
188- if ( record === null || record . userId !== viewer . id ) {
189- this . session . raise ( 'accessToken:modify' ) ;
190+ if ( ! record ) {
191+ return null ;
190192 }
191193
192194 await this . session . assertPerformAction ( {
193- action : 'personalAccessToken:modify' ,
194195 organizationId : record . organizationId ,
195196 params : { organizationId : record . organizationId } ,
197+ action : 'personalAccessToken:modify' ,
196198 } ) ;
197199
200+ if ( record . userId !== viewer . id ) {
201+ return null ;
202+ }
203+
198204 await this . pool . query ( sql `
199205 DELETE
200206 FROM
@@ -220,11 +226,13 @@ export class PersonalAccessTokens {
220226 } ;
221227 }
222228
223- async getPaginated ( args : { organizationId : string ; first : number | null ; after : string | null } ) {
224- const viewer = await this . session . getViewer ( ) ;
229+ async getPaginatedForMembership (
230+ membership : OrganizationMembership ,
231+ args : { first : number | null ; after : string | null } ,
232+ ) {
225233 await this . session . assertPerformAction ( {
226- organizationId : args . organizationId ,
227- params : { organizationId : args . organizationId } ,
234+ organizationId : membership . organizationId ,
235+ params : { organizationId : membership . organizationId } ,
228236 action : 'personalAccessToken:modify' ,
229237 } ) ;
230238
@@ -245,8 +253,8 @@ export class PersonalAccessTokens {
245253 FROM
246254 "personal_access_tokens"
247255 WHERE
248- "organization_id" = ${ args . organizationId }
249- AND "user_id" = ${ viewer . id }
256+ "organization_id" = ${ membership . organizationId }
257+ AND "user_id" = ${ membership . userId }
250258 ${
251259 cursor
252260 ? sql `
@@ -298,6 +306,25 @@ export class PersonalAccessTokens {
298306 } ;
299307 }
300308
309+ async findByIdForMembership ( membership : OrganizationMembership , accessTokenId : string ) {
310+ await this . session . assertPerformAction ( {
311+ organizationId : membership . organizationId ,
312+ params : { organizationId : membership . organizationId } ,
313+ action : 'personalAccessToken:modify' ,
314+ } ) ;
315+
316+ const accessToken = await PersonalAccessTokens . findById ( {
317+ logger : this . logger ,
318+ pool : this . pool ,
319+ } ) ( accessTokenId ) ;
320+
321+ if ( ! accessToken || accessToken . userId !== viewer . id ) {
322+ return null ;
323+ }
324+
325+ return accessToken ;
326+ }
327+
301328 /**
302329 * Implementation for finding a personal access token from the PG database.
303330 * It is a static function, so we can use it for the personal access tokens cache.
@@ -345,6 +372,87 @@ export class PersonalAccessTokens {
345372 return result ;
346373 } ;
347374 }
375+
376+ static computeResources (
377+ personalAccessToken : PersonalAccessToken ,
378+ membership : OrganizationMembership ,
379+ ) {
380+ const legitAssignedResources = intersectResourceAssignments (
381+ membership . assignedRole . resources ,
382+ personalAccessToken . assignedResources ,
383+ ) ;
384+
385+ return legitAssignedResources ;
386+ }
387+
388+ static computePermissions (
389+ personalAccessToken : PersonalAccessToken ,
390+ membership : OrganizationMembership ,
391+ ) {
392+ // If the access token specifies no permissions, we use the permissions of the member role
393+ if ( personalAccessToken . permissions === null ) {
394+ return membership . assignedRole . role . allPermissions ;
395+ }
396+
397+ // The roles permission could have been updated.
398+ // Because of that we always need to filter this list based on the role.
399+ return intersection (
400+ new Set ( personalAccessToken . permissions ) ,
401+ membership . assignedRole . role . allPermissions ,
402+ ) ;
403+ }
404+
405+ static computeAuthorizationStatements (
406+ personalAccessToken : PersonalAccessToken ,
407+ membership : OrganizationMembership ,
408+ ) {
409+ const permissions = PersonalAccessTokens . computePermissions ( personalAccessToken , membership ) ;
410+ const resources = PersonalAccessTokens . computeResources ( personalAccessToken , membership ) ;
411+
412+ const permissionsPerLevel = permissionsToPermissionsPerResourceLevelAssignment ( permissions ) ;
413+ const resolvedResources = resolveResourceAssignment ( {
414+ organizationId : personalAccessToken . organizationId ,
415+ projects : resources ,
416+ } ) ;
417+
418+ return translateResolvedResourcesToAuthorizationPolicyStatements (
419+ personalAccessToken . organizationId ,
420+ permissionsPerLevel ,
421+ resolvedResources ,
422+ ) ;
423+ }
424+
425+ @cache ( ( ...values : Array < string > ) => values . join ( '|' ) )
426+ async getMembership ( organizationId : string , userId : string ) {
427+ const organization = await this . storage . getOrganization ( { organizationId } ) ;
428+ const membership = await this . organizationMembers . findOrganizationMembership ( {
429+ organization,
430+ userId,
431+ } ) ;
432+
433+ if ( ! membership ) {
434+ throw new Error ( 'Should be able to find membership.' ) ;
435+ }
436+
437+ return membership ;
438+ }
439+
440+ async getResourcesForPersonalAccessToken ( personalAccessToken : PersonalAccessToken ) {
441+ const membership = await this . getMembership (
442+ personalAccessToken . organizationId ,
443+ personalAccessToken . userId ,
444+ ) ;
445+
446+ return PersonalAccessTokens . computeResources ( personalAccessToken , membership ) ;
447+ }
448+
449+ async getPermissionsForPersonalAccessToken ( personalAccessToken : PersonalAccessToken ) {
450+ const membership = await this . getMembership (
451+ personalAccessToken . organizationId ,
452+ personalAccessToken . userId ,
453+ ) ;
454+ return PersonalAccessTokens . computePermissions ( personalAccessToken , membership ) ;
455+ }
348456}
349457
350458const personalAccessTokenFields = sql `
@@ -360,58 +468,20 @@ const personalAccessTokenFields = sql`
360468 , "hash"
361469` ;
362470
363- const PersonalAccessTokenModel = z
364- . object ( {
365- id : z . string ( ) . uuid ( ) ,
366- organizationId : z . string ( ) . uuid ( ) ,
367- userId : z . string ( ) . uuid ( ) ,
368- createdAt : z . string ( ) ,
369- title : z . string ( ) ,
370- description : z . string ( ) ,
371- permissions : z . array ( PermissionsModel ) . nullable ( ) ,
372- assignedResources : ResourceAssignmentModel . nullable ( ) . transform (
373- value => value ?? { mode : '*' as const , projects : [ ] } ,
374- ) ,
375- firstCharacters : z . string ( ) ,
376- hash : z . string ( ) ,
377- } )
378- . transform ( record => ( {
379- ...record ,
380- // Only used in the context of authorization, we do not need
381- // to compute when querying a list of organization access tokens via the GraphQL API.
382- // Compared to organization access tokens, we also need to filter down the permissions based on the membership
383- resolveAuthorizationPolicyStatements ( organizationMembership : OrganizationMembership ) {
384- const legitPermissions =
385- // If the access token specifies no permissions, we use the permissions of the member role
386- record . permissions === null
387- ? organizationMembership . assignedRole . role . allPermissions
388- : // The roles permission could have been updated.
389- // Because of that we always need to filter this list based on the role.
390- intersection (
391- new Set ( record . permissions ) ,
392- organizationMembership . assignedRole . role . allPermissions ,
393- ) ;
394-
395- // The membership resources could have been updated.
396- // Because of that we always need to filter this list based on the role.
397- const legitAssignedResources = intersectResourceAssignments (
398- organizationMembership . assignedRole . resources ,
399- record . assignedResources ,
400- ) ;
401-
402- const permissions = permissionsToPermissionsPerResourceLevelAssignment ( legitPermissions ) ;
403- const resolvedResources = resolveResourceAssignment ( {
404- organizationId : record . organizationId ,
405- projects : legitAssignedResources ,
406- } ) ;
407-
408- return translateResolvedResourcesToAuthorizationPolicyStatements (
409- record . organizationId ,
410- permissions ,
411- resolvedResources ,
412- ) ;
413- } ,
414- } ) ) ;
471+ const PersonalAccessTokenModel = z . object ( {
472+ id : z . string ( ) . uuid ( ) ,
473+ organizationId : z . string ( ) . uuid ( ) ,
474+ userId : z . string ( ) . uuid ( ) ,
475+ createdAt : z . string ( ) ,
476+ title : z . string ( ) ,
477+ description : z . string ( ) ,
478+ permissions : z . array ( PermissionsModel ) . nullable ( ) ,
479+ assignedResources : ResourceAssignmentModel . nullable ( ) . transform (
480+ value => value ?? { mode : '*' as const , projects : [ ] } ,
481+ ) ,
482+ firstCharacters : z . string ( ) ,
483+ hash : z . string ( ) ,
484+ } ) ;
415485
416486export type PersonalAccessToken = z . TypeOf < typeof PersonalAccessTokenModel > ;
417487
0 commit comments