From 87a7540303b60c7c46de82d9ee05e32d8d7f86d8 Mon Sep 17 00:00:00 2001 From: Vincent Composieux Date: Mon, 16 Jan 2023 22:40:37 +0100 Subject: [PATCH] Added audit logs --- README.md | 7 +- backend/Makefile | 1 + backend/README.md | 9 +- backend/cmd/main.go | 2 + backend/configs/app.go | 6 + backend/internal/audit/cleaner.go | 70 +++++++ backend/internal/audit/fx.go | 18 ++ backend/internal/audit/subscriber.go | 103 +++++++++++ backend/internal/audit/subscriber_test.go | 83 +++++++++ backend/internal/database/database.go | 1 + backend/internal/entity/fx.go | 10 + backend/internal/entity/manager/audit.go | 34 ++++ backend/internal/entity/manager/audit_mock.go | 63 +++++++ backend/internal/entity/manager/compiled.go | 37 ++-- backend/internal/entity/model/audit.go | 18 ++ backend/internal/entity/model/model.go | 2 +- backend/internal/event/event.go | 11 ++ backend/internal/fixtures/initializer.go | 1 + backend/internal/grpc/handler/check.go | 4 - backend/internal/http/docs/docs.go | 173 +++++++++++++++++- backend/internal/http/docs/swagger.json | 172 ++++++++++++++++- backend/internal/http/docs/swagger.yaml | 112 +++++++++++- backend/internal/http/handler/action.go | 40 ++-- backend/internal/http/handler/audit.go | 47 +++++ backend/internal/http/handler/auth.go | 36 ++-- backend/internal/http/handler/check.go | 22 +-- backend/internal/http/handler/client.go | 74 ++++---- backend/internal/http/handler/handler.go | 3 + backend/internal/http/handler/policy.go | 92 +++++----- backend/internal/http/handler/principal.go | 92 +++++----- backend/internal/http/handler/resource.go | 92 +++++----- backend/internal/http/handler/role.go | 92 +++++----- backend/internal/http/handler/stats.go | 16 +- backend/internal/http/handler/user.go | 74 ++++---- backend/internal/http/routing.go | 17 +- backend/internal/stats/subscriber.go | 7 +- backend/internal/stats/subscriber_test.go | 15 +- backend/schema.mysql.sql | 22 ++- backend/schema.postgres.sql | 54 ++++++ docs/architecture/howitworks.dark.svg | 3 + docs/architecture/howitworks.drawio | 2 +- docs/architecture/howitworks.svg | 2 +- frontend/src/component/DataTable.css | 11 ++ frontend/src/component/DataTable.tsx | 2 + frontend/src/layout/UserMenu.test.tsx | 6 +- frontend/src/layout/UserMenu.tsx | 2 +- .../src/page/clients/component/columns.tsx | 4 +- .../page/dashboard/component/Dashboard.tsx | 169 +++++++++-------- .../src/page/dashboard/component/columns.tsx | 107 +++++++++++ .../src/page/policies/component/columns.tsx | 4 +- .../src/page/principals/component/columns.tsx | 4 +- .../src/page/resources/component/columns.tsx | 4 +- frontend/src/page/roles/component/columns.tsx | 4 +- frontend/src/page/users/component/columns.tsx | 4 +- frontend/src/service/model/audit.ts | 26 +++ frontend/src/service/model/model.ts | 11 ++ 56 files changed, 1643 insertions(+), 454 deletions(-) create mode 100644 backend/internal/audit/cleaner.go create mode 100644 backend/internal/audit/fx.go create mode 100644 backend/internal/audit/subscriber.go create mode 100644 backend/internal/audit/subscriber_test.go create mode 100644 backend/internal/entity/manager/audit.go create mode 100644 backend/internal/entity/manager/audit_mock.go create mode 100644 backend/internal/entity/model/audit.go create mode 100644 backend/internal/http/handler/audit.go create mode 100644 docs/architecture/howitworks.dark.svg create mode 100644 frontend/src/page/dashboard/component/columns.tsx create mode 100644 frontend/src/service/model/audit.ts diff --git a/README.md b/README.md index 8d65fabe..1dffdad1 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,18 @@ You can use both Role-Based Acccess Control (RBAC) and Attribute-Based Access Co ✅ Reliable: Authz uses Authz itself for managing its own internal authorizations +🔍 Audit: We log each check decisions and which policy matched + ## How it works? Authorization is simple: a `principal` wants to make an `action` on a `resource`. That's it. Authz allows you to manage all the authorizations you want to manage. All of them, centralized in a single application. -

Authz

