mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:27:24 +08:00
This PR removes the `types.KindKubeService` objects that were deprecated in Teleport 11. Teleport 12 removed the double heartbeat and for Teleport 13 we can safely drop the support of the old hearbeat based on `types.Server` objects. Clients older than Teleport 12 connecting to a Teleport 13 server will fail since they cannot heartbeat using the old mechanism.
3193 lines
103 KiB
Go
3193 lines
103 KiB
Go
/*
|
|
Copyright 2021 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 (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/arn"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/configure/cstrings"
|
|
"github.com/gravitational/trace"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/vulcand/predicate"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/teleport/api/defaults"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
apiutils "github.com/gravitational/teleport/api/utils"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
awsutils "github.com/gravitational/teleport/lib/utils/aws"
|
|
"github.com/gravitational/teleport/lib/utils/parse"
|
|
)
|
|
|
|
// DefaultImplicitRules provides access to the default set of implicit rules
|
|
// assigned to all roles.
|
|
var DefaultImplicitRules = []types.Rule{
|
|
types.NewRule(types.KindNode, RO()),
|
|
types.NewRule(types.KindProxy, RO()),
|
|
types.NewRule(types.KindAuthServer, RO()),
|
|
types.NewRule(types.KindReverseTunnel, RO()),
|
|
types.NewRule(types.KindCertAuthority, ReadNoSecrets()),
|
|
types.NewRule(types.KindClusterAuthPreference, RO()),
|
|
types.NewRule(types.KindClusterName, RO()),
|
|
types.NewRule(types.KindSSHSession, RO()),
|
|
types.NewRule(types.KindAppServer, RO()),
|
|
types.NewRule(types.KindRemoteCluster, RO()),
|
|
types.NewRule(types.KindKubeServer, RO()),
|
|
types.NewRule(types.KindDatabaseServer, RO()),
|
|
types.NewRule(types.KindDatabase, RO()),
|
|
types.NewRule(types.KindApp, RO()),
|
|
types.NewRule(types.KindWindowsDesktopService, RO()),
|
|
types.NewRule(types.KindWindowsDesktop, RO()),
|
|
types.NewRule(types.KindKubernetesCluster, RO()),
|
|
types.NewRule(types.KindUsageEvent, []string{types.VerbCreate}),
|
|
}
|
|
|
|
// DefaultCertAuthorityRules provides access the minimal set of resources
|
|
// needed for a certificate authority to function.
|
|
var DefaultCertAuthorityRules = []types.Rule{
|
|
types.NewRule(types.KindSession, RO()),
|
|
types.NewRule(types.KindNode, RO()),
|
|
types.NewRule(types.KindAuthServer, RO()),
|
|
types.NewRule(types.KindReverseTunnel, RO()),
|
|
types.NewRule(types.KindCertAuthority, ReadNoSecrets()),
|
|
}
|
|
|
|
// ErrTrustedDeviceRequired is returned by AccessChecker when access to a
|
|
// resource requires a trusted device.
|
|
var ErrTrustedDeviceRequired = trace.AccessDenied("access to resource requires a trusted device")
|
|
|
|
// ErrSessionMFARequired is returned by AccessChecker when access to a resource
|
|
// requires an MFA check.
|
|
var ErrSessionMFARequired = trace.AccessDenied("access to resource requires MFA")
|
|
|
|
// 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
|
|
}
|
|
|
|
// NewImplicitRole is the default implicit role that gets added to all
|
|
// RoleSets.
|
|
func NewImplicitRole() types.Role {
|
|
return &types.RoleV6{
|
|
Kind: types.KindRole,
|
|
Version: types.V3,
|
|
Metadata: types.Metadata{
|
|
Name: constants.DefaultImplicitRole,
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: types.RoleSpecV6{
|
|
Options: types.RoleOptions{
|
|
MaxSessionTTL: types.MaxDuration(),
|
|
// Explicitly disable options that default to true, otherwise the option
|
|
// will always be enabled, as this implicit role is part of every role set.
|
|
PortForwarding: types.NewBoolOption(false),
|
|
RecordSession: &types.RecordSession{
|
|
Desktop: types.NewBoolOption(false),
|
|
},
|
|
},
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: types.CopyRulesSlice(DefaultImplicitRules),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// RoleForUser creates an admin role for a services.User.
|
|
//
|
|
// Used in tests only.
|
|
func RoleForUser(u types.User) types.Role {
|
|
role, _ := types.NewRole(RoleNameForUser(u.GetName()), types.RoleSpecV6{
|
|
Options: types.RoleOptions{
|
|
CertificateFormat: constants.CertificateFormatStandard,
|
|
MaxSessionTTL: types.NewDuration(defaults.MaxCertDuration),
|
|
PortForwarding: types.NewBoolOption(true),
|
|
ForwardAgent: types.NewBool(true),
|
|
BPF: defaults.EnhancedEvents(),
|
|
},
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseServiceLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindRole, RW()),
|
|
types.NewRule(types.KindAuthConnector, RW()),
|
|
types.NewRule(types.KindSession, RO()),
|
|
types.NewRule(types.KindTrustedCluster, RW()),
|
|
types.NewRule(types.KindEvent, RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, RW()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, RW()),
|
|
types.NewRule(types.KindSessionRecordingConfig, RW()),
|
|
types.NewRule(types.KindUIConfig, RW()),
|
|
types.NewRule(types.KindApp, RW()),
|
|
types.NewRule(types.KindDatabase, RW()),
|
|
types.NewRule(types.KindLock, RW()),
|
|
types.NewRule(types.KindToken, RW()),
|
|
types.NewRule(types.KindConnectionDiagnostic, RW()),
|
|
types.NewRule(types.KindKubernetesCluster, RW()),
|
|
types.NewRule(types.KindSessionTracker, RO()),
|
|
},
|
|
JoinSessions: []*types.SessionJoinPolicy{
|
|
{
|
|
Name: "foo",
|
|
Roles: []string{"*"},
|
|
Kinds: []string{string(types.SSHSessionKind)},
|
|
Modes: []string{string(types.SessionPeerMode)},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
return role
|
|
}
|
|
|
|
// RoleForCertAuthority creates role using types.CertAuthority.
|
|
func RoleForCertAuthority(ca types.CertAuthority) types.Role {
|
|
role, _ := types.NewRole(RoleNameForCertAuthority(ca.GetClusterName()), types.RoleSpecV6{
|
|
Options: types.RoleOptions{
|
|
MaxSessionTTL: types.NewDuration(defaults.MaxCertDuration),
|
|
},
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: types.CopyRulesSlice(DefaultCertAuthorityRules),
|
|
},
|
|
})
|
|
return role
|
|
}
|
|
|
|
// ValidateRoleName checks that the role name is allowed to be created.
|
|
func ValidateRoleName(role types.Role) error {
|
|
// System role names are not allowed.
|
|
systemRoles := types.SystemRoles([]types.SystemRole{
|
|
types.SystemRole(role.GetMetadata().Name),
|
|
})
|
|
if err := systemRoles.Check(); err == nil {
|
|
return trace.BadParameter("reserved role: %s", role.GetMetadata().Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateRole parses validates the role, and sets default values.
|
|
func ValidateRole(r types.Role) error {
|
|
if err := r.CheckAndSetDefaults(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// if we find {{ or }} but the syntax is invalid, the role is invalid
|
|
for _, condition := range []types.RoleConditionType{types.Allow, types.Deny} {
|
|
for _, login := range r.GetLogins(condition) {
|
|
if strings.Contains(login, "{{") || strings.Contains(login, "}}") {
|
|
_, err := parse.NewExpression(login)
|
|
if err != nil {
|
|
return trace.BadParameter("invalid login found: %v", login)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rules := append(r.GetRules(types.Allow), r.GetRules(types.Deny)...)
|
|
for _, rule := range rules {
|
|
if err := validateRule(rule); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateRule parses the where and action fields to validate the rule.
|
|
func validateRule(r types.Rule) error {
|
|
if len(r.Where) != 0 {
|
|
parser, err := NewWhereParser(&Context{})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
_, err = parser.Parse(r.Where)
|
|
if err != nil {
|
|
return trace.BadParameter("could not parse 'where' rule: %q, error: %v", r.Where, err)
|
|
}
|
|
}
|
|
|
|
if len(r.Actions) != 0 {
|
|
parser, err := NewActionsParser(&Context{})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
for i, action := range r.Actions {
|
|
_, err = parser.Parse(action)
|
|
if err != nil {
|
|
return trace.BadParameter("could not parse action %v %q, error: %v", i, action, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func filterInvalidUnixLogins(candidates []string) []string {
|
|
// The tests for `ApplyTraits()` require that an empty list is nil
|
|
// rather than a 0-size slice, and I don't understand the potential
|
|
// knock-on effects of changing that, so the default value is `nil`
|
|
output := []string(nil)
|
|
|
|
for _, candidate := range candidates {
|
|
if !cstrings.IsValidUnixUser(candidate) {
|
|
log.Debugf("Skipping login %v, not a valid Unix login.", candidate)
|
|
continue
|
|
}
|
|
|
|
// A valid variable was found in the traits, append it to the list of logins.
|
|
output = append(output, candidate)
|
|
}
|
|
return output
|
|
}
|
|
|
|
func filterInvalidWindowsLogins(candidates []string) []string {
|
|
var output []string
|
|
|
|
// https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb726984(v=technet.10)
|
|
const invalidChars = `"/\[]:;|=,+*?<>`
|
|
|
|
for _, candidate := range candidates {
|
|
if strings.ContainsAny(candidate, invalidChars) {
|
|
log.Debugf("Skipping Windows login %v, not a valid Windows login.", candidate)
|
|
continue
|
|
}
|
|
|
|
output = append(output, candidate)
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
func warnInvalidAzureIdentities(candidates []string) {
|
|
for _, candidate := range candidates {
|
|
if !MatchValidAzureIdentity(candidate) {
|
|
log.Warningf("Invalid format of Azure identity %q", candidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ParseResourceID from Azure SDK is too lenient; we use a strict regexp instead.
|
|
var azureIdentityPattern = regexp.MustCompile(`(?i)^/subscriptions/([a-fA-F0-9-]+)/resourceGroups/([0-9a-zA-Z-_]+)/providers/Microsoft\.ManagedIdentity/userAssignedIdentities/([0-9a-zA-Z-_]+)$`)
|
|
|
|
func MatchValidAzureIdentity(identity string) bool {
|
|
if identity == types.Wildcard {
|
|
return true
|
|
}
|
|
|
|
return azureIdentityPattern.MatchString(identity)
|
|
}
|
|
|
|
// ApplyTraits applies the passed in traits to any variables within the role
|
|
// and returns itself.
|
|
func ApplyTraits(r types.Role, traits map[string][]string) types.Role {
|
|
for _, condition := range []types.RoleConditionType{types.Allow, types.Deny} {
|
|
inLogins := r.GetLogins(condition)
|
|
outLogins := applyValueTraitsSlice(inLogins, traits, "login")
|
|
outLogins = filterInvalidUnixLogins(outLogins)
|
|
r.SetLogins(condition, apiutils.Deduplicate(outLogins))
|
|
|
|
inWindowsLogins := r.GetWindowsLogins(condition)
|
|
outWindowsLogins := applyValueTraitsSlice(inWindowsLogins, traits, "windows_login")
|
|
outWindowsLogins = filterInvalidWindowsLogins(outWindowsLogins)
|
|
r.SetWindowsLogins(condition, apiutils.Deduplicate(outWindowsLogins))
|
|
|
|
inRoleARNs := r.GetAWSRoleARNs(condition)
|
|
outRoleARNs := applyValueTraitsSlice(inRoleARNs, traits, "AWS role ARN")
|
|
r.SetAWSRoleARNs(condition, apiutils.Deduplicate(outRoleARNs))
|
|
|
|
inAzureIdentities := r.GetAzureIdentities(condition)
|
|
outAzureIdentities := applyValueTraitsSlice(inAzureIdentities, traits, "Azure identity")
|
|
warnInvalidAzureIdentities(outAzureIdentities)
|
|
r.SetAzureIdentities(condition, apiutils.Deduplicate(outAzureIdentities))
|
|
|
|
inGCPAccounts := r.GetGCPServiceAccounts(condition)
|
|
outGCPAccounts := applyValueTraitsSlice(inGCPAccounts, traits, "GCP service account")
|
|
r.SetGCPServiceAccounts(condition, apiutils.Deduplicate(outGCPAccounts))
|
|
|
|
// apply templates to kubernetes groups
|
|
inKubeGroups := r.GetKubeGroups(condition)
|
|
outKubeGroups := applyValueTraitsSlice(inKubeGroups, traits, "kube group")
|
|
r.SetKubeGroups(condition, apiutils.Deduplicate(outKubeGroups))
|
|
|
|
// apply templates to kubernetes users
|
|
inKubeUsers := r.GetKubeUsers(condition)
|
|
outKubeUsers := applyValueTraitsSlice(inKubeUsers, traits, "kube user")
|
|
r.SetKubeUsers(condition, apiutils.Deduplicate(outKubeUsers))
|
|
|
|
// apply templates to database names
|
|
inDbNames := r.GetDatabaseNames(condition)
|
|
outDbNames := applyValueTraitsSlice(inDbNames, traits, "database name")
|
|
r.SetDatabaseNames(condition, apiutils.Deduplicate(outDbNames))
|
|
|
|
// apply templates to database users
|
|
inDbUsers := r.GetDatabaseUsers(condition)
|
|
outDbUsers := applyValueTraitsSlice(inDbUsers, traits, "database user")
|
|
r.SetDatabaseUsers(condition, apiutils.Deduplicate(outDbUsers))
|
|
|
|
// apply templates to node labels
|
|
inLabels := r.GetNodeLabels(condition)
|
|
if inLabels != nil {
|
|
r.SetNodeLabels(condition, applyLabelsTraits(inLabels, traits))
|
|
}
|
|
|
|
// apply templates to cluster labels
|
|
inLabels = r.GetClusterLabels(condition)
|
|
if inLabels != nil {
|
|
r.SetClusterLabels(condition, applyLabelsTraits(inLabels, traits))
|
|
}
|
|
|
|
// apply templates to kube labels
|
|
inLabels = r.GetKubernetesLabels(condition)
|
|
if inLabels != nil {
|
|
r.SetKubernetesLabels(condition, applyLabelsTraits(inLabels, traits))
|
|
}
|
|
|
|
// apply templates to app labels
|
|
inLabels = r.GetAppLabels(condition)
|
|
if inLabels != nil {
|
|
r.SetAppLabels(condition, applyLabelsTraits(inLabels, traits))
|
|
}
|
|
|
|
// apply templates to database labels
|
|
inLabels = r.GetDatabaseLabels(condition)
|
|
if inLabels != nil {
|
|
r.SetDatabaseLabels(condition, applyLabelsTraits(inLabels, traits))
|
|
}
|
|
|
|
// apply templates to windows desktop labels
|
|
inLabels = r.GetWindowsDesktopLabels(condition)
|
|
if inLabels != nil {
|
|
r.SetWindowsDesktopLabels(condition, applyLabelsTraits(inLabels, traits))
|
|
}
|
|
|
|
r.SetHostGroups(condition,
|
|
applyValueTraitsSlice(r.GetHostGroups(condition), traits, "host_groups"))
|
|
|
|
r.SetHostSudoers(condition,
|
|
applyValueTraitsSlice(r.GetHostSudoers(condition), traits, "host_sudoers"))
|
|
|
|
options := r.GetOptions()
|
|
for i, ext := range options.CertExtensions {
|
|
vals, err := ApplyValueTraits(ext.Value, traits)
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
log.Warnf("Did not apply trait to cert_extensions.value: %v", err)
|
|
continue
|
|
}
|
|
if len(vals) != 0 {
|
|
options.CertExtensions[i].Value = vals[0]
|
|
}
|
|
}
|
|
|
|
// apply templates to impersonation conditions
|
|
inCond := r.GetImpersonateConditions(condition)
|
|
var outCond types.ImpersonateConditions
|
|
outCond.Users = applyValueTraitsSlice(inCond.Users, traits, "impersonate user")
|
|
outCond.Roles = applyValueTraitsSlice(inCond.Roles, traits, "impersonate role")
|
|
outCond.Users = apiutils.Deduplicate(outCond.Users)
|
|
outCond.Roles = apiutils.Deduplicate(outCond.Roles)
|
|
outCond.Where = inCond.Where
|
|
r.SetImpersonateConditions(condition, outCond)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// applyValueTraitsSlice iterates over a slice of input strings, calling
|
|
// ApplyValueTraits on each.
|
|
func applyValueTraitsSlice(inputs []string, traits map[string][]string, fieldName string) []string {
|
|
var output []string
|
|
for _, value := range inputs {
|
|
outputs, err := ApplyValueTraits(value, traits)
|
|
if err != nil {
|
|
if !trace.IsNotFound(err) {
|
|
log.WithError(err).Debugf("Skipping %s %q.", fieldName, value)
|
|
}
|
|
continue
|
|
}
|
|
output = append(output, outputs...)
|
|
}
|
|
return output
|
|
}
|
|
|
|
// applyLabelsTraits interpolates variables based on the templates
|
|
// and traits from identity provider. For example:
|
|
//
|
|
// cluster_labels:
|
|
//
|
|
// env: ['{{external.groups}}']
|
|
//
|
|
// and groups: ['admins', 'devs']
|
|
//
|
|
// will be interpolated to:
|
|
//
|
|
// cluster_labels:
|
|
//
|
|
// env: ['admins', 'devs']
|
|
func applyLabelsTraits(inLabels types.Labels, traits map[string][]string) types.Labels {
|
|
outLabels := make(types.Labels, len(inLabels))
|
|
// every key will be mapped to the first value
|
|
for key, vals := range inLabels {
|
|
keyVars, err := ApplyValueTraits(key, traits)
|
|
if err != nil {
|
|
// empty key will not match anything
|
|
log.Debugf("Setting empty node label pair %q -> %q: %v", key, vals, err)
|
|
keyVars = []string{""}
|
|
}
|
|
|
|
var values []string
|
|
for _, val := range vals {
|
|
valVars, err := ApplyValueTraits(val, traits)
|
|
if err != nil {
|
|
log.Debugf("Setting empty node label value %q -> %q: %v", key, val, err)
|
|
// empty value will not match anything
|
|
valVars = []string{""}
|
|
}
|
|
values = append(values, valVars...)
|
|
}
|
|
outLabels[keyVars[0]] = apiutils.Deduplicate(values)
|
|
}
|
|
return outLabels
|
|
}
|
|
|
|
// ApplyValueTraits applies the passed in traits to the variable,
|
|
// returns BadParameter in case if referenced variable is unsupported,
|
|
// returns NotFound in case if referenced trait is missing,
|
|
// mapped list of values otherwise, the function guarantees to return
|
|
// at least one value in case if return value is nil
|
|
func ApplyValueTraits(val string, traits map[string][]string) ([]string, error) {
|
|
// Extract the variable from the role variable.
|
|
expr, err := parse.NewExpression(val)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
varValidation := func(namespace string, name string) error {
|
|
// verify that internal traits match the supported variables
|
|
if namespace == teleport.TraitInternalPrefix {
|
|
switch name {
|
|
case constants.TraitLogins, constants.TraitWindowsLogins,
|
|
constants.TraitKubeGroups, constants.TraitKubeUsers,
|
|
constants.TraitDBNames, constants.TraitDBUsers,
|
|
constants.TraitAWSRoleARNs, constants.TraitAzureIdentities,
|
|
constants.TraitGCPServiceAccounts, teleport.TraitJWT:
|
|
default:
|
|
return trace.BadParameter("unsupported variable %q", name)
|
|
}
|
|
}
|
|
// TODO: return a not found error if the variable namespace is not
|
|
// the namespace of `traits`.
|
|
// If e.g. the `traits` belong to the "internal" namespace (as the
|
|
// validation above suggests), and "foo" is a key in `traits`, then
|
|
// "external.foo" will return the value of "internal.foo". This is
|
|
// incorrect, and a not found error should be returned instead.
|
|
// This would be similar to the var validation done in getPAMConfig
|
|
// (lib/srv/ctx.go).
|
|
return nil
|
|
}
|
|
interpolated, err := expr.Interpolate(varValidation, traits)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if len(interpolated) == 0 {
|
|
return nil, trace.NotFound("variable interpolation result is empty")
|
|
}
|
|
return interpolated, nil
|
|
}
|
|
|
|
// ruleScore is a sorting score of the rule, the larger the score, the more
|
|
// specific the rule is
|
|
func ruleScore(r *types.Rule) int {
|
|
score := 0
|
|
// wildcard rules are less specific
|
|
if slices.Contains(r.Resources, types.Wildcard) {
|
|
score -= 4
|
|
} else if len(r.Resources) == 1 {
|
|
// rules that match specific resource are more specific than
|
|
// fields that match several resources
|
|
score += 2
|
|
}
|
|
// rules that have wildcard verbs are less specific
|
|
if slices.Contains(r.Verbs, types.Wildcard) {
|
|
score -= 2
|
|
}
|
|
// rules that supply 'where' or 'actions' are more specific
|
|
// having 'where' or 'actions' is more important than
|
|
// whether the rules are wildcard or not, so here we have +8 vs
|
|
// -4 and -2 score penalty for wildcards in resources and verbs
|
|
if len(r.Where) > 0 {
|
|
score += 8
|
|
}
|
|
// rules featuring actions are more specific
|
|
if len(r.Actions) > 0 {
|
|
score += 8
|
|
}
|
|
return score
|
|
}
|
|
|
|
// CompareRuleScore returns true if the first rule is more specific than the other.
|
|
//
|
|
// * nRule matching wildcard resource is less specific
|
|
// than same rule matching specific resource.
|
|
// * Rule that has wildcard verbs is less specific
|
|
// than the same rules matching specific verb.
|
|
// * Rule that has where section is more specific
|
|
// than the same rule without where section.
|
|
// * Rule that has actions list is more specific than
|
|
// rule without actions list.
|
|
func CompareRuleScore(r *types.Rule, o *types.Rule) bool {
|
|
return ruleScore(r) > ruleScore(o)
|
|
}
|
|
|
|
// RuleSet maps resource to a set of rules defined for it
|
|
type RuleSet map[string][]types.Rule
|
|
|
|
// MakeRuleSet creates a new rule set from a list
|
|
func MakeRuleSet(rules []types.Rule) RuleSet {
|
|
set := make(RuleSet)
|
|
for _, rule := range rules {
|
|
for _, resource := range rule.Resources {
|
|
set[resource] = append(set[resource], rule)
|
|
}
|
|
}
|
|
for resource := range set {
|
|
rules := set[resource]
|
|
// sort rules by most specific rule, the rule that has actions
|
|
// is more specific than the one that has no actions
|
|
sort.Slice(rules, func(i, j int) bool {
|
|
return CompareRuleScore(&rules[i], &rules[j])
|
|
})
|
|
set[resource] = rules
|
|
}
|
|
return set
|
|
}
|
|
|
|
// Match tests if the resource name and verb are in a given list of rules.
|
|
// More specific rules will be matched first. See Rule.IsMoreSpecificThan
|
|
// for exact specs on whether the rule is more or less specific.
|
|
//
|
|
// Specifying order solves the problem on having multiple rules, e.g. one wildcard
|
|
// rule can override more specific rules with 'where' sections that can have
|
|
// 'actions' lists with side effects that will not be triggered otherwise.
|
|
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 matching resource by name
|
|
// the most specific rule should win
|
|
rules := set[resource]
|
|
for _, rule := range rules {
|
|
match, err := matchesWhere(&rule, whereParser)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
if match && (rule.HasVerb(types.Wildcard) || rule.HasVerb(verb)) {
|
|
if err := processActions(&rule, actionsParser); err != nil {
|
|
return true, trace.Wrap(err)
|
|
}
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
// check for wildcard resource matcher
|
|
for _, rule := range set[types.Wildcard] {
|
|
match, err := matchesWhere(&rule, whereParser)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
if match && (rule.HasVerb(types.Wildcard) || rule.HasVerb(verb)) {
|
|
if err := processActions(&rule, actionsParser); err != nil {
|
|
return true, trace.Wrap(err)
|
|
}
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// matchesWhere returns true if Where rule matches.
|
|
// Empty Where block always matches.
|
|
func matchesWhere(r *types.Rule, 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("invalid predicate type for where expression: %v", r.Where)
|
|
}
|
|
return fn(), nil
|
|
}
|
|
|
|
// processActions processes actions specified for this rule
|
|
func processActions(r *types.Rule, 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("invalid predicate type for action expression: %v", action)
|
|
}
|
|
fn()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Slice returns slice from a set
|
|
func (set RuleSet) Slice() []types.Rule {
|
|
var out []types.Rule
|
|
for _, rules := range set {
|
|
out = append(out, rules...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HostUsersInfo keeps information about groups and sudoers entries
|
|
// for a particular host user
|
|
type HostUsersInfo struct {
|
|
// Groups is the list of groups to include host users in
|
|
Groups []string
|
|
// Sudoers is a list of entries for a users sudoers file
|
|
Sudoers []string
|
|
}
|
|
|
|
// RoleFromSpec returns new Role created from spec
|
|
func RoleFromSpec(name string, spec types.RoleSpecV6) (types.Role, error) {
|
|
role, err := types.NewRole(name, spec)
|
|
return role, trace.Wrap(err)
|
|
}
|
|
|
|
// RoleSetFromSpec returns a new RoleSet from spec
|
|
func RoleSetFromSpec(name string, spec types.RoleSpecV6) (RoleSet, error) {
|
|
role, err := RoleFromSpec(name, spec)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return NewRoleSet(role), nil
|
|
}
|
|
|
|
// RW is a shortcut that returns all CRUD verbs.
|
|
func RW() []string {
|
|
return []string{types.VerbList, types.VerbCreate, types.VerbRead, types.VerbUpdate, types.VerbDelete}
|
|
}
|
|
|
|
// RO is a shortcut that returns read only verbs that provide access to secrets.
|
|
func RO() []string {
|
|
return []string{types.VerbList, types.VerbRead}
|
|
}
|
|
|
|
// ReadNoSecrets is a shortcut that returns read only verbs that do not
|
|
// provide access to secrets.
|
|
func ReadNoSecrets() []string {
|
|
return []string{types.VerbList, types.VerbReadNoSecrets}
|
|
}
|
|
|
|
// RoleGetter is an interface that defines GetRole method
|
|
type RoleGetter interface {
|
|
// GetRole returns role by name
|
|
GetRole(ctx context.Context, name string) (types.Role, error)
|
|
}
|
|
|
|
// ExtractFromCertificate will extract roles and traits from a *ssh.Certificate.
|
|
func ExtractFromCertificate(cert *ssh.Certificate) ([]string, wrappers.Traits, error) {
|
|
roles, err := ExtractRolesFromCert(cert)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
traits, err := ExtractTraitsFromCert(cert)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
return roles, traits, nil
|
|
}
|
|
|
|
// ExtractFromIdentity will extract roles and traits from the *x509.Certificate
|
|
// which Teleport passes along as a *tlsca.Identity. If roles and traits do not
|
|
// exist in the certificates, they are extracted from the backend.
|
|
func ExtractFromIdentity(access UserGetter, identity tlsca.Identity) ([]string, wrappers.Traits, error) {
|
|
// Legacy certs are not encoded with roles or traits,
|
|
// so we fallback to the traits and roles in the backend.
|
|
// empty traits are a valid use case in standard certs,
|
|
// so we only check for whether roles are empty.
|
|
if len(identity.Groups) == 0 {
|
|
u, err := access.GetUser(identity.Username, false)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
log.Warnf("Failed to find roles in x509 identity for %v. Fetching "+
|
|
"from backend. If the identity provider allows username changes, this can "+
|
|
"potentially allow an attacker to change the role of the existing user.",
|
|
identity.Username)
|
|
return u.GetRoles(), u.GetTraits(), nil
|
|
}
|
|
|
|
return identity.Groups, identity.Traits, nil
|
|
}
|
|
|
|
// FetchRoleList fetches roles by their names, applies the traits to role
|
|
// variables, and returns the list
|
|
func FetchRoleList(roleNames []string, access RoleGetter, traits map[string][]string) (RoleSet, error) {
|
|
var roles []types.Role
|
|
|
|
for _, roleName := range roleNames {
|
|
role, err := access.GetRole(context.TODO(), roleName)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
roles = append(roles, ApplyTraits(role, traits))
|
|
}
|
|
|
|
return roles, nil
|
|
}
|
|
|
|
// FetchRoles fetches roles by their names, applies the traits to role
|
|
// variables, and returns the RoleSet. Adds runtime roles like the default
|
|
// implicit role to RoleSet.
|
|
func FetchRoles(roleNames []string, access RoleGetter, traits map[string][]string) (RoleSet, error) {
|
|
roles, err := FetchRoleList(roleNames, access, traits)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return NewRoleSet(roles...), nil
|
|
}
|
|
|
|
// CurrentUserRoleGetter limits the interface of auth.ClientI to methods needed by FetchAllClusterRoles.
|
|
type CurrentUserRoleGetter interface {
|
|
GetCurrentUser(context.Context) (types.User, error)
|
|
GetCurrentUserRoles(context.Context) ([]types.Role, error)
|
|
RoleGetter
|
|
}
|
|
|
|
// FetchAllClusterRoles fetches all roles available to the user on the
|
|
// specified cluster, applies traits, and adds runtime roles like the default
|
|
// implicit role to RoleSet.
|
|
func FetchAllClusterRoles(ctx context.Context, access CurrentUserRoleGetter, defaultRoleNames []string, defaultTraits wrappers.Traits) (RoleSet, error) {
|
|
user, err := access.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
// DELETE IN 12.0.0
|
|
if trace.IsNotImplemented(err) {
|
|
// get the role definition for all roles of user.
|
|
// this may only fail if the role which we are looking for does not exist, or we don't have access to it.
|
|
// example scenario when this may happen:
|
|
// 1. we have set of roles [foo bar] from profile.
|
|
// 2. the cluster is remote and maps the [foo, bar] roles to single role [guest]
|
|
// 3. the remote cluster doesn't implement GetCurrentUser(), so we have no way to learn of [guest].
|
|
// 4. FetchRoles([foo bar], ..., ...) fails as [foo bar] does not exist on remote cluster.
|
|
roleSet, err := FetchRoles(defaultRoleNames, access, defaultTraits)
|
|
return roleSet, trace.Wrap(err)
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
roles, err := access.GetCurrentUserRoles(ctx)
|
|
if err != nil {
|
|
// DELETE IN 12.0
|
|
if trace.IsNotImplemented(err) {
|
|
roleSet, err := FetchRoles(user.GetRoles(), access, user.GetTraits())
|
|
return roleSet, trace.Wrap(err)
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
for i := range roles {
|
|
roles[i] = ApplyTraits(roles[i], user.GetTraits())
|
|
}
|
|
return NewRoleSet(roles...), nil
|
|
}
|
|
|
|
// ExtractRolesFromCert extracts roles from certificate metadata extensions.
|
|
func ExtractRolesFromCert(cert *ssh.Certificate) ([]string, error) {
|
|
data, ok := cert.Extensions[teleport.CertExtensionTeleportRoles]
|
|
if !ok {
|
|
return nil, trace.NotFound("no roles found")
|
|
}
|
|
return UnmarshalCertRoles(data)
|
|
}
|
|
|
|
// ExtractTraitsFromCert extracts traits from the certificate extensions.
|
|
func ExtractTraitsFromCert(cert *ssh.Certificate) (wrappers.Traits, error) {
|
|
rawTraits, ok := cert.Extensions[teleport.CertExtensionTeleportTraits]
|
|
if !ok {
|
|
return nil, trace.NotFound("no traits found")
|
|
}
|
|
var traits wrappers.Traits
|
|
err := wrappers.UnmarshalTraits([]byte(rawTraits), &traits)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return traits, nil
|
|
}
|
|
|
|
func ExtractAllowedResourcesFromCert(cert *ssh.Certificate) ([]types.ResourceID, error) {
|
|
allowedResourcesStr, ok := cert.Extensions[teleport.CertExtensionAllowedResources]
|
|
if !ok {
|
|
// if not present in the cert, there are no resource-based restrictions
|
|
return nil, nil
|
|
}
|
|
allowedResources, err := types.ResourceIDsFromString(allowedResourcesStr)
|
|
return allowedResources, trace.Wrap(err)
|
|
}
|
|
|
|
// NewRoleSet returns new RoleSet based on the roles
|
|
func NewRoleSet(roles ...types.Role) RoleSet {
|
|
// unauthenticated Nop role should not have any privileges
|
|
// by default, otherwise it is too permissive
|
|
if len(roles) == 1 && roles[0].GetName() == string(types.RoleNop) {
|
|
return roles
|
|
}
|
|
return append(roles, NewImplicitRole())
|
|
}
|
|
|
|
// RoleSet is a set of roles that implements access control functionality
|
|
type RoleSet []types.Role
|
|
|
|
// EnumerationResult is a result of enumerating a role set against some property, e.g. allowed names or logins.
|
|
type EnumerationResult struct {
|
|
allowedDeniedMap map[string]bool
|
|
wildcardAllowed bool
|
|
wildcardDenied bool
|
|
}
|
|
|
|
func (result *EnumerationResult) filtered(value bool) []string {
|
|
var filtered []string
|
|
|
|
for entity, allow := range result.allowedDeniedMap {
|
|
if allow == value {
|
|
filtered = append(filtered, entity)
|
|
}
|
|
}
|
|
|
|
sort.Strings(filtered)
|
|
|
|
return filtered
|
|
}
|
|
|
|
// Denied returns all explicitly denied users.
|
|
func (result *EnumerationResult) Denied() []string {
|
|
return result.filtered(false)
|
|
}
|
|
|
|
// Allowed returns all known allowed users.
|
|
func (result *EnumerationResult) Allowed() []string {
|
|
if result.WildcardDenied() {
|
|
return nil
|
|
}
|
|
return result.filtered(true)
|
|
}
|
|
|
|
// WildcardAllowed is true if there * username allowed for given rule set.
|
|
func (result *EnumerationResult) WildcardAllowed() bool {
|
|
return result.wildcardAllowed && !result.wildcardDenied
|
|
}
|
|
|
|
// WildcardDenied is true if there * username deny for given rule set.
|
|
func (result *EnumerationResult) WildcardDenied() bool {
|
|
return result.wildcardDenied
|
|
}
|
|
|
|
// NewEnumerationResult returns new EnumerationResult.
|
|
func NewEnumerationResult() EnumerationResult {
|
|
return EnumerationResult{
|
|
allowedDeniedMap: map[string]bool{},
|
|
wildcardAllowed: false,
|
|
wildcardDenied: false,
|
|
}
|
|
}
|
|
|
|
// EnumerateDatabaseUsers works on a given role set to return a minimal description of allowed set of usernames.
|
|
// It is biased towards *allowed* usernames; It is meant to describe what the user can do, rather than cannot do.
|
|
// For that reason if the user isn't allowed to pick *any* entities, the output will be empty.
|
|
//
|
|
// In cases where * is listed in set of allowed users, it may be hard for users to figure out the expected username.
|
|
// For this reason the parameter extraUsers provides an extra set of users to be checked against RoleSet.
|
|
// This extra set of users may be sourced e.g. from user connection history.
|
|
func (set RoleSet) EnumerateDatabaseUsers(database types.Database, extraUsers ...string) EnumerationResult {
|
|
result := NewEnumerationResult()
|
|
|
|
// gather users for checking from the roles, check wildcards.
|
|
var users []string
|
|
for _, role := range set {
|
|
wildcardAllowed := false
|
|
wildcardDenied := false
|
|
|
|
for _, user := range role.GetDatabaseUsers(types.Allow) {
|
|
if user == types.Wildcard {
|
|
wildcardAllowed = true
|
|
} else {
|
|
users = append(users, user)
|
|
}
|
|
}
|
|
|
|
for _, user := range role.GetDatabaseUsers(types.Deny) {
|
|
if user == types.Wildcard {
|
|
wildcardDenied = true
|
|
} else {
|
|
users = append(users, user)
|
|
}
|
|
}
|
|
|
|
result.wildcardDenied = result.wildcardDenied || wildcardDenied
|
|
|
|
if err := NewRoleSet(role).checkAccess(database, AccessState{MFAVerified: true}); err == nil {
|
|
result.wildcardAllowed = result.wildcardAllowed || wildcardAllowed
|
|
}
|
|
|
|
}
|
|
|
|
users = apiutils.Deduplicate(append(users, extraUsers...))
|
|
|
|
// check each individual user against the database.
|
|
for _, user := range users {
|
|
err := set.checkAccess(database, AccessState{MFAVerified: true}, NewDatabaseUserMatcher(database, user))
|
|
result.allowedDeniedMap[user] = err == nil
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetAllowedLoginsForResource returns all of the allowed logins for the passed resource.
|
|
//
|
|
// Supports the following resource types:
|
|
//
|
|
// - types.Server with GetKind() == types.KindNode
|
|
//
|
|
// - types.KindWindowsDesktop
|
|
func (set RoleSet) GetAllowedLoginsForResource(resource AccessCheckable) ([]string, error) {
|
|
// Create a map indexed by all logins in the RoleSet,
|
|
// mapped to false if any role has it in its deny section,
|
|
// true otherwise.
|
|
mapped := make(map[string]bool)
|
|
|
|
for _, role := range set {
|
|
var loginGetter func(types.RoleConditionType) []string
|
|
|
|
switch resource.GetKind() {
|
|
case types.KindNode:
|
|
loginGetter = role.GetLogins
|
|
case types.KindWindowsDesktop:
|
|
loginGetter = role.GetWindowsLogins
|
|
default:
|
|
return nil, trace.BadParameter("received unsupported resource kind: %s", resource.GetKind())
|
|
}
|
|
|
|
for _, login := range loginGetter(types.Allow) {
|
|
mapped[login] = true
|
|
}
|
|
for _, login := range loginGetter(types.Deny) {
|
|
mapped[login] = false
|
|
}
|
|
}
|
|
|
|
// Create a list of only the logins not denied by a role in the set.
|
|
var notDenied []string
|
|
for login, isNotDenied := range mapped {
|
|
if isNotDenied {
|
|
notDenied = append(notDenied, login)
|
|
}
|
|
}
|
|
|
|
var newLoginMatcher func(login string) RoleMatcher
|
|
switch resource.GetKind() {
|
|
case types.KindNode:
|
|
newLoginMatcher = NewLoginMatcher
|
|
case types.KindWindowsDesktop:
|
|
newLoginMatcher = NewWindowsLoginMatcher
|
|
default:
|
|
return nil, trace.BadParameter("received unsupported resource kind: %s", resource.GetKind())
|
|
}
|
|
|
|
// Filter the not-denied logins for those allowed to be used with the given resource.
|
|
var allowed []string
|
|
for _, login := range notDenied {
|
|
err := set.checkAccess(resource, AccessState{MFAVerified: true}, newLoginMatcher(login))
|
|
if err == nil {
|
|
allowed = append(allowed, login)
|
|
}
|
|
}
|
|
|
|
return allowed, nil
|
|
}
|
|
|
|
// MatchNamespace returns true if given list of namespace matches
|
|
// target namespace, wildcard matches everything.
|
|
func MatchNamespace(selectors []string, namespace string) (bool, string) {
|
|
for _, n := range selectors {
|
|
if n == namespace || n == types.Wildcard {
|
|
return true, "matched"
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no match, role selectors %v, server namespace: %v", selectors, namespace)
|
|
}
|
|
|
|
// MatchAWSRoleARN returns true if provided role ARN matches selectors.
|
|
func MatchAWSRoleARN(selectors []string, roleARN string) (bool, string) {
|
|
for _, l := range selectors {
|
|
if l == roleARN {
|
|
return true, "matched"
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no match, role selectors %v, role ARN: %v", selectors, roleARN)
|
|
}
|
|
|
|
// MatchAzureIdentity returns true if provided Azure identity matches selectors.
|
|
func MatchAzureIdentity(selectors []string, identity string, matchWildcard bool) (bool, string) {
|
|
identity = strings.ToLower(identity)
|
|
for _, l := range selectors {
|
|
if strings.ToLower(l) == identity {
|
|
return true, "element matched"
|
|
}
|
|
if matchWildcard && l == types.Wildcard {
|
|
return true, "wildcard matched"
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no match, role selectors %v, identity: %v", selectors, identity)
|
|
}
|
|
|
|
// MatchGCPServiceAccount returns true if provided GCP service account matches selectors.
|
|
func MatchGCPServiceAccount(selectors []string, account string, matchWildcard bool) (bool, string) {
|
|
for _, l := range selectors {
|
|
if l == account {
|
|
return true, "element matched"
|
|
}
|
|
if matchWildcard && l == types.Wildcard {
|
|
return true, "wildcard matched"
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no match, role selectors %v, identity: %v", selectors, account)
|
|
}
|
|
|
|
// MatchDatabaseName returns true if provided database name matches selectors.
|
|
func MatchDatabaseName(selectors []string, name string) (bool, string) {
|
|
for _, n := range selectors {
|
|
if n == name || n == types.Wildcard {
|
|
return true, "matched"
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no match, role selectors %v, database name: %v", selectors, name)
|
|
}
|
|
|
|
// MatchDatabaseUser returns true if provided database user matches selectors.
|
|
func MatchDatabaseUser(selectors []string, user string, matchWildcard bool) (bool, string) {
|
|
for _, u := range selectors {
|
|
if u == user {
|
|
return true, "matched"
|
|
}
|
|
if matchWildcard && u == types.Wildcard {
|
|
return true, "matched"
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no match, role selectors %v, database user: %v", selectors, user)
|
|
}
|
|
|
|
// MatchLabels matches selector against target. Empty selector matches
|
|
// nothing, wildcard matches everything.
|
|
func MatchLabels(selector types.Labels, target map[string]string) (bool, string, error) {
|
|
// Empty selector matches nothing.
|
|
if len(selector) == 0 {
|
|
return false, "no match, empty selector", nil
|
|
}
|
|
|
|
// *: * matches everything even empty target set.
|
|
selectorValues := selector[types.Wildcard]
|
|
if len(selectorValues) == 1 && selectorValues[0] == types.Wildcard {
|
|
return true, "matched", nil
|
|
}
|
|
|
|
// Perform full match.
|
|
for key, selectorValues := range selector {
|
|
targetVal, hasKey := target[key]
|
|
|
|
if !hasKey {
|
|
return false, fmt.Sprintf("no key match: '%v'", key), nil
|
|
}
|
|
|
|
if !slices.Contains(selectorValues, types.Wildcard) {
|
|
result, err := utils.SliceMatchesRegex(targetVal, selectorValues)
|
|
if err != nil {
|
|
return false, "", trace.Wrap(err)
|
|
} else if !result {
|
|
return false, fmt.Sprintf("no value match: got '%v' want: '%v'", targetVal, selectorValues), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, "matched", nil
|
|
}
|
|
|
|
// RoleNames returns a slice with role names. Removes runtime roles like
|
|
// the default implicit role.
|
|
func (set RoleSet) RoleNames() []string {
|
|
out := make([]string, 0, len(set))
|
|
for _, r := range set {
|
|
if r.GetName() == constants.DefaultImplicitRole {
|
|
continue
|
|
}
|
|
out = append(out, r.GetName())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Roles returns the list underlying roles this RoleSet is based on.
|
|
func (set RoleSet) Roles() []types.Role {
|
|
return append([]types.Role{}, set...)
|
|
}
|
|
|
|
// HasRole checks if the role set has the role
|
|
func (set RoleSet) HasRole(role string) bool {
|
|
for _, r := range set {
|
|
if r.GetName() == role {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// WithoutImplicit returns this role set with default implicit role filtered out.
|
|
func (set RoleSet) WithoutImplicit() (out RoleSet) {
|
|
for _, r := range set {
|
|
if r.GetName() == constants.DefaultImplicitRole {
|
|
continue
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// PinSourceIP determines if the role set should use source IP pinning.
|
|
// If one or more roles in the set requires IP pinning then it will be enabled.
|
|
func (set RoleSet) PinSourceIP() bool {
|
|
for _, role := range set {
|
|
if role.GetOptions().PinSourceIP {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetAccessState returns the AccessState, setting [AccessState.MFARequired]
|
|
// according to the user's roles and cluster auth preference.
|
|
func (set RoleSet) GetAccessState(authPref types.AuthPreference) AccessState {
|
|
return AccessState{
|
|
MFARequired: set.getMFARequired(authPref.GetRequireMFAType()),
|
|
// We don't set EnableDeviceVerification here, as both it and DeviceVerified
|
|
// should be set in tandem.
|
|
}
|
|
}
|
|
|
|
func (set RoleSet) getMFARequired(clusterRequireMFAType types.RequireMFAType) MFARequired {
|
|
// per-session MFA is overridden by hardware key PIV touch requirement.
|
|
// check if the auth pref or any roles have this option.
|
|
if clusterRequireMFAType == types.RequireMFAType_HARDWARE_KEY_TOUCH {
|
|
return MFARequiredNever
|
|
}
|
|
for _, role := range set {
|
|
if role.GetOptions().RequireMFAType == types.RequireMFAType_HARDWARE_KEY_TOUCH {
|
|
return MFARequiredNever
|
|
}
|
|
}
|
|
|
|
// MFA is always required according to the cluster auth pref.
|
|
if clusterRequireMFAType.IsSessionMFARequired() {
|
|
return MFARequiredAlways
|
|
}
|
|
|
|
// If MFA requirement is the same across all roles, we can skip the per-role check.
|
|
// Set mfaRequired to the first role's requirement, then check if all other roles match.
|
|
if len(set) > 0 {
|
|
rolesMFARequired := set[0].GetOptions().RequireMFAType.IsSessionMFARequired()
|
|
for _, role := range set[1:] {
|
|
if role.GetOptions().RequireMFAType.IsSessionMFARequired() != rolesMFARequired {
|
|
// This role differs from the MFA requirement of the other roles, return per-role.
|
|
return MFARequiredPerRole
|
|
}
|
|
}
|
|
|
|
if rolesMFARequired {
|
|
return MFARequiredAlways
|
|
}
|
|
}
|
|
|
|
// No roles to check or no roles require MFA.
|
|
return MFARequiredNever
|
|
}
|
|
|
|
// PrivateKeyPolicy returns the enforced private key policy for this role set.
|
|
func (set RoleSet) PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) keys.PrivateKeyPolicy {
|
|
if defaultPolicy == keys.PrivateKeyPolicyHardwareKeyTouch {
|
|
// This is the strictest option so we can return now
|
|
return defaultPolicy
|
|
}
|
|
|
|
policy := defaultPolicy
|
|
for _, role := range set {
|
|
switch rolePolicy := role.GetPrivateKeyPolicy(); rolePolicy {
|
|
case keys.PrivateKeyPolicyHardwareKey:
|
|
policy = rolePolicy
|
|
case keys.PrivateKeyPolicyHardwareKeyTouch:
|
|
// This is the strictest option so we can return now
|
|
return keys.PrivateKeyPolicyHardwareKeyTouch
|
|
}
|
|
}
|
|
|
|
return policy
|
|
}
|
|
|
|
// AdjustSessionTTL will reduce the requested ttl to the lowest max allowed TTL
|
|
// for this role set, otherwise it returns ttl unchanged
|
|
func (set RoleSet) AdjustSessionTTL(ttl time.Duration) time.Duration {
|
|
for _, role := range set {
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if maxSessionTTL != 0 && ttl > maxSessionTTL {
|
|
ttl = maxSessionTTL
|
|
}
|
|
}
|
|
return ttl
|
|
}
|
|
|
|
// MaxConnections returns the maximum number of concurrent ssh connections
|
|
// allowed. If MaxConnections is zero then no maximum was defined
|
|
// and the number of concurrent connections is unconstrained.
|
|
func (set RoleSet) MaxConnections() int64 {
|
|
var mcs int64
|
|
for _, role := range set {
|
|
if m := role.GetOptions().MaxConnections; m != 0 && (m < mcs || mcs == 0) {
|
|
mcs = m
|
|
}
|
|
}
|
|
return mcs
|
|
}
|
|
|
|
// MaxSessions returns the maximum number of concurrent ssh sessions
|
|
// per connection. If MaxSessions is zero then no maximum was defined
|
|
// and the number of sessions is unconstrained.
|
|
func (set RoleSet) MaxSessions() int64 {
|
|
var ms int64
|
|
for _, role := range set {
|
|
if m := role.GetOptions().MaxSessions; m != 0 && (m < ms || ms == 0) {
|
|
ms = m
|
|
}
|
|
}
|
|
return ms
|
|
}
|
|
|
|
// MaxConnections returns the maximum number of concurrent Kubernetes connections
|
|
// allowed. If MaxConnections is zero then no maximum was defined
|
|
// and the number of concurrent connections is unconstrained.
|
|
func (set RoleSet) MaxKubernetesConnections() int64 {
|
|
var mcs int64
|
|
for _, role := range set {
|
|
if m := role.GetOptions().MaxKubernetesConnections; m != 0 && (m < mcs || mcs == 0) {
|
|
mcs = m
|
|
}
|
|
}
|
|
return mcs
|
|
}
|
|
|
|
// SessionPolicySets returns the list of SessionPolicySets for all roles.
|
|
func (set RoleSet) SessionPolicySets() []*types.SessionTrackerPolicySet {
|
|
var policySets []*types.SessionTrackerPolicySet
|
|
for _, role := range set {
|
|
policySet := role.GetSessionPolicySet()
|
|
policySets = append(policySets, &policySet)
|
|
}
|
|
return policySets
|
|
}
|
|
|
|
// AdjustClientIdleTimeout adjusts requested idle timeout
|
|
// to the lowest max allowed timeout, the most restrictive
|
|
// option will be picked, negative values will be assumed as 0
|
|
func (set RoleSet) AdjustClientIdleTimeout(timeout time.Duration) time.Duration {
|
|
if timeout < 0 {
|
|
timeout = 0
|
|
}
|
|
for _, role := range set {
|
|
roleTimeout := role.GetOptions().ClientIdleTimeout
|
|
// 0 means not set, so it can't be most restrictive, disregard it too
|
|
if roleTimeout.Duration() <= 0 {
|
|
continue
|
|
}
|
|
switch {
|
|
// in case if timeout is 0, means that incoming value
|
|
// does not restrict the idle timeout, pick any other value
|
|
// set by the role
|
|
case timeout == 0:
|
|
timeout = roleTimeout.Duration()
|
|
case roleTimeout.Duration() < timeout:
|
|
timeout = roleTimeout.Duration()
|
|
}
|
|
}
|
|
return timeout
|
|
}
|
|
|
|
// AdjustDisconnectExpiredCert adjusts the value based on the role set
|
|
// the most restrictive option will be picked
|
|
func (set RoleSet) AdjustDisconnectExpiredCert(disconnect bool) bool {
|
|
for _, role := range set {
|
|
if role.GetOptions().DisconnectExpiredCert.Value() {
|
|
disconnect = true
|
|
}
|
|
}
|
|
return disconnect
|
|
}
|
|
|
|
// CheckKubeGroupsAndUsers check if role can login into kubernetes
|
|
// and returns two lists of allowed groups and users
|
|
func (set RoleSet) CheckKubeGroupsAndUsers(ttl time.Duration, overrideTTL bool, matchers ...RoleMatcher) ([]string, []string, error) {
|
|
groups := make(map[string]struct{})
|
|
users := make(map[string]struct{})
|
|
var matchedTTL bool
|
|
for _, role := range set {
|
|
ok, err := RoleMatchers(matchers).MatchAll(role, types.Allow)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if overrideTTL || (ttl <= maxSessionTTL && maxSessionTTL != 0) {
|
|
matchedTTL = true
|
|
for _, group := range role.GetKubeGroups(types.Allow) {
|
|
groups[group] = struct{}{}
|
|
}
|
|
for _, user := range role.GetKubeUsers(types.Allow) {
|
|
users[user] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
ok, _, err := RoleMatchers(matchers).MatchAny(role, types.Deny)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, group := range role.GetKubeGroups(types.Deny) {
|
|
delete(groups, group)
|
|
}
|
|
for _, user := range role.GetKubeUsers(types.Deny) {
|
|
delete(users, user)
|
|
}
|
|
}
|
|
if !matchedTTL {
|
|
return nil, nil, trace.AccessDenied("this user cannot request kubernetes access for %v", ttl)
|
|
}
|
|
if len(groups) == 0 && len(users) == 0 {
|
|
return nil, nil, trace.NotFound("this user cannot request kubernetes access, has no assigned groups or users")
|
|
}
|
|
return utils.StringsSliceFromSet(groups), utils.StringsSliceFromSet(users), nil
|
|
}
|
|
|
|
// CheckDatabaseNamesAndUsers checks if the role has any allowed database
|
|
// names or users.
|
|
func (set RoleSet) CheckDatabaseNamesAndUsers(ttl time.Duration, overrideTTL bool) ([]string, []string, error) {
|
|
names := make(map[string]struct{})
|
|
users := make(map[string]struct{})
|
|
var matchedTTL bool
|
|
for _, role := range set {
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if overrideTTL || (ttl <= maxSessionTTL && maxSessionTTL != 0) {
|
|
matchedTTL = true
|
|
for _, name := range role.GetDatabaseNames(types.Allow) {
|
|
names[name] = struct{}{}
|
|
}
|
|
for _, user := range role.GetDatabaseUsers(types.Allow) {
|
|
users[user] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
for _, name := range role.GetDatabaseNames(types.Deny) {
|
|
delete(names, name)
|
|
}
|
|
for _, user := range role.GetDatabaseUsers(types.Deny) {
|
|
delete(users, user)
|
|
}
|
|
}
|
|
if !matchedTTL {
|
|
return nil, nil, trace.AccessDenied("this user cannot request database access for %v", ttl)
|
|
}
|
|
if len(names) == 0 && len(users) == 0 {
|
|
return nil, nil, trace.NotFound("this user cannot request database access, has no assigned database names or users")
|
|
}
|
|
return utils.StringsSliceFromSet(names), utils.StringsSliceFromSet(users), nil
|
|
}
|
|
|
|
// CheckAWSRoleARNs returns a list of AWS role ARNs this role set is allowed to assume.
|
|
func (set RoleSet) CheckAWSRoleARNs(ttl time.Duration, overrideTTL bool) ([]string, error) {
|
|
arns := make(map[string]struct{})
|
|
var matchedTTL bool
|
|
for _, role := range set {
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if overrideTTL || (ttl <= maxSessionTTL && maxSessionTTL != 0) {
|
|
matchedTTL = true
|
|
for _, arn := range role.GetAWSRoleARNs(types.Allow) {
|
|
arns[arn] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
for _, arn := range role.GetAWSRoleARNs(types.Deny) {
|
|
delete(arns, arn)
|
|
}
|
|
}
|
|
if !matchedTTL {
|
|
return nil, trace.AccessDenied("this user cannot request AWS management console access for %v", ttl)
|
|
}
|
|
if len(arns) == 0 {
|
|
return nil, trace.NotFound("this user cannot request AWS management console, has no assigned role ARNs")
|
|
}
|
|
return utils.StringsSliceFromSet(arns), nil
|
|
}
|
|
|
|
// CheckAzureIdentities returns a list of Azure identities the user is allowed to assume.
|
|
func (set RoleSet) CheckAzureIdentities(ttl time.Duration, overrideTTL bool) ([]string, error) {
|
|
identities := make(map[string]string)
|
|
var matchedTTL bool
|
|
for _, role := range set {
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if overrideTTL || (ttl <= maxSessionTTL && maxSessionTTL != 0) {
|
|
matchedTTL = true
|
|
for _, identity := range role.GetAzureIdentities(types.Allow) {
|
|
identities[strings.ToLower(identity)] = identity
|
|
}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
for _, identity := range role.GetAzureIdentities(types.Deny) {
|
|
// deny * cleans options
|
|
if identity == types.Wildcard {
|
|
identities = make(map[string]string)
|
|
}
|
|
// remove particular identity
|
|
delete(identities, strings.ToLower(identity))
|
|
}
|
|
}
|
|
if !matchedTTL {
|
|
return nil, trace.AccessDenied("this user cannot access Azure API for %v", ttl)
|
|
}
|
|
if len(identities) == 0 {
|
|
return nil, trace.NotFound("this user cannot access Azure API, has no assigned identities")
|
|
}
|
|
|
|
out := make([]string, 0, len(identities))
|
|
for _, identity := range identities {
|
|
out = append(out, identity)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
// CheckGCPServiceAccounts returns a list of GCP service accounts this role set is allowed to assume.
|
|
func (set RoleSet) CheckGCPServiceAccounts(ttl time.Duration, overrideTTL bool) ([]string, error) {
|
|
accounts := make(map[string]struct{})
|
|
var matchedTTL bool
|
|
for _, role := range set {
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if overrideTTL || (ttl <= maxSessionTTL && maxSessionTTL != 0) {
|
|
matchedTTL = true
|
|
for _, account := range role.GetGCPServiceAccounts(types.Allow) {
|
|
accounts[strings.ToLower(account)] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
for _, account := range role.GetGCPServiceAccounts(types.Deny) {
|
|
// deny * removes all accounts
|
|
if account == types.Wildcard {
|
|
accounts = make(map[string]struct{})
|
|
}
|
|
// remove particular account
|
|
delete(accounts, strings.ToLower(account))
|
|
}
|
|
}
|
|
if !matchedTTL {
|
|
return nil, trace.AccessDenied("this user cannot request GCP API access for %v", ttl)
|
|
}
|
|
if len(accounts) == 0 {
|
|
return nil, trace.NotFound("this user cannot request GCP API access, has no assigned service accounts")
|
|
}
|
|
return utils.StringsSliceFromSet(accounts), nil
|
|
}
|
|
|
|
// CheckAccessToSAMLIdP checks access to the SAML IdP.
|
|
//
|
|
//nolint:revive // Because we want this to be IdP.
|
|
func (set RoleSet) CheckAccessToSAMLIdP(authPref types.AuthPreference) error {
|
|
if authPref != nil {
|
|
if !authPref.IsSAMLIdPEnabled() {
|
|
return trace.AccessDenied("SAML IdP is disabled at the cluster level")
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
options := role.GetOptions()
|
|
|
|
// This should never happen, but we should make sure that we don't get a nil pointer error here.
|
|
if options.IDP == nil || options.IDP.SAML == nil || options.IDP.SAML.Enabled == nil {
|
|
continue
|
|
}
|
|
|
|
// If any role specifically denies access to the IdP, we'll return AccessDenied.
|
|
if !options.IDP.SAML.Enabled.Value {
|
|
return trace.AccessDenied("user has been denied access to the SAML IdP by role %s", role.GetName())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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, matchedTTL := set.GetLoginsForTTL(ttl)
|
|
if !matchedTTL {
|
|
return nil, trace.AccessDenied("this user cannot request a certificate for %v", ttl)
|
|
}
|
|
|
|
if len(logins) == 0 && !set.hasPossibleLogins() {
|
|
// user was deliberately configured to have no login capability,
|
|
// but ssh certificates must contain at least one valid principal.
|
|
// we add a single distinctive value which should be unique, and
|
|
// will never be a valid unix login (due to leading '-').
|
|
logins = []string{constants.NoLoginPrefix + uuid.New().String()}
|
|
}
|
|
|
|
if len(logins) == 0 {
|
|
return nil, trace.AccessDenied("this user cannot create SSH sessions, has no allowed logins")
|
|
}
|
|
|
|
return logins, nil
|
|
}
|
|
|
|
// GetAllLogins returns all valid unix logins for the RoleSet.
|
|
func (set RoleSet) GetAllLogins() []string {
|
|
logins, _ := set.GetLoginsForTTL(0)
|
|
return logins
|
|
}
|
|
|
|
// GetLoginsForTTL collects all logins that are valid for the given TTL. The matchedTTL
|
|
// value indicates whether the TTL is within scope of *any* role. This helps to distinguish
|
|
// between TTLs which are categorically invalid, and TTLs which are theoretically valid
|
|
// but happen to grant no logins.
|
|
func (set RoleSet) GetLoginsForTTL(ttl time.Duration) (logins []string, matchedTTL bool) {
|
|
for _, role := range set {
|
|
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
|
if ttl <= maxSessionTTL && maxSessionTTL != 0 {
|
|
matchedTTL = true
|
|
logins = append(logins, role.GetLogins(types.Allow)...)
|
|
}
|
|
}
|
|
return apiutils.Deduplicate(logins), matchedTTL
|
|
}
|
|
|
|
// CheckAccessToRemoteCluster checks if a role has access to remote cluster. Deny rules are
|
|
// checked first then allow rules. Access to a cluster is determined by
|
|
// namespaces, labels, and logins.
|
|
func (set RoleSet) CheckAccessToRemoteCluster(rc types.RemoteCluster) error {
|
|
if len(set) == 0 {
|
|
return trace.AccessDenied("access to cluster denied")
|
|
}
|
|
|
|
// Note: logging in this function only happens in debug mode, this is because
|
|
// adding logging to this function (which is called on every server returned
|
|
// by GetRemoteClusters) can slow down this function by 50x for large clusters!
|
|
isDebugEnabled, debugf := rbacDebugLogger()
|
|
|
|
rcLabels := rc.GetMetadata().Labels
|
|
|
|
// For backwards compatibility, if there is no role in the set with labels and the cluster
|
|
// has no labels, assume that the role set has access to the cluster.
|
|
usesLabels := false
|
|
for _, role := range set {
|
|
if len(role.GetClusterLabels(types.Allow)) != 0 || len(role.GetClusterLabels(types.Deny)) != 0 {
|
|
usesLabels = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !usesLabels && len(rcLabels) == 0 {
|
|
debugf("Grant access to cluster %v - no role in %v uses cluster labels and the cluster is not labeled.",
|
|
rc.GetName(), set.RoleNames())
|
|
return nil
|
|
}
|
|
|
|
// Check deny rules first: a single matching label from
|
|
// the deny role set prohibits access.
|
|
var errs []error
|
|
for _, role := range set {
|
|
matchLabels, labelsMessage, err := MatchLabels(role.GetClusterLabels(types.Deny), rcLabels)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matchLabels {
|
|
// This condition avoids formatting calls on large scale.
|
|
debugf("Access to cluster %v denied, deny rule in %v matched; match(label=%v)",
|
|
rc.GetName(), role.GetName(), labelsMessage)
|
|
return trace.AccessDenied("access to cluster denied")
|
|
}
|
|
}
|
|
|
|
// Check allow rules: label has to match in any role in the role set to be granted access.
|
|
for _, role := range set {
|
|
matchLabels, labelsMessage, err := MatchLabels(role.GetClusterLabels(types.Allow), rcLabels)
|
|
debugf("Check access to role(%v) rc(%v, labels=%v) matchLabels=%v, msg=%v, err=%v allow=%v rcLabels=%v",
|
|
role.GetName(), rc.GetName(), rcLabels, matchLabels, labelsMessage, err, role.GetClusterLabels(types.Allow), rcLabels)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matchLabels {
|
|
return nil
|
|
}
|
|
if isDebugEnabled {
|
|
deniedError := trace.AccessDenied("role=%v, match(label=%v)",
|
|
role.GetName(), labelsMessage)
|
|
errs = append(errs, deniedError)
|
|
}
|
|
}
|
|
|
|
debugf("Access to cluster %v denied, no allow rule matched; %v", rc.GetName(), errs)
|
|
return trace.AccessDenied("access to cluster denied")
|
|
}
|
|
|
|
func (set RoleSet) hasPossibleLogins() bool {
|
|
for _, role := range set {
|
|
if role.GetName() == constants.DefaultImplicitRole {
|
|
continue
|
|
}
|
|
if len(role.GetLogins(types.Allow)) != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AWSRoleARNMatcher matches a role against AWS role ARN.
|
|
type AWSRoleARNMatcher struct {
|
|
RoleARN string
|
|
}
|
|
|
|
// Match matches AWS role ARN against provided role and condition.
|
|
func (m *AWSRoleARNMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
match, _ := MatchAWSRoleARN(role.GetAWSRoleARNs(condition), m.RoleARN)
|
|
return match, nil
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *AWSRoleARNMatcher) String() string {
|
|
return fmt.Sprintf("AWSRoleARNMatcher(RoleARN=%v)", m.RoleARN)
|
|
}
|
|
|
|
// AzureIdentityMatcher matches a role against Azure identity.
|
|
type AzureIdentityMatcher struct {
|
|
Identity string
|
|
}
|
|
|
|
// Match matches Azure identity against provided role and condition.
|
|
func (m *AzureIdentityMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
match, _ := MatchAzureIdentity(role.GetAzureIdentities(condition), m.Identity, condition == types.Deny)
|
|
return match, nil
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *AzureIdentityMatcher) String() string {
|
|
return fmt.Sprintf("AzureIdentityMatcher(Identity=%v)", m.Identity)
|
|
}
|
|
|
|
// GCPServiceAccountMatcher matches a role against GCP service account.
|
|
type GCPServiceAccountMatcher struct {
|
|
// ServiceAccount is a GCP service account to match, e.g. teleport@example-123456.iam.gserviceaccount.com.
|
|
// It can also be a wildcard *, but that is only respected for Deny rules.
|
|
ServiceAccount string
|
|
}
|
|
|
|
// Match matches GCP ServiceAccount against provided role and condition.
|
|
func (m *GCPServiceAccountMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
match, _ := MatchGCPServiceAccount(role.GetGCPServiceAccounts(condition), m.ServiceAccount, condition == types.Deny)
|
|
return match, nil
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *GCPServiceAccountMatcher) String() string {
|
|
return fmt.Sprintf("GCPServiceAccountMatcher(ServiceAccount=%v)", m.ServiceAccount)
|
|
}
|
|
|
|
// CanImpersonateSomeone returns true if this checker has any impersonation rules
|
|
func (set RoleSet) CanImpersonateSomeone() bool {
|
|
for _, role := range set {
|
|
cond := role.GetImpersonateConditions(types.Allow)
|
|
if !cond.IsEmpty() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CheckImpersonate returns nil if this role set can impersonate
|
|
// a user and their roles, returns AccessDenied otherwise
|
|
// CheckImpersonate checks whether current user is allowed to impersonate
|
|
// users and roles
|
|
func (set RoleSet) CheckImpersonate(currentUser, impersonateUser types.User, impersonateRoles []types.Role) error {
|
|
ctx := &impersonateContext{
|
|
user: currentUser,
|
|
impersonateUser: impersonateUser,
|
|
}
|
|
whereParser, err := newImpersonateWhereParser(ctx)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// check deny: a single match on a deny rule prohibits access
|
|
for _, role := range set {
|
|
cond := role.GetImpersonateConditions(types.Deny)
|
|
matched, err := matchDenyImpersonateCondition(cond, impersonateUser, impersonateRoles)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matched {
|
|
return trace.AccessDenied("access denied to '%s' to impersonate user '%s' and roles '%s'", currentUser.GetName(), impersonateUser.GetName(), roleNames(impersonateRoles))
|
|
}
|
|
}
|
|
|
|
// check allow: if matches, allow to impersonate
|
|
for _, role := range set {
|
|
cond := role.GetImpersonateConditions(types.Allow)
|
|
matched, err := matchAllowImpersonateCondition(ctx, whereParser, cond, impersonateUser, impersonateRoles)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matched {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return trace.AccessDenied("access denied to '%s' to impersonate user '%s' and roles '%s'", currentUser.GetName(), impersonateUser.GetName(), roleNames(impersonateRoles))
|
|
}
|
|
|
|
// CheckImpersonateRoles validates that the current user can perform role-only impersonation
|
|
// of the given roles. Role-only impersonation requires an allow rule with
|
|
// roles but no users (and no user-less deny rules). All requested roles must
|
|
// be allowed for the check to succeed.
|
|
func (set RoleSet) CheckImpersonateRoles(currentUser types.User, impersonateRoles []types.Role) error {
|
|
ctx := &impersonateContext{
|
|
user: currentUser,
|
|
}
|
|
whereParser, err := newImpersonateWhereParser(ctx)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// TODO: Unlike regular impersonation where all requested roles must be
|
|
// granted by a single impersonation role, it would be reasonable to
|
|
// request several roles whose `allow` conditions are split between
|
|
// several roles. Our initial use-case doesn't require this, so for now
|
|
// we'll assume all requested roles must be granted by a single `allow`.
|
|
|
|
// check deny: a single match on a deny rule prohibits access
|
|
for _, role := range set {
|
|
cond := role.GetImpersonateConditions(types.Deny)
|
|
matched, err := matchDenyRoleImpersonateCondition(cond, impersonateRoles)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matched {
|
|
return trace.AccessDenied("access denied to '%s' to impersonate roles '%s'", currentUser.GetName(), roleNames(impersonateRoles))
|
|
}
|
|
}
|
|
|
|
// check allow: if any one Role satisfies all the role requests, allow impersonation
|
|
for _, role := range set {
|
|
cond := role.GetImpersonateConditions(types.Allow)
|
|
matched, err := matchAllowRoleImpersonateCondition(ctx, whereParser, cond, impersonateRoles)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matched {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return trace.AccessDenied("access denied to '%s' to impersonate roles '%s'", currentUser.GetName(), roleNames(impersonateRoles))
|
|
}
|
|
|
|
// LockingMode returns the locking mode to apply with this RoleSet.
|
|
func (set RoleSet) LockingMode(defaultMode constants.LockingMode) constants.LockingMode {
|
|
mode := defaultMode
|
|
for _, role := range set {
|
|
options := role.GetOptions()
|
|
if options.Lock == constants.LockingModeStrict {
|
|
return constants.LockingModeStrict
|
|
}
|
|
if options.Lock != "" {
|
|
mode = options.Lock
|
|
}
|
|
}
|
|
return mode
|
|
}
|
|
|
|
// CertificateExtensions returns the list of extensions for each role in the RoleSet
|
|
func (set RoleSet) CertificateExtensions() []*types.CertExtension {
|
|
var exts []*types.CertExtension
|
|
for _, role := range set {
|
|
exts = append(exts, role.GetOptions().CertExtensions...)
|
|
}
|
|
return exts
|
|
}
|
|
|
|
// SessionRecordingMode returns the recording mode for a specific service.
|
|
func (set RoleSet) SessionRecordingMode(service constants.SessionRecordingService) constants.SessionRecordingMode {
|
|
defaultValue := constants.SessionRecordingModeBestEffort
|
|
useDefault := true
|
|
|
|
for _, role := range set {
|
|
recordSession := role.GetOptions().RecordSession
|
|
|
|
// If one of the default values is "strict", set it as the value.
|
|
if recordSession.Default == constants.SessionRecordingModeStrict {
|
|
defaultValue = constants.SessionRecordingModeStrict
|
|
}
|
|
|
|
var roleMode constants.SessionRecordingMode
|
|
switch service {
|
|
case constants.SessionRecordingServiceSSH:
|
|
roleMode = recordSession.SSH
|
|
}
|
|
|
|
switch roleMode {
|
|
case constants.SessionRecordingModeStrict:
|
|
// Early return as "strict" since it is the strictest value.
|
|
return constants.SessionRecordingModeStrict
|
|
case constants.SessionRecordingModeBestEffort:
|
|
useDefault = false
|
|
}
|
|
}
|
|
|
|
// Return the strictest default value.
|
|
if useDefault {
|
|
return defaultValue
|
|
}
|
|
|
|
return constants.SessionRecordingModeBestEffort
|
|
}
|
|
|
|
func roleNames(roles []types.Role) string {
|
|
out := make([]string, len(roles))
|
|
for i := range roles {
|
|
out[i] = roles[i].GetName()
|
|
}
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
// matchAllowImpersonateCondition matches impersonate condition,
|
|
// both user, role and where condition has to match
|
|
func matchAllowImpersonateCondition(ctx *impersonateContext, whereParser predicate.Parser, cond types.ImpersonateConditions, impersonateUser types.User, impersonateRoles []types.Role) (bool, error) {
|
|
// User impersonation requires both users and roles. Roles with no users
|
|
// must use RoleRequests instead; however, we can't treat this as an error
|
|
// since this function is tested against all roles regardless of how
|
|
// they'll be used.
|
|
if len(cond.Users) == 0 || len(cond.Roles) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
anyUser, err := parse.NewAnyMatcher(cond.Users)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
if !anyUser.Match(impersonateUser.GetName()) {
|
|
return false, nil
|
|
}
|
|
|
|
anyRole, err := parse.NewAnyMatcher(cond.Roles)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
for _, impersonateRole := range impersonateRoles {
|
|
if !anyRole.Match(impersonateRole.GetName()) {
|
|
return false, nil
|
|
}
|
|
// TODO:
|
|
// This set impersonateRole inside the ctx that is in turn used inside whereParser
|
|
// which is created in CheckImpersonate above but is being used right below.
|
|
// This is unfortunate interface of the parser, instead
|
|
// parser should accept additional context as a first argument.
|
|
ctx.impersonateRole = impersonateRole
|
|
match, err := matchesImpersonateWhere(cond, whereParser)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// matchDenyImpersonateCondition matches impersonate condition,
|
|
// greedy is used for deny type rules, where any user or role can match
|
|
func matchDenyImpersonateCondition(cond types.ImpersonateConditions, impersonateUser types.User, impersonateRoles []types.Role) (bool, error) {
|
|
// As above, user impersonation requires both users and roles. We can't
|
|
// return an error to ensure role impersonation rules are allowed to exist
|
|
// in the system.
|
|
if len(cond.Users) == 0 || len(cond.Roles) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
anyUser, err := parse.NewAnyMatcher(cond.Users)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
if anyUser.Match(impersonateUser.GetName()) {
|
|
return true, nil
|
|
}
|
|
|
|
anyRole, err := parse.NewAnyMatcher(cond.Roles)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
for _, impersonateRole := range impersonateRoles {
|
|
if anyRole.Match(impersonateRole.GetName()) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// matchAllowRoleImpersonateCondition matches an allow impersonate condition
|
|
// specifically for role-only impersonation, where only roles are matched.
|
|
func matchAllowRoleImpersonateCondition(ctx *impersonateContext, whereParser predicate.Parser, cond types.ImpersonateConditions, impersonateRoles []types.Role) (bool, error) {
|
|
// an empty set matches nothing
|
|
if len(cond.Users) == 0 && len(cond.Roles) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// Role impersonation can never apply to users.
|
|
if len(cond.Users) != 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// By this point, at least 1 role is guaranteed.
|
|
anyRole, err := parse.NewAnyMatcher(cond.Roles)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
for _, impersonateRole := range impersonateRoles {
|
|
if !anyRole.Match(impersonateRole.GetName()) {
|
|
return false, nil
|
|
}
|
|
// TODO:
|
|
// This set impersonateRole inside the ctx that is in turn used inside whereParser
|
|
// which is created in CheckImpersonate above but is being used right below.
|
|
// This is unfortunate interface of the parser, instead
|
|
// parser should accept additional context as a first argument.
|
|
ctx.impersonateRole = impersonateRole
|
|
match, err := matchesImpersonateWhere(cond, whereParser)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// matchDenyRoleImpersonateCondition matches a deny impersonate condition
|
|
// specifically for role impersonation, where only roles are matched.
|
|
func matchDenyRoleImpersonateCondition(cond types.ImpersonateConditions, impersonateRoles []types.Role) (bool, error) {
|
|
// an empty set matches nothing
|
|
if len(cond.Users) == 0 && len(cond.Roles) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// Role impersonation can never apply to users.
|
|
if len(cond.Users) != 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// By this point, at least 1 role is guaranteed.
|
|
anyRole, err := parse.NewAnyMatcher(cond.Roles)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
for _, impersonateRole := range impersonateRoles {
|
|
if anyRole.Match(impersonateRole.GetName()) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// RoleMatcher defines an interface for a generic role matcher.
|
|
type RoleMatcher interface {
|
|
Match(types.Role, types.RoleConditionType) (bool, error)
|
|
}
|
|
|
|
// RoleMatchers defines a list of matchers.
|
|
type RoleMatchers []RoleMatcher
|
|
|
|
// MatchAll returns true if all matchers in the set match.
|
|
func (m RoleMatchers) MatchAll(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
for _, matcher := range m {
|
|
match, err := matcher.Match(role, condition)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// MatchAny returns true if at least one of the matchers in the set matches.
|
|
//
|
|
// If the result is true, returns matcher that matched.
|
|
func (m RoleMatchers) MatchAny(role types.Role, condition types.RoleConditionType) (bool, RoleMatcher, error) {
|
|
for _, matcher := range m {
|
|
match, err := matcher.Match(role, condition)
|
|
if err != nil {
|
|
return false, nil, trace.Wrap(err)
|
|
}
|
|
if match {
|
|
return true, matcher, nil
|
|
}
|
|
}
|
|
return false, nil, nil
|
|
}
|
|
|
|
// databaseUserMatcher matches a role against database account name.
|
|
type databaseUserMatcher struct {
|
|
// user is the name of the database user.
|
|
user string
|
|
// alternativeNames is a list of alternative names for the database user.
|
|
alternativeNames []string
|
|
}
|
|
|
|
// NewDatabaseUserMatcher creates a RoleMatcher that checks whether the role's
|
|
// database users match the specified condition.
|
|
func NewDatabaseUserMatcher(db types.Database, user string) RoleMatcher {
|
|
if db.RequireAWSIAMRolesAsUsers() {
|
|
return &databaseUserMatcher{
|
|
user: user,
|
|
alternativeNames: makeAlternativeNamesForAWSRole(db, user),
|
|
}
|
|
}
|
|
|
|
return &databaseUserMatcher{user: user}
|
|
}
|
|
|
|
// Match matches database account name against provided role and condition.
|
|
func (m *databaseUserMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
selectors := role.GetDatabaseUsers(condition)
|
|
if match, _ := MatchDatabaseUser(selectors, m.user, true); match {
|
|
return true, nil
|
|
}
|
|
|
|
for _, altName := range m.alternativeNames {
|
|
if match, _ := MatchDatabaseUser(selectors, altName, false); match {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *databaseUserMatcher) String() string {
|
|
return fmt.Sprintf("databaseUserMatcher(user=%v, alternativeNames=%v)", m.user, m.alternativeNames)
|
|
}
|
|
|
|
func makeAlternativeNamesForAWSRole(db types.Database, user string) []string {
|
|
metadata := db.GetAWS()
|
|
if metadata.Region == "" || metadata.AccountID == "" {
|
|
return nil
|
|
}
|
|
|
|
// If input database user is a role ARN, try the short role name.
|
|
// The input role ARN must have matching partition and account ID in
|
|
// order to try the short role name.
|
|
if arn.IsARN(user) {
|
|
roleName, err := awsutils.ValidateRoleARNAndExtractRoleName(user, metadata.Partition(), metadata.AccountID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return []string{roleName}
|
|
}
|
|
|
|
// If input database user is the short role name, try the full ARN.
|
|
return []string{awsutils.BuildRoleARN(user, metadata.Region, metadata.AccountID)}
|
|
}
|
|
|
|
// DatabaseNameMatcher matches a role against database name.
|
|
type DatabaseNameMatcher struct {
|
|
Name string
|
|
}
|
|
|
|
// Match matches database name against provided role and condition.
|
|
func (m *DatabaseNameMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
match, _ := MatchDatabaseName(role.GetDatabaseNames(condition), m.Name)
|
|
return match, nil
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *DatabaseNameMatcher) String() string {
|
|
return fmt.Sprintf("DatabaseNameMatcher(Name=%v)", m.Name)
|
|
}
|
|
|
|
type loginMatcher struct {
|
|
login string
|
|
}
|
|
|
|
// NewLoginMatcher creates a RoleMatcher that checks whether the role's logins
|
|
// match the specified condition.
|
|
func NewLoginMatcher(login string) RoleMatcher {
|
|
return &loginMatcher{login: login}
|
|
}
|
|
|
|
// Match matches a login against a role.
|
|
func (l *loginMatcher) Match(role types.Role, typ types.RoleConditionType) (bool, error) {
|
|
logins := role.GetLogins(typ)
|
|
for _, login := range logins {
|
|
if l.login == login {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
type windowsLoginMatcher struct {
|
|
login string
|
|
}
|
|
|
|
// NewWindowsLoginMatcher creates a RoleMatcher that checks whether the role's
|
|
// Windows desktop logins match the specified condition.
|
|
func NewWindowsLoginMatcher(login string) RoleMatcher {
|
|
return &windowsLoginMatcher{login: login}
|
|
}
|
|
|
|
// Match matches a Windows Desktop login against a role.
|
|
func (l *windowsLoginMatcher) Match(role types.Role, typ types.RoleConditionType) (bool, error) {
|
|
logins := role.GetWindowsLogins(typ)
|
|
for _, login := range logins {
|
|
if l.login == login {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
type kubernetesClusterLabelMatcher struct {
|
|
clusterLabels map[string]string
|
|
}
|
|
|
|
// NewKubeResourcesMatcher creates a new KubeResourcesMatcher matcher that
|
|
// matches a role against any Kubernetes Resource specified.
|
|
// It also keeps track of the resources that did not match any of user's roles and
|
|
// that shouldn't be included in the resource ids because the user is not allowed
|
|
// to request them.
|
|
func NewKubeResourcesMatcher(resources []types.KubernetesResource) *KubeResourcesMatcher {
|
|
matcher := &KubeResourcesMatcher{
|
|
resources: resources,
|
|
unmatchedReqs: map[string]struct{}{},
|
|
}
|
|
for _, name := range resources {
|
|
matcher.unmatchedReqs[name.ClusterResource()] = struct{}{}
|
|
}
|
|
return matcher
|
|
}
|
|
|
|
// KubeResourcesMatcher matches a role against any Kubernetes Resource specified.
|
|
// It also keeps track of the resources that did not match any of user's roles and
|
|
// that shouldn't be included in the resource ids because the user is not allowed
|
|
// to request them.
|
|
type KubeResourcesMatcher struct {
|
|
resources []types.KubernetesResource
|
|
unmatchedReqs map[string]struct{}
|
|
}
|
|
|
|
// Match matches a Kubernetes resource against provided role and condition.
|
|
func (m *KubeResourcesMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
var finalResult bool
|
|
for _, pod := range m.resources {
|
|
result, err := utils.KubeResourceMatchesRegex(pod, role.GetKubeResources(condition))
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
if result {
|
|
delete(m.unmatchedReqs, pod.ClusterResource())
|
|
finalResult = true
|
|
}
|
|
}
|
|
return finalResult, nil
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *KubeResourcesMatcher) String() string {
|
|
return fmt.Sprintf("KubeResourcesMatcher(Resources=%v)", m.resources)
|
|
}
|
|
|
|
// Unmatched returns the Kubernetes Resource request access that that didn't
|
|
// match with any `search_as_roles` kubernetes resources.
|
|
func (m *KubeResourcesMatcher) Unmatched() []string {
|
|
unmatched := make([]string, 0, len(m.unmatchedReqs))
|
|
for k := range m.unmatchedReqs {
|
|
unmatched = append(unmatched, k)
|
|
}
|
|
return unmatched
|
|
}
|
|
|
|
// KubernetesResourceMatcher matches a role against a Kubernetes Resource.
|
|
// Kind is must be stricly equal but namespace and name allow wildcards.
|
|
type KubernetesResourceMatcher struct {
|
|
resource types.KubernetesResource
|
|
}
|
|
|
|
// NewKubernetesResourceMatcher creates a KubernetesResourceMatcher that checks
|
|
// whether the role's KubeResources match the specified condition.
|
|
func NewKubernetesResourceMatcher(resource types.KubernetesResource) *KubernetesResourceMatcher {
|
|
return &KubernetesResourceMatcher{
|
|
resource: resource,
|
|
}
|
|
}
|
|
|
|
// Match matches a Kubernetes Resource against provided role and condition.
|
|
func (m *KubernetesResourceMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
result, err := utils.KubeResourceMatchesRegex(m.resource, role.GetKubeResources(condition))
|
|
|
|
return result, trace.Wrap(err)
|
|
}
|
|
|
|
// String returns the matcher's string representation.
|
|
func (m *KubernetesResourceMatcher) String() string {
|
|
return fmt.Sprintf("KubernetesResourceMatcher(Resource=%v)", m.resource)
|
|
}
|
|
|
|
// NewKubernetesClusterLabelMatcher creates a RoleMatcher that checks whether a role's
|
|
// Kubernetes service labels match.
|
|
func NewKubernetesClusterLabelMatcher(clustersLabels map[string]string) RoleMatcher {
|
|
return &kubernetesClusterLabelMatcher{clusterLabels: clustersLabels}
|
|
}
|
|
|
|
// Match matches a Kubernetes cluster labels against a role.
|
|
func (l *kubernetesClusterLabelMatcher) Match(role types.Role, typ types.RoleConditionType) (bool, error) {
|
|
ok, _, err := MatchLabels(l.getKubeLabels(role, typ), l.clusterLabels)
|
|
return ok, trace.Wrap(err)
|
|
}
|
|
|
|
// getKubeLabels returns kubernetes_labels based on resource version and role type.
|
|
func (l kubernetesClusterLabelMatcher) getKubeLabels(role types.Role, typ types.RoleConditionType) types.Labels {
|
|
labels := role.GetKubernetesLabels(typ)
|
|
|
|
// After the introduction of https://github.com/gravitational/teleport/pull/9759 the
|
|
// kubernetes_labels started to be respected. Former role behavior evaluated deny rules
|
|
// even if the kubernetes_labels was empty. To preserve this behavior after respecting kubernetes label the label
|
|
// logic needs to be aligned.
|
|
// Default wildcard rules should be added to deny.kubernetes_labels if
|
|
// deny.kubernetes_labels is empty to ensure that deny rule will be evaluated
|
|
// even if kubernetes_labels are empty.
|
|
if len(labels) == 0 && typ == types.Deny {
|
|
return map[string]apiutils.Strings{types.Wildcard: []string{types.Wildcard}}
|
|
}
|
|
return labels
|
|
}
|
|
|
|
// AccessCheckable is the subset of types.Resource required for the RBAC checks.
|
|
type AccessCheckable interface {
|
|
GetKind() string
|
|
GetName() string
|
|
GetMetadata() types.Metadata
|
|
GetAllLabels() map[string]string
|
|
}
|
|
|
|
// rbacDebugLogger creates a debug logger for Teleport's RBAC component.
|
|
// It also returns a flag indicating whether debug logging is enabled,
|
|
// allowing the RBAC system to generate more verbose errors in debug mode.
|
|
func rbacDebugLogger() (debugEnabled bool, debugf func(format string, args ...interface{})) {
|
|
isDebugEnabled := log.IsLevelEnabled(log.DebugLevel)
|
|
log := log.WithField(trace.Component, teleport.ComponentRBAC)
|
|
return isDebugEnabled, log.Debugf
|
|
}
|
|
|
|
// checkAccess checks if this role set has access to a particular resource r,
|
|
// based on the passed AccessState, the resource's labels, and the passed matchers.
|
|
func (set RoleSet) checkAccess(r AccessCheckable, state AccessState, matchers ...RoleMatcher) error {
|
|
// Note: logging in this function only happens in debug mode. This is because
|
|
// adding logging to this function (which is called on every resource returned
|
|
// by the backend) can slow down this function by 50x for large clusters!
|
|
isDebugEnabled, debugf := rbacDebugLogger()
|
|
|
|
if !state.MFAVerified && state.MFARequired == MFARequiredAlways {
|
|
debugf("Access to %v %q denied, cluster requires per-session MFA", r.GetKind(), r.GetName())
|
|
return ErrSessionMFARequired
|
|
}
|
|
|
|
namespace := types.ProcessNamespace(r.GetMetadata().Namespace)
|
|
allLabels := r.GetAllLabels()
|
|
|
|
// Additional message depending on kind of resource
|
|
// so there's more context on why the user might not have access.
|
|
additionalDeniedMessage := ""
|
|
|
|
var getRoleLabels func(types.Role, types.RoleConditionType) types.Labels
|
|
|
|
switch r.GetKind() {
|
|
case types.KindDatabase:
|
|
getRoleLabels = types.Role.GetDatabaseLabels
|
|
additionalDeniedMessage = "Confirm database user and name."
|
|
case types.KindDatabaseService:
|
|
getRoleLabels = types.Role.GetDatabaseServiceLabels
|
|
case types.KindApp:
|
|
getRoleLabels = types.Role.GetAppLabels
|
|
case types.KindNode:
|
|
getRoleLabels = types.Role.GetNodeLabels
|
|
additionalDeniedMessage = "Confirm SSH login."
|
|
case types.KindKubernetesCluster:
|
|
getRoleLabels = types.Role.GetKubernetesLabels
|
|
additionalDeniedMessage = "Confirm Kubernetes user or group."
|
|
case types.KindWindowsDesktop:
|
|
getRoleLabels = types.Role.GetWindowsDesktopLabels
|
|
additionalDeniedMessage = "Confirm Windows user."
|
|
case types.KindWindowsDesktopService:
|
|
getRoleLabels = types.Role.GetWindowsDesktopLabels
|
|
default:
|
|
return trace.BadParameter("cannot match labels for kind %v", r.GetKind())
|
|
}
|
|
|
|
// Check deny rules.
|
|
for _, role := range set {
|
|
matchNamespace, namespaceMessage := MatchNamespace(role.GetNamespaces(types.Deny), namespace)
|
|
if !matchNamespace {
|
|
continue
|
|
}
|
|
|
|
matchLabels, labelsMessage, err := MatchLabels(getRoleLabels(role, types.Deny), allLabels)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matchLabels {
|
|
debugf("Access to %v %q denied, deny rule in role %q matched; match(namespace=%v, label=%v)",
|
|
r.GetKind(), r.GetName(), role.GetName(), namespaceMessage, labelsMessage)
|
|
return trace.AccessDenied("access to %v denied. User does not have permissions. %v",
|
|
r.GetKind(), additionalDeniedMessage)
|
|
}
|
|
|
|
// Deny rules are greedy on purpose. They will always match if
|
|
// at least one of the matchers returns true.
|
|
matchMatchers, matchersMessage, err := RoleMatchers(matchers).MatchAny(role, types.Deny)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matchMatchers {
|
|
debugf("Access to %v %q denied, deny rule in role %q matched; match(matcher=%v)",
|
|
r.GetKind(), r.GetName(), role.GetName(), matchersMessage)
|
|
return trace.AccessDenied("access to %v denied. User does not have permissions. %v",
|
|
r.GetKind(), additionalDeniedMessage)
|
|
}
|
|
}
|
|
|
|
mfaAllowed := state.MFAVerified || state.MFARequired == MFARequiredNever
|
|
|
|
// TODO(codingllama): Consider making EnableDeviceVerification opt-out instead
|
|
// of opt-in.
|
|
deviceAllowed := !state.EnableDeviceVerification || state.DeviceVerified
|
|
|
|
var errs []error
|
|
allowed := false
|
|
// Check allow rules.
|
|
for _, role := range set {
|
|
matchNamespace, namespaceMessage := MatchNamespace(role.GetNamespaces(types.Allow), namespace)
|
|
if !matchNamespace {
|
|
if isDebugEnabled {
|
|
errs = append(errs, trace.AccessDenied("role=%v, match(namespace=%v)",
|
|
role.GetName(), namespaceMessage))
|
|
}
|
|
continue
|
|
}
|
|
|
|
matchLabels, labelsMessage, err := MatchLabels(getRoleLabels(role, types.Allow), allLabels)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if !matchLabels {
|
|
if isDebugEnabled {
|
|
errs = append(errs, trace.AccessDenied("role=%v, match(label=%v)",
|
|
role.GetName(), labelsMessage))
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Allow rules are not greedy. They will match only if all of the
|
|
// matchers return true.
|
|
matchMatchers, err := RoleMatchers(matchers).MatchAll(role, types.Allow)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if !matchMatchers {
|
|
if isDebugEnabled {
|
|
errs = append(errs, fmt.Errorf("role=%v, match(matchers=%v)",
|
|
role.GetName(), matchers))
|
|
}
|
|
continue
|
|
}
|
|
|
|
// If we've reached this point, namespace, labels, and matchers all match.
|
|
//
|
|
// The following checks remain:
|
|
// 1. MFA verification (aka require_session_mfa)
|
|
// 2. Device verification (aka device_trust_mode)
|
|
//
|
|
// The more restrictive setting applies, so either the caller passes all
|
|
// (and gets an early exit) or we need to check every applicable role to
|
|
// ensure the access is permitted.
|
|
|
|
if mfaAllowed && deviceAllowed {
|
|
debugf("Access to %v %q granted, allow rule in role %q matched.",
|
|
r.GetKind(), r.GetName(), role.GetName())
|
|
return nil
|
|
}
|
|
|
|
// MFA verification.
|
|
if !mfaAllowed && role.GetOptions().RequireMFAType.IsSessionMFARequired() {
|
|
debugf("Access to %v %q denied, role %q requires per-session MFA",
|
|
r.GetKind(), r.GetName(), role.GetName())
|
|
return ErrSessionMFARequired
|
|
}
|
|
|
|
// Device verification.
|
|
if !deviceAllowed && role.GetOptions().DeviceTrustMode == constants.DeviceTrustModeRequired {
|
|
debugf("Access to %v %q denied, role %q requires a trusted device",
|
|
r.GetKind(), r.GetName(), role.GetName())
|
|
return ErrTrustedDeviceRequired
|
|
}
|
|
|
|
// Current role allows access, but keep looking for a more restrictive
|
|
// setting.
|
|
allowed = true
|
|
debugf("Access to %v %q granted, allow rule in role %q matched.",
|
|
r.GetKind(), r.GetName(), role.GetName())
|
|
}
|
|
|
|
if allowed {
|
|
return nil
|
|
}
|
|
|
|
debugf("Access to %v %q denied, no allow rule matched; %v", r.GetKind(), r.GetName(), errs)
|
|
return trace.AccessDenied("access to %v denied. User does not have permissions. %v",
|
|
r.GetKind(), additionalDeniedMessage)
|
|
}
|
|
|
|
// CanForwardAgents returns true if role set allows forwarding agents.
|
|
func (set RoleSet) CanForwardAgents() bool {
|
|
for _, role := range set {
|
|
if role.GetOptions().ForwardAgent.Value() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CanPortForward returns true if a role in the RoleSet allows port forwarding.
|
|
func (set RoleSet) CanPortForward() bool {
|
|
for _, role := range set {
|
|
if types.BoolDefaultTrue(role.GetOptions().PortForwarding) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RecordDesktopSession returns true if the role set has enabled desktop
|
|
// session recording. Recording is considered enabled if at least one
|
|
// role in the set has enabled it.
|
|
func (set RoleSet) RecordDesktopSession() bool {
|
|
for _, role := range set {
|
|
var bo *types.BoolOption
|
|
if role.GetOptions().RecordSession != nil {
|
|
bo = role.GetOptions().RecordSession.Desktop
|
|
}
|
|
if types.BoolDefaultTrue(bo) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// DesktopClipboard returns true if the role set has enabled shared
|
|
// clipboard for desktop sessions. Clipboard sharing is disabled if
|
|
// one or more of the roles in the set has disabled it.
|
|
func (set RoleSet) DesktopClipboard() bool {
|
|
for _, role := range set {
|
|
if !types.BoolDefaultTrue(role.GetOptions().DesktopClipboard) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// DesktopDirectorySharing returns true if the role set has directory sharing
|
|
// enabled. This setting is disabled if one or more of the roles in the set has
|
|
// disabled it.
|
|
func (set RoleSet) DesktopDirectorySharing() bool {
|
|
for _, role := range set {
|
|
if !types.BoolDefaultTrue(role.GetOptions().DesktopDirectorySharing) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MaybeCanReviewRequests attempts to guess if this RoleSet belongs
|
|
// to a user who should be submitting access reviews. Because not all rolesets
|
|
// are derived from statically assigned roles, this may return false positives.
|
|
func (set RoleSet) MaybeCanReviewRequests() bool {
|
|
for _, role := range set {
|
|
if !role.GetAccessReviewConditions(types.Allow).IsZero() {
|
|
// at least one nonzero allow directive exists for
|
|
// review submission.
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// PermitX11Forwarding returns true if this RoleSet allows X11 Forwarding.
|
|
func (set RoleSet) PermitX11Forwarding() bool {
|
|
for _, role := range set {
|
|
if role.GetOptions().PermitX11Forwarding.Value() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CanCopyFiles returns true if the role set has enabled remote file
|
|
// operations via SCP or SFTP. Remote file operations are disabled if
|
|
// one or more of the roles in the set has disabled it.
|
|
func (set RoleSet) CanCopyFiles() bool {
|
|
for _, role := range set {
|
|
if !types.BoolDefaultTrue(role.GetOptions().SSHFileCopy) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// CertificateFormat returns the most permissive certificate format in a
|
|
// RoleSet.
|
|
func (set RoleSet) CertificateFormat() string {
|
|
var formats []string
|
|
|
|
for _, role := range set {
|
|
// get the certificate format for each individual role. if a role does not
|
|
// have a certificate format (like implicit roles) skip over it
|
|
certificateFormat := role.GetOptions().CertificateFormat
|
|
if certificateFormat == "" {
|
|
continue
|
|
}
|
|
|
|
formats = append(formats, certificateFormat)
|
|
}
|
|
|
|
// if no formats were found, return standard
|
|
if len(formats) == 0 {
|
|
return constants.CertificateFormatStandard
|
|
}
|
|
|
|
// sort the slice so the most permissive is the first element
|
|
sort.Slice(formats, func(i, j int) bool {
|
|
return certificatePriority(formats[i]) < certificatePriority(formats[j])
|
|
})
|
|
|
|
return formats[0]
|
|
}
|
|
|
|
// EnhancedRecordingSet returns the set of enhanced session recording
|
|
// events to capture for thi role set.
|
|
func (set RoleSet) EnhancedRecordingSet() map[string]bool {
|
|
m := make(map[string]bool)
|
|
|
|
// Loop over all roles and create a set of all options.
|
|
for _, role := range set {
|
|
for _, opt := range role.GetOptions().BPF {
|
|
m[opt] = true
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// HostUsers returns host user information matching a server or nil if
|
|
// a role disallows host user creation
|
|
func (set RoleSet) HostUsers(s types.Server) (*HostUsersInfo, error) {
|
|
groups := make(map[string]struct{})
|
|
sudoers := make(map[string]struct{})
|
|
serverLabels := s.GetAllLabels()
|
|
for _, role := range set {
|
|
result, _, err := MatchLabels(role.GetNodeLabels(types.Allow), serverLabels)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// skip nodes that dont have matching labels
|
|
if !result {
|
|
continue
|
|
}
|
|
createHostUser := role.GetOptions().CreateHostUser
|
|
// if any of the matching roles do not enable create host
|
|
// user, the user should not be allowed on
|
|
if createHostUser == nil || !createHostUser.Value {
|
|
return nil, trace.AccessDenied("user is not allowed to create host users")
|
|
}
|
|
for _, group := range role.GetHostGroups(types.Allow) {
|
|
groups[group] = struct{}{}
|
|
}
|
|
for _, sudoer := range role.GetHostSudoers(types.Allow) {
|
|
sudoers[sudoer] = struct{}{}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
result, _, err := MatchLabels(role.GetNodeLabels(types.Deny), serverLabels)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !result {
|
|
continue
|
|
}
|
|
for _, group := range role.GetHostGroups(types.Deny) {
|
|
delete(groups, group)
|
|
}
|
|
for _, sudoer := range role.GetHostSudoers(types.Deny) {
|
|
if sudoer == "*" {
|
|
sudoers = nil
|
|
break
|
|
}
|
|
delete(sudoers, sudoer)
|
|
}
|
|
}
|
|
|
|
return &HostUsersInfo{
|
|
Groups: utils.StringsSliceFromSet(groups),
|
|
Sudoers: utils.StringsSliceFromSet(sudoers),
|
|
}, nil
|
|
}
|
|
|
|
// certificatePriority returns the priority of the certificate format. The
|
|
// most permissive has lowest value.
|
|
func certificatePriority(s string) int {
|
|
switch s {
|
|
case teleport.CertificateFormatOldSSH:
|
|
return 0
|
|
case constants.CertificateFormatStandard:
|
|
return 1
|
|
default:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
// 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(types.Allow) {
|
|
if role.GetOptions().ForwardAgent.Value() && 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, ","))
|
|
}
|
|
|
|
// GuessIfAccessIsPossible guesses if access is possible for an entire category
|
|
// of resources.
|
|
// It responds the question: "is it possible that there is a resource of this
|
|
// kind that the current user can access?".
|
|
// GuessIfAccessIsPossible is used, mainly, for UI decisions ("should the tab
|
|
// for resource X appear"?). Most callers should use CheckAccessToRule instead.
|
|
func (set RoleSet) GuessIfAccessIsPossible(ctx RuleContext, namespace string, resource string, verb string, silent bool) error {
|
|
// "Where" clause are handled differently by the method:
|
|
// - "allow" rules have their "where" clause always match, as it's assumed
|
|
// that there could be a resource that matches it.
|
|
// - "deny" rules have their "where" clause always fail, as it's assumed that
|
|
// there could be a resource that passes it.
|
|
return set.checkAccessToRuleImpl(checkAccessParams{
|
|
ctx: ctx,
|
|
namespace: namespace,
|
|
resource: resource,
|
|
verb: verb,
|
|
allowWhere: boolParser(true), // always matches
|
|
denyWhere: boolParser(false), // never matches
|
|
silent: silent,
|
|
})
|
|
}
|
|
|
|
type boolParser bool
|
|
|
|
func (p boolParser) Parse(string) (interface{}, error) {
|
|
return predicate.BoolPredicate(func() bool {
|
|
return bool(p)
|
|
}), nil
|
|
}
|
|
|
|
// CheckAccessToRule checks if the RoleSet provides access in the given
|
|
// namespace to the specified resource and verb.
|
|
// silent controls whether the access violations are logged.
|
|
func (set RoleSet) CheckAccessToRule(ctx RuleContext, namespace string, resource string, verb string, silent bool) error {
|
|
whereParser, err := NewWhereParser(ctx)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return set.checkAccessToRuleImpl(checkAccessParams{
|
|
ctx: ctx,
|
|
namespace: namespace,
|
|
resource: resource,
|
|
verb: verb,
|
|
allowWhere: whereParser,
|
|
denyWhere: whereParser,
|
|
silent: silent,
|
|
})
|
|
}
|
|
|
|
// GetKubeResources returns allowed and denied list of Kubernetes Resources configured in the RoleSet.
|
|
func (set RoleSet) GetKubeResources(cluster types.KubeCluster) (allowed, denied []types.KubernetesResource) {
|
|
for _, role := range set {
|
|
matchLabels, _, err := MatchLabels(role.GetKubernetesLabels(types.Allow), cluster.GetAllLabels())
|
|
if err != nil || !matchLabels {
|
|
continue
|
|
}
|
|
allowed = append(allowed, role.GetKubeResources(types.Allow)...)
|
|
}
|
|
|
|
for _, role := range set {
|
|
matchLabels, _, err := MatchLabels(role.GetKubernetesLabels(types.Deny), cluster.GetAllLabels())
|
|
if err != nil || !matchLabels {
|
|
continue
|
|
}
|
|
|
|
denied = append(denied, role.GetKubeResources(types.Deny)...)
|
|
}
|
|
|
|
return deduplicateKubeResources(allowed), deduplicateKubeResources(denied)
|
|
}
|
|
|
|
func deduplicateKubeResources(resources []types.KubernetesResource) []types.KubernetesResource {
|
|
allKeys := make(map[string]struct{})
|
|
copy := make([]types.KubernetesResource, 0, len(resources))
|
|
for _, item := range resources {
|
|
key := item.String()
|
|
if _, value := allKeys[key]; !value {
|
|
allKeys[key] = struct{}{}
|
|
copy = append(copy, item)
|
|
}
|
|
}
|
|
return copy
|
|
}
|
|
|
|
type checkAccessParams struct {
|
|
ctx RuleContext
|
|
namespace string
|
|
resource string
|
|
verb string
|
|
allowWhere, denyWhere predicate.Parser
|
|
silent bool
|
|
}
|
|
|
|
func (set RoleSet) checkAccessToRuleImpl(p checkAccessParams) error {
|
|
actionsParser, err := NewActionsParser(p.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(types.Deny), types.ProcessNamespace(p.namespace))
|
|
if matchNamespace {
|
|
matched, err := MakeRuleSet(role.GetRules(types.Deny)).Match(p.denyWhere, actionsParser, p.resource, p.verb)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matched {
|
|
if !p.silent {
|
|
log.WithFields(log.Fields{
|
|
trace.Component: teleport.ComponentRBAC,
|
|
}).Infof("Access to %v %v in namespace %v denied to %v: deny rule matched.",
|
|
p.verb, p.resource, p.namespace, role.GetName())
|
|
}
|
|
return trace.AccessDenied("access denied to perform action %q on %q", p.verb, p.resource)
|
|
}
|
|
}
|
|
}
|
|
|
|
// check allow: if rule matches, grant access to resource
|
|
for _, role := range set {
|
|
matchNamespace, _ := MatchNamespace(role.GetNamespaces(types.Allow), types.ProcessNamespace(p.namespace))
|
|
if matchNamespace {
|
|
match, err := MakeRuleSet(role.GetRules(types.Allow)).Match(p.allowWhere, actionsParser, p.resource, p.verb)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if match {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if !p.silent {
|
|
log.WithFields(log.Fields{
|
|
trace.Component: teleport.ComponentRBAC,
|
|
}).Infof("Access to %v %v in namespace %v denied to %v: no allow rule matched.",
|
|
p.verb, p.resource, p.namespace, set)
|
|
}
|
|
return trace.AccessDenied("access denied to perform action %q on %q", p.verb, p.resource)
|
|
}
|
|
|
|
// ExtractConditionForIdentifier returns a restrictive filter expression
|
|
// for list queries based on the rules' `where` conditions.
|
|
func (set RoleSet) ExtractConditionForIdentifier(ctx RuleContext, namespace, resource, verb, identifier string) (*types.WhereExpr, error) {
|
|
parser, err := newParserForIdentifierSubcondition(ctx, identifier)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
parseWhere := func(rule types.Rule) (types.WhereExpr, error) {
|
|
if rule.Where == "" {
|
|
return types.WhereExpr{Literal: true}, nil
|
|
}
|
|
out, err := parser.Parse(rule.Where)
|
|
if err != nil {
|
|
return types.WhereExpr{}, trace.Wrap(err)
|
|
}
|
|
expr, ok := out.(types.WhereExpr)
|
|
if !ok {
|
|
return types.WhereExpr{}, trace.BadParameter("invalid type %T when extracting identifier subcondition from %q", out, rule.Where)
|
|
}
|
|
return expr, nil
|
|
}
|
|
|
|
// Gather identifier-related subconditions from the deny rules
|
|
// and concatenate their negations by AND.
|
|
var denyCond *types.WhereExpr
|
|
for _, role := range set {
|
|
matchNamespace, _ := MatchNamespace(role.GetNamespaces(types.Deny), types.ProcessNamespace(namespace))
|
|
if !matchNamespace {
|
|
continue
|
|
}
|
|
rules := MakeRuleSet(role.GetRules(types.Deny))
|
|
for _, rule := range rules[resource] {
|
|
if !rule.HasVerb(verb) && !rule.HasVerb(types.Wildcard) {
|
|
continue
|
|
}
|
|
expr, err := parseWhere(rule)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if b, ok := expr.Literal.(bool); ok {
|
|
if b {
|
|
return nil, trace.AccessDenied("access denied to perform action %q on %q", verb, resource)
|
|
}
|
|
continue
|
|
}
|
|
negated := types.WhereExpr{Not: &expr}
|
|
if denyCond == nil {
|
|
denyCond = &negated
|
|
} else {
|
|
denyCond = &types.WhereExpr{And: types.WhereExpr2{L: denyCond, R: &negated}}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gather identifier-related subconditions from the allow rules
|
|
// and concatenate by OR.
|
|
var allowCond *types.WhereExpr
|
|
for _, role := range set {
|
|
matchNamespace, _ := MatchNamespace(role.GetNamespaces(types.Allow), types.ProcessNamespace(namespace))
|
|
if !matchNamespace {
|
|
continue
|
|
}
|
|
rules := MakeRuleSet(role.GetRules(types.Allow))
|
|
for _, rule := range rules[resource] {
|
|
if !rule.HasVerb(verb) && !rule.HasVerb(types.Wildcard) {
|
|
continue
|
|
}
|
|
expr, err := parseWhere(rule)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if b, ok := expr.Literal.(bool); ok {
|
|
if b {
|
|
return denyCond, nil
|
|
}
|
|
continue
|
|
}
|
|
if allowCond == nil {
|
|
allowCond = &expr
|
|
} else {
|
|
allowCond = &types.WhereExpr{Or: types.WhereExpr2{L: allowCond, R: &expr}}
|
|
}
|
|
}
|
|
}
|
|
|
|
if denyCond == nil {
|
|
if allowCond == nil {
|
|
return nil, trace.AccessDenied("access denied to perform action %q on %q", verb, resource)
|
|
}
|
|
return allowCond, nil
|
|
}
|
|
return &types.WhereExpr{And: types.WhereExpr2{L: denyCond, R: allowCond}}, nil
|
|
}
|
|
|
|
// GetSearchAsRoles returns all SearchAsRoles for this RoleSet.
|
|
func (set RoleSet) GetAllowedSearchAsRoles() []string {
|
|
denied := make(map[string]struct{})
|
|
var allowed []string
|
|
for _, role := range set {
|
|
for _, d := range role.GetSearchAsRoles(types.Deny) {
|
|
denied[d] = struct{}{}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
for _, a := range role.GetSearchAsRoles(types.Allow) {
|
|
if _, ok := denied[a]; !ok {
|
|
allowed = append(allowed, a)
|
|
}
|
|
}
|
|
}
|
|
return apiutils.Deduplicate(allowed)
|
|
}
|
|
|
|
// GetAllowedPreviewAsRoles returns all PreviewAsRoles for this RoleSet.
|
|
func (set RoleSet) GetAllowedPreviewAsRoles() []string {
|
|
denied := make(map[string]struct{})
|
|
var allowed []string
|
|
for _, role := range set {
|
|
for _, d := range role.GetPreviewAsRoles(types.Deny) {
|
|
denied[d] = struct{}{}
|
|
}
|
|
}
|
|
for _, role := range set {
|
|
for _, a := range role.GetPreviewAsRoles(types.Allow) {
|
|
if _, ok := denied[a]; !ok {
|
|
allowed = append(allowed, a)
|
|
}
|
|
}
|
|
}
|
|
return apiutils.Deduplicate(allowed)
|
|
}
|
|
|
|
// AccessState holds state for the present access attempt, including both
|
|
// cluster settings and user state (MFA, device trust, etc).
|
|
type AccessState struct {
|
|
// MFARequired determines whether a user's MFA requirement dynamically changes
|
|
// based on their active role (per-role), or is static across all roles
|
|
// (always/never).
|
|
MFARequired MFARequired
|
|
// MFAVerified is set when MFA has been verified by the caller.
|
|
MFAVerified bool
|
|
// EnableDeviceVerification enables device verification in access checks.
|
|
// It's recommended to set this in tandem with DeviceVerified, so device
|
|
// checks are easier to reason about and have a proper chance of succeeding.
|
|
// Defaults to false for backwards compatibility.
|
|
EnableDeviceVerification bool
|
|
// DeviceVerified is true if the user certificate contains all required
|
|
// device extensions.
|
|
// A value of true enables the caller to clear device trust checks.
|
|
// It's recommended to set this in tandem with EnableDeviceVerification.
|
|
// See [dtauthz.IsTLSDeviceVerified] and [dtauthz.IsSSHDeviceVerified].
|
|
DeviceVerified bool
|
|
}
|
|
|
|
// MFARequired determines when MFA is required for a user to access a resource.
|
|
type MFARequired string
|
|
|
|
const (
|
|
// MFARequiredNever means that MFA is never required for any sessions started by this user. This either
|
|
// means both the cluster auth preference and all roles have per-session MFA off, or at least one of
|
|
// those resources has "require_session_mfa: hardware_key_touch", which overrides per-session MFA.
|
|
MFARequiredNever MFARequired = "never"
|
|
// MFARequiredAlways means that MFA is required for all sessions started by a user. This either
|
|
// means that the cluster auth preference requires per-session MFA, or all of the user's roles require
|
|
// per-session MFA
|
|
MFARequiredAlways MFARequired = "always"
|
|
// MFARequiredPerRole means that MFA requirement is based on which of the user's roles
|
|
// provides access to the session in question.
|
|
MFARequiredPerRole MFARequired = "per-role"
|
|
)
|
|
|
|
// SortedRoles sorts roles by name
|
|
type SortedRoles []types.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]
|
|
}
|
|
|
|
// UnmarshalRole unmarshals the Role resource from JSON.
|
|
func UnmarshalRole(bytes []byte, opts ...MarshalOption) (types.Role, error) {
|
|
var h types.ResourceHeader
|
|
err := json.Unmarshal(bytes, &h)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
cfg, err := CollectOptions(opts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
switch h.Version {
|
|
case types.V6:
|
|
fallthrough
|
|
case types.V5:
|
|
fallthrough
|
|
case types.V4:
|
|
// V4 roles are identical to V3 except for their defaults
|
|
fallthrough
|
|
case types.V3:
|
|
var role types.RoleV6
|
|
if err := utils.FastUnmarshal(bytes, &role); err != nil {
|
|
return nil, trace.BadParameter(err.Error())
|
|
}
|
|
|
|
if err := ValidateRole(&role); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if cfg.ID != 0 {
|
|
role.SetResourceID(cfg.ID)
|
|
}
|
|
if !cfg.Expires.IsZero() {
|
|
role.SetExpiry(cfg.Expires)
|
|
}
|
|
return &role, nil
|
|
}
|
|
|
|
return nil, trace.BadParameter("role version %q is not supported", h.Version)
|
|
}
|
|
|
|
// MarshalRole marshals the Role resource to JSON.
|
|
func MarshalRole(role types.Role, opts ...MarshalOption) ([]byte, error) {
|
|
if err := ValidateRole(role); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
cfg, err := CollectOptions(opts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
switch role := role.(type) {
|
|
case *types.RoleV6:
|
|
if !cfg.PreserveResourceID {
|
|
// avoid modifying the original object
|
|
// to prevent unexpected data races
|
|
copy := *role
|
|
copy.SetResourceID(0)
|
|
role = ©
|
|
}
|
|
return utils.FastMarshal(role)
|
|
default:
|
|
return nil, trace.BadParameter("unrecognized role version %T", role)
|
|
}
|
|
}
|
|
|
|
// DowngradeToV5 converts a V6 role to V5 so that it will be compatible with
|
|
// older instances. Makes a shallow copy if the conversion is necessary. The
|
|
// passed in role will not be mutated.
|
|
// DELETE IN 13.0.0
|
|
func DowngradeRoleToV5(r *types.RoleV6) (*types.RoleV6, error) {
|
|
switch r.Version {
|
|
case types.V3, types.V4, types.V5:
|
|
return r, nil
|
|
case types.V6:
|
|
var downgraded types.RoleV6
|
|
downgraded = *r
|
|
downgraded.Version = types.V5
|
|
return &downgraded, nil
|
|
default:
|
|
return nil, trace.BadParameter("unrecognized role version %T", r.Version)
|
|
}
|
|
}
|