mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:17:32 +00:00
feat: limit audit trail (#6744)
* feat: enable limiting audit trail * support AddExclusiveQuery * fix invalid condition * register event mappers * fix NullDuration validity * test query side for limits * lint * acceptance test audit trail limit * fix acceptance test * translate limits not found * update tests * fix linting * add audit log retention to default instance * fix tests * update docs * remove todo * improve test name
This commit is contained in:
@@ -17,7 +17,7 @@ func (s *Server) ListEvents(ctx context.Context, in *admin_pb.ListEventsRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := s.query.SearchEvents(ctx, filter, s.auditLogRetention)
|
||||
events, err := s.query.SearchEvents(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
@@ -26,14 +25,13 @@ const (
|
||||
|
||||
type Server struct {
|
||||
auth.UnimplementedAuthServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
repo repository.Repository
|
||||
defaults systemdefaults.SystemDefaults
|
||||
assetsAPIDomain func(context.Context) string
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
externalSecure bool
|
||||
auditLogRetention time.Duration
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
repo repository.Repository
|
||||
defaults systemdefaults.SystemDefaults
|
||||
assetsAPIDomain func(context.Context) string
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
externalSecure bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -46,17 +44,15 @@ func CreateServer(command *command.Commands,
|
||||
defaults systemdefaults.SystemDefaults,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
externalSecure bool,
|
||||
auditLogRetention time.Duration,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
repo: authRepo,
|
||||
defaults: defaults,
|
||||
assetsAPIDomain: assets.AssetAPI(externalSecure),
|
||||
userCodeAlg: userCodeAlg,
|
||||
externalSecure: externalSecure,
|
||||
auditLogRetention: auditLogRetention,
|
||||
command: command,
|
||||
query: query,
|
||||
repo: authRepo,
|
||||
defaults: defaults,
|
||||
assetsAPIDomain: assets.AssetAPI(externalSecure),
|
||||
userCodeAlg: userCodeAlg,
|
||||
externalSecure: externalSecure,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -84,7 +84,7 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC
|
||||
query.OrderAsc()
|
||||
}
|
||||
|
||||
changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention)
|
||||
changes, err := s.query.SearchEvents(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -63,7 +63,7 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges
|
||||
query.OrderAsc()
|
||||
}
|
||||
|
||||
response, err := s.query.SearchEvents(ctx, query, s.auditLogRetention)
|
||||
response, err := s.query.SearchEvents(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -87,7 +87,7 @@ func (s *Server) ListProjectGrantChanges(ctx context.Context, req *mgmt_pb.ListP
|
||||
query.OrderAsc()
|
||||
}
|
||||
|
||||
changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention)
|
||||
changes, err := s.query.SearchEvents(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec
|
||||
query.OrderAsc()
|
||||
}
|
||||
|
||||
changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention)
|
||||
changes, err := s.query.SearchEvents(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -70,7 +70,7 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges
|
||||
query.OrderAsc()
|
||||
}
|
||||
|
||||
changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention)
|
||||
changes, err := s.query.SearchEvents(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package management
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
@@ -24,14 +23,13 @@ var _ management.ManagementServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
management.UnimplementedManagementServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
systemDefaults systemdefaults.SystemDefaults
|
||||
assetAPIPrefix func(context.Context) string
|
||||
passwordHashAlg crypto.HashAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
externalSecure bool
|
||||
auditLogRetention time.Duration
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
systemDefaults systemdefaults.SystemDefaults
|
||||
assetAPIPrefix func(context.Context) string
|
||||
passwordHashAlg crypto.HashAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
externalSecure bool
|
||||
}
|
||||
|
||||
func CreateServer(
|
||||
@@ -40,17 +38,15 @@ func CreateServer(
|
||||
sd systemdefaults.SystemDefaults,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
externalSecure bool,
|
||||
auditLogRetention time.Duration,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
systemDefaults: sd,
|
||||
assetAPIPrefix: assets.AssetAPI(externalSecure),
|
||||
passwordHashAlg: crypto.NewBCrypt(sd.SecretGenerators.PasswordSaltCost),
|
||||
userCodeAlg: userCodeAlg,
|
||||
externalSecure: externalSecure,
|
||||
auditLogRetention: auditLogRetention,
|
||||
command: command,
|
||||
query: query,
|
||||
systemDefaults: sd,
|
||||
assetAPIPrefix: assets.AssetAPI(externalSecure),
|
||||
passwordHashAlg: crypto.NewBCrypt(sd.SecretGenerators.PasswordSaltCost),
|
||||
userCodeAlg: userCodeAlg,
|
||||
externalSecure: externalSecure,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -109,7 +109,7 @@ func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChang
|
||||
query.OrderAsc()
|
||||
}
|
||||
|
||||
changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention)
|
||||
changes, err := s.query.SearchEvents(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
32
internal/api/grpc/system/limits.go
Normal file
32
internal/api/grpc/system/limits.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
func (s *Server) SetLimits(ctx context.Context, req *system.SetLimitsRequest) (*system.SetLimitsResponse, error) {
|
||||
details, err := s.command.SetLimits(
|
||||
ctx,
|
||||
req.GetInstanceId(),
|
||||
instanceLimitsPbToCommand(req),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &system.SetLimitsResponse{
|
||||
Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetLimits(ctx context.Context, req *system.ResetLimitsRequest) (*system.ResetLimitsResponse, error) {
|
||||
details, err := s.command.ResetLimits(ctx, req.GetInstanceId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &system.ResetLimitsResponse{
|
||||
Details: object.ChangeToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
||||
}, nil
|
||||
}
|
16
internal/api/grpc/system/limits_converter.go
Normal file
16
internal/api/grpc/system/limits_converter.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
func instanceLimitsPbToCommand(req *system.SetLimitsRequest) *command.SetLimits {
|
||||
var setLimits = new(command.SetLimits)
|
||||
if req.AuditLogRetention != nil {
|
||||
setLimits.AuditLogRetention = gu.Ptr(req.AuditLogRetention.AsDuration())
|
||||
}
|
||||
return setLimits
|
||||
}
|
213
internal/api/grpc/system/limits_integration_test.go
Normal file
213
internal/api/grpc/system/limits_integration_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
//go:build integration
|
||||
|
||||
package system_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
func TestServer_Limits_AuditLogRetention(t *testing.T) {
|
||||
_, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(CTX, SystemCTX)
|
||||
userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t)
|
||||
beforeTime := time.Now()
|
||||
zeroCounts := &eventCounts{}
|
||||
seededCount := requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) {
|
||||
counts.assertAll(t, c, "seeded events are > 0", assert.Greater, zeroCounts)
|
||||
}, "wait for seeded event assertions to pass")
|
||||
produceEvents(iamOwnerCtx, t, userID, appID, projectID, projectGrantID)
|
||||
addedCount := requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) {
|
||||
counts.assertAll(t, c, "added events are > seeded events", assert.Greater, seededCount)
|
||||
}, "wait for added event assertions to pass")
|
||||
_, err := Tester.Client.System.SetLimits(SystemCTX, &system.SetLimitsRequest{
|
||||
InstanceId: instanceID,
|
||||
AuditLogRetention: durationpb.New(time.Now().Sub(beforeTime)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) {
|
||||
counts.assertAll(t, c, "limited events < added events", assert.Less, addedCount)
|
||||
counts.assertAll(t, c, "limited events > 0", assert.Greater, zeroCounts)
|
||||
}, "wait for limited event assertions to pass")
|
||||
_, err = Tester.Client.System.ResetLimits(SystemCTX, &system.ResetLimitsRequest{
|
||||
InstanceId: instanceID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) {
|
||||
counts.assertAll(t, c, "with reset limit, added events are > seeded events", assert.Greater, seededCount)
|
||||
}, "wait for reset event assertions to pass")
|
||||
}
|
||||
|
||||
func requireEventually(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
userID, projectID, appID, projectGrantID string,
|
||||
assertCounts func(assert.TestingT, *eventCounts),
|
||||
msg string,
|
||||
) (counts *eventCounts) {
|
||||
countTimeout := 30 * time.Second
|
||||
assertTimeout := countTimeout + time.Second
|
||||
countCtx, cancel := context.WithTimeout(ctx, countTimeout)
|
||||
defer cancel()
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
counts = countEvents(countCtx, t, userID, projectID, appID, projectGrantID)
|
||||
assertCounts(c, counts)
|
||||
}, assertTimeout, time.Second, msg)
|
||||
return counts
|
||||
}
|
||||
|
||||
var runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randomString(resourceType string, n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = runes[rand.Intn(len(runes))]
|
||||
}
|
||||
return "test" + resourceType + "-" + string(b)
|
||||
}
|
||||
|
||||
func seedObjects(ctx context.Context, t *testing.T) (string, string, string, string) {
|
||||
t.Helper()
|
||||
project, err := Tester.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
|
||||
Name: randomString("project", 5),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
app, err := Tester.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
|
||||
Name: randomString("app", 5),
|
||||
ProjectId: project.GetId(),
|
||||
})
|
||||
org, err := Tester.Client.Mgmt.AddOrg(ctx, &management.AddOrgRequest{
|
||||
Name: randomString("org", 5),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
role := randomString("role", 5)
|
||||
require.NoError(t, err)
|
||||
_, err = Tester.Client.Mgmt.AddProjectRole(ctx, &management.AddProjectRoleRequest{
|
||||
ProjectId: project.GetId(),
|
||||
RoleKey: role,
|
||||
DisplayName: role,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
projectGrant, err := Tester.Client.Mgmt.AddProjectGrant(ctx, &management.AddProjectGrantRequest{
|
||||
ProjectId: project.GetId(),
|
||||
GrantedOrgId: org.GetId(),
|
||||
RoleKeys: []string{role},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
user, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.NoError(t, err)
|
||||
userID := user.GetUser().GetId()
|
||||
requireUserEvent(ctx, t, userID)
|
||||
return userID, project.GetId(), app.GetAppId(), projectGrant.GetGrantId()
|
||||
}
|
||||
|
||||
func produceEvents(ctx context.Context, t *testing.T, machineID, appID, projectID, grantID string) {
|
||||
t.Helper()
|
||||
_, err := Tester.Client.Mgmt.UpdateOrg(ctx, &management.UpdateOrgRequest{
|
||||
Name: randomString("org", 5),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Tester.Client.Mgmt.UpdateProject(ctx, &management.UpdateProjectRequest{
|
||||
Id: projectID,
|
||||
Name: randomString("project", 5),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Tester.Client.Mgmt.UpdateApp(ctx, &management.UpdateAppRequest{
|
||||
AppId: appID,
|
||||
ProjectId: projectID,
|
||||
Name: randomString("app", 5),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireUserEvent(ctx, t, machineID)
|
||||
_, err = Tester.Client.Mgmt.UpdateProjectGrant(ctx, &management.UpdateProjectGrantRequest{
|
||||
ProjectId: projectID,
|
||||
GrantId: grantID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func requireUserEvent(ctx context.Context, t *testing.T, machineID string) {
|
||||
_, err := Tester.Client.Mgmt.UpdateMachine(ctx, &management.UpdateMachineRequest{
|
||||
UserId: machineID,
|
||||
Name: randomString("machine", 5),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
type eventCounts struct {
|
||||
all, myUser, aUser, grant, project, app, org int
|
||||
}
|
||||
|
||||
func (e *eventCounts) assertAll(t *testing.T, c assert.TestingT, name string, compare assert.ComparisonAssertionFunc, than *eventCounts) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
compare(c, e.all, than.all, "ListEvents")
|
||||
compare(c, e.myUser, than.myUser, "ListMyUserChanges")
|
||||
compare(c, e.aUser, than.aUser, "ListUserChanges")
|
||||
compare(c, e.grant, than.grant, "ListProjectGrantChanges")
|
||||
compare(c, e.project, than.project, "ListProjectChanges")
|
||||
compare(c, e.app, than.app, "ListAppChanges")
|
||||
compare(c, e.org, than.org, "ListOrgChanges")
|
||||
})
|
||||
}
|
||||
|
||||
func countEvents(ctx context.Context, t *testing.T, userID, projectID, appID, grantID string) *eventCounts {
|
||||
t.Helper()
|
||||
counts := new(eventCounts)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(7)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Admin.ListEvents(ctx, &admin.ListEventsRequest{})
|
||||
require.NoError(t, err)
|
||||
counts.all = len(result.GetEvents())
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Auth.ListMyUserChanges(ctx, &auth.ListMyUserChangesRequest{})
|
||||
require.NoError(t, err)
|
||||
counts.myUser = len(result.GetResult())
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Mgmt.ListUserChanges(ctx, &management.ListUserChangesRequest{UserId: userID})
|
||||
require.NoError(t, err)
|
||||
counts.aUser = len(result.GetResult())
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Mgmt.ListAppChanges(ctx, &management.ListAppChangesRequest{ProjectId: projectID, AppId: appID})
|
||||
require.NoError(t, err)
|
||||
counts.app = len(result.GetResult())
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Mgmt.ListOrgChanges(ctx, &management.ListOrgChangesRequest{})
|
||||
require.NoError(t, err)
|
||||
counts.org = len(result.GetResult())
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Mgmt.ListProjectChanges(ctx, &management.ListProjectChangesRequest{ProjectId: projectID})
|
||||
require.NoError(t, err)
|
||||
counts.project = len(result.GetResult())
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := Tester.Client.Mgmt.ListProjectGrantChanges(ctx, &management.ListProjectGrantChangesRequest{ProjectId: projectID, GrantId: grantID})
|
||||
require.NoError(t, err)
|
||||
counts.grant = len(result.GetResult())
|
||||
}()
|
||||
wg.Wait()
|
||||
return counts
|
||||
}
|
Reference in New Issue
Block a user