+ + + Text changing depending on mode. Light: 'So light!' Dark: 'So dark!' + 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. diff --git a/backend/Makefile b/backend/Makefile index ffbcef41..b7d41bf6 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -50,6 +50,7 @@ test-mocks: ## Generate unit test mocks mockgen -source=internal/event/dispatcher.go -destination=internal/event/dispatcher_mock.go -package=event mockgen -source=internal/entity/manager/action.go -destination=internal/entity/manager/action_mock.go -package=manager mockgen -source=internal/entity/manager/attribute.go -destination=internal/entity/manager/attribute_mock.go -package=manager + mockgen -source=internal/entity/manager/audit.go -destination=internal/entity/manager/audit_mock.go -package=manager mockgen -source=internal/entity/manager/client.go -destination=internal/entity/manager/client_mock.go -package=manager mockgen -source=internal/entity/manager/compiled.go -destination=internal/entity/manager/compiled_mock.go -package=manager mockgen -source=internal/entity/manager/policy.go -destination=internal/entity/manager/policy_mock.go -package=manager diff --git a/backend/README.md b/backend/README.md index bf4fd675..18850e13 100644 --- a/backend/README.md +++ b/backend/README.md @@ -26,7 +26,12 @@ Here are the available configuration options available as environment variable: | Property | Default value | Description | | -------- | ------------- | ----------- | -| APP_STATS_FLUSH_DELAY | `5s` | Delay in which statistics will be batch into database | +| APP_AUDIT_CLEAN_DAYS_TO_KEEP | `7` | Audit logs number of days to keep in database | +| APP_AUDIT_CLEAN_DELAY | `1h` | Audit logs clean delay | +| APP_AUDIT_FLUSH_DELAY | `3s` | Delay in which audit logs will be batch into database | +| APP_STATS_CLEAN_DAYS_TO_KEEP | `30` | Statistics number of days to keep in database | +| APP_STATS_CLEAN_DELAY | `1h` | Statistics clean delay | +| APP_STATS_FLUSH_DELAY | `3s` | Delay in which statistics will be batch into database | | AUTH_ACCESS_TOKEN_DURATION | `6h` | Access token duration | | AUTH_DOMAIN | `http://localhost:8080` | OAuth domain to be used | | 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: | HTTP_SERVER_CORS_ALLOWED_METHODS | `GET,POST,PATCH,PUT,DELETE,HEAD,OPTIONS` | CORS allowed methods | | HTTP_SERVER_CORS_CACHE_MAX_AGE | `12h` | CORS cache max age value to be returned by server | | LOGGER_LEVEL | `INFO` | Log level, could be `DEBUG`, `INFO`, `WARN` or `ERROR` | -| APP_STATS_CLEAN_DAYS_TO_KEEP | `30` | Statistics number of days to keep in database | -| APP_STATS_CLEAN_DELAY | `1h` | Statistics clean delay | | USER_ADMIN_DEFAULT_PASSWORD | `changeme` | Default admin password updated on app launch | ## Tests diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 48a512ad..222d7357 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -4,6 +4,7 @@ import ( "context" "github.com/eko/authz/backend/configs" + "github.com/eko/authz/backend/internal/audit" "github.com/eko/authz/backend/internal/compile" "github.com/eko/authz/backend/internal/database" "github.com/eko/authz/backend/internal/entity" @@ -25,6 +26,7 @@ func main() { fx.Provide(context.Background), internal_fx.Logger, + audit.FxModule(), compile.FxModule(), configs.FxModule(), database.FxModule(), diff --git a/backend/configs/app.go b/backend/configs/app.go index d43193d9..c2523326 100644 --- a/backend/configs/app.go +++ b/backend/configs/app.go @@ -3,6 +3,9 @@ package configs import "time" type App struct { + AuditCleanDelay time.Duration `config:"app_audit_clean_delay"` + AuditCleanDaysToKeep int `config:"app_audit_clean_days_to_keep"` + AuditFlushDelay time.Duration `config:"app_audit_flush_delay"` DispatcherEventChannelSize int `config:"dispatcher_event_channel_size"` StatsCleanDelay time.Duration `config:"app_stats_clean_delay"` StatsCleanDaysToKeep int `config:"app_stats_clean_days_to_keep"` @@ -11,6 +14,9 @@ type App struct { func newApp() *App { return &App{ + AuditCleanDelay: 1 * time.Hour, + AuditCleanDaysToKeep: 7, + AuditFlushDelay: 3 * time.Second, DispatcherEventChannelSize: 10000, StatsCleanDelay: 1 * time.Hour, StatsCleanDaysToKeep: 30, diff --git a/backend/internal/audit/cleaner.go b/backend/internal/audit/cleaner.go new file mode 100644 index 00000000..0224526a --- /dev/null +++ b/backend/internal/audit/cleaner.go @@ -0,0 +1,70 @@ +package audit + +import ( + "context" + lib_time "time" + + "github.com/eko/authz/backend/configs" + "github.com/eko/authz/backend/internal/entity/manager" + "github.com/eko/authz/backend/internal/entity/repository" + "github.com/eko/authz/backend/internal/helper/time" + "go.uber.org/fx" + "golang.org/x/exp/slog" +) + +type cleaner struct { + logger *slog.Logger + clock time.Clock + statsManager manager.Stats + cleanDelay lib_time.Duration + daysToKeep int +} + +func NewCleaner( + cfg *configs.App, + logger *slog.Logger, + clock time.Clock, + statsManager manager.Stats, +) *cleaner { + return &cleaner{ + logger: logger, + clock: clock, + statsManager: statsManager, + cleanDelay: cfg.StatsCleanDelay, + daysToKeep: cfg.StatsCleanDaysToKeep, + } +} + +func RunCleaner(lc fx.Lifecycle, cleaner *cleaner) { + ticker := lib_time.NewTicker(cleaner.cleanDelay) + + lc.Append(fx.Hook{ + OnStart: func(context.Context) error { + go func() { + for range ticker.C { + cleaner.logger.Info("Stats: cleaning stats older than 30 days") + + if err := cleaner.statsManager.GetRepository().DeleteByFields(map[string]repository.FieldValue{ + "date": { + Operator: "<=", + Value: cleaner.clock.Now().AddDate(0, 0, -cleaner.daysToKeep), + }, + }); err != nil { + cleaner.logger.Error("Stats: unable to clean stats", err) + } + } + }() + + cleaner.logger.Info("Stats: cleaner started") + + return nil + }, + OnStop: func(_ context.Context) error { + ticker.Stop() + + cleaner.logger.Info("Stats: cleaner stopped") + + return nil + }, + }) +} diff --git a/backend/internal/audit/fx.go b/backend/internal/audit/fx.go new file mode 100644 index 00000000..6d72e1b0 --- /dev/null +++ b/backend/internal/audit/fx.go @@ -0,0 +1,18 @@ +package audit + +import ( + "go.uber.org/fx" +) + +func FxModule() fx.Option { + return fx.Module("audit", + fx.Provide( + NewCleaner, + NewSubscriber, + ), + fx.Invoke( + RunCleaner, + RunSubscriber, + ), + ) +} diff --git a/backend/internal/audit/subscriber.go b/backend/internal/audit/subscriber.go new file mode 100644 index 00000000..c48c078f --- /dev/null +++ b/backend/internal/audit/subscriber.go @@ -0,0 +1,103 @@ +package audit + +import ( + "context" + "time" + + "github.com/eko/authz/backend/configs" + "github.com/eko/authz/backend/internal/entity/manager" + "github.com/eko/authz/backend/internal/entity/model" + "github.com/eko/authz/backend/internal/event" + "github.com/eko/authz/backend/internal/helper/spooler" + "go.uber.org/fx" + "golang.org/x/exp/slog" +) + +type subscriber struct { + logger *slog.Logger + dispatcher event.Dispatcher + auditManager manager.Audit + auditFlushDelay time.Duration +} + +func NewSubscriber( + cfg *configs.App, + logger *slog.Logger, + dispatcher event.Dispatcher, + auditManager manager.Audit, +) *subscriber { + return &subscriber{ + logger: logger, + dispatcher: dispatcher, + auditManager: auditManager, + auditFlushDelay: cfg.AuditFlushDelay, + } +} + +func (s *subscriber) subscribeToChecks(lc fx.Lifecycle) { + checkEventChan := s.dispatcher.Subscribe(event.EventTypeCheck) + + lc.Append(fx.Hook{ + OnStart: func(context.Context) error { + go s.handleCheckEvents(checkEventChan) + + s.logger.Info("Audit: subscribed to event dispatchers") + + return nil + }, + OnStop: func(_ context.Context) error { + close(checkEventChan) + + s.logger.Info("Audit: subscription to event dispatcher stopped") + + return nil + }, + }) +} + +func (s *subscriber) handleCheckEvents(eventChan chan *event.Event) { + var spooler = spooler.New(func(values []*event.Event) { + if len(values) == 0 { + return + } + + var audits = []*model.Audit{} + var timestamp int64 + + for _, value := range values { + timestamp = value.Timestamp + + checkEvent, ok := value.Data.(*event.CheckEvent) + if !ok { + continue + } + + audit := &model.Audit{ + Date: time.Unix(timestamp, 0), + Principal: checkEvent.Principal, + ResourceKind: checkEvent.ResourceKind, + ResourceValue: checkEvent.ResourceValue, + Action: checkEvent.Action, + IsAllowed: checkEvent.IsAllowed, + } + + if checkEvent.CompiledPilicy != nil { + audit.PolicyID = checkEvent.CompiledPilicy.PolicyID + } + + audits = append(audits, audit) + } + + if err := s.auditManager.BatchAdd(audits); err != nil { + s.logger.Error("Audit: unable to batch add audit events", err) + } + }, spooler.WithFlushInterval(s.auditFlushDelay)) + + for event := range eventChan { + spooler.Add(event) + } +} + +func RunSubscriber(lc fx.Lifecycle, subscriber *subscriber) { + subscriber.subscribeToChecks(lc) +} diff --git a/backend/internal/audit/subscriber_test.go b/backend/internal/audit/subscriber_test.go new file mode 100644 index 00000000..a15c917f --- /dev/null +++ b/backend/internal/audit/subscriber_test.go @@ -0,0 +1,83 @@ +package audit + +import ( + "testing" + "time" + + "github.com/eko/authz/backend/configs" + "github.com/eko/authz/backend/internal/entity/manager" + "github.com/eko/authz/backend/internal/event" + "github.com/eko/authz/backend/internal/log" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slog" +) + +func TestNewSubscriber(t *testing.T) { + // Given + ctrl := gomock.NewController(t) + + cfg := &configs.App{ + AuditFlushDelay: 10 * time.Millisecond, + } + + logger := slog.New(log.NewNopHandler()) + + dispatcher := event.NewMockDispatcher(ctrl) + + auditManager := manager.NewMockAudit(ctrl) + + // When + subscriberInstance := NewSubscriber(cfg, logger, dispatcher, auditManager) + + // Then + assert := assert.New(t) + + assert.IsType(new(subscriber), subscriberInstance) + + assert.Equal(logger, subscriberInstance.logger) + assert.Equal(dispatcher, subscriberInstance.dispatcher) + assert.Equal(auditManager, subscriberInstance.auditManager) + assert.Equal(cfg.AuditFlushDelay, subscriberInstance.auditFlushDelay) +} + +func TestHandleCheckEvents(t *testing.T) { + // Given + ctrl := gomock.NewController(t) + + cfg := &configs.App{ + AuditFlushDelay: 10 * time.Millisecond, + } + + logger := slog.New(log.NewNopHandler()) + + dispatcher := event.NewMockDispatcher(ctrl) + + auditManager := manager.NewMockAudit(ctrl) + auditManager.EXPECT().BatchAdd(gomock.Len(3)).Times(1) + + subscriber := NewSubscriber(cfg, logger, dispatcher, auditManager) + + eventChan := make(chan *event.Event, 1) + + // When - Then + go subscriber.handleCheckEvents(eventChan) + + eventChan <- &event.Event{ + Timestamp: 123456, + Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "1", Action: "edit", IsAllowed: true}, + } + eventChan <- &event.Event{ + Timestamp: 123456, + Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "2", Action: "edit", IsAllowed: false}, + } + eventChan <- &event.Event{ + Timestamp: 123457, + Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "3", Action: "delete", IsAllowed: true}, + } + + close(eventChan) + + // Wait 20ms to ensure the spool is triggered. + <-time.After(20 * time.Millisecond) +} diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go index a5ee688c..e3cc50b3 100644 --- a/backend/internal/database/database.go +++ b/backend/internal/database/database.go @@ -50,6 +50,7 @@ func New( if cfg.Driver == configs.DriverSqlite { checkErr(slogLogger, db.AutoMigrate(model.Action{})) checkErr(slogLogger, db.AutoMigrate(model.Attribute{})) + checkErr(slogLogger, db.AutoMigrate(model.Audit{})) checkErr(slogLogger, db.AutoMigrate(model.Client{})) checkErr(slogLogger, db.AutoMigrate(model.CompiledPolicy{})) checkErr(slogLogger, db.AutoMigrate(model.Policy{})) diff --git a/backend/internal/entity/fx.go b/backend/internal/entity/fx.go index c74da18b..af57e00e 100644 --- a/backend/internal/entity/fx.go +++ b/backend/internal/entity/fx.go @@ -13,6 +13,7 @@ func FxModule() fx.Option { fx.Provide( manager.NewAction, manager.NewAttribute, + manager.NewAudit, manager.NewClient, manager.NewCompiledPolicy, manager.NewPolicy, @@ -40,6 +41,15 @@ func FxModule() fx.Option { return repository }, + // Audit + func(db *gorm.DB) repository.Base[model.Audit] { + return repository.New[model.Audit](db) + }, + + func(repository repository.Base[model.Audit]) manager.AuditRepository { + return repository + }, + // Client func(db *gorm.DB) repository.Base[model.Client] { return repository.New[model.Client](db) diff --git a/backend/internal/entity/manager/audit.go b/backend/internal/entity/manager/audit.go new file mode 100644 index 00000000..842b2a34 --- /dev/null +++ b/backend/internal/entity/manager/audit.go @@ -0,0 +1,34 @@ +package manager + +import ( + "github.com/eko/authz/backend/internal/entity/model" + "github.com/eko/authz/backend/internal/entity/repository" +) + +type AuditRepository repository.Base[model.Audit] + +type Audit interface { + BatchAdd(audits []*model.Audit) error + GetRepository() AuditRepository +} + +type auditManager struct { + repository AuditRepository +} + +// NewAudit initializes a new audit manager. +func NewAudit( + repository AuditRepository, +) Audit { + return &auditManager{ + repository: repository, + } +} + +func (m *auditManager) GetRepository() AuditRepository { + return m.repository +} + +func (m *auditManager) BatchAdd(audits []*model.Audit) error { + return m.repository.Create(audits...) +} diff --git a/backend/internal/entity/manager/audit_mock.go b/backend/internal/entity/manager/audit_mock.go new file mode 100644 index 00000000..a279a9b1 --- /dev/null +++ b/backend/internal/entity/manager/audit_mock.go @@ -0,0 +1,63 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/entity/manager/audit.go + +// Package manager is a generated GoMock package. +package manager + +import ( + reflect "reflect" + + model "github.com/eko/authz/backend/internal/entity/model" + gomock "github.com/golang/mock/gomock" +) + +// MockAudit is a mock of Audit interface. +type MockAudit struct { + ctrl *gomock.Controller + recorder *MockAuditMockRecorder +} + +// MockAuditMockRecorder is the mock recorder for MockAudit. +type MockAuditMockRecorder struct { + mock *MockAudit +} + +// NewMockAudit creates a new mock instance. +func NewMockAudit(ctrl *gomock.Controller) *MockAudit { + mock := &MockAudit{ctrl: ctrl} + mock.recorder = &MockAuditMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAudit) EXPECT() *MockAuditMockRecorder { + return m.recorder +} + +// BatchAdd mocks base method. +func (m *MockAudit) BatchAdd(audits []*model.Audit) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BatchAdd", audits) + ret0, _ := ret[0].(error) + return ret0 +} + +// BatchAdd indicates an expected call of BatchAdd. +func (mr *MockAuditMockRecorder) BatchAdd(audits interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchAdd", reflect.TypeOf((*MockAudit)(nil).BatchAdd), audits) +} + +// GetRepository mocks base method. +func (m *MockAudit) GetRepository() AuditRepository { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRepository") + ret0, _ := ret[0].(AuditRepository) + return ret0 +} + +// GetRepository indicates an expected call of GetRepository. +func (mr *MockAuditMockRecorder) GetRepository() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepository", reflect.TypeOf((*MockAudit)(nil).GetRepository)) +} diff --git a/backend/internal/entity/manager/compiled.go b/backend/internal/entity/manager/compiled.go index 37a4c954..bf8b001f 100644 --- a/backend/internal/entity/manager/compiled.go +++ b/backend/internal/entity/manager/compiled.go @@ -66,13 +66,13 @@ func (m *compiledPolicyManager) IsAllowed(principalID string, resourceKind strin } } - isAllowed, err := m.isPolicyAllowed(policyIDs, resourceKind, resourceValue, actionID) + isAllowed, compiledPolicy, err := m.isPolicyAllowed(policyIDs, resourceKind, resourceValue, actionID) if err != nil { return false, err } if !isAllowed { - isAllowed, err = m.isPrincipalAllowed(principalID, resourceKind, resourceValue, actionID) + isAllowed, compiledPolicy, err = m.isPrincipalAllowed(principalID, resourceKind, resourceValue, actionID) if err != nil { return false, err } @@ -87,10 +87,21 @@ func (m *compiledPolicyManager) IsAllowed(principalID string, resourceKind strin slog.Bool("result", isAllowed), ) - return isAllowed, err + if err := m.dispatcher.Dispatch(event.EventTypeCheck, &event.CheckEvent{ + Principal: principalID, + ResourceKind: resourceKind, + ResourceValue: resourceValue, + Action: actionID, + IsAllowed: isAllowed, + CompiledPilicy: compiledPolicy, + }); err != nil { + m.logger.Error("unable to dispatch check event", err) + } + + return isAllowed, nil } -func (m *compiledPolicyManager) isPolicyAllowed(policyIDs []string, resourceKind string, resourceValue string, actionID string) (bool, error) { +func (m *compiledPolicyManager) isPolicyAllowed(policyIDs []string, resourceKind string, resourceValue string, actionID string) (bool, *model.CompiledPolicy, error) { fields := map[string]repository.FieldValue{ "policy_id": {Operator: "IN", Value: policyIDs}, "resource_kind": {Operator: "=", Value: resourceKind}, @@ -98,21 +109,21 @@ func (m *compiledPolicyManager) isPolicyAllowed(policyIDs []string, resourceKind "action_id": {Operator: "=", Value: actionID}, } - _, err := m.repository.GetByFields(fields) + commiledPolicy, err := m.repository.GetByFields(fields) if errors.Is(err, gorm.ErrRecordNotFound) { if resourceValue != WildcardValue { return m.isPolicyAllowed(policyIDs, resourceKind, WildcardValue, actionID) } - return false, nil + return false, nil, nil } else if err != nil { - return false, fmt.Errorf("unable to retrieve compiled policies: %v", err) + return false, nil, fmt.Errorf("unable to retrieve compiled policies: %v", err) } - return true, nil + return true, commiledPolicy, nil } -func (m *compiledPolicyManager) isPrincipalAllowed(principalID string, resourceKind string, resourceValue string, actionID string) (bool, error) { +func (m *compiledPolicyManager) isPrincipalAllowed(principalID string, resourceKind string, resourceValue string, actionID string) (bool, *model.CompiledPolicy, error) { fields := map[string]repository.FieldValue{ "principal_id": {Operator: "=", Value: principalID}, "resource_kind": {Operator: "=", Value: resourceKind}, @@ -120,16 +131,16 @@ func (m *compiledPolicyManager) isPrincipalAllowed(principalID string, resourceK "action_id": {Operator: "=", Value: actionID}, } - _, err := m.repository.GetByFields(fields) + compiledPolicy, err := m.repository.GetByFields(fields) if errors.Is(err, gorm.ErrRecordNotFound) { if resourceValue != WildcardValue { return m.isPrincipalAllowed(principalID, resourceKind, WildcardValue, actionID) } - return false, nil + return false, nil, nil } else if err != nil { - return false, fmt.Errorf("unable to retrieve compiled policies: %v", err) + return false, nil, fmt.Errorf("unable to retrieve compiled policies: %v", err) } - return true, nil + return true, compiledPolicy, nil } diff --git a/backend/internal/entity/model/audit.go b/backend/internal/entity/model/audit.go new file mode 100644 index 00000000..7eaf7ccc --- /dev/null +++ b/backend/internal/entity/model/audit.go @@ -0,0 +1,18 @@ +package model + +import "time" + +type Audit struct { + ID int64 `json:"id" gorm:"primarykey;autoIncrement"` + Date time.Time `json:"date"` + Principal string `json:"principal"` + ResourceKind string `json:"resource_kind"` + ResourceValue string `json:"resource_value"` + Action string `json:"action"` + IsAllowed bool `json:"is_allowed"` + PolicyID string `json:"policy_id"` +} + +func (Audit) TableName() string { + return "authz_audit" +} diff --git a/backend/internal/entity/model/model.go b/backend/internal/entity/model/model.go index 907414cf..34343c59 100644 --- a/backend/internal/entity/model/model.go +++ b/backend/internal/entity/model/model.go @@ -2,5 +2,5 @@ package model // Models is a constraint interface that allows only authz library models. type Models interface { - Action | Attribute | Client | CompiledPolicy | Policy | Principal | Resource | Role | Stats | Token | User + Action | Audit | Attribute | Client | CompiledPolicy | Policy | Principal | Resource | Role | Stats | Token | User } diff --git a/backend/internal/event/event.go b/backend/internal/event/event.go index 2f381f5e..b0c00ab6 100644 --- a/backend/internal/event/event.go +++ b/backend/internal/event/event.go @@ -1,5 +1,7 @@ package event +import "github.com/eko/authz/backend/internal/entity/model" + type EventType string const ( @@ -13,3 +15,12 @@ type Event struct { Data any Timestamp int64 } + +type CheckEvent struct { + Principal string + ResourceKind string + ResourceValue string + Action string + IsAllowed bool + CompiledPilicy *model.CompiledPolicy +} diff --git a/backend/internal/fixtures/initializer.go b/backend/internal/fixtures/initializer.go index fc58f112..8818f279 100644 --- a/backend/internal/fixtures/initializer.go +++ b/backend/internal/fixtures/initializer.go @@ -19,6 +19,7 @@ const ( var ( resources = map[string][]string{ "actions": {"list", "get"}, + "audits": {"get"}, "clients": {"list", "get", "create", "delete"}, "policies": {"list", "get", "create", "update", "delete"}, "principals": {"list", "get", "create", "update", "delete"}, diff --git a/backend/internal/grpc/handler/check.go b/backend/internal/grpc/handler/check.go index 5a93f22a..5112e89e 100644 --- a/backend/internal/grpc/handler/check.go +++ b/backend/internal/grpc/handler/check.go @@ -42,10 +42,6 @@ func (h *check) Check(ctx context.Context, req *authz.CheckRequest) (*authz.Chec return nil, status.Error(codes.Internal, err.Error()) } - if err := h.dispatcher.Dispatch(event.EventTypeCheck, isAllowed); err != nil { - h.logger.Error("unable to dispatch check event", err) - } - checkAnswers[i] = &authz.CheckAnswer{ Principal: check.Principal, ResourceKind: check.ResourceKind, diff --git a/backend/internal/http/docs/docs.go b/backend/internal/http/docs/docs.go index f7cf5853..eb2799eb 100644 --- a/backend/internal/http/docs/docs.go +++ b/backend/internal/http/docs/docs.go @@ -123,6 +123,77 @@ const docTemplate = `{ } } }, + "/v1/audits": { + "get": { + "security": [ + { + "Authentication": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Check" + ], + "summary": "Retrieve audits for last days", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "page number", + "name": "page", + "in": "query" + }, + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "page size", + "name": "size", + "in": "query" + }, + { + "type": "string", + "example": "kind:contains:something", + "description": "filter on a field", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "example": "kind:desc", + "description": "sort field and order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Audit" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + } + } + } + }, "/v1/auth": { "post": { "security": [ @@ -1327,6 +1398,45 @@ const docTemplate = `{ } } }, + "/v1/stats": { + "get": { + "security": [ + { + "Authentication": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Check" + ], + "summary": "Retrieve statistics for last days", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Stats" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + } + } + } + }, "/v1/token": { "post": { "security": [ @@ -1572,9 +1682,7 @@ const docTemplate = `{ "key": { "type": "string" }, - "value": { - "type": "string" - } + "value": {} } }, "handler.AuthRequest": { @@ -1941,6 +2049,35 @@ const docTemplate = `{ } } }, + "model.Audit": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "date": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_allowed": { + "type": "boolean" + }, + "policy_id": { + "type": "string" + }, + "principal": { + "type": "string" + }, + "resource_kind": { + "type": "string" + }, + "resource_value": { + "type": "string" + } + } + }, "model.Client": { "type": "object", "properties": { @@ -2091,6 +2228,23 @@ const docTemplate = `{ } } }, + "model.Stats": { + "type": "object", + "properties": { + "checks_allowed_number": { + "type": "integer" + }, + "checks_denied_number": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, "model.User": { "type": "object", "properties": { @@ -2123,17 +2277,24 @@ const docTemplate = `{ } } } + }, + "securityDefinitions": { + "Authentication": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "", + Version: "1.0", Host: "", BasePath: "", Schemes: []string{}, - Title: "", - Description: "", + Title: "Authz API", + Description: "Authorization management HTTP APIs", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, } diff --git a/backend/internal/http/docs/swagger.json b/backend/internal/http/docs/swagger.json index 1471330d..0467b56d 100644 --- a/backend/internal/http/docs/swagger.json +++ b/backend/internal/http/docs/swagger.json @@ -1,7 +1,10 @@ { "swagger": "2.0", "info": { - "contact": {} + "description": "Authorization management HTTP APIs", + "title": "Authz API", + "contact": {}, + "version": "1.0" }, "paths": { "/v1/actions": { @@ -111,6 +114,77 @@ } } }, + "/v1/audits": { + "get": { + "security": [ + { + "Authentication": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Check" + ], + "summary": "Retrieve audits for last days", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "page number", + "name": "page", + "in": "query" + }, + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "page size", + "name": "size", + "in": "query" + }, + { + "type": "string", + "example": "kind:contains:something", + "description": "filter on a field", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "example": "kind:desc", + "description": "sort field and order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Audit" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + } + } + } + }, "/v1/auth": { "post": { "security": [ @@ -1315,6 +1389,45 @@ } } }, + "/v1/stats": { + "get": { + "security": [ + { + "Authentication": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Check" + ], + "summary": "Retrieve statistics for last days", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Stats" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.ErrorResponse" + } + } + } + } + }, "/v1/token": { "post": { "security": [ @@ -1560,9 +1673,7 @@ "key": { "type": "string" }, - "value": { - "type": "string" - } + "value": {} } }, "handler.AuthRequest": { @@ -1929,6 +2040,35 @@ } } }, + "model.Audit": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "date": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_allowed": { + "type": "boolean" + }, + "policy_id": { + "type": "string" + }, + "principal": { + "type": "string" + }, + "resource_kind": { + "type": "string" + }, + "resource_value": { + "type": "string" + } + } + }, "model.Client": { "type": "object", "properties": { @@ -2079,6 +2219,23 @@ } } }, + "model.Stats": { + "type": "object", + "properties": { + "checks_allowed_number": { + "type": "integer" + }, + "checks_denied_number": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, "model.User": { "type": "object", "properties": { @@ -2111,5 +2268,12 @@ } } } + }, + "securityDefinitions": { + "Authentication": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } } \ No newline at end of file diff --git a/backend/internal/http/docs/swagger.yaml b/backend/internal/http/docs/swagger.yaml index 4022a19f..28e6b621 100644 --- a/backend/internal/http/docs/swagger.yaml +++ b/backend/internal/http/docs/swagger.yaml @@ -3,8 +3,7 @@ definitions: properties: key: type: string - value: - type: string + value: {} required: - key - value @@ -251,6 +250,25 @@ definitions: value: type: string type: object + model.Audit: + properties: + action: + type: string + date: + type: string + id: + type: integer + is_allowed: + type: boolean + policy_id: + type: string + principal: + type: string + resource_kind: + type: string + resource_value: + type: string + type: object model.Client: properties: client_id: @@ -349,6 +367,17 @@ definitions: updated_at: type: string type: object + model.Stats: + properties: + checks_allowed_number: + type: integer + checks_denied_number: + type: integer + date: + type: string + id: + type: string + type: object model.User: properties: created_at: @@ -372,6 +401,9 @@ definitions: type: object info: contact: {} + description: Authorization management HTTP APIs + title: Authz API + version: "1.0" paths: /v1/actions: get: @@ -442,6 +474,53 @@ paths: summary: Retrieve an action tags: - Action + /v1/audits: + get: + parameters: + - description: page number + example: 1 + in: query + name: page + type: integer + - default: 100 + description: page size + in: query + maximum: 1000 + minimum: 1 + name: size + type: integer + - description: filter on a field + example: kind:contains:something + in: query + name: filter + type: string + - description: sort field and order + example: kind:desc + in: query + name: sort + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.Audit' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.ErrorResponse' + security: + - Authentication: [] + summary: Retrieve audits for last days + tags: + - Check /v1/auth: post: parameters: @@ -1202,6 +1281,30 @@ paths: summary: Updates a role tags: - Role + /v1/stats: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.Stats' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.ErrorResponse' + security: + - Authentication: [] + summary: Retrieve statistics for last days + tags: + - Check /v1/token: post: parameters: @@ -1349,4 +1452,9 @@ paths: summary: Retrieve a user tags: - User +securityDefinitions: + Authentication: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/backend/internal/http/handler/action.go b/backend/internal/http/handler/action.go index 269a3b50..560c3b10 100644 --- a/backend/internal/http/handler/action.go +++ b/backend/internal/http/handler/action.go @@ -14,18 +14,18 @@ import ( // Lists actions. // -// @security Authentication -// @Summary Lists actions -// @Tags Action -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(name:contains:something) -// @Param sort query string false "sort field and order" example(name:desc) -// @Success 200 {object} []model.Action -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/actions [Get] +// @security Authentication +// @Summary Lists actions +// @Tags Action +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(name:contains:something) +// @Param sort query string false "sort field and order" example(name:desc) +// @Success 200 {object} []model.Action +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/actions [Get] func ActionList( actionManager manager.Action, ) fiber.Handler { @@ -52,14 +52,14 @@ func ActionList( // Retrieve an action. // -// @security Authentication -// @Summary Retrieve an action -// @Tags Action -// @Produce json -// @Success 200 {object} model.Action -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/actions/{identifier} [Get] +// @security Authentication +// @Summary Retrieve an action +// @Tags Action +// @Produce json +// @Success 200 {object} model.Action +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/actions/{identifier} [Get] func ActionGet( actionManager manager.Action, ) fiber.Handler { diff --git a/backend/internal/http/handler/audit.go b/backend/internal/http/handler/audit.go new file mode 100644 index 00000000..a44f9a26 --- /dev/null +++ b/backend/internal/http/handler/audit.go @@ -0,0 +1,47 @@ +package handler + +import ( + "net/http" + + "github.com/eko/authz/backend/internal/entity/manager" + "github.com/eko/authz/backend/internal/entity/repository" + "github.com/eko/authz/backend/internal/http/handler/model" + "github.com/gofiber/fiber/v2" +) + +// Retrieve audits for last days +// +// @security Authentication +// @Summary Retrieve audits for last days +// @Tags Check +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(kind:contains:something) +// @Param sort query string false "sort field and order" example(kind:desc) +// @Success 200 {object} []model.Audit +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/audits [Get] +func AuditGet( + auditManager manager.Audit, +) fiber.Handler { + return func(c *fiber.Ctx) error { + page, size, err := paginate(c) + if err != nil { + return returnError(c, http.StatusInternalServerError, err) + } + + audits, total, err := auditManager.GetRepository().Find( + repository.WithPage(page), + repository.WithSize(size), + repository.WithFilter(httpFilterToORM(c)), + repository.WithSort(httpSortToORM(c)), + ) + if err != nil { + return returnError(c, http.StatusInternalServerError, err) + } + + return c.JSON(model.NewPaginated(audits, total, page, size)) + } +} diff --git a/backend/internal/http/handler/auth.go b/backend/internal/http/handler/auth.go index 8d504f52..c109dc86 100644 --- a/backend/internal/http/handler/auth.go +++ b/backend/internal/http/handler/auth.go @@ -44,15 +44,15 @@ type TokenResponse struct { // Authenticates a user // -// @security Authentication -// @Summary Authenticates a user -// @Tags Auth -// @Produce json -// @Param default body AuthRequest true "Authentication request" -// @Success 200 {object} AuthResponse -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/auth [Post] +// @security Authentication +// @Summary Authenticates a user +// @Tags Auth +// @Produce json +// @Param default body AuthRequest true "Authentication request" +// @Success 200 {object} AuthResponse +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/auth [Post] func Authenticate( validate *validator.Validate, userManager manager.User, @@ -101,15 +101,15 @@ func Authenticate( // Retrieve a client token // -// @security Authentication -// @Summary Retrieve a client token -// @Tags Auth -// @Produce json -// @Param default body TokenRequest true "Token request" -// @Success 200 {object} TokenResponse -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/token [Post] +// @security Authentication +// @Summary Retrieve a client token +// @Tags Auth +// @Produce json +// @Param default body TokenRequest true "Token request" +// @Success 200 {object} TokenResponse +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/token [Post] func TokenNew( server *server.Server, ) http.HandlerFunc { diff --git a/backend/internal/http/handler/check.go b/backend/internal/http/handler/check.go index 206d3c01..4a3acf1a 100644 --- a/backend/internal/http/handler/check.go +++ b/backend/internal/http/handler/check.go @@ -32,15 +32,15 @@ type CheckResponse struct { // Check if a principal has access to do action on resource. // -// @security Authentication -// @Summary Check if a principal has access to do action on resource -// @Tags Check -// @Produce json -// @Param default body CheckRequest true "Check request" -// @Success 200 {object} CheckResponse -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/check [Post] +// @security Authentication +// @Summary Check if a principal has access to do action on resource +// @Tags Check +// @Produce json +// @Param default body CheckRequest true "Check request" +// @Success 200 {object} CheckResponse +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/check [Post] func Check( logger *slog.Logger, validate *validator.Validate, @@ -69,10 +69,6 @@ func Check( return returnError(c, http.StatusInternalServerError, err) } - if err := dispatcher.Dispatch(event.EventTypeCheck, isAllowed); err != nil { - logger.Error("unable to dispatch check event", err) - } - responseChecks[i] = &CheckResponseQuery{ CheckRequestQuery: check, IsAllowed: isAllowed, diff --git a/backend/internal/http/handler/client.go b/backend/internal/http/handler/client.go index 164a4c21..d363f4dc 100644 --- a/backend/internal/http/handler/client.go +++ b/backend/internal/http/handler/client.go @@ -20,15 +20,15 @@ type ClientCreateRequest struct { // Creates a new client // -// @security Authentication -// @Summary Creates a new client -// @Tags Client -// @Produce json -// @Param default body ClientCreateRequest true "Client creation request" -// @Success 200 {object} model.Client -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/clients [Post] +// @security Authentication +// @Summary Creates a new client +// @Tags Client +// @Produce json +// @Param default body ClientCreateRequest true "Client creation request" +// @Success 200 {object} model.Client +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/clients [Post] func ClientCreate( validate *validator.Validate, clientManager manager.Client, @@ -58,18 +58,18 @@ func ClientCreate( // Lists clients. // -// @security Authentication -// @Summary Lists clients -// @Tags Client -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(name:contains:something) -// @Param sort query string false "sort field and order" example(name:desc) -// @Success 200 {object} []model.Client -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/clients [Get] +// @security Authentication +// @Summary Lists clients +// @Tags Client +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(name:contains:something) +// @Param sort query string false "sort field and order" example(name:desc) +// @Success 200 {object} []model.Client +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/clients [Get] func ClientList( clientManager manager.Client, ) fiber.Handler { @@ -96,14 +96,14 @@ func ClientList( // Retrieve a client. // -// @security Authentication -// @Summary Retrieve a client -// @Tags Client -// @Produce json -// @Success 200 {object} model.Client -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/clients/{identifier} [Get] +// @security Authentication +// @Summary Retrieve a client +// @Tags Client +// @Produce json +// @Success 200 {object} model.Client +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/clients/{identifier} [Get] func ClientGet( clientManager manager.Client, ) fiber.Handler { @@ -130,14 +130,14 @@ func ClientGet( // Deletes a client. // -// @security Authentication -// @Summary Deletes a client -// @Tags Client -// @Produce json -// @Success 200 {object} model.Client -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/clients/{identifier} [Delete] +// @security Authentication +// @Summary Deletes a client +// @Tags Client +// @Produce json +// @Success 200 {object} model.Client +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/clients/{identifier} [Delete] func ClientDelete( clientManager manager.Client, ) fiber.Handler { diff --git a/backend/internal/http/handler/handler.go b/backend/internal/http/handler/handler.go index dcdee404..c0e1d39f 100644 --- a/backend/internal/http/handler/handler.go +++ b/backend/internal/http/handler/handler.go @@ -15,6 +15,7 @@ import ( const ( ActionGetKey = "action-get" ActionListKey = "action-list" + AuditGetKey = "audit-get" AuthAuthenticateKey = "auth-authenticate" AuthTokenNewKey = "auth-token-new" CheckKey = "check" @@ -63,6 +64,7 @@ func NewHandlers( tokenManager jwt.Manager, dispatcher event.Dispatcher, actionManager manager.Action, + auditManager manager.Audit, clientManager manager.Client, compiledManager manager.CompiledPolicy, policyManager manager.Policy, @@ -76,6 +78,7 @@ func NewHandlers( return Handlers{ ActionGetKey: ActionGet(actionManager), ActionListKey: ActionList(actionManager), + AuditGetKey: AuditGet(auditManager), AuthAuthenticateKey: Authenticate(validate, userManager, tokenManager), AuthTokenNewKey: adaptor.HTTPHandlerFunc(TokenNew(oauthServer)), CheckKey: Check(logger, validate, compiledManager, dispatcher), diff --git a/backend/internal/http/handler/policy.go b/backend/internal/http/handler/policy.go index 12096b1a..35b3da57 100644 --- a/backend/internal/http/handler/policy.go +++ b/backend/internal/http/handler/policy.go @@ -28,15 +28,15 @@ type UpdatePolicyRequest struct { // Creates a new policy. // -// @security Authentication -// @Summary Creates a new policy -// @Tags Policy -// @Produce json -// @Param default body CreatePolicyRequest true "Policy creation request" -// @Success 200 {object} model.Policy -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/policies [Post] +// @security Authentication +// @Summary Creates a new policy +// @Tags Policy +// @Produce json +// @Param default body CreatePolicyRequest true "Policy creation request" +// @Success 200 {object} model.Policy +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/policies [Post] func PolicyCreate( validate *validator.Validate, policyManager manager.Policy, @@ -71,18 +71,18 @@ func PolicyCreate( // Lists policies. // -// @security Authentication -// @Summary Lists policies -// @Tags Policy -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(kind:contains:something) -// @Param sort query string false "sort field and order" example(kind:desc) -// @Success 200 {object} []model.Policy -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/policies [Get] +// @security Authentication +// @Summary Lists policies +// @Tags Policy +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(kind:contains:something) +// @Param sort query string false "sort field and order" example(kind:desc) +// @Success 200 {object} []model.Policy +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/policies [Get] func PolicyList( policyManager manager.Policy, ) fiber.Handler { @@ -110,14 +110,14 @@ func PolicyList( // Retrieve a policy. // -// @security Authentication -// @Summary Retrieve a policy -// @Tags Policy -// @Produce json -// @Success 200 {object} model.Policy -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/policies/{identifier} [Get] +// @security Authentication +// @Summary Retrieve a policy +// @Tags Policy +// @Produce json +// @Success 200 {object} model.Policy +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/policies/{identifier} [Get] func PolicyGet( policyManager manager.Policy, ) fiber.Handler { @@ -147,15 +147,15 @@ func PolicyGet( // Updates a policy. // -// @security Authentication -// @Summary Updates a policy -// @Tags Policy -// @Produce json -// @Param default body UpdatePolicyRequest true "Policy update request" -// @Success 200 {object} model.Policy -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/policies/{identifier} [Put] +// @security Authentication +// @Summary Updates a policy +// @Tags Policy +// @Produce json +// @Param default body UpdatePolicyRequest true "Policy update request" +// @Success 200 {object} model.Policy +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/policies/{identifier} [Put] func PolicyUpdate( validate *validator.Validate, policyManager manager.Policy, @@ -195,14 +195,14 @@ func PolicyUpdate( // Deletes a policy. // -// @security Authentication -// @Summary Deletes a policy -// @Tags Policy -// @Produce json -// @Success 200 {object} model.Policy -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/policies/{identifier} [Delete] +// @security Authentication +// @Summary Deletes a policy +// @Tags Policy +// @Produce json +// @Success 200 {object} model.Policy +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/policies/{identifier} [Delete] func PolicyDelete( policyManager manager.Policy, ) fiber.Handler { diff --git a/backend/internal/http/handler/principal.go b/backend/internal/http/handler/principal.go index 76f040d9..6557923b 100644 --- a/backend/internal/http/handler/principal.go +++ b/backend/internal/http/handler/principal.go @@ -26,15 +26,15 @@ type UpdatePrincipalRequest struct { // Creates a new principal. // -// @security Authentication -// @Summary Creates a new principal -// @Tags Principal -// @Produce json -// @Param default body CreatePrincipalRequest true "Principal creation request" -// @Success 200 {object} model.Principal -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/principals [Post] +// @security Authentication +// @Summary Creates a new principal +// @Tags Principal +// @Produce json +// @Param default body CreatePrincipalRequest true "Principal creation request" +// @Success 200 {object} model.Principal +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/principals [Post] func PrincipalCreate( validate *validator.Validate, principalManager manager.Principal, @@ -64,18 +64,18 @@ func PrincipalCreate( // Lists principals. // -// @security Authentication -// @Summary Lists principals -// @Tags Principal -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(name:contains:something) -// @Param sort query string false "sort field and order" example(name:desc) -// @Success 200 {object} []model.Principal -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/principals [Get] +// @security Authentication +// @Summary Lists principals +// @Tags Principal +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(name:contains:something) +// @Param sort query string false "sort field and order" example(name:desc) +// @Success 200 {object} []model.Principal +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/principals [Get] func PrincipalList( principalManager manager.Principal, ) fiber.Handler { @@ -103,14 +103,14 @@ func PrincipalList( // Retrieve a principal. // -// @security Authentication -// @Summary Retrieve a principal -// @Tags Principal -// @Produce json -// @Success 200 {object} model.Principal -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/principals/{identifier} [Get] +// @security Authentication +// @Summary Retrieve a principal +// @Tags Principal +// @Produce json +// @Success 200 {object} model.Principal +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/principals/{identifier} [Get] func PrincipalGet( principalManager manager.Principal, ) fiber.Handler { @@ -140,15 +140,15 @@ func PrincipalGet( // Updates a principal. // -// @security Authentication -// @Summary Updates a principal -// @Tags Principal -// @Produce json -// @Param default body UpdatePrincipalRequest true "Principal update request" -// @Success 200 {object} model.Principal -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/principals/{identifier} [Put] +// @security Authentication +// @Summary Updates a principal +// @Tags Principal +// @Produce json +// @Param default body UpdatePrincipalRequest true "Principal update request" +// @Success 200 {object} model.Principal +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/principals/{identifier} [Put] func PrincipalUpdate( validate *validator.Validate, principalManager manager.Principal, @@ -183,14 +183,14 @@ func PrincipalUpdate( // Deletes a principal. // -// @security Authentication -// @Summary Deletes a principal -// @Tags Principal -// @Produce json -// @Success 200 {object} model.Principal -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/principals/{identifier} [Delete] +// @security Authentication +// @Summary Deletes a principal +// @Tags Principal +// @Produce json +// @Success 200 {object} model.Principal +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/principals/{identifier} [Delete] func PrincipalDelete( principalManager manager.Principal, ) fiber.Handler { diff --git a/backend/internal/http/handler/resource.go b/backend/internal/http/handler/resource.go index 9454c9ab..a1634647 100644 --- a/backend/internal/http/handler/resource.go +++ b/backend/internal/http/handler/resource.go @@ -47,15 +47,15 @@ type UpdateResourceRequest struct { // Creates a new resource. // -// @security Authentication -// @Summary Creates a new resource -// @Tags Resource -// @Produce json -// @Param default body CreateResourceRequest true "Resource creation request" -// @Success 200 {object} model.Resource -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/resources [Post] +// @security Authentication +// @Summary Creates a new resource +// @Tags Resource +// @Produce json +// @Param default body CreateResourceRequest true "Resource creation request" +// @Success 200 {object} model.Resource +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/resources [Post] func ResourceCreate( validate *validator.Validate, resourceManager manager.Resource, @@ -86,18 +86,18 @@ func ResourceCreate( // Lists resources. // -// @security Authentication -// @Summary Lists resources -// @Tags Resource -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(kind:contains:something) -// @Param sort query string false "sort field and order" example(kind:desc) -// @Success 200 {object} []model.Resource -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/resources [Get] +// @security Authentication +// @Summary Lists resources +// @Tags Resource +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(kind:contains:something) +// @Param sort query string false "sort field and order" example(kind:desc) +// @Success 200 {object} []model.Resource +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/resources [Get] func ResourceList( resourceManager manager.Resource, ) fiber.Handler { @@ -125,14 +125,14 @@ func ResourceList( // Retrieve a resource. // -// @security Authentication -// @Summary Retrieve a resource -// @Tags Resource -// @Produce json -// @Success 200 {object} model.Resource -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/resources/{identifier} [Get] +// @security Authentication +// @Summary Retrieve a resource +// @Tags Resource +// @Produce json +// @Success 200 {object} model.Resource +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/resources/{identifier} [Get] func ResourceGet( resourceManager manager.Resource, ) fiber.Handler { @@ -162,15 +162,15 @@ func ResourceGet( // Updates a resource. // -// @security Authentication -// @Summary Updates a resource -// @Tags Resource -// @Produce json -// @Param default body UpdateResourceRequest true "Resource update request" -// @Success 200 {object} model.Resource -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/resources/{identifier} [Put] +// @security Authentication +// @Summary Updates a resource +// @Tags Resource +// @Produce json +// @Param default body UpdateResourceRequest true "Resource update request" +// @Success 200 {object} model.Resource +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/resources/{identifier} [Put] func ResourceUpdate( validate *validator.Validate, resourceManager manager.Resource, @@ -205,14 +205,14 @@ func ResourceUpdate( // Deletes a resource. // -// @security Authentication -// @Summary Deletes a resource -// @Tags Resource -// @Produce json -// @Success 200 {object} model.Resource -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/resources/{identifier} [Delete] +// @security Authentication +// @Summary Deletes a resource +// @Tags Resource +// @Produce json +// @Success 200 {object} model.Resource +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/resources/{identifier} [Delete] func ResourceDelete( resourceManager manager.Resource, ) fiber.Handler { diff --git a/backend/internal/http/handler/role.go b/backend/internal/http/handler/role.go index ddec294c..0b971464 100644 --- a/backend/internal/http/handler/role.go +++ b/backend/internal/http/handler/role.go @@ -24,15 +24,15 @@ type UpdateRoleRequest struct { // Creates a new role. // -// @security Authentication -// @Summary Creates a new role -// @Tags Role -// @Produce json -// @Param default body CreateRoleRequest true "Role creation request" -// @Success 200 {object} model.Role -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/roles [Post] +// @security Authentication +// @Summary Creates a new role +// @Tags Role +// @Produce json +// @Param default body CreateRoleRequest true "Role creation request" +// @Success 200 {object} model.Role +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/roles [Post] func RoleCreate( validate *validator.Validate, roleManager manager.Role, @@ -63,18 +63,18 @@ func RoleCreate( // Lists roles. // -// @security Authentication -// @Summary Lists roles -// @Tags Role -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(kind:contains:something) -// @Param sort query string false "sort field and order" example(kind:desc) -// @Success 200 {object} []model.Role -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/roles [Get] +// @security Authentication +// @Summary Lists roles +// @Tags Role +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(kind:contains:something) +// @Param sort query string false "sort field and order" example(kind:desc) +// @Success 200 {object} []model.Role +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/roles [Get] func RoleList( roleManager manager.Role, ) fiber.Handler { @@ -102,14 +102,14 @@ func RoleList( // Retrieve a role. // -// @security Authentication -// @Summary Retrieve a role -// @Tags Role -// @Produce json -// @Success 200 {object} model.Role -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/roles/{identifier} [Get] +// @security Authentication +// @Summary Retrieve a role +// @Tags Role +// @Produce json +// @Success 200 {object} model.Role +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/roles/{identifier} [Get] func RoleGet( roleManager manager.Role, ) fiber.Handler { @@ -139,15 +139,15 @@ func RoleGet( // Updates a role. // -// @security Authentication -// @Summary Updates a role -// @Tags Role -// @Produce json -// @Param default body UpdateRoleRequest true "Role update request" -// @Success 200 {object} model.Role -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/roles/{identifier} [Put] +// @security Authentication +// @Summary Updates a role +// @Tags Role +// @Produce json +// @Param default body UpdateRoleRequest true "Role update request" +// @Success 200 {object} model.Role +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/roles/{identifier} [Put] func RoleUpdate( validate *validator.Validate, roleManager manager.Role, @@ -182,14 +182,14 @@ func RoleUpdate( // Deletes a role. // -// @security Authentication -// @Summary Deletes a role -// @Tags Role -// @Produce json -// @Success 200 {object} model.Role -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/roles/{identifier} [Delete] +// @security Authentication +// @Summary Deletes a role +// @Tags Role +// @Produce json +// @Success 200 {object} model.Role +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/roles/{identifier} [Delete] func RoleDelete( roleManager manager.Role, ) fiber.Handler { diff --git a/backend/internal/http/handler/stats.go b/backend/internal/http/handler/stats.go index c1e1eb95..61520e21 100644 --- a/backend/internal/http/handler/stats.go +++ b/backend/internal/http/handler/stats.go @@ -10,14 +10,14 @@ import ( // Retrieve statistics for last days // -// @security Authentication -// @Summary Retrieve statistics for last days -// @Tags Check -// @Produce json -// @Success 200 {object} []model.Stats -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/stats [Get] +// @security Authentication +// @Summary Retrieve statistics for last days +// @Tags Check +// @Produce json +// @Success 200 {object} []model.Stats +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/stats [Get] func StatsGet( statsManager manager.Stats, ) fiber.Handler { diff --git a/backend/internal/http/handler/user.go b/backend/internal/http/handler/user.go index 37ddb15b..632cd98d 100644 --- a/backend/internal/http/handler/user.go +++ b/backend/internal/http/handler/user.go @@ -19,15 +19,15 @@ type UserCreateRequest struct { // Creates a new user // -// @security Authentication -// @Summary Creates a new user -// @Tags User -// @Produce json -// @Param default body UserCreateRequest true "User creation request" -// @Success 200 {object} model.User -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/users [Post] +// @security Authentication +// @Summary Creates a new user +// @Tags User +// @Produce json +// @Param default body UserCreateRequest true "User creation request" +// @Success 200 {object} model.User +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/users [Post] func UserCreate( validate *validator.Validate, userManager manager.User, @@ -56,18 +56,18 @@ func UserCreate( // Lists users. // -// @security Authentication -// @Summary Lists users -// @Tags User -// @Produce json -// @Param page query int false "page number" example(1) -// @Param size query int false "page size" minimum(1) maximum(1000) default(100) -// @Param filter query string false "filter on a field" example(name:contains:something) -// @Param sort query string false "sort field and order" example(name:desc) -// @Success 200 {object} []model.User -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/users [Get] +// @security Authentication +// @Summary Lists users +// @Tags User +// @Produce json +// @Param page query int false "page number" example(1) +// @Param size query int false "page size" minimum(1) maximum(1000) default(100) +// @Param filter query string false "filter on a field" example(name:contains:something) +// @Param sort query string false "sort field and order" example(name:desc) +// @Success 200 {object} []model.User +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/users [Get] func UserList( userManager manager.User, ) fiber.Handler { @@ -94,14 +94,14 @@ func UserList( // Retrieve a user. // -// @security Authentication -// @Summary Retrieve a user -// @Tags User -// @Produce json -// @Success 200 {object} model.User -// @Failure 404 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/users/{identifier} [Get] +// @security Authentication +// @Summary Retrieve a user +// @Tags User +// @Produce json +// @Success 200 {object} model.User +// @Failure 404 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/users/{identifier} [Get] func UserGet( userManager manager.User, ) fiber.Handler { @@ -130,14 +130,14 @@ func UserGet( // Deletes a user. // -// @security Authentication -// @Summary Deletes a user -// @Tags User -// @Produce json -// @Success 200 {object} model.User -// @Failure 400 {object} model.ErrorResponse -// @Failure 500 {object} model.ErrorResponse -// @Router /v1/users/{identifier} [Delete] +// @security Authentication +// @Summary Deletes a user +// @Tags User +// @Produce json +// @Success 200 {object} model.User +// @Failure 400 {object} model.ErrorResponse +// @Failure 500 {object} model.ErrorResponse +// @Router /v1/users/{identifier} [Delete] func UserDelete( userManager manager.User, ) fiber.Handler { diff --git a/backend/internal/http/routing.go b/backend/internal/http/routing.go index 2f309d0f..1b6b4dae 100644 --- a/backend/internal/http/routing.go +++ b/backend/internal/http/routing.go @@ -16,13 +16,13 @@ func (s *Server) setSwagger() { s.app.Get("/swagger/*", swagger.HandlerDefault) } -// @title Authz API -// @version 1.0 -// @description Authorization management HTTP APIs -// @securitydefinitions.apikey Authentication -// @in header -// @name Authorization -// @BasePath /v1 +// @title Authz API +// @version 1.0 +// @description Authorization management HTTP APIs +// @securitydefinitions.apikey Authentication +// @in header +// @name Authorization +// @BasePath /v1 func (s *Server) setRoutes() { s.app.Use( cors.New(cors.Config{ @@ -47,6 +47,9 @@ func (s *Server) setRoutes() { actions.Get("", s.authorized("authz.actions", "list", s.handlers.Get(handler.ActionListKey))...) actions.Get("/:identifier", s.authorized("authz.actions", "get", s.handlers.Get(handler.ActionGetKey))...) + audits := authenticated.Group("/audits") + audits.Get("", s.authorized("authz.audits", "get", s.handlers.Get(handler.AuditGetKey))...) + clients := authenticated.Group("/clients") clients.Post("", s.authorized("authz.clients", "create", s.handlers.Get(handler.ClientCreateKey))...) clients.Get("", s.authorized("authz.clients", "list", s.handlers.Get(handler.ClientListKey))...) diff --git a/backend/internal/stats/subscriber.go b/backend/internal/stats/subscriber.go index ad2e5bcc..eef09cb9 100644 --- a/backend/internal/stats/subscriber.go +++ b/backend/internal/stats/subscriber.go @@ -66,7 +66,12 @@ func (s *subscriber) handleCheckEvents(eventChan chan *event.Event) { for _, value := range values { timestamp = value.Timestamp - if value.Data.(bool) { + checkEvent, ok := value.Data.(*event.CheckEvent) + if !ok { + continue + } + + if checkEvent.IsAllowed { allowed++ } else { denied++ diff --git a/backend/internal/stats/subscriber_test.go b/backend/internal/stats/subscriber_test.go index 21618dab..d17fbd6a 100644 --- a/backend/internal/stats/subscriber_test.go +++ b/backend/internal/stats/subscriber_test.go @@ -63,9 +63,18 @@ func TestHandleCheckEvents(t *testing.T) { // When - Then go subscriber.handleCheckEvents(eventChan) - eventChan <- &event.Event{Timestamp: 123456, Data: true} - eventChan <- &event.Event{Timestamp: 123456, Data: false} - eventChan <- &event.Event{Timestamp: 123457, Data: true} + eventChan <- &event.Event{ + Timestamp: 123456, + Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "1", Action: "edit", IsAllowed: true}, + } + eventChan <- &event.Event{ + Timestamp: 123456, + Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "2", Action: "edit", IsAllowed: false}, + } + eventChan <- &event.Event{ + Timestamp: 123457, + Data: &event.CheckEvent{Principal: "user1", ResourceKind: "post", ResourceValue: "3", Action: "delete", IsAllowed: true}, + } close(eventChan) diff --git a/backend/schema.mysql.sql b/backend/schema.mysql.sql index 4ba596b2..a7f45bd1 100644 --- a/backend/schema.mysql.sql +++ b/backend/schema.mysql.sql @@ -45,6 +45,26 @@ CREATE TABLE `authz_attributes` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `authz_audit` +-- + +DROP TABLE IF EXISTS `authz_audit`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `authz_audit` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `date` datetime(3) DEFAULT NULL, + `principal` longtext, + `resource_kind` longtext, + `resource_value` longtext, + `action` longtext, + `is_allowed` tinyint(1) DEFAULT NULL, + `policy_id` longtext, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `authz_clients` -- @@ -317,4 +337,4 @@ CREATE TABLE `authz_users` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2023-01-15 12:37:11 +-- Dump completed on 2023-01-16 19:38:26 diff --git a/backend/schema.postgres.sql b/backend/schema.postgres.sql index a99d01e1..6e3b65ed 100644 --- a/backend/schema.postgres.sql +++ b/backend/schema.postgres.sql @@ -67,6 +67,45 @@ ALTER TABLE public.authz_attributes_id_seq OWNER TO root; ALTER SEQUENCE public.authz_attributes_id_seq OWNED BY public.authz_attributes.id; +-- +-- Name: authz_audit; Type: TABLE; Schema: public; Owner: root +-- + +CREATE TABLE public.authz_audit ( + id bigint NOT NULL, + date timestamp with time zone, + principal text, + resource_kind text, + resource_value text, + action text, + is_allowed boolean, + policy_id text +); + + +ALTER TABLE public.authz_audit OWNER TO root; + +-- +-- Name: authz_audit_id_seq; Type: SEQUENCE; Schema: public; Owner: root +-- + +CREATE SEQUENCE public.authz_audit_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.authz_audit_id_seq OWNER TO root; + +-- +-- Name: authz_audit_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: root +-- + +ALTER SEQUENCE public.authz_audit_id_seq OWNED BY public.authz_audit.id; + + -- -- Name: authz_clients; Type: TABLE; Schema: public; Owner: root -- @@ -305,6 +344,13 @@ ALTER TABLE public.authz_users OWNER TO root; ALTER TABLE ONLY public.authz_attributes ALTER COLUMN id SET DEFAULT nextval('public.authz_attributes_id_seq'::regclass); +-- +-- Name: authz_audit id; Type: DEFAULT; Schema: public; Owner: root +-- + +ALTER TABLE ONLY public.authz_audit ALTER COLUMN id SET DEFAULT nextval('public.authz_audit_id_seq'::regclass); + + -- -- Name: authz_oauth_tokens id; Type: DEFAULT; Schema: public; Owner: root -- @@ -328,6 +374,14 @@ ALTER TABLE ONLY public.authz_attributes ADD CONSTRAINT authz_attributes_pkey PRIMARY KEY (id); +-- +-- Name: authz_audit authz_audit_pkey; Type: CONSTRAINT; Schema: public; Owner: root +-- + +ALTER TABLE ONLY public.authz_audit + ADD CONSTRAINT authz_audit_pkey PRIMARY KEY (id); + + -- -- Name: authz_clients authz_clients_pkey; Type: CONSTRAINT; Schema: public; Owner: root -- diff --git a/docs/architecture/howitworks.dark.svg b/docs/architecture/howitworks.dark.svg new file mode 100644 index 00000000..7c1c5499 --- /dev/null +++ b/docs/architecture/howitworks.dark.svg @@ -0,0 +1,3 @@ + + +
Your application(s)


