diff --git a/constants.go b/constants.go index 1a740f59ea38c..9963f45dc5c54 100644 --- a/constants.go +++ b/constants.go @@ -129,6 +129,9 @@ const ( // SAML means authentication will happen remotly using an SAML connector. SAML = "saml" + + // JSON means JSON serialization format + JSON = "json" ) const ( diff --git a/lib/services/parser.go b/lib/services/parser.go new file mode 100644 index 0000000000000..dd40dafebe3e6 --- /dev/null +++ b/lib/services/parser.go @@ -0,0 +1,217 @@ +/* +Copyright 2017 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "fmt" + "strings" + "time" + + "github.com/gravitational/teleport" + + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + log "github.com/sirupsen/logrus" + "github.com/vulcand/predicate" +) + +// RuleContext specifies context passed to the +// rule processing matcher, and contains information +// about current session, e.g. current user +type RuleContext interface { + // GetIdentifier returns identifier defined in a context + GetIdentifier(fields []string) (interface{}, error) + // String returns human friendly representation of a context + String() string +} + +// NewWhereParser returns standard parser for `where` section in access rules +func NewWhereParser(ctx RuleContext) (predicate.Parser, error) { + return predicate.NewParser(predicate.Def{ + Operators: predicate.Operators{ + AND: predicate.And, + OR: predicate.Or, + }, + Functions: map[string]interface{}{ + "equals": predicate.Equals, + }, + GetIdentifier: ctx.GetIdentifier, + GetProperty: predicate.GetStringMapValue, + }) +} + +// NewActionsParser returns standard parser for 'actions' section in access rules +func NewActionsParser(ctx RuleContext) (predicate.Parser, error) { + return predicate.NewParser(predicate.Def{ + Operators: predicate.Operators{}, + Functions: map[string]interface{}{ + "log": NewLogActionFn(ctx), + }, + GetIdentifier: ctx.GetIdentifier, + GetProperty: predicate.GetStringMapValue, + }) +} + +// NewLogActionFn creates logger functions +func NewLogActionFn(ctx RuleContext) interface{} { + return (&LogAction{ctx: ctx}).Log +} + +// LogAction represents action that will emit log entry +// when specified in the actions of a matched rule +type LogAction struct { + ctx RuleContext +} + +// Log logs with specified level and formatting string with arguments +func (l *LogAction) Log(level, format string, args ...interface{}) predicate.BoolPredicate { + return func() bool { + ilevel, err := log.ParseLevel(level) + if err != nil { + ilevel = log.DebugLevel + } + writer := log.StandardLogger().WriterLevel(ilevel) + writer.Write([]byte(fmt.Sprintf("%v %v", l.ctx, fmt.Sprintf(format, args...)))) + return true + } +} + +// Context is a default rule context used in teleport +type Context struct { + // User is currently authenticated user + User User + // Resource is an optional resource, in case if the rule + // checks access to the resource + Resource Resource +} + +// String returns user friendly representation of this context +func (ctx *Context) String() string { + return fmt.Sprintf("user %v, resource: %v", ctx.User, ctx.Resource) +} + +const ( + // UserIdentifier represents user registered identifier in the rules + UserIdentifier = "user" + // ResourceIdentifier represents resource registered identifer in the rules + ResourceIdentifier = "resource" +) + +// GetIdentifier returns identifier defined in a context +func (ctx *Context) GetIdentifier(fields []string) (interface{}, error) { + switch fields[0] { + case UserIdentifier: + var user User + if ctx.User == nil { + user = emptyUser + } else { + user = ctx.User + } + return predicate.GetFieldByTag(user, teleport.JSON, fields[1:]) + case ResourceIdentifier: + var resource Resource + if ctx.Resource == nil { + resource = emptyResource + } else { + resource = ctx.Resource + } + return predicate.GetFieldByTag(resource, "json", fields[1:]) + default: + return nil, trace.NotFound("%v is not defined", strings.Join(fields, ".")) + } +} + +// NewParserFn returns function that creates parser of 'where' section +// in access rules +type NewParserFn func(ctx RuleContext) (predicate.Parser, error) + +var whereParser = NewWhereParser +var actionsParser = NewActionsParser + +// GetWhereParserFn returns global function that creates where parsers +// this function is used in external tools to override and extend 'where' in rules +func GetWhereParserFn() NewParserFn { + marshalerMutex.RLock() + defer marshalerMutex.RUnlock() + return whereParser +} + +// SetWhereParserFn sets global function that creates where parsers +// this function is used in external tools to override and extend 'where' in rules +func SetWhereParserFn(fn NewParserFn) { + marshalerMutex.Lock() + defer marshalerMutex.Unlock() + whereParser = fn +} + +// GetActionsParserFn returns global function that creates where parsers +// this function is used in external tools to override and extend actions in rules +func GetActionsParserFn() NewParserFn { + marshalerMutex.RLock() + defer marshalerMutex.RUnlock() + return actionsParser +} + +// SetActionsParserFn sets global function that creates actions parsers +// this function is used in external tools to override and extend actions in rules +func SetActionsParserFn(fn NewParserFn) { + marshalerMutex.Lock() + defer marshalerMutex.Unlock() + actionsParser = fn +} + +// emptyResource is used when no resource is specified +var emptyResource = &EmptyResource{} + +// emptyUser is used when no user is specified +var emptyUser = &UserV2{} + +// EmptyResource is used to represent a use case when no resource +// is specified in the rules matcher +type EmptyResource struct { + ResourceHeader +} + +// SetExpiry sets expiry time for the object. +func (r *EmptyResource) SetExpiry(expires time.Time) { + r.Metadata.SetExpiry(expires) +} + +// Expiry returns the expiry time for the object. +func (r *EmptyResource) Expiry() time.Time { + return r.Metadata.Expiry() +} + +// SetTTL sets TTL header using realtime clock. +func (r *EmptyResource) SetTTL(clock clockwork.Clock, ttl time.Duration) { + r.Metadata.SetTTL(clock, ttl) +} + +// SetName sets the role name and is a shortcut for SetMetadata().Name. +func (r *EmptyResource) SetName(s string) { + r.Metadata.Name = s +} + +// GetName gets the role name and is a shortcut for GetMetadata().Name. +func (r *EmptyResource) GetName() string { + return r.Metadata.Name +} + +// GetMetadata returns role metadata. +func (r *EmptyResource) GetMetadata() Metadata { + return r.Metadata +} diff --git a/lib/services/role.go b/lib/services/role.go index e070a70c3b47f..7a66ff248e848 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -32,6 +32,7 @@ import ( "github.com/jonboulle/clockwork" log "github.com/sirupsen/logrus" + "github.com/vulcand/predicate" ) // DefaultUserRules provides access to the default set of rules assigned to @@ -683,11 +684,47 @@ func NewRule(resource string, verbs []string) Rule { // Rule represents allow or deny rule that is executed to check // if user or service have access to resource type Rule struct { - // Resources is a list of + // Resources is a list of resources Resources []string `json:"resources"` - Verbs []string `json:"verbs"` - Where string `json:"where,omitempty"` - Actions []string `json:"actions,omitempty"` + // Verbs is a list of verbs + Verbs []string `json:"verbs"` + // Where specifies optional advanced matcher + Where string `json:"where,omitempty"` + // Actions specifies optional actions taken when this rule matches + Actions []string `json:"actions,omitempty"` +} + +// MatchesWhere returns true if Where rule matches +// Empty Where block always matches +func (r *Rule) MatchesWhere(parser predicate.Parser) (bool, error) { + if r.Where == "" { + return true, nil + } + ifn, err := parser.Parse(r.Where) + if err != nil { + return false, trace.Wrap(err) + } + fn, ok := ifn.(predicate.BoolPredicate) + if !ok { + return false, trace.BadParameter("unsupported type: %T", ifn) + } + return fn(), nil +} + +// ProcessActions processes actions specified for this rule +func (r *Rule) ProcessActions(parser predicate.Parser) error { + for _, action := range r.Actions { + ifn, err := parser.Parse(action) + if err != nil { + return trace.Wrap(err) + } + fn, ok := ifn.(predicate.BoolPredicate) + if !ok { + return trace.BadParameter("unsupported type: %T", ifn) + } + fn() + } + return nil } // HasVerb returns true if the rule has verb, @@ -722,27 +759,41 @@ func (r *Rule) Equals(other Rule) bool { type RuleSet map[string][]Rule // MatchRule tests if the resource name and verb are in a given list of rules. -func (set RuleSet) Match(resource string, verb string) bool { +func (set RuleSet) Match(whereParser predicate.Parser, actionsParser predicate.Parser, resource string, verb string) (bool, error) { // empty set matches nothing if len(set) == 0 { - return false + return false, nil } // check for wildcard resource matcher for _, rule := range set[Wildcard] { - if rule.HasVerb(Wildcard) || rule.HasVerb(verb) { - return true + match, err := rule.MatchesWhere(whereParser) + if err != nil { + return false, trace.Wrap(err) + } + if match && (rule.HasVerb(Wildcard) || rule.HasVerb(verb)) { + if err := rule.ProcessActions(actionsParser); err != nil { + return true, trace.Wrap(err) + } + return true, nil } } // check for matching resource by name for _, rule := range set[resource] { - if rule.HasVerb(Wildcard) || rule.HasVerb(verb) { - return true + match, err := rule.MatchesWhere(whereParser) + if err != nil { + return false, trace.Wrap(err) + } + if match && (rule.HasVerb(Wildcard) || rule.HasVerb(verb)) { + if err := rule.ProcessActions(actionsParser); err != nil { + return true, trace.Wrap(err) + } + return true, nil } } - return false + return false, nil } // Slice returns slice from a set @@ -1289,20 +1340,40 @@ func (set RoleSet) String() string { return fmt.Sprintf("roles %v", strings.Join(roleNames, ",")) } -func (set RoleSet) CheckAccessToRule(namespace string, resource string, verb string) error { +func (set RoleSet) CheckAccessToRule(ctx RuleContext, namespace string, resource string, verb string) error { + whereParser, err := GetWhereParserFn()(ctx) + if err != nil { + return trace.Wrap(err) + } + actionsParser, err := GetActionsParserFn()(ctx) + if err != nil { + return trace.Wrap(err) + } // check deny: a single match on a deny rule prohibits access for _, role := range set { matchNamespace := MatchNamespace(role.GetNamespaces(Deny), ProcessNamespace(namespace)) - if matchNamespace && MakeRuleSet(role.GetRules(Deny)).Match(resource, verb) { - return trace.AccessDenied("%v access to %v in namespace %v is denied for %v: deny rule matched", verb, resource, namespace, role) + if matchNamespace { + matched, err := MakeRuleSet(role.GetRules(Deny)).Match(whereParser, actionsParser, resource, verb) + if err != nil { + return trace.Wrap(err) + } + if matched { + return trace.AccessDenied("%v access to %v in namespace %v is denied for %v: deny rule matched", verb, resource, namespace, role) + } } } // check allow: if rule matches, grant access to resource for _, role := range set { matchNamespace := MatchNamespace(role.GetNamespaces(Allow), ProcessNamespace(namespace)) - if matchNamespace && MakeRuleSet(role.GetRules(Allow)).Match(resource, verb) { - return nil + if matchNamespace { + match, err := MakeRuleSet(role.GetRules(Allow)).Match(whereParser, actionsParser, resource, verb) + if err != nil { + return trace.Wrap(err) + } + if match { + return nil + } } } @@ -1414,6 +1485,13 @@ const RoleSpecV3SchemaDefinitions = ` "verbs": { "type": "array", "items": { "type": "string" } + }, + "where": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { "type": "string" } } } } diff --git a/lib/services/role_test.go b/lib/services/role_test.go index 646d34ed63e69..b557f37a0087e 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -429,7 +429,7 @@ func (s *RoleSuite) TestCheckRuleAccess(c *C) { } for j, check := range tc.checks { comment := Commentf("test case %v '%v', check %v", i, tc.name, j) - result := set.CheckAccessToRule(check.namespace, check.rule, check.verb) + result := set.CheckAccessToRule(&Context{}, check.namespace, check.rule, check.verb) if check.hasAccess { c.Assert(result, IsNil, comment) } else {