diff --git a/README.md b/README.md index 7834c9c..053a472 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ ORY builds solutions for better internet security and accessibility. We have a c - [Adding Custom Conditions](#adding-custom-conditions) - [Persistence](#persistence) - [Access Control (Warden)](#access-control-warden) + - [Audit Log (Warden)](#audit-log-warden) +- [Limitations](#limitations) + - [Regular expressions](#regular-expressions) - [Examples](#examples) - [Good to know](#good-to-know) - [Useful commands](#useful-commands) @@ -570,6 +573,28 @@ func main() { } ``` +### Audit Log (Warden) + +In order to keep track of authorization grants and denials, it is possible to attach a `ladon.AuditLogger`. +The provided `ladon.AuditLoggerInfo` outputs information about the policies involved when responding to authorization requests. + +```go +import "github.com/ory/ladon" +import manager "github.com/ory/ladon/manager/memory" + +func main() { + + warden := ladon.Ladon{ + Manager: manager.NewMemoryManager(), + AuditLogger: ladon.AuditLoggerInfo{} + } + + // ... + +``` + +It will output to `stderr` by default. + ## Limitations Ladon's limitations are listed here. diff --git a/audit_logger.go b/audit_logger.go new file mode 100644 index 0000000..6ecf2c4 --- /dev/null +++ b/audit_logger.go @@ -0,0 +1,7 @@ +package ladon + +// AuditLogger tracks denied and granted authorizations. +type AuditLogger interface { + LogRejectedAccessRequest(request *Request, pool Policies, deciders Policies) + LogGrantedAccessRequest(request *Request, pool Policies, deciders Policies) +} diff --git a/audit_logger_info.go b/audit_logger_info.go new file mode 100644 index 0000000..70d9d95 --- /dev/null +++ b/audit_logger_info.go @@ -0,0 +1,44 @@ +package ladon + +import ( + "log" + "os" + "strings" +) + +// AuditLoggerInfo outputs information about granting or rejecting policies. +type AuditLoggerInfo struct { + Logger *log.Logger +} + +func (a *AuditLoggerInfo) logger() *log.Logger { + if a.Logger == nil { + a.Logger = log.New(os.Stderr, "", log.LstdFlags) + } + return a.Logger +} + +func (a *AuditLoggerInfo) LogRejectedAccessRequest(r *Request, p Policies, d Policies) { + if len(d) > 1 { + allowed := joinPoliciesNames(d[0 : len(d)-1]) + denied := d[len(d)-1].GetID() + a.Logger.Printf("policies %s allow access, but policy %s forcefully denied it", allowed, denied) + } else if len(d) == 1 { + denied := d[len(d)-1].GetID() + a.Logger.Printf("policy %s forcefully denied the access", denied) + } else { + a.Logger.Printf("no policy allowed access") + } +} + +func (a *AuditLoggerInfo) LogGrantedAccessRequest(r *Request, p Policies, d Policies) { + a.Logger.Printf("policies %s allow access", joinPoliciesNames(d)) +} + +func joinPoliciesNames(policies Policies) string { + names := []string{} + for _, policy := range policies { + names = append(names, policy.GetID()) + } + return strings.Join(names, ", ") +} diff --git a/audit_logger_noop.go b/audit_logger_noop.go new file mode 100644 index 0000000..64c6569 --- /dev/null +++ b/audit_logger_noop.go @@ -0,0 +1,9 @@ +package ladon + +// AuditLoggerNoOp is the default AuditLogger, that tracks nothing. +type AuditLoggerNoOp struct{} + +func (*AuditLoggerNoOp) LogRejectedAccessRequest(r *Request, p Policies, d Policies) {} +func (*AuditLoggerNoOp) LogGrantedAccessRequest(r *Request, p Policies, d Policies) {} + +var DefaultAuditLogger = &AuditLoggerNoOp{} diff --git a/audit_logger_test.go b/audit_logger_test.go new file mode 100644 index 0000000..f2a0a72 --- /dev/null +++ b/audit_logger_test.go @@ -0,0 +1,74 @@ +package ladon_test + +import ( + "bytes" + "log" + "testing" + + . "github.com/ory/ladon" + . "github.com/ory/ladon/manager/memory" + "github.com/stretchr/testify/assert" +) + +func TestAuditLogger(t *testing.T) { + var output bytes.Buffer + + warden := &Ladon{ + Manager: NewMemoryManager(), + AuditLogger: &AuditLoggerInfo{ + Logger: log.New(&output, "", 0), + }, + } + + warden.Manager.Create(&DefaultPolicy{ + ID: "no-updates", + Subjects: []string{"<.*>"}, + Actions: []string{"update"}, + Resources: []string{"<.*>"}, + Effect: DenyAccess, + }) + warden.Manager.Create(&DefaultPolicy{ + ID: "yes-deletes", + Subjects: []string{"<.*>"}, + Actions: []string{"delete"}, + Resources: []string{"<.*>"}, + Effect: AllowAccess, + }) + warden.Manager.Create(&DefaultPolicy{ + ID: "no-bob", + Subjects: []string{"bob"}, + Actions: []string{"delete"}, + Resources: []string{"<.*>"}, + Effect: DenyAccess, + }) + + r := &Request{} + assert.NotNil(t, warden.IsAllowed(r)) + assert.Equal(t, "no policy allowed access\n", output.String()) + + output.Reset() + + r = &Request{ + Action: "update", + } + assert.NotNil(t, warden.IsAllowed(r)) + assert.Equal(t, "policy no-updates forcefully denied the access\n", output.String()) + + output.Reset() + + r = &Request{ + Subject: "bob", + Action: "delete", + } + assert.NotNil(t, warden.IsAllowed(r)) + assert.Equal(t, "policies yes-deletes allow access, but policy no-bob forcefully denied it\n", output.String()) + + output.Reset() + + r = &Request{ + Subject: "alice", + Action: "delete", + } + assert.Nil(t, warden.IsAllowed(r)) + assert.Equal(t, "policies yes-deletes allow access\n", output.String()) +} diff --git a/ladon.go b/ladon.go index 34960e7..981b8a5 100644 --- a/ladon.go +++ b/ladon.go @@ -20,8 +20,9 @@ import ( // Ladon is an implementation of Warden. type Ladon struct { - Manager Manager - Matcher matcher + Manager Manager + Matcher matcher + AuditLogger AuditLogger } func (l *Ladon) matcher() matcher { @@ -31,6 +32,13 @@ func (l *Ladon) matcher() matcher { return l.Matcher } +func (l *Ladon) auditLogger() AuditLogger { + if l.AuditLogger == nil { + l.AuditLogger = DefaultAuditLogger + } + return l.AuditLogger +} + // IsAllowed returns nil if subject s has permission p on resource r with context c or an error otherwise. func (l *Ladon) IsAllowed(r *Request) (err error) { policies, err := l.Manager.FindRequestCandidates(r) @@ -48,6 +56,7 @@ func (l *Ladon) IsAllowed(r *Request) (err error) { // The IsAllowed interface should be preferred since it uses the manager directly. This is a lower level interface for when you don't want to use the ladon manager. func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { var allowed = false + var deciders = Policies{} // Iterate through all policies for _, p := range policies { @@ -88,15 +97,21 @@ func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { // Is the policies effect deny? If yes, this overrides all allow policies -> access denied. if !p.AllowAccess() { + deciders = append(deciders, p) + l.auditLogger().LogRejectedAccessRequest(r, policies, deciders) return errors.WithStack(ErrRequestForcefullyDenied) } + allowed = true + deciders = append(deciders, p) } if !allowed { + l.auditLogger().LogRejectedAccessRequest(r, policies, deciders) return errors.WithStack(ErrRequestDenied) } + l.auditLogger().LogGrantedAccessRequest(r, policies, deciders) return nil }