Skip to content

Commit 06b6f30

Browse files
authored
Merge pull request #3 from eko/added-audit-logs
Added audit logs
2 parents e75bfe5 + 87a7540 commit 06b6f30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1643
-454
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ You can use both Role-Based Acccess Control (RBAC) and Attribute-Based Access Co
2424

2525
✅ Reliable: Authz uses Authz itself for managing its own internal authorizations
2626

27+
🔍 Audit: We log each check decisions and which policy matched
28+
2729
## How it works?
2830

2931
Authorization is simple: a `principal` wants to make an `action` on a `resource`. That's it.
3032

3133
Authz allows you to manage all the authorizations you want to manage. All of them, centralized in a single application.
3234

33-
<h1 align="center"><img src="docs/architecture/howitworks.svg" alt="Authz" width="600"></h1>
35+
<picture>
36+
<source media="(prefers-color-scheme: dark)" srcset="docs/architecture/howitworks.dark.svg">
37+
<img alt="Text changing depending on mode. Light: 'So light!' Dark: 'So dark!'" src="docs/architecture/howitworks.svg">
38+
</picture>
3439

3540
All you need to do is to host the backend server (a Go single binary), the frontend (static files) if you want it and use our SDKs.
3641

backend/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ test-mocks: ## Generate unit test mocks
5050
mockgen -source=internal/event/dispatcher.go -destination=internal/event/dispatcher_mock.go -package=event
5151
mockgen -source=internal/entity/manager/action.go -destination=internal/entity/manager/action_mock.go -package=manager
5252
mockgen -source=internal/entity/manager/attribute.go -destination=internal/entity/manager/attribute_mock.go -package=manager
53+
mockgen -source=internal/entity/manager/audit.go -destination=internal/entity/manager/audit_mock.go -package=manager
5354
mockgen -source=internal/entity/manager/client.go -destination=internal/entity/manager/client_mock.go -package=manager
5455
mockgen -source=internal/entity/manager/compiled.go -destination=internal/entity/manager/compiled_mock.go -package=manager
5556
mockgen -source=internal/entity/manager/policy.go -destination=internal/entity/manager/policy_mock.go -package=manager

backend/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ Here are the available configuration options available as environment variable:
2626

2727
| Property | Default value | Description |
2828
| -------- | ------------- | ----------- |
29-
| APP_STATS_FLUSH_DELAY | `5s` | Delay in which statistics will be batch into database |
29+
| APP_AUDIT_CLEAN_DAYS_TO_KEEP | `7` | Audit logs number of days to keep in database |
30+
| APP_AUDIT_CLEAN_DELAY | `1h` | Audit logs clean delay |
31+
| APP_AUDIT_FLUSH_DELAY | `3s` | Delay in which audit logs will be batch into database |
32+
| APP_STATS_CLEAN_DAYS_TO_KEEP | `30` | Statistics number of days to keep in database |
33+
| APP_STATS_CLEAN_DELAY | `1h` | Statistics clean delay |
34+
| APP_STATS_FLUSH_DELAY | `3s` | Delay in which statistics will be batch into database |
3035
| AUTH_ACCESS_TOKEN_DURATION | `6h` | Access token duration |
3136
| AUTH_DOMAIN | `http://localhost:8080` | OAuth domain to be used |
3237
| AUTH_JWT_SIGN_STRING | `4uthz-s3cr3t-valu3-pl3as3-ch4ng3!` | Default HMAC to use for JWT tokens |
@@ -48,8 +53,6 @@ Here are the available configuration options available as environment variable:
4853
| HTTP_SERVER_CORS_ALLOWED_METHODS | `GET,POST,PATCH,PUT,DELETE,HEAD,OPTIONS` | CORS allowed methods |
4954
| HTTP_SERVER_CORS_CACHE_MAX_AGE | `12h` | CORS cache max age value to be returned by server |
5055
| LOGGER_LEVEL | `INFO` | Log level, could be `DEBUG`, `INFO`, `WARN` or `ERROR` |
51-
| APP_STATS_CLEAN_DAYS_TO_KEEP | `30` | Statistics number of days to keep in database |
52-
| APP_STATS_CLEAN_DELAY | `1h` | Statistics clean delay |
5356
| USER_ADMIN_DEFAULT_PASSWORD | `changeme` | Default admin password updated on app launch |
5457