Your application(s)...
SDK
SDK
DB
DB
Services
Services
Manage resources
Manage resources
Manage principals
Manage principals
Policies
DB
Policies...
Check authorizations
Check authorizations
user-1 / post.1 / delete
user-1 / post.1 / delet...
user-1 / post.1 / edit
user-1 / post.1 / edit
Match
principals with resources
Match...
WEB UI
WEB UI
ALLOWED
ALLOWED
DENIED
DENIED
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/architecture/howitworks.drawio b/docs/architecture/howitworks.drawio index 9349df96..138ed98c 100644 --- a/docs/architecture/howitworks.drawio +++ b/docs/architecture/howitworks.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/docs/architecture/howitworks.svg b/docs/architecture/howitworks.svg index 749088b7..1432911d 100644 --- a/docs/architecture/howitworks.svg +++ b/docs/architecture/howitworks.svg @@ -1,3 +1,3 @@ -
Your application(s)


Your application(s)...
SDK
SDK
DB
DB
Services
Services
Manage resources
Manage resources
Manage principals
Manage principals
Policies
DB
Policies...
Check authorizations
Check authorizations
user-1 / post.1 / delete
user-1 / post.1 / delet...
user-1 / post.1 / edit
user-1 / post.1 / edit
Match
principals with resources
Match...
WEB UI
WEB UI
Text is not SVG - cannot display
\ No newline at end of file +
Your application(s)


