-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: audit log for organizations (#5530)
Co-authored-by: Laurin Quast <[email protected]> Co-authored-by: Dotan Simha <[email protected]>
- Loading branch information
1 parent
1932427
commit 38c14e2
Showing
42 changed files
with
1,872 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
'hive': minor | ||
--- | ||
|
||
Add organization audit log. | ||
|
||
Each organization now has an audit log of all user actions that can be exported by admins. | ||
Exported audit logs are stored on the pre-configured S3 storage. | ||
|
||
In case you want to store exported audit logs on a separate S3 bucket, you can use the `S3_AUDIT_LOG` prefixed environment variables for the configuration. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
integration-tests/tests/api/audit-logs/audit-log-record.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { endOfDay, startOfDay } from 'date-fns'; | ||
import { graphql } from 'testkit/gql'; | ||
import { ProjectType } from 'testkit/gql/graphql'; | ||
import { execute } from 'testkit/graphql'; | ||
import { initSeed } from 'testkit/seed'; | ||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; | ||
|
||
const s3Client = new S3Client({ | ||
endpoint: 'http://127.0.0.1:9000', | ||
region: 'auto', | ||
credentials: { | ||
accessKeyId: 'minioadmin', | ||
secretAccessKey: 'minioadmin', | ||
}, | ||
forcePathStyle: true, | ||
}); | ||
|
||
const ExportAllAuditLogs = graphql(` | ||
mutation exportAllAuditLogs($input: ExportOrganizationAuditLogInput!) { | ||
exportOrganizationAuditLog(input: $input) { | ||
ok { | ||
url | ||
} | ||
error { | ||
message | ||
} | ||
__typename | ||
} | ||
} | ||
`); | ||
|
||
const today = endOfDay(new Date()); | ||
const lastYear = startOfDay(new Date(new Date().setFullYear(new Date().getFullYear() - 1))); | ||
|
||
test.concurrent( | ||
'Try to export Audit Logs from an Organization with unauthorized user - should throw error', | ||
async () => { | ||
const { createOrg } = await initSeed().createOwner(); | ||
const { createProject, organization } = await createOrg(); | ||
await createProject(ProjectType.Single); | ||
const secondOrg = await initSeed().createOwner(); | ||
const secondToken = secondOrg.ownerToken; | ||
|
||
await execute({ | ||
document: ExportAllAuditLogs, | ||
variables: { | ||
input: { | ||
selector: { | ||
organizationSlug: organization.id, | ||
}, | ||
filter: { | ||
startDate: lastYear.toISOString(), | ||
endDate: today.toISOString(), | ||
}, | ||
}, | ||
}, | ||
token: secondToken, | ||
}).then(r => r.expectGraphQLErrors()); | ||
}, | ||
); | ||
|
||
test.concurrent('Try to export Audit Logs from an Organization with authorized user', async () => { | ||
const { createOrg, ownerToken } = await initSeed().createOwner(); | ||
const { createProject, organization } = await createOrg(); | ||
await createProject(ProjectType.Single); | ||
|
||
const exportAuditLogs = await execute({ | ||
document: ExportAllAuditLogs, | ||
variables: { | ||
input: { | ||
selector: { | ||
organizationSlug: organization.id, | ||
}, | ||
filter: { | ||
startDate: lastYear.toISOString(), | ||
endDate: today.toISOString(), | ||
}, | ||
}, | ||
}, | ||
token: ownerToken, | ||
}); | ||
expect(exportAuditLogs.rawBody.data?.exportOrganizationAuditLog.error).toBeNull(); | ||
const url = exportAuditLogs.rawBody.data?.exportOrganizationAuditLog.ok?.url; | ||
const parsedUrl = new URL(String(url)); | ||
const pathParts = parsedUrl.pathname.split('/'); | ||
const bucketName = pathParts[1]; | ||
const key = pathParts.slice(2).join('/'); | ||
const getObjectCommand = new GetObjectCommand({ | ||
Bucket: bucketName, | ||
Key: key, | ||
}); | ||
const result = await s3Client.send(getObjectCommand); | ||
const bodyStream = await result.Body?.transformToString(); | ||
expect(bodyStream).toBeDefined(); | ||
|
||
const rows = bodyStream?.split('\n'); | ||
expect(rows?.length).toBeGreaterThan(1); // At least header and one row | ||
const header = rows?.[0].split(','); | ||
const expectedHeader = ['id', 'created_at', 'event_type', 'user_id', 'user_email', 'metadata']; | ||
expect(header).toEqual(expectedHeader); | ||
// Sometimes the order of the rows is not guaranteed, so we need to check if the expected rows are present | ||
expect(rows?.find(row => row.includes('ORGANIZATION_CREATED'))).toBeDefined(); | ||
expect(rows?.find(row => row.includes('PROJECT_CREATED'))).toBeDefined(); | ||
expect(rows?.find(row => row.includes('TARGET_CREATED'))).toBeDefined(); | ||
}); |
22 changes: 22 additions & 0 deletions
22
packages/migrations/src/clickhouse-actions/011-audit-logs.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { Action } from '../clickhouse'; | ||
|
||
export const action: Action = async exec => { | ||
await exec(` | ||
CREATE TABLE IF NOT EXISTS "audit_logs" ( | ||
id String CODEC(ZSTD(1)), | ||
timestamp DateTime('UTC') CODEC(DoubleDelta, LZ4), | ||
organization_id LowCardinality(String) CODEC(ZSTD(1)), | ||
event_action LowCardinality(String) CODEC(ZSTD(1)), | ||
user_id String CODEC(ZSTD(1)), | ||
user_email String CODEC(ZSTD(1)), | ||
metadata String CODEC(ZSTD(1)), | ||
INDEX idx_action event_action TYPE set(0) GRANULARITY 1, | ||
INDEX idx_user_id user_id TYPE bloom_filter(0.001) GRANULARITY 1, | ||
) | ||
ENGINE = MergeTree | ||
PARTITION BY toYearWeek(timestamp, 1, 'UTC') | ||
ORDER BY (organization_id, timestamp) | ||
TTL timestamp + INTERVAL 1 YEAR | ||
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1; | ||
`); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { createModule } from 'graphql-modules'; | ||
import { ClickHouse } from '../operations/providers/clickhouse-client'; | ||
import { AuditLogRecorder } from './providers/audit-log-recorder'; | ||
import { AuditLogManager } from './providers/audit-logs-manager'; | ||
import { resolvers } from './resolvers.generated'; | ||
import { typeDefs } from './module.graphql'; | ||
|
||
export const auditLogsModule = createModule({ | ||
id: 'audit-logs', | ||
dirname: __dirname, | ||
typeDefs, | ||
resolvers, | ||
providers: [AuditLogManager, AuditLogRecorder, ClickHouse], | ||
}); |
8 changes: 8 additions & 0 deletions
8
packages/services/api/src/modules/audit-logs/module.graphql.mappers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { AuditLogType } from './providers/audit-logs-types'; | ||
|
||
export type AuditLogMapper = AuditLogType; | ||
export type AuditLogIdRecordMapper = { | ||
organizationId: string; | ||
userEmail: string; | ||
userId: string; | ||
}; |
32 changes: 32 additions & 0 deletions
32
packages/services/api/src/modules/audit-logs/module.graphql.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { gql } from 'graphql-modules'; | ||
|
||
export const typeDefs = gql` | ||
extend type Mutation { | ||
exportOrganizationAuditLog( | ||
input: ExportOrganizationAuditLogInput! | ||
): ExportOrganizationAuditLogResult! | ||
} | ||
input ExportOrganizationAuditLogInput { | ||
selector: OrganizationSelectorInput! | ||
filter: AuditLogFilter! | ||
} | ||
input AuditLogFilter { | ||
startDate: DateTime! | ||
endDate: DateTime! | ||
} | ||
type ExportOrganizationAuditLogError implements Error { | ||
message: String! | ||
} | ||
type ExportOrganizationAuditLogPayload { | ||
url: String! | ||
} | ||
type ExportOrganizationAuditLogResult { | ||
ok: ExportOrganizationAuditLogPayload | ||
error: ExportOrganizationAuditLogError | ||
} | ||
`; |
Oops, something went wrong.