mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 17:53:28 +00:00
1661 lines
46 KiB
Go
1661 lines
46 KiB
Go
/*
|
|
Copyright 2016 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 (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/utils/parse"
|
|
|
|
"github.com/gravitational/configure/cstrings"
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/jonboulle/clockwork"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/vulcand/predicate"
|
|
)
|
|
|
|
// AdminUserRules provides access to the default set of rules assigned to
|
|
// all users.
|
|
var AdminUserRules = []Rule{
|
|
NewRule(KindRole, RW()),
|
|
NewRule(KindAuthConnector, RW()),
|
|
NewRule(KindSession, RO()),
|
|
NewRule(KindTrustedCluster, RW()),
|
|
}
|
|
|
|
// DefaultImplicitRules provides access to the default set of implicit rules
|
|
// assigned to all roles.
|
|
var DefaultImplicitRules = []Rule{
|
|
NewRule(KindNode, RO()),
|
|
NewRule(KindAuthServer, RO()),
|
|
NewRule(KindReverseTunnel, RO()),
|
|
NewRule(KindCertAuthority, RO()),
|
|
NewRule(KindClusterAuthPreference, RO()),
|
|
NewRule(KindClusterName, RO()),
|
|
NewRule(KindSSHSession, RO()),
|
|
}
|
|
|
|
// DefaultCertAuthorityRules provides access the minimal set of resources
|
|
// needed for a certificate authority to function.
|
|
var DefaultCertAuthorityRules = []Rule{
|
|
NewRule(KindSession, RO()),
|
|
NewRule(KindNode, RO()),
|
|
NewRule(KindAuthServer, RO()),
|
|
NewRule(KindReverseTunnel, RO()),
|
|
NewRule(KindCertAuthority, RO()),
|
|
}
|
|
|
|
// RoleNameForUser returns role name associated with a user.
|
|
func RoleNameForUser(name string) string {
|
|
return "user:" + name
|
|
}
|
|
|
|
// RoleNameForCertAuthority returns role name associated with a certificate
|
|
// authority.
|
|
func RoleNameForCertAuthority(name string) string {
|
|
return "ca:" + name
|
|
}
|
|
|
|
// NewAdminRole is the default admin role for all local users if another role
|
|
// is not explicitly assigned (Enterprise only).
|
|
func NewAdminRole() Role {
|
|
return &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: teleport.AdminRoleName,
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: NewDuration(defaults.MaxCertDuration),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Logins: []string{teleport.TraitInternalRoleVariable},
|
|
NodeLabels: map[string]string{Wildcard: Wildcard},
|
|
Rules: CopyRulesSlice(AdminUserRules),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewImplicitRole is the default implicit role that gets added to all
|
|
// RoleSets.
|
|
func NewImplicitRole() Role {
|
|
return &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: teleport.DefaultImplicitRole,
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: NewDuration(defaults.MaxCertDuration),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: CopyRulesSlice(DefaultImplicitRules),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// RoleForUser creates an admin role for a services.User.
|
|
func RoleForUser(u User) Role {
|
|
return &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: RoleNameForUser(u.GetName()),
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: NewDuration(defaults.MaxCertDuration),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
NodeLabels: map[string]string{Wildcard: Wildcard},
|
|
Rules: CopyRulesSlice(AdminUserRules),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// RoleForCertauthority creates role using services.CertAuthority.
|
|
func RoleForCertAuthority(ca CertAuthority) Role {
|
|
return &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: RoleNameForCertAuthority(ca.GetClusterName()),
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: NewDuration(defaults.MaxCertDuration),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
NodeLabels: map[string]string{Wildcard: Wildcard},
|
|
Rules: CopyRulesSlice(DefaultCertAuthorityRules),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ConvertV1CertAuthority converts V1 cert authority for new CA and Role
|
|
func ConvertV1CertAuthority(v1 *CertAuthorityV1) (CertAuthority, Role) {
|
|
ca := v1.V2()
|
|
role := RoleForCertAuthority(ca)
|
|
role.SetLogins(Allow, v1.AllowedLogins)
|
|
ca.AddRole(role.GetName())
|
|
return ca, role
|
|
}
|
|
|
|
// Access service manages roles and permissions
|
|
type Access interface {
|
|
// GetRoles returns a list of roles
|
|
GetRoles() ([]Role, error)
|
|
|
|
// UpsertRole creates or updates role
|
|
UpsertRole(role Role, ttl time.Duration) error
|
|
|
|
// DeleteAllRoles deletes all roles
|
|
DeleteAllRoles() error
|
|
|
|
// GetRole returns role by name
|
|
GetRole(name string) (Role, error)
|
|
|
|
// DeleteRole deletes role by name
|
|
DeleteRole(name string) error
|
|
}
|
|
|
|
const (
|
|
// ForwardAgent is SSH agent forwarding.
|
|
ForwardAgent = "forward_agent"
|
|
|
|
// MaxSessionTTL defines how long a SSH session can last for.
|
|
MaxSessionTTL = "max_session_ttl"
|
|
)
|
|
|
|
const (
|
|
// Allow is the set of conditions that allow access.
|
|
Allow RoleConditionType = true
|
|
// Deny is the set of conditions that prevent access.
|
|
Deny RoleConditionType = false
|
|
)
|
|
|
|
// RoleConditionType specifies if it's an allow rule (true) or deny rule (false).
|
|
type RoleConditionType bool
|
|
|
|
// Role contains a set of permissions or settings
|
|
type Role interface {
|
|
// Resource provides common resource methods.
|
|
Resource
|
|
// CheckAndSetDefaults checks and set default values for any missing fields.
|
|
CheckAndSetDefaults() error
|
|
// Equals returns true if the roles are equal. Roles are equal if options and
|
|
// conditions match.
|
|
Equals(other Role) bool
|
|
// ApplyTraits applies the passed in traits to any variables within the role
|
|
// and returns itself.
|
|
ApplyTraits(map[string][]string) Role
|
|
// GetRawObject returns the raw object stored in the backend without any
|
|
// conversions applied, used in migrations.
|
|
GetRawObject() interface{}
|
|
|
|
// GetOptions gets role options.
|
|
GetOptions() RoleOptions
|
|
// SetOptions sets role options
|
|
SetOptions(opt RoleOptions)
|
|
|
|
// GetLogins gets *nix system logins for allow or deny condition.
|
|
GetLogins(RoleConditionType) []string
|
|
// SetLogins sets *nix system logins for allow or deny condition.
|
|
SetLogins(RoleConditionType, []string)
|
|
|
|
// GetNamespaces gets a list of namespaces this role is allowed or denied access to.
|
|
GetNamespaces(RoleConditionType) []string
|
|
// GetNamespaces sets a list of namespaces this role is allowed or denied access to.
|
|
SetNamespaces(RoleConditionType, []string)
|
|
|
|
// GetNodeLabels gets the map of node labels this role is allowed or denied access to.
|
|
GetNodeLabels(RoleConditionType) map[string]string
|
|
// SetNodeLabels sets the map of node labels this role is allowed or denied access to.
|
|
SetNodeLabels(RoleConditionType, map[string]string)
|
|
|
|
// GetRules gets all allow or deny rules.
|
|
GetRules(rct RoleConditionType) []Rule
|
|
// SetRules sets an allow or deny rule.
|
|
SetRules(rct RoleConditionType, rules []Rule)
|
|
}
|
|
|
|
// ApplyTraits applies the passed in traits to any variables within the role
|
|
// and returns itself.
|
|
func ApplyTraits(r Role, traits map[string][]string) Role {
|
|
for _, condition := range []RoleConditionType{Allow, Deny} {
|
|
inLogins := r.GetLogins(condition)
|
|
|
|
var outLogins []string
|
|
for _, login := range inLogins {
|
|
// extract the variablePrefix and variableName from the role variable
|
|
variablePrefix, variableName, err := parse.IsRoleVariable(login)
|
|
|
|
// if we didn't find a variable (found a normal login) then append it and
|
|
// go on to the next login
|
|
if trace.IsNotFound(err) {
|
|
outLogins = append(outLogins, login)
|
|
continue
|
|
}
|
|
|
|
// for internal traits, we only support internal.logins at the moment
|
|
if variablePrefix == teleport.TraitInternalPrefix {
|
|
if variableName != teleport.TraitLogins {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// if we can't find the variable in the traits, skip it
|
|
variableValue, ok := traits[variableName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// we found the variable in the traits, append it to the list of logins
|
|
outLogins = append(outLogins, variableValue...)
|
|
}
|
|
|
|
r.SetLogins(condition, utils.Deduplicate(outLogins))
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// RoleV3 represents role resource specification
|
|
type RoleV3 struct {
|
|
// Kind is the type of resource.
|
|
Kind string `json:"kind"`
|
|
// Version is the resource version.
|
|
Version string `json:"version"`
|
|
// Metadata is resource metadata.
|
|
Metadata Metadata `json:"metadata"`
|
|
// Spec contains resource specification.
|
|
Spec RoleSpecV3 `json:"spec"`
|
|
// rawObject is the raw object stored in the backend without any
|
|
// conversions applied, used in migrations.
|
|
rawObject interface{}
|
|
}
|
|
|
|
// Equals returns true if the roles are equal. Roles are equal if options,
|
|
// namespaces, logins, labels, and conditions match.
|
|
func (r *RoleV3) Equals(other Role) bool {
|
|
if !r.GetOptions().Equals(other.GetOptions()) {
|
|
return false
|
|
}
|
|
|
|
for _, condition := range []RoleConditionType{Allow, Deny} {
|
|
if !utils.StringSlicesEqual(r.GetLogins(condition), other.GetLogins(condition)) {
|
|
return false
|
|
}
|
|
if !utils.StringSlicesEqual(r.GetNamespaces(condition), other.GetNamespaces(condition)) {
|
|
return false
|
|
}
|
|
if !utils.StringMapsEqual(r.GetNodeLabels(condition), other.GetNodeLabels(condition)) {
|
|
return false
|
|
}
|
|
if !RuleSlicesEqual(r.GetRules(condition), other.GetRules(condition)) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ApplyTraits applies the passed in traits to any variables within the role
|
|
// and returns itself.
|
|
func (r *RoleV3) ApplyTraits(traits map[string][]string) Role {
|
|
return ApplyTraits(r, traits)
|
|
}
|
|
|
|
// SetRawObject sets raw object as it was stored in the database
|
|
// used for migrations and should not be modifed
|
|
func (r *RoleV3) SetRawObject(raw interface{}) {
|
|
r.rawObject = raw
|
|
}
|
|
|
|
// GetRawObject returns the raw object stored in the backend without any
|
|
// conversions applied, used in migrations.
|
|
func (r *RoleV3) GetRawObject() interface{} {
|
|
return r.rawObject
|
|
}
|
|
|
|
// SetExpiry sets expiry time for the object.
|
|
func (r *RoleV3) SetExpiry(expires time.Time) {
|
|
r.Metadata.SetExpiry(expires)
|
|
}
|
|
|
|
// Expiry returns the expiry time for the object.
|
|
func (r *RoleV3) Expiry() time.Time {
|
|
return r.Metadata.Expiry()
|
|
}
|
|
|
|
// SetTTL sets TTL header using realtime clock.
|
|
func (r *RoleV3) 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 *RoleV3) SetName(s string) {
|
|
r.Metadata.Name = s
|
|
}
|
|
|
|
// GetName gets the role name and is a shortcut for GetMetadata().Name.
|
|
func (r *RoleV3) GetName() string {
|
|
return r.Metadata.Name
|
|
}
|
|
|
|
// GetMetadata returns role metadata.
|
|
func (r *RoleV3) GetMetadata() Metadata {
|
|
return r.Metadata
|
|
}
|
|
|
|
// GetOptions gets role options.
|
|
func (r *RoleV3) GetOptions() RoleOptions {
|
|
return r.Spec.Options
|
|
}
|
|
|
|
// SetOptions sets role options.
|
|
func (r *RoleV3) SetOptions(options RoleOptions) {
|
|
r.Spec.Options = utils.CopyStringMapInterface(options)
|
|
}
|
|
|
|
// GetLogins gets system logins for allow or deny condition.
|
|
func (r *RoleV3) GetLogins(rct RoleConditionType) []string {
|
|
if rct == Allow {
|
|
return r.Spec.Allow.Logins
|
|
}
|
|
return r.Spec.Deny.Logins
|
|
}
|
|
|
|
// SetLogins sets system logins for allow or deny condition.
|
|
func (r *RoleV3) SetLogins(rct RoleConditionType, logins []string) {
|
|
lcopy := utils.CopyStrings(logins)
|
|
|
|
if rct == Allow {
|
|
r.Spec.Allow.Logins = lcopy
|
|
} else {
|
|
r.Spec.Deny.Logins = lcopy
|
|
}
|
|
}
|
|
|
|
// GetNamespaces gets a list of namespaces this role is allowed or denied access to.
|
|
func (r *RoleV3) GetNamespaces(rct RoleConditionType) []string {
|
|
if rct == Allow {
|
|
return r.Spec.Allow.Namespaces
|
|
}
|
|
return r.Spec.Deny.Namespaces
|
|
}
|
|
|
|
// GetNamespaces sets a list of namespaces this role is allowed or denied access to.
|
|
func (r *RoleV3) SetNamespaces(rct RoleConditionType, namespaces []string) {
|
|
ncopy := utils.CopyStrings(namespaces)
|
|
|
|
if rct == Allow {
|
|
r.Spec.Allow.Namespaces = ncopy
|
|
} else {
|
|
r.Spec.Deny.Namespaces = ncopy
|
|
}
|
|
}
|
|
|
|
// GetNodeLabels gets the map of node labels this role is allowed or denied access to.
|
|
func (r *RoleV3) GetNodeLabels(rct RoleConditionType) map[string]string {
|
|
if rct == Allow {
|
|
return r.Spec.Allow.NodeLabels
|
|
}
|
|
return r.Spec.Deny.NodeLabels
|
|
}
|
|
|
|
// SetNodeLabels sets the map of node labels this role is allowed or denied access to.
|
|
func (r *RoleV3) SetNodeLabels(rct RoleConditionType, labels map[string]string) {
|
|
lcopy := utils.CopyStringMap(labels)
|
|
|
|
if rct == Allow {
|
|
r.Spec.Allow.NodeLabels = lcopy
|
|
} else {
|
|
r.Spec.Deny.NodeLabels = lcopy
|
|
}
|
|
}
|
|
|
|
// GetRules gets all allow or deny rules.
|
|
func (r *RoleV3) GetRules(rct RoleConditionType) []Rule {
|
|
if rct == Allow {
|
|
return r.Spec.Allow.Rules
|
|
}
|
|
return r.Spec.Deny.Rules
|
|
}
|
|
|
|
// SetRules sets an allow or deny rule.
|
|
func (r *RoleV3) SetRules(rct RoleConditionType, in []Rule) {
|
|
rcopy := CopyRulesSlice(in)
|
|
|
|
if rct == Allow {
|
|
r.Spec.Allow.Rules = rcopy
|
|
} else {
|
|
r.Spec.Deny.Rules = rcopy
|
|
}
|
|
}
|
|
|
|
// Check checks validity of all parameters and sets defaults
|
|
func (r *RoleV3) CheckAndSetDefaults() error {
|
|
err := r.Metadata.CheckAndSetDefaults()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// make sure we have defaults for all fields
|
|
if r.Spec.Options == nil {
|
|
r.Spec.Options = map[string]interface{}{
|
|
MaxSessionTTL: NewDuration(defaults.MaxCertDuration),
|
|
}
|
|
}
|
|
if r.Spec.Allow.Namespaces == nil {
|
|
r.Spec.Allow.Namespaces = []string{defaults.Namespace}
|
|
}
|
|
if r.Spec.Allow.NodeLabels == nil {
|
|
r.Spec.Allow.NodeLabels = map[string]string{Wildcard: Wildcard}
|
|
}
|
|
if r.Spec.Allow.Rules == nil {
|
|
r.Spec.Allow.Rules = CopyRulesSlice(AdminUserRules)
|
|
}
|
|
|
|
// if we find {{ or }} but the syntax is invalid, the role is invalid
|
|
for _, condition := range []RoleConditionType{Allow, Deny} {
|
|
for _, login := range r.GetLogins(condition) {
|
|
if strings.Contains(login, "{{") || strings.Contains(login, "}}") {
|
|
_, _, err := parse.IsRoleVariable(login)
|
|
if err != nil {
|
|
return trace.BadParameter("invalid login found: %v", login)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check and correct the session ttl
|
|
maxSessionTTL, err := r.Spec.Options.GetDuration(MaxSessionTTL)
|
|
if err != nil {
|
|
return trace.BadParameter("invalid duration: %v", err)
|
|
}
|
|
if maxSessionTTL.Duration == 0 {
|
|
r.Spec.Options.Set(MaxSessionTTL, NewDuration(defaults.MaxCertDuration))
|
|
}
|
|
if maxSessionTTL.Duration < defaults.MinCertDuration {
|
|
return trace.BadParameter("maximum session TTL can not be less than, minimal certificate duration")
|
|
}
|
|
|
|
// restrict wildcards
|
|
for _, login := range r.Spec.Allow.Logins {
|
|
if login == Wildcard {
|
|
return trace.BadParameter("wildcard matcher is not allowed in logins")
|
|
}
|
|
if !cstrings.IsValidUnixUser(login) {
|
|
return trace.BadParameter("%q is not a valid user name", login)
|
|
}
|
|
}
|
|
for key, val := range r.Spec.Allow.NodeLabels {
|
|
if key == Wildcard && val != Wildcard {
|
|
return trace.BadParameter("selector *:<val> is not supported")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// String returns the human readable representation of a role.
|
|
func (r *RoleV3) String() string {
|
|
return fmt.Sprintf("Role(Name=%v,Options=%v,Allow=%+v,Deny=%+v)",
|
|
r.GetName(), r.Spec.Options, r.Spec.Allow, r.Spec.Deny)
|
|
}
|
|
|
|
// RoleSpecV3 is role specification for RoleV3.
|
|
type RoleSpecV3 struct {
|
|
// Options is for OpenSSH options like agent forwarding.
|
|
Options RoleOptions `json:"options,omitempty"`
|
|
// Allow is the set of conditions evaluated to grant access.
|
|
Allow RoleConditions `json:"allow,omitempty"`
|
|
// Deny is the set of conditions evaluated to deny access. Deny takes priority over allow.
|
|
Deny RoleConditions `json:"deny,omitempty"`
|
|
}
|
|
|
|
// RoleOptions are key/value pairs that always exist for a role.
|
|
type RoleOptions map[string]interface{}
|
|
|
|
// UnmarshalJSON is used when parsing RoleV3 to convert MaxSessionTTL into the
|
|
// correct type.
|
|
func (o *RoleOptions) UnmarshalJSON(data []byte) error {
|
|
var raw map[string]interface{}
|
|
err := json.Unmarshal(data, &raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rmap := make(map[string]interface{})
|
|
for k, v := range raw {
|
|
switch k {
|
|
case MaxSessionTTL:
|
|
d, err := time.ParseDuration(v.(string))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rmap[MaxSessionTTL] = NewDuration(d)
|
|
default:
|
|
rmap[k] = v
|
|
}
|
|
}
|
|
|
|
*o = rmap
|
|
return nil
|
|
}
|
|
|
|
// Set an option key/value pair.
|
|
func (o RoleOptions) Set(key string, value interface{}) {
|
|
o[key] = value
|
|
}
|
|
|
|
// Get returns the option as an interface{}, it is the responsibility of the
|
|
// caller to convert to the correct type.
|
|
func (o RoleOptions) Get(key string) (interface{}, error) {
|
|
valueI, ok := o[key]
|
|
if !ok {
|
|
return nil, trace.NotFound("key %q not found in options", key)
|
|
}
|
|
|
|
return valueI, nil
|
|
}
|
|
|
|
// GetString returns the option as a string or returns an error.
|
|
func (o RoleOptions) GetString(key string) (string, error) {
|
|
valueI, ok := o[key]
|
|
if !ok {
|
|
return "", trace.NotFound("key %q not found in options", key)
|
|
}
|
|
|
|
value, ok := valueI.(string)
|
|
if !ok {
|
|
return "", trace.BadParameter("type %T for key %q is not a string", valueI, key)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// GetBoolean returns the option as a bool or returns an error.
|
|
func (o RoleOptions) GetBoolean(key string) (bool, error) {
|
|
valueI, ok := o[key]
|
|
if !ok {
|
|
return false, trace.NotFound("key %q not found in options", key)
|
|
}
|
|
|
|
value, ok := valueI.(bool)
|
|
if !ok {
|
|
return false, trace.BadParameter("type %T for key %q is not a bool", valueI, key)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// GetDuration returns the option as a services.Duration or returns an error.
|
|
func (o RoleOptions) GetDuration(key string) (Duration, error) {
|
|
valueI, ok := o[key]
|
|
if !ok {
|
|
return NewDuration(defaults.MinCertDuration), trace.NotFound("key %q not found in options", key)
|
|
}
|
|
|
|
value, ok := valueI.(Duration)
|
|
if !ok {
|
|
return NewDuration(defaults.MinCertDuration), trace.BadParameter("type %T for key %q is not a Duration", valueI, key)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// Equals checks if all the key/values in the RoleOptions map match.
|
|
func (o RoleOptions) Equals(other RoleOptions) bool {
|
|
return utils.InterfaceMapsEqual(o, other)
|
|
}
|
|
|
|
// RoleConditions is a set of conditions that must all match to be allowed or
|
|
// denied access.
|
|
type RoleConditions struct {
|
|
// Logins is a list of *nix system logins.
|
|
Logins []string `json:"logins,omitempty"`
|
|
// Namespaces is a list of namespaces (used to partition a cluster). The
|
|
// field should be called "namespaces" when it returns in Teleport 2.4.
|
|
Namespaces []string `json:"-"`
|
|
// NodeLabels is a map of node labels (used to dynamically grant access to nodes).
|
|
NodeLabels map[string]string `json:"node_labels,omitempty"`
|
|
|
|
// Rules is a list of rules and their access levels. Rules are a high level
|
|
// construct used for access control.
|
|
Rules []Rule `json:"rules,omitempty"`
|
|
}
|
|
|
|
// Equals returns true if the role conditions (logins, namespaces, labels,
|
|
// and rules) are equal and false if they are not.
|
|
func (r *RoleConditions) Equals(o RoleConditions) bool {
|
|
if !utils.StringSlicesEqual(r.Logins, o.Logins) {
|
|
return false
|
|
}
|
|
if !utils.StringSlicesEqual(r.Namespaces, o.Namespaces) {
|
|
return false
|
|
}
|
|
if !utils.StringMapsEqual(r.NodeLabels, o.NodeLabels) {
|
|
return false
|
|
}
|
|
if len(r.Rules) != len(o.Rules) {
|
|
return false
|
|
}
|
|
for i := range r.Rules {
|
|
if !r.Rules[i].Equals(o.Rules[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// NewRule creates a rule based on a resource name and a list of verbs
|
|
func NewRule(resource string, verbs []string) Rule {
|
|
return Rule{
|
|
Resources: []string{resource},
|
|
Verbs: verbs,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
Resources []string `json:"resources"`
|
|
// 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,
|
|
// this method also matches wildcard
|
|
func (r *Rule) HasVerb(verb string) bool {
|
|
for _, v := range r.Verbs {
|
|
if v == verb {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Equals returns true if the rule equals to another
|
|
func (r *Rule) Equals(other Rule) bool {
|
|
if !utils.StringSlicesEqual(r.Resources, other.Resources) {
|
|
return false
|
|
}
|
|
if !utils.StringSlicesEqual(r.Verbs, other.Verbs) {
|
|
return false
|
|
}
|
|
if !utils.StringSlicesEqual(r.Actions, other.Actions) {
|
|
return false
|
|
}
|
|
if r.Where != other.Where {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RuleSet maps resource to a set of rules defined for it
|
|
type RuleSet map[string][]Rule
|
|
|
|
// MatchRule tests if the resource name and verb are in a given list of rules.
|
|
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, nil
|
|
}
|
|
// check for wildcard resource matcher
|
|
for _, rule := range set[Wildcard] {
|
|
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] {
|
|
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, nil
|
|
}
|
|
|
|
// Slice returns slice from a set
|
|
func (set RuleSet) Slice() []Rule {
|
|
var out []Rule
|
|
for _, rules := range set {
|
|
out = append(out, rules...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MakeRuleSet converts slice of rules to the set of rules
|
|
func MakeRuleSet(rules []Rule) RuleSet {
|
|
set := make(RuleSet)
|
|
for _, rule := range rules {
|
|
for _, resource := range rule.Resources {
|
|
rules, ok := set[resource]
|
|
if !ok {
|
|
set[resource] = []Rule{rule}
|
|
} else {
|
|
rules = append(rules, rule)
|
|
set[resource] = rules
|
|
}
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
|
|
// CopyRulesSlice copies input slice of Rules and returns the copy
|
|
func CopyRulesSlice(in []Rule) []Rule {
|
|
out := make([]Rule, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
// RuleSlicesEqual returns true if two rule slices are equal
|
|
func RuleSlicesEqual(a, b []Rule) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if !a[i].Equals(b[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RoleV2 represents role resource specification
|
|
type RoleV2 struct {
|
|
// Kind is a resource kind - always resource
|
|
Kind string `json:"kind"`
|
|
// Version is a resource version
|
|
Version string `json:"version"`
|
|
// Metadata is Role metadata
|
|
Metadata Metadata `json:"metadata"`
|
|
// Spec contains role specification
|
|
Spec RoleSpecV2 `json:"spec"`
|
|
}
|
|
|
|
// Equals test roles for equality. Roles are considered equal if all resources,
|
|
// logins, namespaces, labels, and options match.
|
|
func (r *RoleV2) Equals(other Role) bool {
|
|
return r.V3().Equals(other)
|
|
}
|
|
|
|
// SetResource sets resource rule
|
|
func (r *RoleV2) SetResource(kind string, actions []string) {
|
|
if r.Spec.Resources == nil {
|
|
r.Spec.Resources = make(map[string][]string)
|
|
}
|
|
r.Spec.Resources[kind] = actions
|
|
}
|
|
|
|
// RemoveResource deletes resource entry
|
|
func (r *RoleV2) RemoveResource(kind string) {
|
|
delete(r.Spec.Resources, kind)
|
|
}
|
|
|
|
// SetLogins sets logins for role
|
|
func (r *RoleV2) SetLogins(logins []string) {
|
|
r.Spec.Logins = logins
|
|
}
|
|
|
|
// SetNodeLabels sets node labels for role
|
|
func (r *RoleV2) SetNodeLabels(labels map[string]string) {
|
|
r.Spec.NodeLabels = labels
|
|
}
|
|
|
|
// SetMaxSessionTTL sets a maximum TTL for SSH or Web session
|
|
func (r *RoleV2) SetMaxSessionTTL(duration time.Duration) {
|
|
r.Spec.MaxSessionTTL.Duration = duration
|
|
}
|
|
|
|
// SetExpiry sets expiry time for the object
|
|
func (r *RoleV2) SetExpiry(expires time.Time) {
|
|
r.Metadata.SetExpiry(expires)
|
|
}
|
|
|
|
// Expires retuns object expiry setting
|
|
func (r *RoleV2) Expiry() time.Time {
|
|
return r.Metadata.Expiry()
|
|
}
|
|
|
|
// SetTTL sets Expires header using realtime clock
|
|
func (r *RoleV2) SetTTL(clock clockwork.Clock, ttl time.Duration) {
|
|
r.Metadata.SetTTL(clock, ttl)
|
|
}
|
|
|
|
// SetName is a shortcut for SetMetadata().Name
|
|
func (r *RoleV2) SetName(s string) {
|
|
r.Metadata.Name = s
|
|
}
|
|
|
|
// GetName returns role name and is a shortcut for GetMetadata().Name
|
|
func (r *RoleV2) GetName() string {
|
|
return r.Metadata.Name
|
|
}
|
|
|
|
// GetMetadata returns role metadata
|
|
func (r *RoleV2) GetMetadata() Metadata {
|
|
return r.Metadata
|
|
}
|
|
|
|
// GetMaxSessionTTL is a maximum SSH or Web session TTL
|
|
func (r *RoleV2) GetMaxSessionTTL() Duration {
|
|
return r.Spec.MaxSessionTTL
|
|
}
|
|
|
|
// GetLogins returns a list of linux logins allowed for this role
|
|
func (r *RoleV2) GetLogins() []string {
|
|
return r.Spec.Logins
|
|
}
|
|
|
|
// GetNodeLabels returns a list of matchign nodes this role has access to
|
|
func (r *RoleV2) GetNodeLabels() map[string]string {
|
|
return r.Spec.NodeLabels
|
|
}
|
|
|
|
// GetNamespaces returns a list of namespaces this role has access to
|
|
func (r *RoleV2) GetNamespaces() []string {
|
|
return r.Spec.Namespaces
|
|
}
|
|
|
|
// SetNamespaces sets a list of namespaces this role has access to
|
|
func (r *RoleV2) SetNamespaces(namespaces []string) {
|
|
r.Spec.Namespaces = namespaces
|
|
}
|
|
|
|
// GetResources returns access to resources
|
|
func (r *RoleV2) GetResources() map[string][]string {
|
|
return r.Spec.Resources
|
|
}
|
|
|
|
// CanForwardAgent returns true if this role is allowed
|
|
// to request agent forwarding
|
|
func (r *RoleV2) CanForwardAgent() bool {
|
|
return r.Spec.ForwardAgent
|
|
}
|
|
|
|
// SetForwardAgent sets forward agent property
|
|
func (r *RoleV2) SetForwardAgent(forwardAgent bool) {
|
|
r.Spec.ForwardAgent = forwardAgent
|
|
}
|
|
|
|
// Check checks validity of all parameters and sets defaults
|
|
func (r *RoleV2) CheckAndSetDefaults() error {
|
|
// make sure we have defaults for all fields
|
|
if r.Metadata.Name == "" {
|
|
return trace.BadParameter("missing parameter Name")
|
|
}
|
|
if r.Metadata.Namespace == "" {
|
|
r.Metadata.Namespace = defaults.Namespace
|
|
}
|
|
if r.Spec.MaxSessionTTL.Duration == 0 {
|
|
r.Spec.MaxSessionTTL.Duration = defaults.MaxCertDuration
|
|
}
|
|
if r.Spec.MaxSessionTTL.Duration < defaults.MinCertDuration {
|
|
return trace.BadParameter("maximum session TTL can not be less than")
|
|
}
|
|
if r.Spec.Namespaces == nil {
|
|
r.Spec.Namespaces = []string{defaults.Namespace}
|
|
}
|
|
if r.Spec.NodeLabels == nil {
|
|
r.Spec.NodeLabels = map[string]string{Wildcard: Wildcard}
|
|
}
|
|
if r.Spec.Resources == nil {
|
|
r.Spec.Resources = map[string][]string{
|
|
KindSSHSession: RO(),
|
|
KindRole: RO(),
|
|
KindNode: RO(),
|
|
KindAuthServer: RO(),
|
|
KindReverseTunnel: RO(),
|
|
KindCertAuthority: RO(),
|
|
}
|
|
}
|
|
|
|
// restrict wildcards
|
|
for _, login := range r.Spec.Logins {
|
|
if login == Wildcard {
|
|
return trace.BadParameter("wildcard matcher is not allowed in logins")
|
|
}
|
|
if !cstrings.IsValidUnixUser(login) {
|
|
return trace.BadParameter("'%v' is not a valid user name", login)
|
|
}
|
|
}
|
|
for key, val := range r.Spec.NodeLabels {
|
|
if key == Wildcard && val != Wildcard {
|
|
return trace.BadParameter("selector *:<val> is not supported")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *RoleV2) V3() *RoleV3 {
|
|
role := &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: r.Metadata,
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: r.GetMaxSessionTTL(),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: r.GetLogins(),
|
|
Namespaces: r.GetNamespaces(),
|
|
NodeLabels: r.GetNodeLabels(),
|
|
},
|
|
},
|
|
rawObject: *r,
|
|
}
|
|
|
|
// translate old v2 agent forwarding to a v3 option
|
|
if r.CanForwardAgent() {
|
|
role.Spec.Options[ForwardAgent] = true
|
|
}
|
|
|
|
// translate old v2 resources to v3 rules
|
|
rules := []Rule{}
|
|
for resource, actions := range r.GetResources() {
|
|
var verbs []string
|
|
|
|
containsRead := utils.SliceContainsStr(actions, ActionRead)
|
|
containsWrite := utils.SliceContainsStr(actions, ActionWrite)
|
|
|
|
if containsRead && containsWrite {
|
|
verbs = RW()
|
|
} else if containsRead {
|
|
verbs = RO()
|
|
} else if containsWrite {
|
|
// in RoleV2 ActionWrite implied the ability to read secrets.
|
|
verbs = []string{VerbCreate, VerbUpdate, VerbDelete}
|
|
}
|
|
|
|
rules = append(rules, NewRule(resource, verbs))
|
|
}
|
|
role.Spec.Allow.Rules = rules
|
|
|
|
return role
|
|
}
|
|
|
|
func (r *RoleV2) String() string {
|
|
return fmt.Sprintf("Role(Name=%v,MaxSessionTTL=%v,Logins=%v,NodeLabels=%v,Namespaces=%v,Resources=%v,CanForwardAgent=%v)",
|
|
r.GetName(), r.GetMaxSessionTTL(), r.GetLogins(), r.GetNodeLabels(), r.GetNamespaces(), r.GetResources(), r.CanForwardAgent())
|
|
}
|
|
|
|
// RoleSpecV2 is role specification for RoleV2
|
|
type RoleSpecV2 struct {
|
|
// MaxSessionTTL is a maximum SSH or Web session TTL
|
|
MaxSessionTTL Duration `json:"max_session_ttl" yaml:"max_session_ttl"`
|
|
// Logins is a list of linux logins allowed for this role
|
|
Logins []string `json:"logins,omitempty" yaml:"logins,omitempty"`
|
|
// NodeLabels is a set of matching labels that users of this role
|
|
// will be allowed to access
|
|
NodeLabels map[string]string `json:"node_labels,omitempty" yaml:"node_labels,omitempty"`
|
|
// Namespaces is a list of namespaces, guarding accesss to resources
|
|
Namespaces []string `json:"namespaces,omitempty" yaml:"namespaces,omitempty"`
|
|
// Resources limits access to resources
|
|
Resources map[string][]string `json:"resources,omitempty" yaml:"resources,omitempty"`
|
|
// ForwardAgent permits SSH agent forwarding if requested by the client
|
|
ForwardAgent bool `json:"forward_agent" yaml:"forward_agent"`
|
|
}
|
|
|
|
// AccessChecker interface implements access checks for given role
|
|
type AccessChecker interface {
|
|
// CheckAccessToServer checks access to server.
|
|
CheckAccessToServer(login string, server Server) error
|
|
|
|
// CheckAccessToRule checks access to a rule within a namespace.
|
|
CheckAccessToRule(context RuleContext, namespace string, rule string, verb string) error
|
|
|
|
// CheckLoginDuration checks if role set can login up to given duration and
|
|
// returns a combined list of allowed logins.
|
|
CheckLoginDuration(ttl time.Duration) ([]string, error)
|
|
|
|
// AdjustSessionTTL will reduce the requested ttl to lowest max allowed TTL
|
|
// for this role set, otherwise it returns ttl unchanged
|
|
AdjustSessionTTL(ttl time.Duration) time.Duration
|
|
|
|
// CheckAgentForward checks if the role can request agent forward for this user
|
|
CheckAgentForward(login string) error
|
|
|
|
// CanForwardAgents returns true if this role set offers capability to forward agents
|
|
CanForwardAgents() bool
|
|
}
|
|
|
|
// FromSpec returns new RoleSet created from spec
|
|
func FromSpec(name string, spec RoleSpecV3) (RoleSet, error) {
|
|
role, err := NewRole(name, spec)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return NewRoleSet(role), nil
|
|
}
|
|
|
|
// RW is a shortcut that returns all verbs.
|
|
func RW() []string {
|
|
return []string{VerbConnect, VerbList, VerbCreate, VerbRead, VerbUpdate, VerbDelete}
|
|
}
|
|
|
|
// RO is a shortcut that returns read only verbs.
|
|
func RO() []string {
|
|
return []string{VerbList, VerbRead}
|
|
}
|
|
|
|
// NewRole constructs new standard role
|
|
func NewRole(name string, spec RoleSpecV3) (Role, error) {
|
|
role := RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: name,
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: spec,
|
|
}
|
|
if err := role.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &role, nil
|
|
}
|
|
|
|
// RoleGetter is an interface that defines GetRole method
|
|
type RoleGetter interface {
|
|
// GetRole returns role by name
|
|
GetRole(name string) (Role, error)
|
|
}
|
|
|
|
// FetchRoles fetches roles by their names, applies the traits to role
|
|
// variables, and returns the RoleSet.
|
|
func FetchRoles(roleNames []string, access RoleGetter, traits map[string][]string) (RoleSet, error) {
|
|
var roles []Role
|
|
|
|
for _, roleName := range roleNames {
|
|
role, err := access.GetRole(roleName)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
roles = append(roles, role.ApplyTraits(traits))
|
|
}
|
|
|
|
return NewRoleSet(roles...), nil
|
|
}
|
|
|
|
// NewRoleSet returns new RoleSet based on the roles
|
|
func NewRoleSet(roles ...Role) RoleSet {
|
|
return append(roles, NewImplicitRole())
|
|
}
|
|
|
|
// RoleSet is a set of roles that implements access control functionality
|
|
type RoleSet []Role
|
|
|
|
// MatchLogin returns true if attempted login matches any of the logins
|
|
func MatchLogin(logins []string, login string) bool {
|
|
for _, l := range logins {
|
|
if l == login {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MatchNamespace returns true if given list of namespace matches
|
|
// target namespace, wildcard matches everything
|
|
func MatchNamespace(selector []string, namespace string) bool {
|
|
for _, n := range selector {
|
|
if n == namespace || n == Wildcard {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MatchLabels matches selector against target
|
|
func MatchLabels(selector map[string]string, target map[string]string) bool {
|
|
// empty selector matches nothing
|
|
if len(selector) == 0 {
|
|
return false
|
|
}
|
|
// *: * matches everything even empty target set
|
|
if selector[Wildcard] == Wildcard {
|
|
return true
|
|
}
|
|
for key, val := range selector {
|
|
if targetVal, ok := target[key]; !ok || (val != targetVal && val != Wildcard) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// AdjustSessionTTL will reduce the requested ttl to lowest max allowed TTL
|
|
// for this role set, otherwise it returns ttl unchanges
|
|
func (set RoleSet) AdjustSessionTTL(ttl time.Duration) time.Duration {
|
|
for _, role := range set {
|
|
maxSessionTTL, err := role.GetOptions().GetDuration(MaxSessionTTL)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if ttl > maxSessionTTL.Duration {
|
|
ttl = maxSessionTTL.Duration
|
|
}
|
|
}
|
|
return ttl
|
|
}
|
|
|
|
// CheckLoginDuration checks if role set can login up to given duration and
|
|
// returns a combined list of allowed logins.
|
|
func (set RoleSet) CheckLoginDuration(ttl time.Duration) ([]string, error) {
|
|
logins := make(map[string]bool)
|
|
|
|
var matchedTTL bool
|
|
for _, role := range set {
|
|
maxSessionTTL, err := role.GetOptions().GetDuration(MaxSessionTTL)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if ttl <= maxSessionTTL.Duration && maxSessionTTL.Duration != 0 {
|
|
matchedTTL = true
|
|
|
|
for _, login := range role.GetLogins(Allow) {
|
|
logins[login] = true
|
|
}
|
|
}
|
|
}
|
|
if !matchedTTL {
|
|
return nil, trace.AccessDenied("this user cannot request a certificate for %v", ttl)
|
|
}
|
|
if len(logins) == 0 {
|
|
return nil, trace.AccessDenied("this user cannot create SSH sessions, has no allowed logins")
|
|
}
|
|
out := make([]string, 0, len(logins))
|
|
for login := range logins {
|
|
out = append(out, login)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// CheckAccessToServer checks if a role has access to a node. Deny rules are
|
|
// checked first then allow rules. Access to a node is determined by
|
|
// namespaces, labels, and logins.
|
|
func (set RoleSet) CheckAccessToServer(login string, s Server) error {
|
|
var errs []error
|
|
|
|
// check deny rules first: a single matching namespace, label, or login from
|
|
// the deny role set prohibits access.
|
|
for _, role := range set {
|
|
matchNamespace := MatchNamespace(role.GetNamespaces(Deny), s.GetNamespace())
|
|
matchLabels := MatchLabels(role.GetNodeLabels(Deny), s.GetAllLabels())
|
|
matchLogin := MatchLogin(role.GetLogins(Deny), login)
|
|
if matchNamespace || matchLabels || matchLogin {
|
|
errorMessage := fmt.Sprintf("role %v denied access to node %v: deny rule matched; match(namespace=%v, label=%v, login=%v)",
|
|
role.GetName(), s.GetHostname(), matchNamespace, matchLabels, matchLogin)
|
|
log.Warnf("[RBAC] Denied access to server: " + errorMessage)
|
|
return trace.AccessDenied(errorMessage)
|
|
}
|
|
}
|
|
|
|
// check allow rules: namespace, label, and login have to all match in
|
|
// one role in the role set to be granted access.
|
|
for _, role := range set {
|
|
matchNamespace := MatchNamespace(role.GetNamespaces(Allow), s.GetNamespace())
|
|
matchLabels := MatchLabels(role.GetNodeLabels(Allow), s.GetAllLabels())
|
|
matchLogin := MatchLogin(role.GetLogins(Allow), login)
|
|
if matchNamespace && matchLabels && matchLogin {
|
|
return nil
|
|
}
|
|
|
|
errorMessage := fmt.Sprintf("role %v denied access: allow rules did not match; match(namespace=%v, label=%v, login=%v)",
|
|
role.GetName(), matchNamespace, matchLabels, matchLogin)
|
|
errs = append(errs, trace.AccessDenied(errorMessage))
|
|
}
|
|
|
|
errorMessage := fmt.Sprintf("access to node %v is denied to role(s): %v", s.GetHostname(), errs)
|
|
log.Warnf("[RBAC] Denied access to server: " + errorMessage)
|
|
return trace.AccessDenied(errorMessage)
|
|
}
|
|
|
|
// CanForwardAgents returns true if role set allows forwarding agents.
|
|
func (set RoleSet) CanForwardAgents() bool {
|
|
for _, role := range set {
|
|
forwardAgent, err := role.GetOptions().GetBoolean(ForwardAgent)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if forwardAgent == true {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CheckAgentForward checks if the role can request to forward the SSH agent
|
|
// for this user.
|
|
func (set RoleSet) CheckAgentForward(login string) error {
|
|
// check if we have permission to login and forward agent. we don't check
|
|
// for deny rules because if you can't forward an agent if you can't login
|
|
// in the first place.
|
|
for _, role := range set {
|
|
for _, l := range role.GetLogins(Allow) {
|
|
forwardAgent, err := role.GetOptions().GetBoolean(ForwardAgent)
|
|
if err != nil {
|
|
return trace.AccessDenied("unable to parse ForwardAgent: %v", err)
|
|
}
|
|
if forwardAgent && l == login {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return trace.AccessDenied("%v can not forward agent for %v", set, login)
|
|
}
|
|
|
|
func (set RoleSet) String() string {
|
|
if len(set) == 0 {
|
|
return "user without assigned roles"
|
|
}
|
|
roleNames := make([]string, len(set))
|
|
for i, role := range set {
|
|
roleNames[i] = role.GetName()
|
|
}
|
|
return fmt.Sprintf("roles %v", strings.Join(roleNames, ","))
|
|
}
|
|
|
|
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 {
|
|
matched, err := MakeRuleSet(role.GetRules(Deny)).Match(whereParser, actionsParser, resource, verb)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matched {
|
|
log.Infof("[RBAC] %s access to %s [namespace %s] denied for role %q: deny rule matched", verb, resource, namespace, role.GetName())
|
|
return trace.AccessDenied("access denied to perform action '%s' on %s", verb, resource)
|
|
}
|
|
}
|
|
}
|
|
|
|
// check allow: if rule matches, grant access to resource
|
|
for _, role := range set {
|
|
matchNamespace := MatchNamespace(role.GetNamespaces(Allow), ProcessNamespace(namespace))
|
|
if matchNamespace {
|
|
match, err := MakeRuleSet(role.GetRules(Allow)).Match(whereParser, actionsParser, resource, verb)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if match {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Infof("[RBAC] %s access to %s [namespace %s] denied for %v: no allow rule matched", verb, resource, namespace, set)
|
|
return trace.AccessDenied("access denied to perform action '%s' on %s", verb, resource)
|
|
}
|
|
|
|
// ProcessNamespace sets default namespace in case if namespace is empty
|
|
func ProcessNamespace(namespace string) string {
|
|
if namespace == "" {
|
|
return defaults.Namespace
|
|
}
|
|
return namespace
|
|
}
|
|
|
|
// MaxDuration returns maximum duration that is possible
|
|
func MaxDuration() Duration {
|
|
return NewDuration(1<<63 - 1)
|
|
}
|
|
|
|
// NewDuration returns Duration struct based on time.Duration
|
|
func NewDuration(d time.Duration) Duration {
|
|
return Duration{Duration: d}
|
|
}
|
|
|
|
// Duration is a wrapper around duration to set up custom marshal/unmarshal
|
|
type Duration struct {
|
|
time.Duration
|
|
}
|
|
|
|
// MarshalJSON marshals Duration to string
|
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(fmt.Sprintf("%v", d.Duration))
|
|
}
|
|
|
|
// UnmarshalJSON marshals Duration to string
|
|
func (d *Duration) UnmarshalJSON(data []byte) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
var stringVar string
|
|
if err := json.Unmarshal(data, &stringVar); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
out, err := time.ParseDuration(stringVar)
|
|
if err != nil {
|
|
return trace.BadParameter(err.Error())
|
|
}
|
|
d.Duration = out
|
|
return nil
|
|
}
|
|
|
|
func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
var stringVar string
|
|
if err := unmarshal(&stringVar); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
out, err := time.ParseDuration(stringVar)
|
|
if err != nil {
|
|
return trace.BadParameter(err.Error())
|
|
}
|
|
d.Duration = out
|
|
return nil
|
|
}
|
|
|
|
const RoleSpecV3SchemaTemplate = `{
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"max_session_ttl": { "type": "string" },
|
|
"options": {
|
|
"type": "object",
|
|
"patternProperties": {
|
|
"^[a-zA-Z/.0-9_]$": { "type": "string" }
|
|
}
|
|
},
|
|
"allow": { "$ref": "#/definitions/role_condition" },
|
|
"deny": { "$ref": "#/definitions/role_condition" }%v
|
|
}
|
|
}
|
|
`
|
|
|
|
const RoleSpecV3SchemaDefinitions = `
|
|
"definitions": {
|
|
"role_condition": {
|
|
"namespaces": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"node_labels": {
|
|
"type": "object",
|
|
"patternProperties": {
|
|
"^[a-zA-Z/.0-9_]$": { "type": "string" }
|
|
}
|
|
},
|
|
"logins": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"rules": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"resources": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"verbs": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"where": {
|
|
"type": "string"
|
|
},
|
|
"actions": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const RoleSpecV2SchemaTemplate = `{
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"max_session_ttl": {"type": "string"},
|
|
"forward_agent": {"type": "boolean"},
|
|
"node_labels": {
|
|
"type": "object",
|
|
"patternProperties": {
|
|
"^[a-zA-Z/.0-9_]$": { "type": "string" }
|
|
}
|
|
},
|
|
"namespaces": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"logins": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"patternProperties": {
|
|
"^[a-zA-Z/.0-9_]$": { "type": "array", "items": {"type": "string"} }
|
|
}
|
|
}%v
|
|
}
|
|
}`
|
|
|
|
// GetRoleSchema returns role schema for the version requested with optionally
|
|
// injected schema for extensions.
|
|
func GetRoleSchema(version string, extensionSchema string) string {
|
|
schemaDefinitions := "," + RoleSpecV3SchemaDefinitions
|
|
if version == V2 {
|
|
schemaDefinitions = DefaultDefinitions
|
|
}
|
|
|
|
schemaTemplate := RoleSpecV3SchemaTemplate
|
|
if version == V2 {
|
|
schemaTemplate = RoleSpecV2SchemaTemplate
|
|
}
|
|
|
|
schema := fmt.Sprintf(schemaTemplate, ``)
|
|
if extensionSchema != "" {
|
|
schema = fmt.Sprintf(schemaTemplate, ","+extensionSchema)
|
|
}
|
|
|
|
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, schema, schemaDefinitions)
|
|
}
|
|
|
|
// UnmarshalRole unmarshals role from JSON, sets defaults, and checks schema.
|
|
func UnmarshalRole(data []byte) (*RoleV3, error) {
|
|
var h ResourceHeader
|
|
err := json.Unmarshal(data, &h)
|
|
if err != nil {
|
|
h.Version = V2
|
|
}
|
|
|
|
switch h.Version {
|
|
case V2:
|
|
var role RoleV2
|
|
if err := utils.UnmarshalWithSchema(GetRoleSchema(V2, ""), &role, data); err != nil {
|
|
return nil, trace.BadParameter(err.Error())
|
|
}
|
|
|
|
if err := role.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
roleV3 := role.V3()
|
|
roleV3.rawObject = role
|
|
|
|
return roleV3, nil
|
|
case V3:
|
|
var role RoleV3
|
|
if err := utils.UnmarshalWithSchema(GetRoleSchema(V3, ""), &role, data); err != nil {
|
|
return nil, trace.BadParameter(err.Error())
|
|
}
|
|
|
|
if err := role.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &role, nil
|
|
}
|
|
|
|
return nil, trace.BadParameter("role version %q is not supported", h.Version)
|
|
}
|
|
|
|
var roleMarshaler RoleMarshaler = &TeleportRoleMarshaler{}
|
|
|
|
func SetRoleMarshaler(m RoleMarshaler) {
|
|
marshalerMutex.Lock()
|
|
defer marshalerMutex.Unlock()
|
|
roleMarshaler = m
|
|
}
|
|
|
|
func GetRoleMarshaler() RoleMarshaler {
|
|
marshalerMutex.Lock()
|
|
defer marshalerMutex.Unlock()
|
|
return roleMarshaler
|
|
}
|
|
|
|
// RoleMarshaler implements marshal/unmarshal of Role implementations
|
|
// mostly adds support for extended versions
|
|
type RoleMarshaler interface {
|
|
// UnmarshalRole from binary representation
|
|
UnmarshalRole(bytes []byte) (Role, error)
|
|
// MarshalRole to binary representation
|
|
MarshalRole(u Role, opts ...MarshalOption) ([]byte, error)
|
|
}
|
|
|
|
type TeleportRoleMarshaler struct{}
|
|
|
|
// UnmarshalRole unmarshals role from JSON.
|
|
func (*TeleportRoleMarshaler) UnmarshalRole(bytes []byte) (Role, error) {
|
|
return UnmarshalRole(bytes)
|
|
}
|
|
|
|
// MarshalRole marshalls role into JSON.
|
|
func (*TeleportRoleMarshaler) MarshalRole(u Role, opts ...MarshalOption) ([]byte, error) {
|
|
return json.Marshal(u)
|
|
}
|
|
|
|
// SortedRoles sorts roles by name
|
|
type SortedRoles []Role
|
|
|
|
// Len returns length of a role list
|
|
func (s SortedRoles) Len() int {
|
|
return len(s)
|
|
}
|
|
|
|
// Less compares roles by name
|
|
func (s SortedRoles) Less(i, j int) bool {
|
|
return s[i].GetName() < s[j].GetName()
|
|
}
|
|
|
|
// Swap swaps two roles in a list
|
|
func (s SortedRoles) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|