5558
## Tests

backend/cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/eko/authz/backend/configs"
7+
"github.com/eko/authz/backend/internal/audit"
78
"github.com/eko/authz/backend/internal/compile"
89
"github.com/eko/authz/backend/internal/database"
910
"github.com/eko/authz/backend/internal/entity"
@@ -25,6 +26,7 @@ func main() {
2526
fx.Provide(context.Background),
2627
internal_fx.Logger,
2728

29+
audit.FxModule(),
2830
compile.FxModule(),
2931
configs.FxModule(),
3032
database.FxModule(),

backend/configs/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package configs
33
import "time"
44

55
type App struct {
6+
AuditCleanDelay time.Duration `config:"app_audit_clean_delay"`
7+
AuditCleanDaysToKeep int `config:"app_audit_clean_days_to_keep"`
8+
AuditFlushDelay time.Duration `config:"app_audit_flush_delay"`
69
DispatcherEventChannelSize int `config:"dispatcher_event_channel_size"`
710
StatsCleanDelay time.Duration `config:"app_stats_clean_delay"`
811
StatsCleanDaysToKeep int `config:"app_stats_clean_days_to_keep"`
@@ -11,6 +14,9 @@ type App struct {
1114

1215
func newApp() *App {
1316
return &App{
17+
AuditCleanDelay: 1 * time.Hour,
18+
AuditCleanDaysToKeep: 7,
19+
AuditFlushDelay: 3 * time.Second,
1420
DispatcherEventChannelSize: 10000,
1521
StatsCleanDelay: 1 * time.Hour,
1622
StatsCleanDaysToKeep: 30,

backend/internal/audit/cleaner.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package audit
2+
3+
import (
4+
"context"
5+
lib_time "time"
6+
7+
"github.com/eko/authz/backend/configs"
8+
"github.com/eko/authz/backend/internal/entity/manager"
9+
"github.com/eko/authz/backend/internal/entity/repository"
10+
"github.com/eko/authz/backend/internal/helper/time"
11+
"go.uber.org/fx"
12+
"golang.org/x/exp/slog"
13+
)
14+
15+
type cleaner struct {
16+
logger *slog.Logger
17+
clock time.Clock
18+
statsManager manager.Stats
19+
cleanDelay lib_time.Duration
20+
daysToKeep int
21+
}
22+
23+
func NewCleaner(
24+
cfg *configs.App,
25+
logger *slog.Logger,
26+
clock time.Clock,
27+
statsManager manager.Stats,
28+
) *cleaner {
29+
return &cleaner{
30+
logger: logger,
31+
clock: clock,
32+
statsManager: statsManager,
33+
cleanDelay: cfg.StatsCleanDelay,
34+
daysToKeep: cfg.StatsCleanDaysToKeep,
35+
}
36+
}
37+
38+
func RunCleaner(lc fx.Lifecycle, cleaner *cleaner) {
39+
ticker := lib_time.NewTicker(cleaner.cleanDelay)
40+
41+
lc.Append(fx.Hook{
42+
OnStart: func(context.Context) error {
43+
go func() {
44+
for range ticker.C {
45+
cleaner.logger.Info("Stats: cleaning stats older than 30 days")
46+
47+
if err := cleaner.statsManager.GetRepository().DeleteByFields(map[string]repository.FieldValue{
48+
"date": {
49+
Operator: "<=",
50+
Value: cleaner.clock.Now().AddDate(0, 0, -cleaner.daysToKeep),
51+
},
52+
}); err != nil {
53+
cleaner.logger.Error("Stats: unable to clean stats", err)
54+
}
55+
}
56+
}()
57+
58+
cleaner.logger.Info("Stats: cleaner started")
59+
60+
return nil
61+
},
62+
OnStop: func(_ context.Context) error {
63+
ticker.Stop()
64+
65+
cleaner.logger.Info("Stats: cleaner stopped")
66+
67+
return nil
68+
},
69+
})
70+
}

backend/internal/audit/fx.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package audit
2+
3+
import (
4+
"go.uber.org/fx"
5+
)
6+
7+
func FxModule() fx.Option {
8+
return fx.Module("audit",
9+
fx.Provide(
10+
NewCleaner,
11+
NewSubscriber,
12+
),
13+
fx.Invoke(
14+
RunCleaner,
15+
RunSubscriber,
16+
),
17+
)
18+
}

backend/internal/audit/subscriber.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package audit
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/eko/authz/backend/configs"
8+
"github.com/eko/authz/backend/internal/entity/manager"
9+
"github.com/eko/authz/backend/internal/entity/model"
10+
"github.com/eko/authz/backend/internal/event"
11+
"github.com/eko/authz/backend/internal/helper/spooler"
12+
"go.uber.org/fx"
13+
"golang.org/x/exp/slog"
14+
)
15+
16+
type subscriber struct {
17+
logger *slog.Logger
18+
dispatcher event.Dispatcher
19+
auditManager manager.Audit
20+
auditFlushDelay time.Duration
21+
}
22+
23+
func NewSubscriber(
24+
cfg *configs.App,
25+
logger *slog.Logger,
26+
dispatcher event.Dispatcher,
27+
auditManager manager.Audit,
28+
) *subscriber {
29+
return &subscriber{
30+
logger: logger,
31+
dispatcher: dispatcher,
32+
auditManager: auditManager,
33+
auditFlushDelay: cfg.AuditFlushDelay,
34+
}
35+
}
36+
37+
func (s *subscriber) subscribeToChecks(lc fx.Lifecycle) {
38+
checkEventChan := s.dispatcher.Subscribe(event.EventTypeCheck)
39+
40+
lc.Append(fx.Hook{
41+
OnStart: func(context.Context) error {
42+
go s.handleCheckEvents(checkEventChan)
43+
44+
s.logger.Info("Audit: subscribed to event dispatchers")
45+
46+
return nil
47+
},
48+
OnStop: func(_ context.Context) error {
49+
close(checkEventChan)
50+
51+
s.logger.Info("Audit: subscription to event dispatcher stopped")
52+
53+
return nil
54+
},
55+
})
56+
}
57+
58+
func (s *subscriber) handleCheckEvents(eventChan chan *event.Event) {
59+
var spooler = spooler.New(func(values []*event.Event) {
60+
if len(values) == 0 {
61+
return
62+
}
63+
64+
var audits = []*model.Audit{}
65+
var timestamp int64
66+
67+
for _, value := range values {
68+
timestamp = value.Timestamp
69+
70+
checkEvent, ok := value.Data.(*event.CheckEvent)
71+
if !ok {
72+
continue
73+
}
74+
75+
audit := &model.Audit{
76+
Date: time.Unix(timestamp, 0),
77+
Principal: checkEvent.Principal,
78+
ResourceKind: checkEvent.ResourceKind,
79+
ResourceValue: checkEvent.ResourceValue,
80+
Action: checkEvent.Action,
81+
IsAllowed: checkEvent.IsAllowed,
82+
}
83+
84+
if checkEvent.CompiledPilicy != nil {
85+
audit.PolicyID = checkEvent.CompiledPilicy.PolicyID
86+
}
87+
88+
audits = append(audits, audit)
89+
}
90+
91+
if err := s.auditManager.BatchAdd(audits); err != nil {
92+
s.logger.Error("Audit: unable to batch add audit events", err)
93+
}
94+
}, spooler.WithFlushInterval(s.auditFlushDelay))
95+
96+
for event := range eventChan {
97+
spooler.Add(event)
98+
}
99+
}
100+
101+
func RunSubscriber(lc fx.Lifecycle, subscriber *subscriber) {
102+
subscriber.subscribeToChecks(lc)
103+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package audit
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/eko/authz/backend/configs"
8+
"github.com/eko/authz/backend/internal/entity/manager"
9+
"github.com/eko/authz/backend/internal/event"
10+
"github.com/eko/authz/backend/internal/log"
11+
"github.com/golang/mock/gomock"
12+
"github.com/stretchr/testify/assert"
13+
"golang.org/x/exp/slog"
14+
)
15+
16+
func TestNewSubscriber(t *testing.T) {
17+
// Given
18+
ctrl := gomock.NewController(t)
19+
20+
cfg := &configs.App{
21+
AuditFlushDelay: 10 * time.Millisecond,
22+
}
23+
24+
logger := slog.New(log.NewNopHandler())
25+
26+
dispatcher := event.NewMockDispatcher(ctrl)
27+
28+
auditManager := manager.NewMockAudit(ctrl)
29+
30+
// When
31+
subscriberInstance := NewSubscriber(cfg, logger, dispatcher, auditManager)
32+
33+
// Then
34+
assert := assert.New(t)
35+
36+
assert.IsType(new(subscriber), subscriberInstance)
37+
38+
assert.Equal(logger, subscriberInstance.logger)
39+
assert.Equal(dispatcher, subscriberInstance.dispatcher)
40+
assert.Equal(auditManager, subscriberInstance.auditManager)
41+
assert.Equal(cfg.AuditFlushDelay, subscriberInstance.auditFlushDelay)
42+
}
43+
44+
func TestHandleCheckEvents(t *testing.T) {
45+
// Given
46+
ctrl := gomock.NewController(t)
47+
48+
cfg := &configs.App{
49+
AuditFlushDelay: 10 * time.Millisecond,
50+
}
51+
52+
logger := slog.New(log.NewNopHandler())
53+
54+
dispatcher := event.NewMockDispatcher(ctrl)
55+
56+
auditManager := manager.NewMockAudit(ctrl)
57+
auditManager.EXPECT().BatchAdd(gomock.Len(3)).Times(1)
58+
59+
subscriber := NewSubscriber(cfg, logger, dispatcher, auditManager)
60+
61+
eventChan := make(chan *event.Event, 1)
62+
63+
// When - Then
64+
go subscriber.handleCheckEvents(eventChan)
65+
66+
eventChan <- &event.Event{
67+
Timestamp: 123456,
68+
Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "1", Action: "edit", IsAllowed: true},
69+
}
70+
eventChan <- &event.Event{
71+
Timestamp: 123456,
72+
Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "2", Action: "edit", IsAllowed: false},
73+
}
74+
eventChan <- &event.Event{
75+
Timestamp: 123457,
76+
Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "3", Action: "delete", IsAllowed: true},
77+
}
78+
79+
close(eventChan)
80+
81+
// Wait 20ms to ensure the spool is triggered.
82+
<-time.After(20 * time.Millisecond)
83+
}

backend/internal/database/database.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func New(
5050
if cfg.Driver == configs.DriverSqlite {
5151
checkErr(slogLogger, db.AutoMigrate(model.Action{}))
5252
checkErr(slogLogger, db.AutoMigrate(model.Attribute{}))
53+
checkErr(slogLogger, db.AutoMigrate(model.Audit{}))
5354
checkErr(slogLogger, db.AutoMigrate(model.Client{}))
5455
checkErr(slogLogger, db.AutoMigrate(model.CompiledPolicy{}))
5556
checkErr(slogLogger, db.AutoMigrate(model.Policy{}))

backend/internal/entity/fx.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func FxModule() fx.Option {
1313
fx.Provide(
1414
manager.NewAction,
1515
manager.NewAttribute,
16+
manager.NewAudit,
1617
manager.NewClient,
1718
manager.NewCompiledPolicy,
1819
manager.NewPolicy,
@@ -40,6 +41,15 @@ func FxModule() fx.Option {
4041
return repository
4142
},
4243

44+
// Audit
45+
func(db *gorm.DB) repository.Base[model.Audit] {
46+
return repository.New[model.Audit](db)
47+
},
48+
49+
func(repository repository.Base[model.Audit]) manager.AuditRepository {
50+
return repository
51+
},
52+
4353
// Client
4454
func(db *gorm.DB) repository.Base[model.Client] {
4555
return repository.New[model.Client](db)

0 commit comments

Comments
 (0)