Add log export for audit logs and SCIM events
Route audit-log and SCIM-event exports through export_jobs with typed arguments, an iam BuildAndUploadExport/SendExportEmail implementation, and a concurrent export-job worker with stale recovery. Stream JSONL via page.WalkAll into S3, and expose the request flow on console, connect, MCP, and CLI. Co-authored-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -7,9 +7,12 @@ package connect_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
@@ -41,6 +44,42 @@ func (r *auditLogEntryConnectionResolver) TotalCount(ctx context.Context, obj *t
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// RequestAuditLogExport is the resolver for the requestAuditLogExport field.
|
||||
func (r *mutationResolver) RequestAuditLogExport(ctx context.Context, input types.RequestAuditLogExportInput) (*types.RequestAuditLogExportPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.OrganizationID, iam.ActionAuditLogExport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
logExport, err := r.iam.OrganizationService.RequestLogExport(
|
||||
ctx,
|
||||
scope,
|
||||
iam.RequestLogExportRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Type: coredata.ExportJobTypeAuditLog,
|
||||
FromTime: input.FromTime,
|
||||
ToTime: input.ToTime,
|
||||
RecipientEmail: identity.EmailAddress,
|
||||
RecipientName: identity.FullName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidLogExportTimeRange](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot request audit log export", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.RequestAuditLogExportPayload{
|
||||
ExportJobID: logExport.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuditLogEntry returns schema.AuditLogEntryResolver implementation.
|
||||
func (r *Resolver) AuditLogEntry() schema.AuditLogEntryResolver { return &auditLogEntryResolver{r} }
|
||||
|
||||
|
||||
@@ -70,3 +70,19 @@ type AuditLogEntryEdge {
|
||||
cursor: CursorKey!
|
||||
node: AuditLogEntry!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
requestAuditLogExport(
|
||||
input: RequestAuditLogExportInput!
|
||||
): RequestAuditLogExportPayload @authentication(required: PRESENT) @sessionOnly
|
||||
}
|
||||
|
||||
input RequestAuditLogExportInput {
|
||||
organizationId: ID!
|
||||
fromTime: Datetime!
|
||||
toTime: Datetime!
|
||||
}
|
||||
|
||||
type RequestAuditLogExportPayload {
|
||||
exportJobId: ID!
|
||||
}
|
||||
|
||||
@@ -140,6 +140,9 @@ extend type Mutation {
|
||||
updateSCIMBridge(
|
||||
input: UpdateSCIMBridgeInput!
|
||||
): UpdateSCIMBridgePayload @authentication(required: PRESENT)
|
||||
requestSCIMEventExport(
|
||||
input: RequestSCIMEventExportInput!
|
||||
): RequestSCIMEventExportPayload @authentication(required: PRESENT) @sessionOnly
|
||||
}
|
||||
|
||||
input CreateSCIMConfigurationInput {
|
||||
@@ -181,3 +184,13 @@ type RegenerateSCIMTokenPayload {
|
||||
type UpdateSCIMBridgePayload {
|
||||
scimBridge: SCIMBridge!
|
||||
}
|
||||
|
||||
input RequestSCIMEventExportInput {
|
||||
organizationId: ID!
|
||||
fromTime: Datetime!
|
||||
toTime: Datetime!
|
||||
}
|
||||
|
||||
type RequestSCIMEventExportPayload {
|
||||
exportJobId: ID!
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
@@ -107,6 +108,42 @@ func (r *mutationResolver) UpdateSCIMBridge(ctx context.Context, input types.Upd
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RequestSCIMEventExport is the resolver for the requestSCIMEventExport field.
|
||||
func (r *mutationResolver) RequestSCIMEventExport(ctx context.Context, input types.RequestSCIMEventExportInput) (*types.RequestSCIMEventExportPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.OrganizationID, iam.ActionSCIMEventExport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
logExport, err := r.iam.OrganizationService.RequestLogExport(
|
||||
ctx,
|
||||
scope,
|
||||
iam.RequestLogExportRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Type: coredata.ExportJobTypeSCIMEvent,
|
||||
FromTime: input.FromTime,
|
||||
ToTime: input.ToTime,
|
||||
RecipientEmail: identity.EmailAddress,
|
||||
RecipientName: identity.FullName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidLogExportTimeRange](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot request SCIM event export", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.RequestSCIMEventExportPayload{
|
||||
ExportJobID: logExport.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ScimConfiguration is the resolver for the scimConfiguration field.
|
||||
func (r *sCIMBridgeResolver) ScimConfiguration(ctx context.Context, obj *types.SCIMBridge) (*types.SCIMConfiguration, error) {
|
||||
if _, err := r.authorize(ctx, obj.ScimConfiguration.ID, iam.ActionSCIMConfigurationGet); err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||
@@ -67,6 +68,42 @@ func (r *auditLogEntryConnectionResolver) TotalCount(ctx context.Context, obj *t
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// RequestAuditLogExport is the resolver for the requestAuditLogExport field.
|
||||
func (r *mutationResolver) RequestAuditLogExport(ctx context.Context, input types.RequestAuditLogExportInput) (*types.RequestAuditLogExportPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.OrganizationID, iam.ActionAuditLogExport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
logExport, err := r.iam.OrganizationService.RequestLogExport(
|
||||
ctx,
|
||||
scope,
|
||||
iam.RequestLogExportRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Type: coredata.ExportJobTypeAuditLog,
|
||||
FromTime: input.FromTime,
|
||||
ToTime: input.ToTime,
|
||||
RecipientEmail: identity.EmailAddress,
|
||||
RecipientName: identity.FullName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidLogExportTimeRange](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot request audit log export", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.RequestAuditLogExportPayload{
|
||||
ExportJobID: logExport.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuditLogEntry returns schema.AuditLogEntryResolver implementation.
|
||||
func (r *Resolver) AuditLogEntry() schema.AuditLogEntryResolver { return &auditLogEntryResolver{r} }
|
||||
|
||||
|
||||
@@ -68,3 +68,19 @@ type AuditLogEntryEdge {
|
||||
cursor: CursorKey!
|
||||
node: AuditLogEntry!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
requestAuditLogExport(
|
||||
input: RequestAuditLogExportInput!
|
||||
): RequestAuditLogExportPayload!
|
||||
}
|
||||
|
||||
input RequestAuditLogExportInput {
|
||||
organizationId: ID!
|
||||
fromTime: Datetime!
|
||||
toTime: Datetime!
|
||||
}
|
||||
|
||||
type RequestAuditLogExportPayload {
|
||||
exportJobId: ID!
|
||||
}
|
||||
|
||||
@@ -7351,3 +7351,69 @@ func (r *Resolver) DeleteCommitmentTool(ctx context.Context, req *mcp.CallToolRe
|
||||
|
||||
return nil, types.DeleteCommitmentOutput{DeletedCommitmentID: input.ID}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) RequestAuditLogExportTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RequestAuditLogExportInput) (*mcp.CallToolResult, types.RequestAuditLogExportOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.OrganizationID, iam.ActionAuditLogExport)
|
||||
if err != nil {
|
||||
return nil, types.RequestAuditLogExportOutput{}, err
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
logExport, err := r.iamSvc.OrganizationService.RequestLogExport(
|
||||
ctx,
|
||||
scope,
|
||||
iam.RequestLogExportRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Type: coredata.ExportJobTypeAuditLog,
|
||||
FromTime: input.FromTime,
|
||||
ToTime: input.ToTime,
|
||||
RecipientEmail: identity.EmailAddress,
|
||||
RecipientName: identity.FullName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidLogExportTimeRange](err); ok {
|
||||
return nil, types.RequestAuditLogExportOutput{}, err
|
||||
}
|
||||
|
||||
return nil, types.RequestAuditLogExportOutput{}, fmt.Errorf("cannot request audit log export: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.RequestAuditLogExportOutput{
|
||||
ExportJobID: logExport.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) RequestSCIMEventExportTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RequestSCIMEventExportInput) (*mcp.CallToolResult, types.RequestSCIMEventExportOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.OrganizationID, iam.ActionSCIMEventExport)
|
||||
if err != nil {
|
||||
return nil, types.RequestSCIMEventExportOutput{}, err
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
logExport, err := r.iamSvc.OrganizationService.RequestLogExport(
|
||||
ctx,
|
||||
scope,
|
||||
iam.RequestLogExportRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Type: coredata.ExportJobTypeSCIMEvent,
|
||||
FromTime: input.FromTime,
|
||||
ToTime: input.ToTime,
|
||||
RecipientEmail: identity.EmailAddress,
|
||||
RecipientName: identity.FullName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidLogExportTimeRange](err); ok {
|
||||
return nil, types.RequestSCIMEventExportOutput{}, err
|
||||
}
|
||||
|
||||
return nil, types.RequestSCIMEventExportOutput{}, fmt.Errorf("cannot request SCIM event export: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.RequestSCIMEventExportOutput{
|
||||
ExportJobID: logExport.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8487,6 +8487,66 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/AuditLogEntry"
|
||||
|
||||
RequestAuditLogExportInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
- from_time
|
||||
- to_time
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
from_time:
|
||||
type: string
|
||||
format: date-time
|
||||
go.probo.inc/mcpgen/type: time.Time
|
||||
description: Start of the time range (inclusive). The range must not exceed 1 year.
|
||||
to_time:
|
||||
type: string
|
||||
format: date-time
|
||||
go.probo.inc/mcpgen/type: time.Time
|
||||
description: End of the time range (exclusive). The range must not exceed 1 year.
|
||||
|
||||
RequestAuditLogExportOutput:
|
||||
type: object
|
||||
required:
|
||||
- export_job_id
|
||||
properties:
|
||||
export_job_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: ID of the created log export
|
||||
|
||||
RequestSCIMEventExportInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
- from_time
|
||||
- to_time
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
from_time:
|
||||
type: string
|
||||
format: date-time
|
||||
go.probo.inc/mcpgen/type: time.Time
|
||||
description: Start of the time range (inclusive). The range must not exceed 1 year.
|
||||
to_time:
|
||||
type: string
|
||||
format: date-time
|
||||
go.probo.inc/mcpgen/type: time.Time
|
||||
description: End of the time range (exclusive). The range must not exceed 1 year.
|
||||
|
||||
RequestSCIMEventExportOutput:
|
||||
type: object
|
||||
required:
|
||||
- export_job_id
|
||||
properties:
|
||||
export_job_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: ID of the created log export
|
||||
|
||||
AuditLogEntry:
|
||||
type: object
|
||||
required:
|
||||
@@ -13955,6 +14015,24 @@ tools:
|
||||
$ref: "#/components/schemas/ListAuditLogEntriesInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/ListAuditLogEntriesOutput"
|
||||
- name: requestAuditLogExport
|
||||
description: Request an export of audit log entries for the organization within a time range. The export will be emailed as a JSONL download link.
|
||||
hints:
|
||||
readonly: false
|
||||
idempotent: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/RequestAuditLogExportInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/RequestAuditLogExportOutput"
|
||||
- name: requestSCIMEventExport
|
||||
description: Request an export of SCIM events for the organization within a time range. The export will be emailed as a JSONL download link.
|
||||
hints:
|
||||
readonly: false
|
||||
idempotent: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/RequestSCIMEventExportInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/RequestSCIMEventExportOutput"
|
||||
- name: listWebhookSubscriptions
|
||||
description: List all webhook subscriptions for the organization
|
||||
hints:
|
||||
|
||||
Reference in New Issue
Block a user