1
+ import * as sp from 'node:stream/promises' ;
2
+ import * as csvp from 'csv-parse' ;
1
3
import { endOfDay , startOfDay } from 'date-fns' ;
2
4
import { graphql } from 'testkit/gql' ;
3
- import { ProjectType , RuleInstanceSeverityLevel } from 'testkit/gql/graphql' ;
5
+ import * as GraphQLSchema from 'testkit/gql/graphql' ;
4
6
import { execute } from 'testkit/graphql' ;
5
7
import { initSeed } from 'testkit/seed' ;
6
8
import { GetObjectCommand , S3Client } from '@aws-sdk/client-s3' ;
@@ -15,6 +17,52 @@ const s3Client = new S3Client({
15
17
forcePathStyle : true ,
16
18
} ) ;
17
19
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
+
18
66
const ExportAllAuditLogs = graphql ( `
19
67
mutation exportAllAuditLogs($input: ExportOrganizationAuditLogInput!) {
20
68
exportOrganizationAuditLog(input: $input) {
@@ -37,7 +85,7 @@ test.concurrent(
37
85
async ( ) => {
38
86
const { createOrg } = await initSeed ( ) . createOwner ( ) ;
39
87
const { createProject, organization } = await createOrg ( ) ;
40
- await createProject ( ProjectType . Single ) ;
88
+ await createProject ( GraphQLSchema . ProjectType . Single ) ;
41
89
const secondOrg = await initSeed ( ) . createOwner ( ) ;
42
90
const secondToken = secondOrg . ownerToken ;
43
91
@@ -62,7 +110,7 @@ test.concurrent(
62
110
test . concurrent ( 'Try to export Audit Logs from an Organization with authorized user' , async ( ) => {
63
111
const { createOrg, ownerToken } = await initSeed ( ) . createOwner ( ) ;
64
112
const { createProject, organization } = await createOrg ( ) ;
65
- await createProject ( ProjectType . Single ) ;
113
+ await createProject ( GraphQLSchema . ProjectType . Single ) ;
66
114
67
115
const exportAuditLogs = await execute ( {
68
116
document : ExportAllAuditLogs ,
@@ -81,22 +129,19 @@ test.concurrent('Try to export Audit Logs from an Organization with authorized u
81
129
} ) ;
82
130
expect ( exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . error ) . toBeNull ( ) ;
83
131
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
98
135
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
+ ] ;
100
145
expect ( header ) . toEqual ( expectedHeader ) ;
101
146
// Sometimes the order of the rows is not guaranteed, so we need to check if the expected rows are present
102
147
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
107
152
test . concurrent ( 'export audit log for schema policy' , async ( ) => {
108
153
const { createOrg, ownerToken } = await initSeed ( ) . createOwner ( ) ;
109
154
const { createProject, setOrganizationSchemaPolicy, organization } = await createOrg ( ) ;
110
- await createProject ( ProjectType . Single ) ;
155
+ await createProject ( GraphQLSchema . ProjectType . Single ) ;
111
156
await setOrganizationSchemaPolicy (
112
157
{
113
158
rules : [
114
159
{
115
160
configuration : { definitions : false } ,
116
161
ruleId : 'alphabetize' ,
117
- severity : RuleInstanceSeverityLevel . Warning ,
162
+ severity : GraphQLSchema . RuleInstanceSeverityLevel . Warning ,
118
163
} ,
119
164
] ,
120
165
} ,
@@ -137,3 +182,53 @@ test.concurrent('export audit log for schema policy', async () => {
137
182
token : ownerToken ,
138
183
} ) . then ( res => res . expectNoGraphQLErrors ( ) ) ;
139
184
} ) ;
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