From 4805963990dc69fa2e7fbaa534a9f04686fb344b Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Fri, 1 Dec 2017 11:54:23 +0100 Subject: [PATCH] Define policies in doorman package --- doorman/doorman.go | 50 ++++++++++++++++ doorman/doorman_ladon.go | 85 +++++++++++++++------------- doorman/doorman_ladon_loader_file.go | 20 +------ doorman/doorman_ladon_test.go | 16 +----- doorman/middleware_test.go | 13 ++--- 5 files changed, 105 insertions(+), 79 deletions(-) diff --git a/doorman/doorman.go b/doorman/doorman.go index 88a7150..aa3308c 100644 --- a/doorman/doorman.go +++ b/doorman/doorman.go @@ -9,6 +9,56 @@ type Config struct { Sources []string } +// Tags map tag names to principals. +type Tags map[string]Principals + +// Condition either do or do not fulfill an access request. +type Condition struct { + Type string + Options map[string]interface{} +} + +// Conditions is a collection of conditions. +type Conditions map[string]Condition + +// Policy represents an access control. +type Policy struct { + ID string + Description string + Principals []string + Effect string + Resources []string + Actions []string + Conditions Conditions +} + +// Policies is a collection of policies. +type Policies []Policy + +// ServiceConfig represents the policies file content. +type ServiceConfig struct { + Service string + JWTIssuer string `yaml:"jwtIssuer"` + Tags Tags + Policies Policies +} + +// GetTags returns the tags principals for the ones specified. +func (c *ServiceConfig) GetTags(principals Principals) Principals { + result := Principals{} + for tag, members := range c.Tags { + for _, member := range members { + for _, principal := range principals { + if principal == member { + prefixed := fmt.Sprintf("tag:%s", tag) + result = append(result, prefixed) + } + } + } + } + return result +} + // Context is used as request's context. type Context map[string]interface{} diff --git a/doorman/doorman_ladon.go b/doorman/doorman_ladon.go index c34fab5..7df50a8 100644 --- a/doorman/doorman_ladon.go +++ b/doorman/doorman_ladon.go @@ -1,6 +1,7 @@ package doorman import ( + "encoding/json" "fmt" "github.com/ory/ladon" @@ -10,48 +11,23 @@ import ( const maxInt int64 = 1<<63 - 1 -// Tags map tag names to principals. -type Tags map[string]Principals - // LadonDoorman is the backend in charge of checking requests against policies. type LadonDoorman struct { config Config services map[string]*ServiceConfig _auditLogger *auditLogger -} -// ServiceConfig represents the policies file content. -type ServiceConfig struct { - Service string - JWTIssuer string `json:"jwtIssuer"` - Tags Tags - Policies []*ladon.DefaultPolicy - - ladon *ladon.Ladon - jwtValidator JWTValidator -} - -// GetTags returns the tags principals for the ones specified. -func (c *ServiceConfig) GetTags(principals Principals) Principals { - result := Principals{} - for tag, members := range c.Tags { - for _, member := range members { - for _, principal := range principals { - if principal == member { - prefixed := fmt.Sprintf("tag:%s", tag) - result = append(result, prefixed) - } - } - } - } - return result + ladons map[string]*ladon.Ladon + jwtValidators map[string]JWTValidator } // NewDefaultLadon instantiates a new doorman. func NewDefaultLadon(config Config) *LadonDoorman { w := &LadonDoorman{ - config: config, - services: map[string]*ServiceConfig{}, + config: config, + services: map[string]*ServiceConfig{}, + ladons: map[string]*ladon.Ladon{}, + jwtValidators: map[string]JWTValidator{}, } return w } @@ -66,6 +42,8 @@ func (doorman *LadonDoorman) auditLogger() *auditLogger { // LoadPolicies (re)loads configuration and policies from the YAML files. func (doorman *LadonDoorman) LoadPolicies() error { // First, load each configuration file. + newLadons := map[string]*ladon.Ladon{} + newJWTValidators := map[string]JWTValidator{} newConfigs := map[string]*ServiceConfig{} for _, source := range doorman.config.Sources { services, err := loadSource(source) @@ -84,18 +62,45 @@ func (doorman *LadonDoorman) LoadPolicies() error { if err != nil { return err } - config.jwtValidator = v + newJWTValidators[config.Service] = v } else { log.Warningf("No JWT verification for %q.", config.Service) } - config.ladon = &ladon.Ladon{ + newLadons[config.Service] = &ladon.Ladon{ Manager: manager.NewMemoryManager(), AuditLogger: doorman.auditLogger(), } for _, pol := range config.Policies { - log.Debugf("Load policy %q: %s", pol.GetID(), pol.GetDescription()) - err := config.ladon.Manager.Create(pol) + log.Debugf("Load policy %q: %s", pol.ID, pol.Description) + + var conditions = ladon.Conditions{} + for field, cond := range pol.Conditions { + factory, found := ladon.ConditionFactories[cond.Type] + if !found { + return fmt.Errorf("unknown condition type %s", cond.Type) + } + c := factory() + if len(cond.Options) > 0 { + // Leverage Ladon JSON unmarshall code to instantiate conditions. + str, _ := json.Marshal(cond.Options) + if err := json.Unmarshal(str, c); err != nil { + return err + } + } + conditions.AddCondition(field, c) + } + + policy := &ladon.DefaultPolicy{ + ID: pol.ID, + Description: pol.Description, + Subjects: pol.Principals, + Effect: pol.Effect, + Resources: pol.Resources, + Actions: pol.Actions, + Conditions: conditions, + } + err := newLadons[config.Service].Manager.Create(policy) if err != nil { return err } @@ -105,16 +110,18 @@ func (doorman *LadonDoorman) LoadPolicies() error { } // Only if everything went well, replace existing services with new ones. doorman.services = newConfigs + doorman.ladons = newLadons + doorman.jwtValidators = newJWTValidators return nil } // JWTValidator returns the JWT validator for the specified service. func (doorman *LadonDoorman) JWTValidator(service string) (JWTValidator, error) { - c, ok := doorman.services[service] + v, ok := doorman.jwtValidators[service] if !ok { return nil, fmt.Errorf("unknown service %q", service) } - return c.jwtValidator, nil + return v, nil } // IsAllowed is responsible for deciding if subject can perform action on a resource with a context. @@ -131,7 +138,7 @@ func (doorman *LadonDoorman) IsAllowed(service string, request *Request) bool { Context: context, } - c, ok := doorman.services[service] + l, ok := doorman.ladons[service] if !ok { // Explicitly log denied request using audit logger. doorman.auditLogger().logRequest(false, r, ladon.Policies{}) @@ -141,7 +148,7 @@ func (doorman *LadonDoorman) IsAllowed(service string, request *Request) bool { // For each principal, use it as the subject and query ladon backend. for _, principal := range request.Principals { r.Subject = principal - if err := c.ladon.IsAllowed(r); err == nil { + if err := l.IsAllowed(r); err == nil { return true } } diff --git a/doorman/doorman_ladon_loader_file.go b/doorman/doorman_ladon_loader_file.go index 0f40e09..a3e7adb 100644 --- a/doorman/doorman_ladon_loader_file.go +++ b/doorman/doorman_ladon_loader_file.go @@ -1,8 +1,6 @@ package doorman import ( - "bytes" - "encoding/json" "fmt" "io/ioutil" "os" @@ -10,8 +8,6 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" - - "github.com/mozilla/doorman/utilities" ) type fileLoader struct{} @@ -67,23 +63,9 @@ func loadFile(filename string) (*ServiceConfig, error) { if len(fileContent) == 0 { return nil, fmt.Errorf("empty file %q", filename) } - // Replace "principals" in config by "subjects" (ladon vocabulary) - adjusted := bytes.Replace(fileContent, []byte("principals:"), []byte("subjects:"), -1) - - // Ladon does not support un/marshaling YAML. - // https://github.com/ory/ladon/issues/83 - var generic interface{} - if err := yaml.Unmarshal(adjusted, &generic); err != nil { - return nil, err - } - asJSON := utilities.Yaml2JSON(generic) - jsonData, err := json.Marshal(asJSON) - if err != nil { - return nil, err - } var config ServiceConfig - if err := json.Unmarshal(jsonData, &config); err != nil { + if err := yaml.Unmarshal(fileContent, &config); err != nil { return nil, err } diff --git a/doorman/doorman_ladon_test.go b/doorman/doorman_ladon_test.go index 0ba0687..a8068e3 100644 --- a/doorman/doorman_ladon_test.go +++ b/doorman/doorman_ladon_test.go @@ -78,16 +78,6 @@ policies: `) assert.Nil(t, err) - // Bad service - _, err = loadTempFiles(` -service: 1 -policies: - - - id: "1" - effect: allow -`) - assert.NotNil(t, err) - // Bad policies conditions _, err = loadTempFiles(` service: a @@ -222,18 +212,18 @@ policies: func TestReloadPolicies(t *testing.T) { doorman := sampleDoorman() - loaded, _ := doorman.services["https://sample.yaml"].ladon.Manager.GetAll(0, maxInt) + loaded, _ := doorman.ladons["https://sample.yaml"].Manager.GetAll(0, maxInt) assert.Equal(t, 6, len(loaded)) // Second load. doorman.LoadPolicies() - loaded, _ = doorman.services["https://sample.yaml"].ladon.Manager.GetAll(0, maxInt) + loaded, _ = doorman.ladons["https://sample.yaml"].Manager.GetAll(0, maxInt) assert.Equal(t, 6, len(loaded)) // Load bad policies, does not affect existing. doorman.config.Sources = []string{"/tmp/unknown.yaml"} doorman.LoadPolicies() - _, ok := doorman.services["https://sample.yaml"] + _, ok := doorman.ladons["https://sample.yaml"] assert.True(t, ok) } diff --git a/doorman/middleware_test.go b/doorman/middleware_test.go index 3178d2a..137c7e7 100644 --- a/doorman/middleware_test.go +++ b/doorman/middleware_test.go @@ -38,9 +38,7 @@ func TestJWTMiddleware(t *testing.T) { // Associate a fake JWT validator to this issuer. v := &TestValidator{} - doorman.services[audience] = &ServiceConfig{ - jwtValidator: v, - } + doorman.jwtValidators[audience] = v // Extract claims is ran on every request. claims := &Claims{ @@ -84,9 +82,8 @@ func TestJWTMiddleware(t *testing.T) { assert.False(t, ok) // JWT not configured for this origin. - doorman.services["https://open"] = &ServiceConfig{ - jwtValidator: nil, - } + doorman.jwtValidators["https://open"] = nil + c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Origin", "https://open") handler(c) @@ -100,7 +97,7 @@ func TestJWTMiddleware(t *testing.T) { } v = &TestValidator{} v.On("ExtractClaims", mock.Anything).Return(claims, nil) - doorman.services[audience].jwtValidator = v + doorman.jwtValidators[audience] = v c, _ = gin.CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Origin", audience) @@ -115,7 +112,7 @@ func TestJWTMiddleware(t *testing.T) { } v = &TestValidator{} v.On("ExtractClaims", mock.Anything).Return(claims, nil) - doorman.services[audience].jwtValidator = v + doorman.jwtValidators[audience] = v c, _ = gin.CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Origin", audience)