1+ import * as sp from 'node:stream/promises' ;
2+ import * as csvp from 'csv-parse' ;
13import { endOfDay , startOfDay } from 'date-fns' ;
24import { graphql } from 'testkit/gql' ;
3- import { ProjectType , RuleInstanceSeverityLevel } from 'testkit/gql/graphql' ;
5+ import * as GraphQLSchema from 'testkit/gql/graphql' ;
46import { execute } from 'testkit/graphql' ;
57import { initSeed } from 'testkit/seed' ;
68import { GetObjectCommand , S3Client } from '@aws-sdk/client-s3' ;
@@ -15,6 +17,52 @@ const s3Client = new S3Client({
1517 forcePathStyle : true ,
1618} ) ;
1719
20+ /** Utility function for getting the s3 report without hazzle */
21+ async function fetchAuditLogFromS3Bucket ( url : string ) : Promise < string > {
22+ const parsedUrl = new URL ( url ) ;
23+ const pathParts = parsedUrl . pathname . split ( '/' ) ;
24+ const bucketName = pathParts [ 1 ] ;
25+ const key = pathParts . slice ( 2 ) . join ( '/' ) ;
26+ const getObjectCommand = new GetObjectCommand ( {
27+ Bucket : bucketName ,
28+ Key : key ,
29+ } ) ;
30+ const result = await s3Client . send ( getObjectCommand ) ;
31+ const body = await result . Body ?. transformToString ( ) ;
32+ if ( ! body ) {
33+ throw new Error ( 'Body is empty lol.' ) ;
34+ }
35+ return body ;
36+ }
37+
38+ /** Parse the audit log into a json object */
39+ async function parseAuditLog ( contents : string ) : Promise < Array < any > > {
40+ const parser = csvp . parse ( ) ;
41+ parser . write ( contents ) ;
42+ parser . end ( ) ;
43+
44+ const d : any = [ ] ;
45+
46+ let headerMapping : Array < [ string , number ] > | null = null ;
47+
48+ parser . on ( 'data' , ( chunk : Array < string > ) => {
49+ if ( ! headerMapping ) {
50+ headerMapping = chunk . map ( ( key , index ) => [ key , index ] as const ) ;
51+ } else {
52+ d . push (
53+ Object . fromEntries (
54+ headerMapping . map ( ( [ key , index ] ) => [
55+ key ,
56+ key === 'metadata' ? JSON . parse ( chunk [ index ] ) : chunk [ index ] ,
57+ ] ) ,
58+ ) ,
59+ ) ;
60+ }
61+ } ) ;
62+ await sp . finished ( parser ) ;
63+ return d ;
64+ }
65+
1866const ExportAllAuditLogs = graphql ( `
1967 mutation exportAllAuditLogs($input: ExportOrganizationAuditLogInput!) {
2068 exportOrganizationAuditLog(input: $input) {
@@ -37,7 +85,7 @@ test.concurrent(
3785 async ( ) => {
3886 const { createOrg } = await initSeed ( ) . createOwner ( ) ;
3987 const { createProject, organization } = await createOrg ( ) ;
40- await createProject ( ProjectType . Single ) ;
88+ await createProject ( GraphQLSchema . ProjectType . Single ) ;
4189 const secondOrg = await initSeed ( ) . createOwner ( ) ;
4290 const secondToken = secondOrg . ownerToken ;
4391
@@ -62,7 +110,7 @@ test.concurrent(
62110test . concurrent ( 'Try to export Audit Logs from an Organization with authorized user' , async ( ) => {
63111 const { createOrg, ownerToken } = await initSeed ( ) . createOwner ( ) ;
64112 const { createProject, organization } = await createOrg ( ) ;
65- await createProject ( ProjectType . Single ) ;
113+ await createProject ( GraphQLSchema . ProjectType . Single ) ;
66114
67115 const exportAuditLogs = await execute ( {
68116 document : ExportAllAuditLogs ,
@@ -81,22 +129,19 @@ test.concurrent('Try to export Audit Logs from an Organization with authorized u
81129 } ) ;
82130 expect ( exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . error ) . toBeNull ( ) ;
83131 const url = exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . ok ?. url ;
84- const parsedUrl = new URL ( String ( url ) ) ;
85- const pathParts = parsedUrl . pathname . split ( '/' ) ;
86- const bucketName = pathParts [ 1 ] ;
87- const key = pathParts . slice ( 2 ) . join ( '/' ) ;
88- const getObjectCommand = new GetObjectCommand ( {
89- Bucket : bucketName ,
90- Key : key ,
91- } ) ;
92- const result = await s3Client . send ( getObjectCommand ) ;
93- const bodyStream = await result . Body ?. transformToString ( ) ;
94- expect ( bodyStream ) . toBeDefined ( ) ;
95-
96- const rows = bodyStream ?. split ( '\n' ) ;
97- expect ( rows ?. length ) . toBeGreaterThan ( 1 ) ; // At least header and one row
132+ const bodyStream = await fetchAuditLogFromS3Bucket ( String ( url ) ) ;
133+ const rows = bodyStream . split ( '\n' ) ;
134+ expect ( rows . length ) . toBeGreaterThan ( 1 ) ; // At least header and one row
98135 const header = rows ?. [ 0 ] . split ( ',' ) ;
99- const expectedHeader = [ 'id' , 'created_at' , 'event_type' , 'user_id' , 'user_email' , 'metadata' ] ;
136+ const expectedHeader = [
137+ 'id' ,
138+ 'created_at' ,
139+ 'event_type' ,
140+ 'user_id' ,
141+ 'user_email' ,
142+ 'access_token_id' ,
143+ 'metadata' ,
144+ ] ;
100145 expect ( header ) . toEqual ( expectedHeader ) ;
101146 // Sometimes the order of the rows is not guaranteed, so we need to check if the expected rows are present
102147 expect ( rows ?. find ( row => row . includes ( 'ORGANIZATION_CREATED' ) ) ) . toBeDefined ( ) ;
@@ -107,14 +152,14 @@ test.concurrent('Try to export Audit Logs from an Organization with authorized u
107152test . concurrent ( 'export audit log for schema policy' , async ( ) => {
108153 const { createOrg, ownerToken } = await initSeed ( ) . createOwner ( ) ;
109154 const { createProject, setOrganizationSchemaPolicy, organization } = await createOrg ( ) ;
110- await createProject ( ProjectType . Single ) ;
155+ await createProject ( GraphQLSchema . ProjectType . Single ) ;
111156 await setOrganizationSchemaPolicy (
112157 {
113158 rules : [
114159 {
115160 configuration : { definitions : false } ,
116161 ruleId : 'alphabetize' ,
117- severity : RuleInstanceSeverityLevel . Warning ,
162+ severity : GraphQLSchema . RuleInstanceSeverityLevel . Warning ,
118163 } ,
119164 ] ,
120165 } ,
@@ -137,3 +182,53 @@ test.concurrent('export audit log for schema policy', async () => {
137182 token : ownerToken ,
138183 } ) . then ( res => res . expectNoGraphQLErrors ( ) ) ;
139184} ) ;
185+
186+ test . concurrent ( 'access token actions are stored within the audit log' , async ( ) => {
187+ const { createOrg, ownerToken } = await initSeed ( ) . createOwner ( ) ;
188+ const { organization, createOrganizationAccessToken } = await createOrg ( ) ;
189+ // First we create an access token with the organization owner token
190+ const accessToken = await createOrganizationAccessToken ( {
191+ permissions : [ 'organization:describe' , 'project:describe' , 'accessToken:modify' ] ,
192+ resources : { mode : GraphQLSchema . ResourceAssignmentModeType . All } ,
193+ } ) ;
194+
195+ // Now we create a new one using the access token
196+ await createOrganizationAccessToken (
197+ {
198+ permissions : [ 'organization:describe' , 'project:describe' ] ,
199+ resources : { mode : GraphQLSchema . ResourceAssignmentModeType . All } ,
200+ } ,
201+ accessToken . privateAccessKey ,
202+ ) ;
203+
204+ const exportAuditLogs = await execute ( {
205+ document : ExportAllAuditLogs ,
206+ variables : {
207+ input : {
208+ selector : {
209+ organizationSlug : organization . slug ,
210+ } ,
211+ filter : {
212+ startDate : lastYear . toISOString ( ) ,
213+ endDate : today . toISOString ( ) ,
214+ } ,
215+ } ,
216+ } ,
217+ token : ownerToken ,
218+ } ) . then ( res => res . expectNoGraphQLErrors ( ) ) ;
219+
220+ const url = exportAuditLogs ?. exportOrganizationAuditLog . ok ?. url ;
221+ const contents = await fetchAuditLogFromS3Bucket ( String ( url ) ) ;
222+ const logs = await parseAuditLog ( contents ) ;
223+ /** Find the log entry of the access token we used */
224+ const logEntries = logs . filter (
225+ log => log . access_token_id === accessToken . createdOrganizationAccessToken . id ,
226+ ) ;
227+
228+ expect ( logEntries . length ) . toEqual ( 1 ) ;
229+ const [ logEntry ] = logEntries ;
230+ expect ( logEntry . event_type ) . toEqual ( 'ORGANIZATION_ACCESS_TOKEN_CREATED' ) ;
231+ expect (
232+ accessToken . privateAccessKey . startsWith ( logEntry . metadata . accessToken . firstCharacters ) ,
233+ ) . toEqual ( true ) ;
234+ } ) ;
0 commit comments