Your application(s)...
SDK
SDK
DB
DB
Services
Services
Manage resources
Manage resources
Manage principals
Manage principals
Policies
DB
Policies...
Check authorizations
Check authorizations
user-1 / post.1 / delete
user-1 / post.1 / delet...
user-1 / post.1 / edit
user-1 / post.1 / edit
Match
principals with resources
Match...
WEB UI
WEB UI
Text is not SVG - cannot display
\ No newline at end of file diff --git a/frontend/src/component/DataTable.css b/frontend/src/component/DataTable.css index 687a4980..8152f6b9 100644 --- a/frontend/src/component/DataTable.css +++ b/frontend/src/component/DataTable.css @@ -3,6 +3,17 @@ div.MuiDataGrid-columnHeader:focus-within { outline: none !important; } +/* Column headers */ +div.MuiDataGrid-columnHeaders { + background-image: url('../../public/background.svg'); + color: #ffffff; +} + +button.MuiIconButton-root { + color: #ffffff; + opacity: 0.9; +} + /* Disable cell border on focus */ div.MuiDataGrid-row div.MuiDataGrid-cell:focus-within { outline: none !important; diff --git a/frontend/src/component/DataTable.tsx b/frontend/src/component/DataTable.tsx index b2ab36c6..6d5dab0a 100644 --- a/frontend/src/component/DataTable.tsx +++ b/frontend/src/component/DataTable.tsx @@ -74,6 +74,7 @@ export default function DataTable({ const handleOnSortModelChange = useCallback((sortModel: GridSortModel) => { if (sortModel.length !== 1) { + setSort(undefined); return; } @@ -83,6 +84,7 @@ export default function DataTable({ const handleOnFilterModelChange = useCallback((filterModel: GridFilterModel) => { if (filterModel.items.length !== 1) { + setFilter(undefined); return; } diff --git a/frontend/src/layout/UserMenu.test.tsx b/frontend/src/layout/UserMenu.test.tsx index 04a19507..bb779b33 100644 --- a/frontend/src/layout/UserMenu.test.tsx +++ b/frontend/src/layout/UserMenu.test.tsx @@ -55,7 +55,7 @@ test('user menu: should not be visible when user not authenticated', async () => }); // Then - expect(screen.queryByText('Déconnexion')).toBeNull(); + expect(screen.queryByText('Logout')).toBeNull(); }); test('user menu: should be visible when user authenticated', async () => { @@ -88,7 +88,7 @@ test('user menu: should be visible when user authenticated', async () => { await userEvent.click(screen.getByRole('user-menu')); // Then - expect(screen.queryByText('Déconnexion')).toBeVisible(); + expect(screen.queryByText('Logout')).toBeVisible(); expect(logout).toBeCalledTimes(0); }); @@ -120,7 +120,7 @@ test('user menu: click on logout button', async () => { }); await userEvent.click(screen.getByRole('user-menu')); - await userEvent.click(screen.getByText('Déconnexion')); + await userEvent.click(screen.getByText('Logout')); // Then expect(logout).toBeCalledTimes(1); diff --git a/frontend/src/layout/UserMenu.tsx b/frontend/src/layout/UserMenu.tsx index cc319413..2a18d44f 100644 --- a/frontend/src/layout/UserMenu.tsx +++ b/frontend/src/layout/UserMenu.tsx @@ -83,7 +83,7 @@ export default function UserMenu() { - Déconnexion + Logout diff --git a/frontend/src/page/clients/component/columns.tsx b/frontend/src/page/clients/component/columns.tsx index 8e7eed46..f7a620d1 100644 --- a/frontend/src/page/clients/component/columns.tsx +++ b/frontend/src/page/clients/component/columns.tsx @@ -50,7 +50,7 @@ export const ListColumns = ({ const date = moment(params.row.created_at); return ( -
+
{date.fromNow()}
) @@ -69,7 +69,7 @@ export const ListColumns = ({ const date = moment(params.row.updated_at); return ( -
+
{date.fromNow()}
) diff --git a/frontend/src/page/dashboard/component/Dashboard.tsx b/frontend/src/page/dashboard/component/Dashboard.tsx index f337274c..4c83b6a0 100644 --- a/frontend/src/page/dashboard/component/Dashboard.tsx +++ b/frontend/src/page/dashboard/component/Dashboard.tsx @@ -3,13 +3,18 @@ import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; import { ResponsiveBar } from '@nivo/bar'; import { ResponsivePie } from '@nivo/pie'; +import DataTable from 'component/DataTable'; import { AuthContext } from 'context/auth'; -import { ToastContext, useToast } from 'context/toast'; +import { useToast } from 'context/toast'; import moment from 'moment'; import { useContext, useEffect, useState } from 'react'; import { isAPIError } from 'service/error/model'; +import { getAudits } from 'service/model/audit'; import { Stats } from 'service/model/model'; import { getStats } from 'service/model/stats'; +import { AuditColumns } from 'page/dashboard/component/columns'; +import { FilterRequest } from 'service/common/filter'; +import { SortRequest } from 'service/common/sort'; type BarData = BarDataItem[] type PieData = PieDataItem[] @@ -42,7 +47,7 @@ export default function Dashboard() { return; } - const fetch = async () => { + const fetchStats = async () => { const response = await getStats(user?.token!); if (isAPIError(response)) { @@ -52,7 +57,7 @@ export default function Dashboard() { } }; - fetch(); + fetchStats(); // eslint-disable-next-line }, [user]); @@ -83,80 +88,98 @@ export default function Dashboard() { ]); }, [stats]); + const auditColumns = AuditColumns(); + + const fetcher = (page?: number, size?: number, filter?: FilterRequest, sort?: SortRequest) => { + return getAudits(user?.token!, page, size, filter, sort); + }; + return ( - - - - - Checks decisions per day - - + + + + + Checks decisions per day + + - + /> + + + + + + + Total of check decisions + + + + - - - - Total of check decisions - - - + + - + ); } \ No newline at end of file diff --git a/frontend/src/page/dashboard/component/columns.tsx b/frontend/src/page/dashboard/component/columns.tsx new file mode 100644 index 00000000..50521d24 --- /dev/null +++ b/frontend/src/page/dashboard/component/columns.tsx @@ -0,0 +1,107 @@ +import { Chip, List, ListItemButton, ListItemIcon, ListItemText } from "@mui/material"; +import { getGridBooleanOperators, getGridStringOperators, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import LinkIcon from '@mui/icons-material/Link'; +import moment from "moment"; + +export const AuditColumns = (): GridColDef[] => [ + { + field: 'date', + headerName: 'Date', + width: 200, + sortable: true, + filterable: false, + renderCell: (params: GridRenderCellParams) => { + if (params.row.date.startsWith('0001-01-01')) { + return (Unknown); + } + + const date = moment(params.row.created_at); + return ( +
+ {date.format('L')} at {date.format('LTS')} +
+ ) + }, + }, + { + field: 'is_allowed', + headerName: 'Is allowed?', + width: 100, + sortable: true, + filterable: true, + filterOperators: getGridBooleanOperators(), + renderCell: (params: GridRenderCellParams) => { + return ( + <> + {params.row.is_allowed ? ( + + ) : ( + + )} + + ) + }, + }, + { + field: 'policy_id', + headerName: 'Matched policy', + width: 250, + sortable: true, + filterable: true, + filterOperators: getGridStringOperators().filter( + (operator) => operator.value === 'contains', + ), + renderCell: (params: GridRenderCellParams) => { + return ( + + + + + + + + + ) + }, + }, + { + field: 'principal', + headerName: 'Principal', + width: 250, + sortable: true, + filterable: true, + filterOperators: getGridStringOperators().filter( + (operator) => operator.value === 'contains', + ), + }, + { + field: 'resource_kind', + headerName: 'Resource kind', + width: 200, + sortable: true, + filterable: true, + filterOperators: getGridStringOperators().filter( + (operator) => operator.value === 'contains', + ), + }, + { + field: 'resource_value', + headerName: 'Resource value', + width: 200, + sortable: true, + filterable: true, + filterOperators: getGridStringOperators().filter( + (operator) => operator.value === 'contains', + ), + }, + { + field: 'action', + headerName: 'Action', + width: 150, + sortable: true, + filterable: true, + filterOperators: getGridStringOperators().filter( + (operator) => operator.value === 'contains', + ), + }, +]; \ No newline at end of file diff --git a/frontend/src/page/policies/component/columns.tsx b/frontend/src/page/policies/component/columns.tsx index ba3e28fe..8ec85728 100644 --- a/frontend/src/page/policies/component/columns.tsx +++ b/frontend/src/page/policies/component/columns.tsx @@ -81,7 +81,7 @@ export const ListColumns = ({ const date = moment(params.row.created_at); return ( -
+
{date.fromNow()}
) @@ -100,7 +100,7 @@ export const ListColumns = ({ const date = moment(params.row.updated_at); return ( -
+
{date.fromNow()}
) diff --git a/frontend/src/page/principals/component/columns.tsx b/frontend/src/page/principals/component/columns.tsx index 593348af..c2454e1e 100644 --- a/frontend/src/page/principals/component/columns.tsx +++ b/frontend/src/page/principals/component/columns.tsx @@ -40,7 +40,7 @@ export const ListColumns = ({ const date = moment(params.row.created_at); return ( -
+
{date.fromNow()}
) @@ -59,7 +59,7 @@ export const ListColumns = ({ const date = moment(params.row.updated_at); return ( -
+
{date.fromNow()}
) diff --git a/frontend/src/page/resources/component/columns.tsx b/frontend/src/page/resources/component/columns.tsx index 6725fd0e..fa579655 100644 --- a/frontend/src/page/resources/component/columns.tsx +++ b/frontend/src/page/resources/component/columns.tsx @@ -60,7 +60,7 @@ export const ListColumns = ({ const date = moment(params.row.created_at); return ( -
+
{date.fromNow()}
) @@ -79,7 +79,7 @@ export const ListColumns = ({ const date = moment(params.row.updated_at); return ( -
+
{date.fromNow()}
) diff --git a/frontend/src/page/roles/component/columns.tsx b/frontend/src/page/roles/component/columns.tsx index 946d1d83..53f4267a 100644 --- a/frontend/src/page/roles/component/columns.tsx +++ b/frontend/src/page/roles/component/columns.tsx @@ -63,7 +63,7 @@ export const ListColumns = ({ const date = moment(params.row.created_at); return ( -
+
{date.fromNow()}
) @@ -82,7 +82,7 @@ export const ListColumns = ({ const date = moment(params.row.updated_at); return ( -
+
{date.fromNow()}
) diff --git a/frontend/src/page/users/component/columns.tsx b/frontend/src/page/users/component/columns.tsx index fee1e105..0588f1c1 100644 --- a/frontend/src/page/users/component/columns.tsx +++ b/frontend/src/page/users/component/columns.tsx @@ -39,7 +39,7 @@ export const ListColumns = ({ const date = moment(params.row.created_at); return ( -
+
{date.fromNow()}
) @@ -58,7 +58,7 @@ export const ListColumns = ({ const date = moment(params.row.updated_at); return ( -
+
{date.fromNow()}
) diff --git a/frontend/src/service/model/audit.ts b/frontend/src/service/model/audit.ts new file mode 100644 index 00000000..50db1166 --- /dev/null +++ b/frontend/src/service/model/audit.ts @@ -0,0 +1,26 @@ +import { baseUrl } from "service/common/api" +import { FilterRequest } from "service/common/filter"; +import { paginate, Paginated } from "service/common/paginate"; +import { SortRequest } from "service/common/sort"; +import { catchError } from "service/error/catch"; +import { APIError } from "service/error/model"; +import { Audit } from "service/model/model"; + +export const getAudits = async ( + token: string, + page?: number, + size?: number, + filter?: FilterRequest, + sort?: SortRequest, +): Promise | APIError> => { + return await catchError>(async () => { + return await paginate({ + url: baseUrl() + '/audits', + token: token, + page: page, + size: size, + filter: filter, + sort: sort, + }); + }); +} \ No newline at end of file diff --git a/frontend/src/service/model/model.ts b/frontend/src/service/model/model.ts index 6a7f2cb2..a5eaf832 100644 --- a/frontend/src/service/model/model.ts +++ b/frontend/src/service/model/model.ts @@ -4,6 +4,17 @@ export type Action = { updated_at: Date } +export type Audit = { + id: string + date: string + principal: string + resource_kind: string + resource_value: string + action: string + is_allowed: boolean + policy_id: string +} + export type Client = { client_id: string client_secret: string