Files
teleport/lib/services/role.go
T
Julia Ogris 677b0d6e1c types: Declare all app_resources fields (#69500)
* types: Declare all app_resources fields in the proto

Declare the eight reserved AppResource fields (paths, methods, where,
allow_encoded, allow_code, allow_reason, deny_code_hint,
deny_reason_hint) and the AppResourcesExpressions role condition, and
regenerate the proto stubs, the role CRDs, the Terraform schema and
docs, and the derived equality functions. Declaring the fields before
the fine-grained matcher merges lets every client of this version name
them, so a client that round-trips a role written by a newer auth
preserves the fields instead of silently dropping them on write-back,
which would widen the rule.

Tighten IsAllowAllOnly to require every declared field besides
allow_all to be unset. Without this, a rule such as {allow_all: true,
methods: [GET]} written by a newer auth would pass the check once the
fields are named, and this version would treat a method-restricted
rule as unrestricted.

Add GetAppResourcesExpressions to the Role interface, and read it in
decideMinimalV9 so the app agent denies a request governed by a
predicate it cannot evaluate. A predicate restricts the rules it
accompanies, so a role that pairs allow_all with one is not
unrestricted, and a deny-side predicate blocks another role's
allow_all the same way a deny-side rule does. Auth already treats both
as restricting when it downgrades a role for a pre-v9 client, so
without this the agent and auth disagree about the same role.

Strip AppResourcesExpressions in the v9-to-v8 downgrade alongside
AppResources, and reject it at write, so a role can neither reach a
pre-v9 client with the restriction silently removed nor be stored
while this version cannot enforce it.

* docs: List the declared app_resources fields

Honour the docs review on the app-resources reference page. A run of
inline code-style field names inside a sentence is hard to scan, so the
eight rejected `app_resources` fields become a list, and
`app_resources_expressions` gets its own sentence rather than trailing
the same run.

* buf: Trim the reserved-deletion except comment

Drop the two sentences explaining why buf's ignore lists cannot scope the
except. The rule name and the removal condition are what a reader needs
here, and the scoping attempts are recorded on the pull request.

* types: Reword the app_resources_expressions and Methods docs

Honour the review on the proto field comments. Drop "desugared" from
`AppResourcesExpressions`, which needs the RFD to gloss, and state the
relation between the two forms directly instead. Open `Methods` with the
plural, matching `Paths` on the row above.

Regenerate the proto stubs, the role CRDs, the Terraform schema and docs.
2026-08-12 05:19:01 +00:00

4337 lines
147 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package services
import (
"bytes"
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"os"
"path"
"reflect"
"regexp"
"slices"
"sort"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/google/uuid"
"github.com/gravitational/trace"
jsoniter "github.com/json-iterator/go"
"github.com/vulcand/predicate"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
"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"
dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz"
"github.com/gravitational/teleport/lib/services/label"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/utils/parse"
setutils "github.com/gravitational/teleport/lib/utils/set"
)
// 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.KindDynamicWindowsDesktop, RO()),
types.NewRule(types.KindLinuxDesktop, RO()),
types.NewRule(types.KindKubernetesCluster, RO()),
types.NewRule(types.KindUsageEvent, []string{types.VerbCreate}),
types.NewRule(types.KindVnetConfig, RO()),
types.NewRule(types.KindSPIFFEFederation, RO()),
types.NewRule(types.KindSAMLIdPServiceProvider, RO()),
types.NewRule(types.KindIdentityCenter, RO()),
types.NewRule(types.KindGitServer, RO()),
}
// 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.
// It's an alias to [dtauthz.ErrTrustedDeviceRequired].
var ErrTrustedDeviceRequired = dtauthz.ErrTrustedDeviceRequired
// ErrSessionMFARequired is returned by AccessChecker when access to a resource
// requires an MFA check.
var ErrSessionMFARequired = &trace.AccessDeniedError{
Message: "access to resource requires MFA",
}
// ErrSessionMFANotRequired indicates that per session mfa will not grant
// access to a resource.
var ErrSessionMFANotRequired = &trace.AccessDeniedError{
Message: "MFA is not required to access resource",
}
// 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 newImplicitRole(types.CopyRulesSlice(DefaultImplicitRules))
}
// newScopedImplicitRole is the default implicit role as it applies to scoped identities. It confers
// the same privileges as [NewImplicitRole], except that secret-inclusive read is replaced with
// secret-exclusive read.
func newScopedImplicitRole() types.Role {
rules := types.CopyRulesSlice(DefaultImplicitRules)
for i, rule := range rules {
// CopyRulesSlice is a shallow copy, so build a replacement slice rather than assigning into it.
verbs := make([]string, len(rule.Verbs))
for j, verb := range rule.Verbs {
if verb == types.VerbRead {
verb = types.VerbReadNoSecrets
}
verbs[j] = verb
}
rules[i].Verbs = verbs
}
return newImplicitRole(rules)
}
func newImplicitRole(rules []types.Rule) 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(),
RecordSession: &types.RecordSession{
Desktop: types.NewBoolOption(false),
},
},
Allow: types.RoleConditions{
Namespaces: []string{defaults.Namespace},
Rules: rules,
},
},
}
}
// RoleForUser creates an admin role for a services.User.
//
// Used in tests only.
func RoleForUser(u types.User) types.Role {
return RoleWithVersionForUser(u, types.DefaultRoleVersion)
}
// RoleWithVersionForUser creates an admin role for a services.User.
//
// Used in tests only.
func RoleWithVersionForUser(u types.User, v string) types.Role {
role, _ := types.NewRoleWithVersion(RoleNameForUser(u.GetName()), v, 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}},
GroupLabels: 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}},
MCP: &types.MCPPermissions{
Tools: []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()),
types.NewRule(types.KindUserGroup, RW()),
types.NewRule(types.KindSAMLIdPServiceProvider, RW()),
},
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.
if types.SystemRole(role.GetMetadata().Name).IsValid() {
return trace.BadParameter("reserved role: %+q", role.GetMetadata().Name)
}
return nil
}
// ValidateRole checks and sets defaults for role fields and validates
// expression syntax.
//
// This function should be called on the write path (role create/update)
// and NOT on read paths to avoid bricking clusters with existing roles
// that may not parse with newer parsers. Read paths should call plain
// CheckAndSetDefaults to reject truly unusable roles.
func ValidateRole(r types.Role) error {
if err := CheckAndSetDefaults(r); err != nil {
return trace.Wrap(err)
}
var errs []error
if err := validateRoleExpressions(r); err != nil {
errs = append(errs, err)
}
if err := validateRoleWildcards(r); err != nil {
errs = append(errs, err)
}
if err := validateSessionPolicies(r); err != nil {
errs = append(errs, err)
}
if err := validateAppResources(r); err != nil {
errs = append(errs, err)
}
return trace.NewAggregate(errs...)
}
// validateAppResources rejects an app_resources rule set that this version
// cannot enforce, for example a rule with an unknown field. It also rejects
// any app_resources_expressions. It runs on create and update only, not on
// read.
func validateAppResources(r types.Role) error {
if len(r.GetAppResources(types.Deny)) > 0 {
return trace.BadParameter("app_resources is not allowed under deny")
}
if len(r.GetAppResourcesExpressions(types.Deny)) > 0 {
return trace.BadParameter("app_resources_expressions is not allowed under deny")
}
if len(r.GetAppResourcesExpressions(types.Allow)) > 0 {
return trace.BadParameter("app_resources_expressions is not supported in this version, only app_resources with allow_all is honored")
}
allow := r.GetAppResources(types.Allow)
for i, rule := range allow {
// The backend JSON marshal drops unknown fields. Storing such a
// rule would silently widen it to unrestricted access.
if !rule.IsAllowAllOnly() {
return trace.BadParameter("app_resources[%d]: this version implements allow_all only, so a rule must set allow_all and nothing else", i)
}
}
// Every rule sets allow_all at this point, so more than one rule can
// only mean allow_all next to another rule.
if len(allow) > 1 {
return trace.BadParameter("app_resources: a rule setting allow_all must be the only rule")
}
return nil
}
// validateRoleExpressions validates all expression and predicate syntax in a role.
func validateRoleExpressions(r types.Role) error {
var errs []error
for _, condition := range []struct {
name string
condition types.RoleConditionType
}{
{"allow", types.Allow},
{"deny", types.Deny},
} {
// Rules
for i, rule := range r.GetRules(condition.condition) {
if err := validateRule(rule); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.rules[%d]: %v", condition.name, i, err))
}
}
// Trait templates in slice fields
for _, values := range []struct {
name string
values []string
}{
{"logins", r.GetLogins(condition.condition)},
{"windows_desktop_logins", r.GetWindowsLogins(condition.condition)},
{"linux_desktop_logins", r.GetLinuxDesktopLogins(condition.condition)},
{"aws_role_arns", r.GetAWSRoleARNs(condition.condition)},
{"azure_identities", r.GetAzureIdentities(condition.condition)},
{"gcp_service_accounts", r.GetGCPServiceAccounts(condition.condition)},
{"kubernetes_groups", r.GetKubeGroups(condition.condition)},
{"kubernetes_users", r.GetKubeUsers(condition.condition)},
{"db_names", r.GetDatabaseNames(condition.condition)},
{"db_users", r.GetDatabaseUsers(condition.condition)},
{"db_roles", r.GetDatabaseRoles(condition.condition)},
{"host_groups", r.GetHostGroups(condition.condition)},
{"host_sudoers", r.GetHostSudoers(condition.condition)},
{"desktop_groups", r.GetDesktopGroups(condition.condition)},
{"impersonate.users", r.GetImpersonateConditions(condition.condition).Users},
{"impersonate.roles", r.GetImpersonateConditions(condition.condition).Roles},
} {
for _, value := range values.values {
if _, err := parse.NewTraitsTemplateExpression(value); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.%s expression: %v", condition.name, values.name, err))
}
}
}
// Impersonate where clause
if where := r.GetImpersonateConditions(condition.condition).Where; where != "" {
// Stub the context, the predicate parser resolves identifiers at parse
// time, so a nil context rejects valid expressions.
parser, err := newImpersonateWhereParser(&impersonateContext{
user: emptyUser,
impersonateUser: emptyUser,
impersonateRole: &types.RoleV6{},
})
if err != nil {
errs = append(errs, trace.BadParameter("%s.impersonate.where: failed to create parser: %v", condition.name, err))
} else if _, err = parser.Parse(where); err != nil {
errs = append(errs, trace.BadParameter("%s.impersonate.where: invalid expression %q: %v", condition.name, where, err))
}
}
// Trait templates in kubernetes_resources
for i, ks := range r.GetKubeResources(condition.condition) {
if _, err := parse.NewTraitsTemplateExpression(ks.Namespace); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.kubernetes_resources[%d].namespace expression: %v", condition.name, i, err))
}
if _, err := parse.NewTraitsTemplateExpression(ks.Name); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.kubernetes_resources[%d].name expression: %v", condition.name, i, err))
}
for _, verb := range ks.Verbs {
if _, err := parse.NewTraitsTemplateExpression(verb); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.kubernetes_resources[%d].verbs expression: %v", condition.name, i, err))
}
}
}
// Label value trait templates and label expressions
for _, labels := range []struct {
name string
kind string
}{
{"cluster_labels", types.KindRemoteCluster},
{"node_labels", types.KindNode},
{"kubernetes_labels", types.KindKubernetesCluster},
{"app_labels", types.KindApp},
{"saml_idp_service_provider", types.KindSAMLIdPServiceProvider},
{"db_labels", types.KindDatabase},
{"db_service_labels", types.KindDatabaseService},
{"windows_desktop_labels", types.KindWindowsDesktop},
{"windows_desktop_labels", types.KindDynamicWindowsDesktop},
{"group_labels", types.KindUserGroup},
{"workload_identity_labels", types.KindWorkloadIdentity},
{"beam_labels", types.KindBeam},
} {
labelMatchers, err := r.GetLabelMatchers(condition.condition, labels.kind)
if err != nil {
return trace.Wrap(err)
}
for _, labelValues := range labelMatchers.Labels {
for _, label := range labelValues {
if _, err := parse.NewTraitsTemplateExpression(label); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.%s template expression: %v", condition.name, labels.name, err))
}
}
}
if len(labelMatchers.Expression) > 0 {
if _, err := label.ParseExpression(labelMatchers.Expression); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.%s_expression: %v", condition.name, labels.name, err))
}
}
}
// Trait templates in github_permissions.organizations
for i, perm := range r.GetGitHubPermissions(condition.condition) {
for _, org := range perm.Organizations {
if _, err := parse.NewTraitsTemplateExpression(org); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.github_permissions[%d].organizations expression: %v", condition.name, i, err))
}
}
}
// Trait templates in mcp.tools
if mcp := r.GetMCPPermissions(condition.condition); mcp != nil {
for i, tool := range mcp.Tools {
if _, err := parse.NewTraitsTemplateExpression(tool); err != nil {
errs = append(errs, trace.BadParameter("parsing %s.mcp.tools[%d] %q: %v", condition.name, i, tool, err))
}
}
}
}
// Trait templates in options.cert_extensions.value
for i, ext := range r.GetOptions().CertExtensions {
if ext == nil {
continue
}
if _, err := parse.NewTraitsTemplateExpression(ext.Value); err != nil {
errs = append(errs, trace.BadParameter("parsing options.cert_extensions[%d].value expression: %v", i, err))
}
}
// Session require policy expressions
for i, policy := range r.GetSessionRequirePolicies() {
if policy == nil || policy.Filter == "" {
continue
}
parser, err := NewWhereParser(sessionFilterValidationContext{})
if err != nil {
errs = append(errs, trace.BadParameter("require_session_join[%d]: failed to create where parser: %v", i, err))
continue
}
if _, err = parser.Parse(policy.Filter); err != nil {
errs = append(errs, trace.BadParameter("require_session_join[%d]: invalid filter %q: %v", i, policy.Filter, err))
}
}
// Access predicates
if err := ValidateAccessPredicates(r); err != nil {
errs = append(errs, err)
}
return trace.NewAggregate(errs...)
}
// sessionFilterValidationContext validates require_session_join filters using
// the same identifiers as moderation.SessionAccessContext at runtime including
// the legacy user.roles alias for user.spec.roles.
//
// Keep in sync with moderation.SessionAccessContext.GetIdentifier in
// lib/auth/moderation/session_access.go.
type sessionFilterValidationContext struct{}
func (sessionFilterValidationContext) GetIdentifier(fields []string) (any, error) {
if fields[0] == "user" && (len(fields) == 2 || len(fields) == 3) {
idx := 1
if len(fields) == 3 && fields[1] == "spec" {
idx = 2
}
switch fields[idx] {
case "name":
return "", nil
case "roles":
return []string{}, nil
}
}
return nil, trace.NotFound("%v is not defined", strings.Join(fields, "."))
}
func (sessionFilterValidationContext) GetResource() (types.Resource, error) {
return nil, trace.NotFound("resource is not used in session filter validation")
}
func (sessionFilterValidationContext) GetAccessChecker() (AccessChecker, error) {
return nil, trace.NotFound("access checker is not used in session filter validation")
}
// validateRoleWildcards rejects wildcards in fields that don't support them.
func validateRoleWildcards(r types.Role) error {
var errs []error
for _, side := range []struct {
name string
rct types.RoleConditionType
}{
{"allow", types.Allow},
{"deny", types.Deny},
} {
for _, field := range []struct {
name string
values []string
}{
{"request.search_as_roles", r.GetSearchAsRoles(side.rct)},
{"review_requests.preview_as_roles", r.GetPreviewAsRoles(side.rct)},
} {
if slices.Contains(field.values, types.Wildcard) {
errs = append(errs, trace.BadParameter("wildcard is not allowed in %s.%s", side.name, field.name))
}
}
}
return trace.NewAggregate(errs...)
}
// validateSessionPolicies validates fields (kinds, modes,
// on_leave) on require_session_join join_sessions policies.
func validateSessionPolicies(r types.Role) error {
var errs []error
// require_session_join
for i, p := range r.GetSessionRequirePolicies() {
if p == nil {
continue
}
if p.Count < 0 {
errs = append(errs, trace.BadParameter("require_session_join[%d]: count cannot be negative, got %d", i, p.Count))
}
if err := validateSessionKinds(p.Kinds); err != nil {
errs = append(errs, trace.BadParameter("require_session_join[%d]: %v", i, err))
}
if err := validateSessionParticipantModes(p.Modes); err != nil {
errs = append(errs, trace.BadParameter("require_session_join[%d]: %v", i, err))
}
switch types.OnSessionLeaveAction(p.OnLeave) {
case "", types.OnSessionLeaveTerminate, types.OnSessionLeavePause:
default:
errs = append(errs, trace.BadParameter("require_session_join[%d]: invalid on_leave action %q, expected one of %q, %q",
i, p.OnLeave, types.OnSessionLeaveTerminate, types.OnSessionLeavePause))
}
}
// join_sessions
for i, p := range r.GetSessionJoinPolicies() {
if p == nil {
continue
}
if err := validateSessionKinds(p.Kinds); err != nil {
errs = append(errs, trace.BadParameter("join_sessions[%d]: %v", i, err))
}
if err := validateSessionParticipantModes(p.Modes); err != nil {
errs = append(errs, trace.BadParameter("join_sessions[%d]: %v", i, err))
}
}
return trace.NewAggregate(errs...)
}
func validateSessionKinds(kinds []string) error {
for _, kind := range kinds {
// "*" is accepted by SessionAccessEvaluator.matchesKind at runtime
if kind == types.Wildcard {
continue
}
switch types.SessionKind(kind) {
case types.SSHSessionKind, types.KubernetesSessionKind, types.DatabaseSessionKind,
types.AppSessionKind, types.WindowsDesktopSessionKind, types.GitSessionKind:
default:
return trace.BadParameter("invalid session kind %q", kind)
}
}
return nil
}
func validateSessionParticipantModes(modes []string) error {
for _, mode := range modes {
switch types.SessionParticipantMode(mode) {
case types.SessionObserverMode, types.SessionModeratorMode, types.SessionPeerMode:
default:
return trace.BadParameter("invalid participant mode %q", mode)
}
}
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{},
ConditionalOption(
slices.Contains(r.Resources, types.KindSession),
WithCanViewFunction(),
),
)
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 {
var output []string
for _, candidate := range candidates {
if utils.IsValidUnixUser(candidate) {
// A valid variable was found in the traits, append it to the list of logins.
output = append(output, candidate)
continue
}
// Log any invalid logins which were added by a user but ignore any
// Teleport internal logins which are known to be invalid.
if candidate != teleport.SSHSessionJoinPrincipal && !strings.HasPrefix(candidate, "no-login-") {
slog.DebugContext(context.Background(), "Skipping invalid Unix login.", "login", 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) {
slog.DebugContext(context.Background(), "Skipping invalid Windows login.", "login", candidate)
continue
}
output = append(output, candidate)
}
return output
}
func warnInvalidAzureIdentities(candidates []string) {
for _, candidate := range candidates {
if !MatchValidAzureIdentity(candidate) {
slog.WarnContext(context.Background(), "Invalid format of Azure identity", "identity", 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)
}
// RoleTemplateContext is the runtime context used to evaluate role template
// expressions.
type RoleTemplateContext struct {
Username string
Traits map[string][]string
}
// 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, error) {
return ApplyTraitsWithContext(r, RoleTemplateContext{Traits: traits})
}
// ApplyTraitsWithContext applies the passed in role template context to any
// variables within the role and returns itself.
//
// Keep in sync with validateRoleExpressions.
func ApplyTraitsWithContext(r types.Role, ctx RoleTemplateContext) (types.Role, error) {
for _, condition := range []types.RoleConditionType{types.Allow, types.Deny} {
inLogins := r.GetLogins(condition)
outLogins := applyValueTraitsSlice(inLogins, ctx, "login")
outLogins = filterInvalidUnixLogins(outLogins)
r.SetLogins(condition, apiutils.Deduplicate(outLogins))
inWindowsLogins := r.GetWindowsLogins(condition)
outWindowsLogins := applyValueTraitsSlice(inWindowsLogins, ctx, "windows_login")
outWindowsLogins = filterInvalidWindowsLogins(outWindowsLogins)
r.SetWindowsLogins(condition, apiutils.Deduplicate(outWindowsLogins))
inLinuxDesktopLogins := r.GetLinuxDesktopLogins(condition)
outLinuxDesktopLogins := applyValueTraitsSlice(inLinuxDesktopLogins, ctx, "linux_desktop_login")
outLinuxDesktopLogins = filterInvalidUnixLogins(outLinuxDesktopLogins)
r.SetLinuxDesktopLogins(condition, apiutils.Deduplicate(outLinuxDesktopLogins))
inRoleARNs := r.GetAWSRoleARNs(condition)
outRoleARNs := applyValueTraitsSlice(inRoleARNs, ctx, "AWS role ARN")
r.SetAWSRoleARNs(condition, apiutils.Deduplicate(outRoleARNs))
inAzureIdentities := r.GetAzureIdentities(condition)
outAzureIdentities := applyValueTraitsSlice(inAzureIdentities, ctx, "Azure identity")
warnInvalidAzureIdentities(outAzureIdentities)
r.SetAzureIdentities(condition, apiutils.Deduplicate(outAzureIdentities))
inGCPAccounts := r.GetGCPServiceAccounts(condition)
outGCPAccounts := applyValueTraitsSlice(inGCPAccounts, ctx, "GCP service account")
r.SetGCPServiceAccounts(condition, apiutils.Deduplicate(outGCPAccounts))
// apply templates to kubernetes groups
inKubeGroups := r.GetKubeGroups(condition)
outKubeGroups := applyValueTraitsSlice(inKubeGroups, ctx, "kube group")
r.SetKubeGroups(condition, apiutils.Deduplicate(outKubeGroups))
// apply templates to kubernetes users
inKubeUsers := r.GetKubeUsers(condition)
outKubeUsers := applyValueTraitsSlice(inKubeUsers, ctx, "kube user")
r.SetKubeUsers(condition, apiutils.Deduplicate(outKubeUsers))
// apply templates to database names
inDbNames := r.GetDatabaseNames(condition)
outDbNames := applyValueTraitsSlice(inDbNames, ctx, "database name")
r.SetDatabaseNames(condition, apiutils.Deduplicate(outDbNames))
// apply templates to database users
inDbUsers := r.GetDatabaseUsers(condition)
outDbUsers := applyValueTraitsSlice(inDbUsers, ctx, "database user")
r.SetDatabaseUsers(condition, apiutils.Deduplicate(outDbUsers))
// apply templates to database roles
inDbRoles := r.GetDatabaseRoles(condition)
outDbRoles := applyValueTraitsSlice(inDbRoles, ctx, "database role")
r.SetDatabaseRoles(condition, apiutils.Deduplicate(outDbRoles))
githubPermissions := r.GetGitHubPermissions(condition)
for i, perm := range githubPermissions {
githubPermissions[i].Organizations = applyValueTraitsSlice(perm.Organizations, ctx, "github organizations")
}
r.SetGitHubPermissions(condition, githubPermissions)
var out []types.KubernetesResource
// we access the resources in the role using the role conditions
// to avoid receiving the compatibility resources added in GetKubernetesResources
// for roles <v7
for _, rec := range r.GetRoleConditions(condition).KubernetesResources {
namespaces := applyValueTraitsSlice([]string{rec.Namespace}, ctx, "kubernetes resource namespace")
if rec.Namespace == "" {
namespaces = []string{""}
}
names := applyValueTraitsSlice([]string{rec.Name}, ctx, "kubernetes resource name")
if rec.Name == "" {
names = []string{""}
}
verbs := applyValueTraitsSlice(rec.Verbs, ctx, "kubernetes resource verb")
// A trait can reintroduce a wildcard alongside other verbs after
// validation has run, so collapse to just the wildcard.
if slices.Contains(verbs, types.Wildcard) {
verbs = []string{types.Wildcard}
}
for _, namespace := range namespaces {
for _, name := range names {
out = append(out, types.KubernetesResource{
Kind: rec.Kind,
Namespace: namespace,
Name: name,
Verbs: verbs,
APIGroup: rec.APIGroup,
})
}
}
}
r.SetKubeResources(condition, out)
for _, kind := range []string{
types.KindRemoteCluster,
types.KindNode,
types.KindKubernetesCluster,
types.KindApp,
types.KindDatabase,
types.KindDatabaseService,
types.KindWindowsDesktop,
types.KindLinuxDesktop,
types.KindUserGroup,
types.KindSAMLIdPServiceProvider,
types.KindWorkloadIdentity,
types.KindBeam,
} {
labelMatchers, err := r.GetLabelMatchers(condition, kind)
if err != nil {
return nil, trace.Wrap(err)
}
// Only labelMatchers.Labels is templated, if empty we can skip
// these label matchers. labelMatchers.Expression can reference user
// traits later during the access check through the expression
// environment, they are not templated in here.
if len(labelMatchers.Labels) == 0 {
continue
}
labelMatchers.Labels = applyLabelsTraits(labelMatchers.Labels, ctx)
if err := r.SetLabelMatchers(condition, kind, labelMatchers); err != nil {
return nil, trace.Wrap(err)
}
}
r.SetHostGroups(condition,
applyValueTraitsSlice(r.GetHostGroups(condition), ctx, "host_groups"))
r.SetHostSudoers(condition,
applyValueTraitsSlice(r.GetHostSudoers(condition), ctx, "host_sudoers"))
r.SetDesktopGroups(condition,
applyValueTraitsSlice(r.GetDesktopGroups(condition), ctx, "desktop_groups"))
options := r.GetOptions()
for i, ext := range options.CertExtensions {
vals, err := ApplyValueTraitsWithContext(ext.Value, ctx)
if err != nil && !trace.IsNotFound(err) {
slog.WarnContext(context.Background(), "Failed to apply trait to cert_extensions.value", "error", 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, ctx, "impersonate user")
outCond.Roles = applyValueTraitsSlice(inCond.Roles, ctx, "impersonate role")
outCond.Users = apiutils.Deduplicate(outCond.Users)
outCond.Roles = apiutils.Deduplicate(outCond.Roles)
outCond.Where = inCond.Where
r.SetImpersonateConditions(condition, outCond)
if mcp := r.GetMCPPermissions(condition); mcp != nil {
mcp.Tools = applyValueTraitsSlice(mcp.Tools, ctx, "mcp.tools")
r.SetMCPPermissions(condition, mcp)
}
}
return r, nil
}
// applyValueTraitsSlice iterates over a slice of input strings, calling
// ApplyValueTraitsWithContext on each.
func applyValueTraitsSlice(inputs []string, ctx RoleTemplateContext, fieldName string) []string {
var output []string
for _, value := range inputs {
outputs, err := ApplyValueTraitsWithContext(value, ctx)
if err != nil {
if !trace.IsNotFound(err) {
slog.DebugContext(context.Background(), "Skipping trait value.", "field", fieldName, "value", value, "error", err)
}
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, ctx RoleTemplateContext) types.Labels {
outLabels := make(types.Labels, len(inLabels))
// every key will be mapped to the first value
for key, vals := range inLabels {
keyVars, err := ApplyValueTraitsWithContext(key, ctx)
if err != nil {
// empty key will not match anything
slog.DebugContext(context.Background(), "Setting empty node label pair", "key", key, "values", vals, "error", err)
keyVars = []string{""}
}
var values []string
for _, val := range vals {
valVars, err := ApplyValueTraitsWithContext(val, ctx)
if err != nil {
slog.DebugContext(context.Background(), "Setting empty node label value", "key", key, "value", val, "error", 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) {
return ApplyValueTraitsWithContext(val, RoleTemplateContext{Traits: traits})
}
// ApplyValueTraitsWithContext applies the passed in role template context to
// the variable.
func ApplyValueTraitsWithContext(val string, ctx RoleTemplateContext) ([]string, error) {
// Extract the variable from the role variable.
expr, err := parse.NewTraitsTemplateExpression(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.TraitLinuxDesktopLogins,
constants.TraitKubeGroups, constants.TraitKubeUsers,
constants.TraitDBNames, constants.TraitDBUsers, constants.TraitDBRoles,
constants.TraitAWSRoleARNs, constants.TraitAzureIdentities,
constants.TraitGCPServiceAccounts, constants.TraitJWT,
constants.TraitGitHubOrgs, constants.TraitMCPTools,
constants.TraitDefaultRelayAddr, constants.TraitIDToken:
default:
return trace.BadParameter("unsupported variable %q", name)
}
}
// The "external" trait namespace is explicitly allowed to reference
// "internal" traits listed above. This is for multiple reasons:
// - back compat, it's always been this way
// - IdPs are allowed to set those trait names so it wouldn't make
// sense to block them when referenced via "external"
// - The user resource spec.traits can include the "internal" trait
// names listed above, as well as any other trait name - but other
// trait names must be referenced in the "external" namespace. It
// wouldn't make a lot of sense to change the namespace
// based only on the trait name, especially given that we tend to
// expand the list of "internal" traits, and that would be a breaking
// change if someone already referred to one of the "new" internal
// traits in the "external" namespace.
return nil
}
interpolated, err := expr.InterpolateWithUser(varValidation, ctx.Username, ctx.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
}
// 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
}
// WO is a shortcut that returns create and update verbs, granting the ability
// to emit/write resources but not list, read, or delete them.
func WO() []string {
return []string{types.VerbCreate, types.VerbUpdate}
}
// 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)
}
// 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(ctx context.Context, 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(ctx, identity.Username, false)
if err != nil {
return nil, nil, trace.Wrap(err)
}
const msg = "Failed to find roles in x509 identity. Fetching " +
"from backend. If the identity provider allows username changes, this can " +
"potentially allow an attacker to change the role of the existing user."
slog.WarnContext(ctx, msg, "username", 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) {
return FetchRoleListWithContext(roleNames, access, RoleTemplateContext{Traits: traits})
}
// FetchRoleListWithContext fetches roles by their names, applies the role
// template context to role variables, and returns the list.
func FetchRoleListWithContext(roleNames []string, access RoleGetter, ctx RoleTemplateContext) (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)
}
role, err = ApplyTraitsWithContext(role, ctx)
if err != nil {
return nil, trace.Wrap(err)
}
roles = append(roles, role)
}
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) {
return FetchRolesWithContext(roleNames, access, RoleTemplateContext{Traits: traits})
}
// FetchRolesWithContext fetches roles by their names, applies the role
// template context to role variables, and returns the RoleSet. Adds runtime
// roles like the default implicit role to RoleSet.
func FetchRolesWithContext(roleNames []string, access RoleGetter, ctx RoleTemplateContext) (RoleSet, error) {
roles, err := FetchRoleListWithContext(roleNames, access, ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return NewRoleSet(roles...), nil
}
// FetchRolesForUser fetches a user's roles using their username and traits as
// role template context.
func FetchRolesForUser(user UserAccessState, access RoleGetter) (RoleSet, error) {
return FetchRolesWithContext(user.GetRoles(), access, RoleTemplateContext{
Username: user.GetName(),
Traits: user.GetTraits(),
})
}
// 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 entities.
func (result *EnumerationResult) Denied() []string {
return result.filtered(false)
}
// Allowed returns all known allowed entities.
func (result *EnumerationResult) Allowed() []string {
if result.WildcardDenied() {
return nil
}
return result.filtered(true)
}
// WildcardAllowed is true if the * entity is allowed for a given rule set.
func (result *EnumerationResult) WildcardAllowed() bool {
return result.wildcardAllowed && !result.wildcardDenied
}
// WildcardDenied is true if the * entity is denied for a given rule set.
func (result *EnumerationResult) WildcardDenied() bool {
return result.wildcardDenied
}
// ToEntities converts result back to allowed and denied entity slices.
//
// If wildcard is denied, only "*" is returned for the denied slice.
// If wildcard is allowed, allowed entities will be appended to the allowed
// slice after the "*" as a hint for users to select.
// Denied entities is only included if the wildcard is allowed.
func (result *EnumerationResult) ToEntities() (allowed, denied []string) {
if result.wildcardDenied {
return nil, []string{types.Wildcard}
}
if result.wildcardAllowed {
return append([]string{types.Wildcard}, result.Allowed()...), result.Denied()
}
return result.Allowed(), nil
}
// NewEnumerationResult returns new EnumerationResult.
func NewEnumerationResult() EnumerationResult {
return EnumerationResult{
allowedDeniedMap: map[string]bool{},
wildcardAllowed: false,
wildcardDenied: false,
}
}
// NewEnumerationResultFromEntities creates a new EnumerationResult and
// populates the result with provided allowed and denied entries.
func NewEnumerationResultFromEntities(allowed, denied []string) EnumerationResult {
var wildcardAllowed bool
var wildcardDenied bool
allowedDeniedMap := make(map[string]bool)
for _, allow := range allowed {
if allow == types.Wildcard {
wildcardAllowed = true
} else {
allowedDeniedMap[allow] = true
}
}
for _, deny := range denied {
if deny == types.Wildcard {
wildcardDenied = true
wildcardAllowed = false
break
}
allowedDeniedMap[deny] = false
}
return EnumerationResult{
allowedDeniedMap: allowedDeniedMap,
wildcardAllowed: wildcardAllowed,
wildcardDenied: wildcardDenied,
}
}
// 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) {
if slices.Contains(selectors, 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, caseFold bool) (bool, string) {
for _, u := range selectors {
if caseFold {
if strings.EqualFold(u, user) {
return true, "matched"
}
} else 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) {
return MatchLabelGetter(selector, label.MapLabelGetter(target))
}
// MatchLabelGetter matches selector against labelGetter. Empty selector matches
// nothing, wildcard matches everything.
//
// Keep in sync with front-end implementation;
// - web/packages/teleport/src/Bots/Add/Shared/kubernetes.ts:34
func MatchLabelGetter(selector types.Labels, labelGetter label.LabelGetter) (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 := labelGetter.GetLabel(key)
if !hasKey {
return false, fmt.Sprintf("no key match: '%v'", key), nil
}
if slices.Contains(selectorValues, types.Wildcard) {
continue
}
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 slices.Clone(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 readonly.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 {
// 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(authPreferencePolicy keys.PrivateKeyPolicy) (keys.PrivateKeyPolicy, error) {
policySet := []keys.PrivateKeyPolicy{authPreferencePolicy}
for _, role := range set {
policySet = append(policySet, role.GetPrivateKeyPolicy())
}
return keys.PolicyThatSatisfiesSet(policySet)
}
// 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
}
// AdjustMFAVerificationInterval will reduce the requested ttl to the lowest mfa verification interval
// for this role set if the role forces MFA tap, otherwise it returns ttl unchanged
func (set RoleSet) AdjustMFAVerificationInterval(ttl time.Duration, enforce bool) time.Duration {
for _, role := range set {
mfaVerificationInterval := role.GetOptions().MFAVerificationInterval
if role.GetOptions().RequireMFAType == types.RequireMFAType_OFF && !enforce {
continue
}
if mfaVerificationInterval != 0 && ttl > mfaVerificationInterval {
ttl = mfaVerificationInterval
}
}
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
}
// MaxKubernetesConnections implements [AccessChecker].
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 := setutils.New[string]()
users := setutils.New[string]()
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.Add(group)
}
for _, user := range role.GetKubeUsers(types.Allow) {
users.Add(user)
}
}
}
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) {
groups.Remove(group)
}
for _, user := range role.GetKubeUsers(types.Deny) {
users.Remove(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 groups.Elements(), users.Elements(), 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 := setutils.New[string]()
users := setutils.New[string]()
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.Add(name)
}
for _, user := range role.GetDatabaseUsers(types.Allow) {
users.Add(user)
}
}
}
for _, role := range set {
for _, name := range role.GetDatabaseNames(types.Deny) {
names.Remove(name)
}
for _, user := range role.GetDatabaseUsers(types.Deny) {
users.Remove(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 names.Elements(), users.Elements(), 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 := setutils.New[string]()
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.Add(arn)
}
}
}
for _, role := range set {
for _, arn := range role.GetAWSRoleARNs(types.Deny) {
arns.Remove(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 arns.Elements(), 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 := setutils.New[string]()
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.Add(strings.ToLower(account))
}
}
}
for _, role := range set {
for _, account := range role.GetGCPServiceAccounts(types.Deny) {
// deny * removes all accounts
if account == types.Wildcard {
accounts = setutils.New[string]()
}
// remove particular account
accounts.Remove(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 accounts.Elements(), nil
}
// checkAccessToSAMLIdPLegacy checks access to the SAML IdP based on
// IDP enabled/disabled in role option and MFA. The IDP option is enforced
// in Teleport role version v7 and below.
func checkAccessToSAMLIdPLegacy(state AccessState, role types.Role) error {
ctx := context.Background()
if state.MFARequired == MFARequiredAlways && !state.MFAVerified {
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Access to SAML IdP denied, cluster requires per-session MFA")
return trace.Wrap(ErrSessionMFARequired)
}
mfaAllowed := state.MFAVerified || state.MFARequired == MFARequiredNever
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 {
return nil
}
// 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())
}
if !mfaAllowed && options.RequireMFAType.IsSessionMFARequired() {
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Access to SAML IdP denied, role requires per-session MFA",
slog.String("role", role.GetName()),
)
return trace.Wrap(ErrSessionMFARequired)
}
return nil
}
// CheckAccessToSAMLIdP checks access to SAML service provider resource.
// For Teleport role version v7 and below (legacy SAML IdP RBAC), only MFA
// and IDP role option is checked.
// For Teleport role version v8 and above (non-legacy SAML IdP RBAC),
// labels, MFA and Device Trust is checked.
// IDP option in the auth preference is checked in both the cases.
func (set RoleSet) CheckAccessToSAMLIdP(r AccessCheckable, username string, traits wrappers.Traits, authPref readonly.AuthPreference, state AccessState, matchers ...RoleMatcher) error {
if authPref != nil {
if !authPref.IsSAMLIdPEnabled() {
return trace.AccessDenied("SAML IdP is disabled at the cluster level")
}
}
if len(set) == 0 {
return trace.AccessDenied("access to %v denied. User does not have permissions. %v",
r.GetKind(), "No roles assigned to user")
}
var v8RoleSet RoleSet
for _, role := range set {
if !types.IsLegacySAMLRBAC(role.GetVersion()) {
v8RoleSet = append(v8RoleSet, role)
continue
}
if err := checkAccessToSAMLIdPLegacy(state, role); err != nil {
return trace.Wrap(err)
}
}
// We checked for empty roleset early on this method. Reaching this part
// and zero non-legacy roleset means that the user was allowed access
// with legacy roles. We'll honor that and return, otherwise, checkAccess
// will deny access on empty role set.
if len(v8RoleSet) == 0 {
return nil
}
if _, err := v8RoleSet.checkAccess(r, username, traits, state, matchers...); err != nil {
return trace.Wrap(err)
}
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
}
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. [email protected].
// 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 {
matched, err := matchDenyRoleImpersonateCondition(role, 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 {
matched, err := matchAllowRoleImpersonateCondition(ctx, whereParser, role, 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))
}
// CheckSubmitForUser checks whether the current user is allowed to
// submit reviews for other users, to be used by plugins.
func (set RoleSet) CheckSubmitForUser(currentUser, submitForUser types.User) error {
for _, role := range set {
denyUsers := role.GetSubmitForUsers(types.Deny)
anyDenyUser, err := parse.NewAnyMatcher(denyUsers)
if err != nil {
return trace.Wrap(err)
}
if anyDenyUser.Match(submitForUser.GetName()) {
return trace.AccessDenied("access denied for '%s' to submit for user '%s'", currentUser.GetName(), submitForUser.GetName())
}
}
for _, role := range set {
allowUsers := role.GetSubmitForUsers(types.Allow)
anyAllowUser, err := parse.NewAnyMatcher(allowUsers)
if err != nil {
return trace.Wrap(err)
}
if anyAllowUser.Match(submitForUser.GetName()) {
return nil
}
}
return trace.AccessDenied("access denied for '%s' to submit for user '%s'", currentUser.GetName(), submitForUser.GetName())
}
// 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 contains[S ~[]E, E any](s S, f func(E) (bool, error)) (bool, error) {
for i := range s {
match, err := f(s[i])
if err != nil {
return false, err
}
if match {
return true, nil
}
}
return false, nil
}
// matchSPIFFESVIDDenyConditions compares a slice of SPIFFE Role Conditions against
// a requested SPIFFE SVID generation. Any field within a condition must match,
// and any condition in the slice can match for the function to return true.
func matchSPIFFESVIDDenyConditions(
conds []*types.SPIFFERoleCondition,
spiffeIDPath string,
dnsSANs []string,
ipSANs []net.IP,
) (bool, error) {
return contains(conds, func(cond *types.SPIFFERoleCondition) (bool, error) {
// Match SPIFFE ID path.
isPathMatch, err := utils.MatchString(spiffeIDPath, cond.Path)
if err != nil {
return false, trace.Wrap(err)
}
if !isPathMatch {
return false, nil
}
// if any DNS SAN in the condition matches, we say the DNS SAN part
// of the condition matches
isDNSMatch := true
for _, dnsSANMatcher := range cond.DNSSANs {
isDNSMatch, err = contains(dnsSANs, func(reqDNSSAN string) (bool, error) {
return utils.MatchString(reqDNSSAN, dnsSANMatcher)
})
if err != nil {
return false, trace.Wrap(err)
}
if isDNSMatch {
break
}
}
if !isDNSMatch {
return false, nil
}
// Any IP SAN requested can match one of the IP SAN matchers in the
// condition.
isIPMatch := true
for _, ipSANMatcher := range cond.IPSANs {
isIPMatch, err = contains(ipSANs, func(reqIPSAN net.IP) (bool, error) {
_, cidr, err := net.ParseCIDR(ipSANMatcher)
if err != nil {
return false, trace.Wrap(err, "parsing cidr")
}
return cidr.Contains(reqIPSAN), nil
})
if err != nil {
return false, trace.Wrap(err)
}
if isIPMatch {
break
}
}
// all other conditions met
return isIPMatch, nil
})
}
// matchSPIFFESVIDAllowConditions compares a slice of SPIFFE Role Conditions against
// a requested SPIFFE SVID generation. All fields within a condition must match,
// but any condition in the slice can match for the function to return true.
func matchSPIFFESVIDAllowConditions(
conds []*types.SPIFFERoleCondition,
spiffeIDPath string,
dnsSANs []string,
ipSANs []net.IP,
) (bool, error) {
return contains(conds, func(cond *types.SPIFFERoleCondition) (bool, error) {
// Match SPIFFE ID path.
match, err := utils.MatchString(spiffeIDPath, cond.Path)
if err != nil {
return false, trace.Wrap(err)
}
if !match {
// No match - skip to next condition.
return false, nil
}
// All DNS SANs requested must match one of the DNS SAN matchers in the
// condition.
for _, dnsSAN := range dnsSANs {
match, err := contains(cond.DNSSANs, func(s string) (bool, error) {
match, err := utils.MatchString(dnsSAN, s)
if err != nil {
return false, trace.Wrap(err)
}
return match, nil
})
if err != nil {
return false, trace.Wrap(err)
}
if !match {
return false, nil
}
}
// All IP SANs requested must match one of the IP SAN matchers in the
// condition.
for _, ipSAN := range ipSANs {
match, err := contains(cond.IPSANs, func(s string) (bool, error) {
_, cidr, err := net.ParseCIDR(s)
if err != nil {
return false, trace.Wrap(err, "parsing cidr")
}
return cidr.Contains(ipSAN), nil
})
if err != nil {
return false, trace.Wrap(err)
}
if !match {
return false, nil
}
}
// All condition fields matched.
return true, nil
})
}
// CheckSPIFFESVID checks if the role set has access to generating the
// requested SPIFFE ID. Returns an error if the role set does not have the
// ability to generate the requested SVID.
func (set RoleSet) CheckSPIFFESVID(spiffeIDPath string, dnsSANs []string, ipSANs []net.IP) error {
accessDenied := trace.AccessDenied("access denied to generate SVID %q", spiffeIDPath)
// check deny: a single match on a deny rule prohibits generation
for _, role := range set {
cond := role.GetSPIFFEConditions(types.Deny)
matched, err := matchSPIFFESVIDDenyConditions(cond, spiffeIDPath, dnsSANs, ipSANs)
if err != nil {
return trace.Wrap(err)
}
if matched {
return accessDenied
}
}
// check allow: if a single condition matches, allow generation
for _, role := range set {
cond := role.GetSPIFFEConditions(types.Allow)
matched, err := matchSPIFFESVIDAllowConditions(cond, spiffeIDPath, dnsSANs, ipSANs)
if err != nil {
return trace.Wrap(err)
}
if matched {
return nil
}
}
return accessDenied
}
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, role types.Role, impersonateRoles []types.Role) (bool, error) {
cond := role.GetImpersonateConditions(types.Allow)
// 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 {
slog.WarnContext(context.Background(),
"Allow rule did not match due to users being set. For role-only impersonation, only roles should be set in allow/deny rules.",
"role", role.GetName(),
)
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(role types.Role, impersonateRoles []types.Role) (bool, error) {
cond := role.GetImpersonateConditions(types.Deny)
// an empty set matches nothing
if len(cond.Users) == 0 && len(cond.Roles) == 0 {
return false, nil
}
// If any users are defined in a role-impersonation deny rule, it always
// matches. This functionally disables role impersonation for rules
// containing a `users` deny entry, which is acceptable because only bots
// should ever use role impersonation.
if len(cond.Users) != 0 {
slog.WarnContext(context.Background(),
"Deny rule matched due to users being set. For role-only impersonation, only roles should be set in allow/deny rules.",
"role", role.GetName(),
)
return true, 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
}
// RoleMatcherFunc is a convenience type for creating a role matcher from a function.
type RoleMatcherFunc func(types.Role, types.RoleConditionType) (bool, error)
func (f RoleMatcherFunc) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
return f(role, condition)
}
// 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
}
// AnyOf returns a RoleMatcher that succeeds if ANY of the underlying matchers match.
func (m RoleMatchers) AnyOf() RoleMatcher {
return RoleMatcherFunc(func(r types.Role, cond types.RoleConditionType) (bool, error) {
ok, _, err := m.MatchAny(r, cond)
return ok, err
})
}
// 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
// caseInsensitive specifies if the username is case insensitive.
caseInsensitive bool
}
// 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.SupportAWSIAMRoleARNAsUsers() {
return &databaseUserMatcher{
user: user,
alternativeNames: makeUsernamesForAWSRoleARN(db, user),
caseInsensitive: db.IsUsernameCaseInsensitive(),
}
}
if db.RequireAWSIAMRolesAsUsers() {
return &databaseUserMatcher{
user: user,
alternativeNames: makeAlternativeNamesForAWSRole(db, user),
caseInsensitive: db.IsUsernameCaseInsensitive(),
}
}
return &databaseUserMatcher{
user: user,
caseInsensitive: db.IsUsernameCaseInsensitive(),
}
}
// 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 /*matchWildcard*/, m.caseInsensitive); match {
return true, nil
}
for _, altName := range m.alternativeNames {
if match, _ := MatchDatabaseUser(selectors, altName, false /*matchWildcard*/, m.caseInsensitive); 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.
roleARN, err := awsutils.BuildRoleARN(user, metadata.Region, metadata.AccountID)
if err != nil {
return nil
}
return []string{roleARN}
}
// makeUsernamesForAWSRoleARN builds ARN alternatives for database users who are full or
// partial ARN.
func makeUsernamesForAWSRoleARN(db types.Database, user string) []string {
if !awsutils.IsRoleARN(user) {
return nil
}
metadata := db.GetAWS()
if metadata.Region != "" && metadata.AccountID != "" && awsutils.IsPartialRoleARN(user) {
roleARN, err := awsutils.BuildRoleARN(user, metadata.Region, metadata.AccountID)
if err != nil {
return nil
}
return []string{roleARN}
}
roleARN, err := awsutils.ParseRoleARN(user)
if err != nil {
return nil
}
return []string{roleARN.Resource}
}
// 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)
if slices.Contains(logins, l.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)
if slices.Contains(logins, l.login) {
return true, nil
}
return false, nil
}
type linuxDesktopLoginMatcher struct {
login string
}
// NewLinuxDesktopLoginMatcher creates a RoleMatcher that checks whether the role's
// Linux desktop logins match the specified condition.
func NewLinuxDesktopLoginMatcher(login string) RoleMatcher {
return &linuxDesktopLoginMatcher{login: login}
}
// Match matches a Linux Desktop login against a role.
func (l *linuxDesktopLoginMatcher) Match(role types.Role, typ types.RoleConditionType) (bool, error) {
logins := role.GetLinuxDesktopLogins(typ)
return slices.Contains(logins, l.login), nil
}
type awsAppLoginMatcher struct {
awsRole string
}
// NewAppAWSLoginMatcher creates a RoleMatcher that checks whether the role's
// AWS Role ARN match the specified condition.
func NewAppAWSLoginMatcher(awsRole string) RoleMatcher {
return &awsAppLoginMatcher{awsRole: awsRole}
}
// Match matches an AWS Role ARN login against a role.
func (l *awsAppLoginMatcher) Match(role types.Role, typ types.RoleConditionType) (bool, error) {
awsRoles := role.GetAWSRoleARNs(typ)
if slices.Contains(awsRoles, l.awsRole) {
return true, nil
}
return false, nil
}
type kubernetesClusterLabelMatcher struct {
clusterLabels map[string]string
username string
userTraits wrappers.Traits
}
// 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 _, r := range resources {
matcher.unmatchedReqs[unmatchedKey(r)] = struct{}{}
}
return matcher
}
// unmatchedKey returns a unique key for a Kubernetes resource.
// It is used to keep track of the resources that did not match any of user's roles.
// Format: <kind>/<namespace>/<name>
func unmatchedKey(r types.KubernetesResource) string {
return path.Join(r.Kind, r.ClusterResource())
}
// 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 _, resource := range m.resources {
// We use utils.KubeResourceMatchesRegexWithVerbsCollector instead of utils.KubeResourceMatchesRegex
// because KubeResourcesMatcher is used to match access request resources at creation time against
// the roles specified in the `search_as_roles` field. This means that we don't have the request verb
// at this point and we need to match the resource against all the verbs specified in the role.
// If the resource matches any of the verbs, we consider the resource as matched.
// Verbs are enforced at the request time when the user is trying to access the Kubernetes Pod.
result, _, err := utils.KubeResourceMatchesRegexWithVerbsCollector(resource, role.GetKubeResources(condition))
if err != nil {
return false, trace.Wrap(err)
}
if result {
delete(m.unmatchedReqs, unmatchedKey(resource))
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
isClusterWideResource bool
}
// NewKubernetesResourceMatcher creates a KubernetesResourceMatcher that checks
// whether the role's KubeResources match the specified condition.
func NewKubernetesResourceMatcher(resource types.KubernetesResource, isClusterWideResource bool) *KubernetesResourceMatcher {
return &KubernetesResourceMatcher{
resource: resource,
isClusterWideResource: isClusterWideResource,
}
}
// 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, m.isClusterWideResource, role.GetKubeResources(condition), 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, username string, userTraits wrappers.Traits) RoleMatcher {
return &kubernetesClusterLabelMatcher{clusterLabels: clustersLabels, username: username, userTraits: userTraits}
}
// Match matches a Kubernetes cluster labels against a role.
func (l *kubernetesClusterLabelMatcher) Match(role types.Role, typ types.RoleConditionType) (bool, error) {
labelMatchers, err := l.getKubeLabelMatchers(role, typ)
if err != nil {
return false, trace.Wrap(err)
}
ok, _, err := CheckLabelsMatch(typ, labelMatchers, l.username, l.userTraits, label.MapLabelGetter(l.clusterLabels), false)
return ok, trace.Wrap(err)
}
// getKubeLabelMatchers returns kubernetes_labels based on resource version and role type.
func (l kubernetesClusterLabelMatcher) getKubeLabelMatchers(role types.Role, typ types.RoleConditionType) (types.LabelMatchers, error) {
labelMatchers, err := role.GetLabelMatchers(typ, types.KindKubernetesCluster)
if err != nil {
return types.LabelMatchers{}, trace.Wrap(err)
}
// 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 labelMatchers.Empty() && typ == types.Deny {
labelMatchers.Labels = types.Labels{types.Wildcard: []string{types.Wildcard}}
}
return labelMatchers, nil
}
// AccessCheckable is the subset of types.Resource required for the RBAC checks.
type AccessCheckable interface {
GetKind() string
GetSubKind() string
GetName() string
GetMetadata() types.Metadata
GetLabel(key string) (value string, ok bool)
GetAllLabels() map[string]string
}
var rbacLogger = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentRBAC)
// resourceRequiresLabelMatching decides if a resource requires label matching
// when making RBAC access decisions.
func resourceRequiresLabelMatching(r AccessCheckable) bool {
// Some resources do not need label matching when assessing whether the user
// should be granted access. Enable it by default, but turn it off in the
// special cases.
switch r.GetKind() {
case types.KindIdentityCenterAccount, types.KindIdentityCenterAccountAssignment:
return false
case types.KindApp, types.KindAppServer:
return r.GetSubKind() != types.KindIdentityCenterAccount
}
return true
}
// RoleGrantsResource reports whether role alone grants access to r, checking
// the namespace and label conditions only. It skips the MFA, device trust and
// lock checks, which apply to a whole role set rather than one role.
func RoleGrantsResource(role types.Role, r AccessCheckable, username string, traits wrappers.Traits) bool {
_, err := NewRoleSet(role).checkAccess(r, username, traits, AccessState{MFAVerified: true})
return err == nil
}
// checkAccess determines whether access should be granted to a resource based on the provided roles, resource
// attributes, user traits, access state (MFA, device trust, etc.), and optional matchers. If state.ReturnPreconditions
// is true, it returns a list of preconditions (e.g., MFA required) that must be satisfied for access. If
// state.ReturnPreconditions is false, it returns an error immediately if access is denied.
func (set RoleSet) checkAccess(
r AccessCheckable,
username string,
traits wrappers.Traits,
state AccessState,
matchers ...RoleMatcher,
) ([]*decisionpb.Precondition, error) {
// Note: logging in this function only happens in trace 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!
ctx := context.Background()
logger := rbacLogger
isLoggingEnabled := logger.Handler().Enabled(ctx, logutils.TraceLevel)
if isLoggingEnabled {
logger = logger.With("resource_kind", r.GetKind(), "resource_name", r.GetName())
}
// Collect preconditions to return to the caller.
var preconds []*decisionpb.Precondition
// If the cluster requires per-session MFA and it hasn't been verified yet, add an MFA precondition or deny access early.
// If the legacy out-of-band MFA flow is allowed (see below) and MFA has already been verified for this session, skip this check.
//
// The legacy out-of-band MFA flow is allowed as long as TELEPORT_UNSTABLE_FORCE_IN_BAND_MFA is not set to "yes".
// When TELEPORT_UNSTABLE_FORCE_IN_BAND_MFA is set to "yes", only in-band MFA is allowed and enforced.
//
// TODO(cthach): Remove in v20.0 when the legacy out-of-band MFA flow is removed.
if state.MFARequired == MFARequiredAlways && (os.Getenv(teleport.EnvVarUnstableForceInBandMFA) == "yes" || !state.MFAVerified) {
// If the caller doesn't want preconditions returned, deny access early to avoid unnecessary work.
if !state.ReturnPreconditions {
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, cluster requires per-session MFA")
return nil, ErrSessionMFARequired
}
// Mark that MFA is required and continue evaluating access.
preconds = append(preconds, decisionpb.Precondition_builder{Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_IN_BAND_MFA}.Build())
}
requiresLabelMatching := resourceRequiresLabelMatching(r)
namespace := types.ProcessNamespace(r.GetMetadata().Namespace)
// Additional message depending on kind of resource
// so there's more context on why the user might not have access.
additionalDeniedMessage := ""
switch r.GetKind() {
case types.KindDatabase:
additionalDeniedMessage = "Confirm database user and name."
case types.KindNode:
additionalDeniedMessage = "Confirm SSH login."
case types.KindKubernetesCluster:
additionalDeniedMessage = "Confirm Kubernetes user or group."
case types.KindWindowsDesktop:
additionalDeniedMessage = "Confirm Windows user."
case types.KindSAMLIdPServiceProvider:
additionalDeniedMessage = "Confirm app_labels."
}
// Check deny rules.
for _, role := range set {
matchNamespace, namespaceMessage := MatchNamespace(role.GetNamespaces(types.Deny), namespace)
if !matchNamespace {
continue
}
if requiresLabelMatching {
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, username, traits, r, isLoggingEnabled)
if err != nil {
return nil, trace.Wrap(err)
}
if matchLabels {
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, deny rule in role matched",
slog.String("role", role.GetName()),
slog.String("namespace_message", namespaceMessage),
slog.String("label_message", labelsMessage),
)
return nil, trace.AccessDenied("access to %v denied. User does not have permissions. %v",
r.GetKind(), additionalDeniedMessage)
}
} else {
logger.LogAttrs(ctx, logutils.TraceLevel, "Role label matching skipped")
}
// 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 nil, trace.Wrap(err)
}
if matchMatchers {
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, deny rule in role matched",
slog.String("role", role.GetName()),
slog.Any("matcher_message", matchersMessage),
)
return nil, trace.AccessDenied("access to %v denied. User does not have permissions. %v",
r.GetKind(), additionalDeniedMessage)
}
}
// MFA checks can be bypassed if either:
// 1. The cluster doesn't require per-session MFA (MFARequiredNever), OR
// 2. Legacy out-of-band MFA has already been verified for the session AND
// a. The legacy out-of-band MFA flow is allowed (TELEPORT_UNSTABLE_FORCE_IN_BAND_MFA is not set to "yes") OR
// b. The caller doesn't want preconditions returned (state.ReturnPreconditions is false)
//
// Listing resources sets state.MFAVerified to true and state.ReturnPreconditions to false to allow bypassing MFA
// checks for resources that require per-session MFA. This is because listing resources is a read-only operation and
// MFA is not required to list resources, even if MFA is required to access the resource. The actual enforcement
// will happen at connection time, so this is not a concern from a security perspective.
//
// TODO(cthach): Remove in v20.0 when the legacy out-of-band MFA flow is removed.
bypassMFAChecks := state.MFARequired == MFARequiredNever ||
(state.MFAVerified && (os.Getenv(teleport.EnvVarUnstableForceInBandMFA) != "yes" || !state.ReturnPreconditions))
// TODO(codingllama): Consider making EnableDeviceVerification opt-out instead
// of opt-in.
deviceTrusted := !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 isLoggingEnabled {
errs = append(errs, trace.AccessDenied("role=%v, match(namespace=%v)",
role.GetName(), namespaceMessage))
}
continue
}
if requiresLabelMatching {
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, username, traits, r, isLoggingEnabled)
if err != nil {
return nil, trace.Wrap(err)
}
if !matchLabels {
if isLoggingEnabled {
errs = append(errs, trace.AccessDenied("role=%v, match(%s)",
role.GetName(), labelsMessage))
}
continue
}
} else {
logger.LogAttrs(ctx, logutils.TraceLevel, "Role label matching skipped for resource")
}
// 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 nil, trace.Wrap(err)
}
if !matchMatchers {
if isLoggingEnabled {
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 bypassMFAChecks && deviceTrusted {
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource granted, allow rule in role matched",
slog.String("role", role.GetName()),
)
return deduplicateAndSortPreconditions(preconds), nil
}
// Check if MFA is required at the role-level.
if !bypassMFAChecks && role.GetOptions().RequireMFAType.IsSessionMFARequired() {
// If the caller doesn't want preconditions returned, deny access early to avoid unnecessary work.
if !state.ReturnPreconditions {
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, role requires per-session MFA",
slog.String("role", role.GetName()),
)
return nil, ErrSessionMFARequired
}
// Mark that MFA is required and continue evaluating access.
preconds = append(preconds, decisionpb.Precondition_builder{Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_IN_BAND_MFA}.Build())
}
// Device verification.
if err := dtauthz.VerifyTrustedDeviceMode(
role.GetOptions().DeviceTrustMode,
dtauthz.VerifyTrustedDeviceModeParams{
IsTrustedDevice: deviceTrusted,
IsBot: state.IsBot,
AllowEmptyMode: true, // Empty mode on roles is equivalent to "off".
},
); err != nil {
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, role requires a trusted device",
slog.String("role", role.GetName()),
)
return nil, trace.Wrap(err)
}
// Current role allows access, but keep looking for a more restrictive
// setting.
allowed = true
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource granted, allow rule in role matched",
slog.String("role", role.GetName()),
)
}
if allowed {
return deduplicateAndSortPreconditions(preconds), nil
}
logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, no allow rule matched",
slog.Any("errors", errs),
)
return nil, trace.AccessDenied("access to %v denied. User does not have permissions. %v",
r.GetKind(), additionalDeniedMessage)
}
func deduplicateAndSortPreconditions(preconds []*decisionpb.Precondition) []*decisionpb.Precondition {
// Deduplicate preconditions by kind.
preconds = slices.CompactFunc(
preconds, func(a, b *decisionpb.Precondition) bool {
return a.GetKind() == b.GetKind()
},
)
// Sort by kind for deterministic ordering during enforcement.
slices.SortFunc(
preconds,
func(a, b *decisionpb.Precondition) int {
return cmp.Compare(a.GetKind(), b.GetKind())
},
)
return preconds
}
// checkRoleLabelsMatch checks if the [role] matches the labels of [resource]
// for [condition].
// It considers both the role labels (<kind>_labels) and label expression
// (<kind>_labels_expression).
//
// Returns a match boolean, a debug message, and any unexpected error.
//
// If [condition] is types.Deny, the match is greedy, if either one matches it's
// considered a match.
//
// If [condition] is types.Allow, the match is not greedy, if either doesn't
// match it's not considered a match.
//
// If neither is set, it's not a match in either case.
func checkRoleLabelsMatch(
condition types.RoleConditionType,
role types.Role,
username string,
userTraits wrappers.Traits,
resource AccessCheckable,
debug bool,
) (bool, string, error) {
labelMatchers, err := role.GetLabelMatchers(condition, resource.GetKind())
if err != nil {
return false, "", trace.Wrap(err)
}
return CheckLabelsMatch(condition, labelMatchers, username, userTraits, resource, debug)
}
// CheckLabelsMatch checks if the [labelMatchers] match the labels of [resource]
// for [condition].
// It considers both [labelMatchers.Labels] and [labelMatchers.Expression].
//
// Returns a match boolean, a debug message, and any unexpected error.
//
// If [condition] is types.Deny, the match is greedy, if either one matches it's
// considered a match.
//
// If [condition] is types.Allow, the match is not greedy, if either doesn't
// match it's not considered a match.
//
// If neither is set, it's not a match in either case.
func CheckLabelsMatch(
condition types.RoleConditionType,
labelMatchers types.LabelMatchers,
username string,
userTraits wrappers.Traits,
resource label.LabelGetter,
debug bool,
) (bool, string, error) {
if labelMatchers.Empty() {
return false, "no label matchers or label expression", nil
}
var message string
labelsUnsetOrMatch, expressionUnsetOrMatch := true, true
if len(labelMatchers.Labels) > 0 {
match, msg, err := MatchLabelGetter(labelMatchers.Labels, resource)
if err != nil {
return false, "", trace.Wrap(err)
}
if debug {
message += "label=" + msg
}
// Deny rules are greedy, if either matches, it's a match.
if condition == types.Deny && match {
return true, message, nil
}
labelsUnsetOrMatch = match
}
if len(labelMatchers.Expression) > 0 {
match, msg, err := matchLabelExpression(labelMatchers.Expression, resource, username, userTraits)
if err != nil {
return false, "", trace.Wrap(err)
}
if debug {
message = strings.Join([]string{message, "expression=" + msg}, ", ")
}
// Deny rules are greedy, if either matches, it's a match.
if condition == types.Deny {
return match, message, nil
}
expressionUnsetOrMatch = match
}
if condition == types.Deny {
// Either branch would have returned if it was a match.
return false, message, nil
}
// Allow rules are not greedy, both must match if they are set.
return labelsUnsetOrMatch && expressionUnsetOrMatch, message, nil
}
func matchLabelExpression(labelExpression string, resource label.LabelGetter, username string, userTraits wrappers.Traits) (bool, string, error) {
parsedExpr, err := label.ParseExpression(labelExpression)
if err != nil {
return false, "", trace.Wrap(err)
}
match, err := parsedExpr.Evaluate(label.ExpressionEnv{
ResourceLabelGetter: resource,
Username: username,
UserTraits: userTraits,
})
if err != nil {
return false, "", trace.Wrap(err, "evaluating label expression %q", labelExpression)
}
if match {
return true, "matched", nil
}
return false, "no match", nil
}
// 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
}
// SSHPortForwardMode returns the SSHPortForwardMode permitted by a RoleSet. Port forwarding is implicitly allowed, but explicit denies take
// precedence of explicit allows when using SSHPortForwarding. The legacy PortForwarding field prefers explicit allows for backwards
// compatibility reasons, but is only evaluated in the absence of an SSHPortForwarding config on the same role.
func (set RoleSet) SSHPortForwardMode() decisionpb.SSHPortForwardMode {
var denyRemote, denyLocal, legacyDeny bool
legacyCanDeny := true
for _, role := range set {
config := role.GetOptions().SSHPortForwarding
// only consider legacy allows when config isn't provided on the same role
if config == nil {
//nolint:staticcheck // this field is preserved for backwards compatibility, but shouldn't be used going forward
if legacy := role.GetOptions().PortForwarding; legacy != nil {
if legacy.Value {
return decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_ON
}
legacyDeny = true
}
continue
}
if config.Remote != nil && config.Remote.Enabled != nil {
if !config.Remote.Enabled.Value {
denyRemote = true
}
// an explicit legacy deny is only possible if no explicit SSHPortForwarding config has been provided
legacyCanDeny = false
}
if config.Local != nil && config.Local.Enabled != nil {
if !config.Local.Enabled.Value {
denyLocal = true
}
// an explicit legacy deny is only possible if no explicit SSHPortForwarding config has been provided
legacyCanDeny = false
}
}
// enforcing implicit allow and preferring allow over explicit deny
switch {
case denyRemote && denyLocal:
return decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_OFF
case legacyDeny && legacyCanDeny:
return decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_OFF
case denyRemote:
return decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_LOCAL
case denyLocal:
return decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_REMOTE
default:
return decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_ON
}
}
// CanPortForward returns true if the RoleSet allows both local and remote port forwarding.
func (set RoleSet) CanPortForward() bool {
return set.SSHPortForwardMode() == decisionpb.SSHPortForwardMode_SSH_PORT_FORWARD_MODE_ON
}
// 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
}
// GetWebTerminalClipboardMode returns the Web UI terminal clipboard mode from the role set.
func (set RoleSet) GetWebTerminalClipboardMode() types.WebTerminalClipboardMode {
var mode types.WebTerminalClipboardMode
for _, r := range set {
switch r.GetOptions().WebTerminalClipboardMode {
// Return immediately if any role has explicitly set the clipboard mode to no-copy, as that should take precedence over any unrestricted's.
case types.WebTerminalClipboardMode_WEB_TERMINAL_CLIPBOARD_MODE_NO_COPY:
return types.WebTerminalClipboardMode_WEB_TERMINAL_CLIPBOARD_MODE_NO_COPY
case types.WebTerminalClipboardMode_WEB_TERMINAL_CLIPBOARD_MODE_UNRESTRICTED:
mode = types.WebTerminalClipboardMode_WEB_TERMINAL_CLIPBOARD_MODE_UNRESTRICTED
}
}
return mode
}
// CanJoinSessions returns true if at least one role in the role set
// allows the user to join active sessions.
func (set RoleSet) CanJoinSessions() bool {
return slices.ContainsFunc(set, func(r types.Role) bool {
return len(r.GetSessionJoinPolicies()) > 0
})
}
// 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
}
// 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) 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
})
}
type boolParser bool
func (p boolParser) Parse(string) (any, 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) error {
whereParser, err := NewWhereParser(
ctx,
// register can_view function if the resource is a session.
ConditionalOption(resource == types.KindSession, WithCanViewFunction()),
)
if err != nil {
return trace.Wrap(err)
}
return set.checkAccessToRuleImpl(checkAccessParams{
ctx: ctx,
namespace: namespace,
resource: resource,
verb: verb,
allowWhere: whereParser,
denyWhere: whereParser,
})
}
// GetKubeResources returns allowed and denied list of Kubernetes Resources configured in the RoleSet.
func (set RoleSet) GetKubeResources(cluster types.KubeCluster, username string, userTraits wrappers.Traits) (allowed, denied []types.KubernetesResource) {
for _, role := range set {
matchLabels, _, err := checkRoleLabelsMatch(types.Allow, role, username, userTraits, cluster, false)
if err != nil || !matchLabels {
continue
}
allowed = append(allowed, role.GetKubeResources(types.Allow)...)
}
for _, role := range set {
// deny rules are not checked for labels because they are greedy. It means that
// if there is a deny rule for a cluster, it will deny access to all resources
// in that cluster, regardless of kubernetes_resources (i.e. making them irrelevant).
// If the goal is to deny access to a specific resource, it should be done by collecting
// all kube resources in deny rules and ignoring if the role matches or not
// the cluster (i.e. no labels check).
denied = append(denied, role.GetKubeResources(types.Deny)...)
}
return deduplicateKubeResources(allowed), deduplicateKubeResources(denied)
}
func deduplicateKubeResources(resources []types.KubernetesResource) []types.KubernetesResource {
allKeys := setutils.New[string]()
copy := make([]types.KubernetesResource, 0, len(resources))
for _, item := range resources {
key := item.String()
if !allKeys.Contains(key) {
allKeys.Add(key)
copy = append(copy, item)
}
}
return copy
}
type checkAccessParams struct {
ctx RuleContext
namespace string
resource string
verb string
allowWhere, denyWhere predicate.Parser
}
type accessExplicitlyDenied struct {
inner error
}
// AccessExplicitlyDenied is an error type that indicates an AccessDenied error
// where a deny rule matched and access is explicitly denied, in contrast to
// cases where there is no matching deny or allow rule and access is only
// implicitly denied.
func AccessExplicitlyDenied(inner error) error {
return &accessExplicitlyDenied{inner}
}
// IsAccessExplicitlyDenied returns true if any of the errors in err's chain is
// an AccessExplicitlyDenied error.
func IsAccessExplicitlyDenied(err error) bool {
var target *accessExplicitlyDenied
return errors.As(err, &target)
}
func (a *accessExplicitlyDenied) Error() string {
return a.inner.Error()
}
func (a *accessExplicitlyDenied) Unwrap() error {
return a.inner
}
func (set RoleSet) checkAccessToRuleImpl(p checkAccessParams) (err error) {
ctx := context.Background()
// Every unknown error, which could be due to a bad role or an expression
// that can't parse, should be considered an explicit denial.
explicitDeny := true
defer func() {
if explicitDeny && err != nil {
err = AccessExplicitlyDenied(err)
}
}()
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 {
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Access denied, deny rule matched",
slog.String("verb", p.verb),
slog.String("resource", p.resource),
slog.String("namespace", p.namespace),
slog.String("role", 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
}
}
}
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Access denied, no allow rule matched",
slog.String("verb", p.verb),
slog.String("resource", p.resource),
slog.String("namespace", p.namespace),
slog.Any("set", set),
)
// At this point no deny rule has matched and there are no more unknown
// errors, so this is only an implicit denial.
explicitDeny = false
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
}
// SearchAsRolesOption is a functional option for filtering SearchAsRoles.
type SearchAsRolesOption func(role types.Role) bool
// GetSearchAsRoles returns all SearchAsRoles for this RoleSet.
func (set RoleSet) GetAllowedSearchAsRoles(allowFilters ...SearchAsRolesOption) []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 {
if slices.ContainsFunc(allowFilters, func(filter SearchAsRolesOption) bool {
return !filter(role)
}) {
// Don't consider this base role if it's filtered out.
continue
}
for _, a := range role.GetSearchAsRoles(types.Allow) {
if _, isDenied := denied[a]; isDenied {
continue
}
allowed = append(allowed, a)
}
}
return apiutils.Deduplicate(allowed)
}
type gk struct{ group, kind string }
// noramlize the give kube kind. Maps legacy values to plural+group, trim the kube: prefix.
// Returns <kind>[.<group>].
func normalizeKubernetesKind(in string) (out gk) {
// Check if we have a legacy kind.
out.group = types.KubernetesResourcesV7KindGroups[in]
out.kind = types.KubernetesResourcesKindsPlurals[in]
if out.kind == "" {
switch {
case in == types.KindKubeNamespace:
out.kind = "namespaces"
return out
case strings.HasPrefix(in, types.AccessRequestPrefixKindKubeNamespaced):
out.kind = strings.TrimPrefix(in, types.AccessRequestPrefixKindKubeNamespaced)
case strings.HasPrefix(in, types.AccessRequestPrefixKindKubeClusterWide):
out.kind = strings.TrimPrefix(in, types.AccessRequestPrefixKindKubeClusterWide)
// Subset if the two first used in search. Must be last.
case strings.HasPrefix(in, types.AccessRequestPrefixKindKube):
out.kind = strings.TrimPrefix(in, types.AccessRequestPrefixKindKube)
}
}
if out.group != "" { // If we have a group, we are dealing with legacy value, we have the noramlized version.
return out
}
// Otherwise, parse the group from the trimmed input.
if i := strings.Index(out.kind, "."); i != -1 {
out.group = out.kind[i+1:]
out.kind = out.kind[:i]
return out
}
return out
}
// matchRequestKubernetesResources checks if the input matches the reference
// based on the condition type.
//
// Similar logic as utils.KubeResourceMatchesRegex(), but with support for wildcard input
// and without support for verbs/names/namespaces.
//
// Examples:
// Request: *.apps Deny: deployments.apps -> match.
// Request: *.apps Deny: deployments.* -> match. (*.apps could be deployments.apps which matches deployments.*)
// Request: *.apps Deny: *.* -> match.
// Request: deployments.* Deny: deployments.apps -> match.
// Request: deployments.* Deny: deployments.* -> match.
// Request: deployments.* Deny: *.* -> match.
// Request: *.* Deny: deployments.apps -> match.
// Request: *.* Deny: deployments.* -> match.
// Request: *.* Deny: *.* -> match.
func matchRequestKubernetesResources(input gk, reference types.RequestKubernetesResource, cond types.RoleConditionType) bool {
// If we have an exact match, we are done.
if input.kind == reference.Kind && input.group == reference.APIGroup {
return true
}
// If the reference is a wildcard and the input kube_cluster, we don't match allow, but we match deny.
// Ref:
// https://github.com/gravitational/teleport/blob/master/rfd/0183-access-request-kube-resource-allow-list.md#as-an-admin-i-want-to-require-users-to-request-for-kubernetes-subresources-instead-of-the-whole-kubernetes-cluster
if reference.Kind == types.Wildcard && input.kind == types.KindKubernetesCluster {
return cond == types.Deny
}
if cond == types.Allow {
// In allow mode, if the reference kind is not a wildcard and doesn't match exactly, we reject.
if reference.Kind != types.Wildcard && input.kind != reference.Kind {
return false
}
// If the reference api group is a wildcard or is an exact match, we are done.
if reference.APIGroup == types.Wildcard || input.group == reference.APIGroup {
return true
}
// Otherwise, attempt to match the api group pattern.
ok, _ := utils.MatchString(input.group, reference.APIGroup)
return ok
}
// In deny mode, we reject only if both input/ref are not wildcard and are not equal.
if reference.Kind != types.Wildcard && input.kind != types.Wildcard && input.kind != reference.Kind {
return false
}
// If there is no conflict on the kind, check the group. As we support pattern matching, check both sides.
ok1, _ := utils.MatchString(input.group, reference.APIGroup)
ok2, _ := utils.MatchString(reference.APIGroup, input.group)
return ok1 || ok2
}
// GetAllowedSearchAsRolesForKubeResourceKind returns all of the allowed SearchAsRoles
// that allowed requesting to the requested Kubernetes resource kind.
func (set RoleSet) GetAllowedSearchAsRolesForKubeResourceKind(requestedKubeResourceKind string) []string {
// Return no results if encountering any denies since its globally matched.
for _, role := range set {
for _, kr := range role.GetRequestKubernetesResources(types.Deny) {
if matchRequestKubernetesResources(normalizeKubernetesKind(requestedKubeResourceKind), kr, types.Deny) {
return nil
}
}
}
return set.GetAllowedSearchAsRoles(WithAllowedKubernetesResourceKindFilter(requestedKubeResourceKind))
}
// WithAllowedKubernetesResourceKindFilter returns a SearchAsRolesOption func
// that will check that the requestedKubeResourceKind exists in the allow list
// for the current role.
func WithAllowedKubernetesResourceKindFilter(requestedKubeResourceKind string) SearchAsRolesOption {
return func(role types.Role) bool {
allowed := role.GetAccessRequestConditions(types.Allow).KubernetesResources
// Any kind is allowed if nothing was configured.
if len(allowed) == 0 {
return true
}
for _, kr := range role.GetRequestKubernetesResources(types.Allow) {
if matchRequestKubernetesResources(normalizeKubernetesKind(requestedKubeResourceKind), kr, types.Allow) {
return true
}
}
return false
}
}
// 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)
}
// GetCreateDatabaseUserMode returns the create database user mode of the rule
// set.
func (set RoleSet) GetCreateDatabaseUserMode() types.CreateDatabaseUserMode {
var mode types.CreateDatabaseUserMode
for _, r := range set {
if roleMode := r.GetCreateDatabaseUserMode(); roleMode > mode {
mode = roleMode
}
}
return mode
}
// 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.
// Used for role-based device mode checks.
// 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
// IsBot determines whether the user certificate belongs to a bot. It's used
// when deciding whether to enforce device verification.
IsBot bool
// ReturnPreconditions, when set to true, causes access checks to return a set of preconditions (such as MFA or
// device verification requirements) instead of immediately returning an access error. This allows callers to
// programmatically determine what additional steps are required for access, rather than failing outright.
ReturnPreconditions 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 means that it is not required by the cluster auth preference or any of the user's roles.
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"
)
// UserSessionRoleNotFoundErrorMsg is added to "role not found" errors when they occur
// during user session roles validation. This allows the Web UI to distinguish between
// a user session role lookup error (which should prompt the user to re-login) vs. other role lookup
// failures.
// Keep in sync with teleport/src/services/api/api.ts(isUserSessionRoleNotFoundError)
const UserSessionRoleNotFoundErrorMsg = "user session role not found"
// knownAppResourceFields lists the JSON field names this version understands
// on an app_resources rule, derived from the AppResource message so a new
// proto field extends it automatically.
var knownAppResourceFields = func() map[string]struct{} {
fields := make(map[string]struct{})
for f := range reflect.TypeFor[types.AppResource]().Fields() {
name, _, _ := strings.Cut(f.Tag.Get("json"), ",")
if name != "" && name != "-" {
fields[name] = struct{}{}
}
}
return fields
}()
// denyAppAccessForUnknownFields empties any v9 allow app_resources rule whose
// stored JSON carried a field this version does not recognize, so the rule
// grants no access. Worst case is over-deny.
func denyAppAccessForUnknownFields(role *types.RoleV6, raw []byte) {
if role.Version != types.V9 {
return
}
for i := range role.Spec.Allow.AppResources {
for _, key := range jsoniter.Get(raw, "spec", "allow", "app_resources", i).Keys() {
if _, known := knownAppResourceFields[key]; !known {
role.Spec.Allow.AppResources[i] = types.AppResource{}
break
}
}
}
}
// UnmarshalRole unmarshals the Role resource from JSON.
func UnmarshalRole(bytes []byte, opts ...MarshalOption) (types.Role, error) {
return UnmarshalRoleV6(bytes, opts...)
}
// UnmarshalRoleV6 unmarshals the RoleV6 resource from JSON.
func UnmarshalRoleV6(bytes []byte, opts ...MarshalOption) (*types.RoleV6, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
version := jsoniter.Get(bytes, "version").ToString()
switch version {
// these are all backed by the same shape of data, they just have different semantics and defaults
case types.V3, types.V4, types.V5, types.V6, types.V7, types.V8, types.V9:
default:
return nil, trace.BadParameter("role version %q is not supported", version)
}
var role types.RoleV6
if err := utils.FastUnmarshal(bytes, &role); err != nil {
return nil, trace.BadParameter("%s", err)
}
if role.Version != version {
return nil, trace.BadParameter("inconsistent version in role data, got %q and %q", role.Version, version)
}
if cfg.DisallowUnknown {
if err := checkUnknownFields(bytes); err != nil {
return nil, trace.Wrap(err)
}
}
if err := CheckAndSetDefaults(&role); err != nil {
return nil, trace.Wrap(err)
}
denyAppAccessForUnknownFields(&role, bytes)
if cfg.Revision != "" {
role.SetRevision(cfg.Revision)
}
if !cfg.Expires.IsZero() {
role.SetExpiry(cfg.Expires)
}
return &role, nil
}
// MarshalRole marshals the Role resource to JSON.
func MarshalRole(role types.Role, opts ...MarshalOption) ([]byte, error) {
if err := CheckAndSetDefaults(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:
return utils.FastMarshal(maybeResetProtoRevision(cfg.PreserveRevision, role))
default:
return nil, trace.BadParameter("unrecognized role version %T", role)
}
}
// checkUnknownFields rejects JSON with fields not defined in the RoleV6 struct.
func checkUnknownFields(data []byte) error {
var unused types.RoleV6
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&unused); err != nil {
return trace.BadParameter("role has unknown or misspelled fields: %v", err)
}
return nil
}
// AuthPreferenceGetter defines an interface for getting the authentication
// preferences.
type AuthPreferenceGetter interface {
// GetAuthPreference fetches the cluster authentication preferences.
GetAuthPreference(ctx context.Context) (types.AuthPreference, error)
}
// AccessStateFromSSHIdentity populates access state based on user's SSH
// identity and auth preference.
func AccessStateFromSSHIdentity(ctx context.Context, ident *sshca.Identity, checker AccessChecker, authPrefGetter AuthPreferenceGetter) (AccessState, error) {
authPref, err := authPrefGetter.GetAuthPreference(ctx)
if err != nil {
return AccessState{}, trace.Wrap(err)
}
state := checker.GetAccessState(authPref)
state.MFAVerified = ident.MFAVerified != ""
// Certain hardware-key based private key policies are treated as MFA verification.
if ident.PrivateKeyPolicy.MFAVerified() {
state.MFAVerified = true
}
state.EnableDeviceVerification = true
state.DeviceVerified = dtauthz.IsSSHDeviceVerified(ident)
state.IsBot = ident.IsBot()
return state, nil
}
// AccessStateFromTLSIdentity populates access state based on user's TLS
// identity and auth preference.
func AccessStateFromTLSIdentity(ctx context.Context, ident *tlsca.Identity, checker AccessChecker, authPrefGetter AuthPreferenceGetter) (AccessState, error) {
authPref, err := authPrefGetter.GetAuthPreference(ctx)
if err != nil {
return AccessState{}, trace.Wrap(err)
}
state := checker.GetAccessState(authPref)
state.MFAVerified = ident.MFAVerified != ""
// Certain hardware-key based private key policies are treated as MFA verification.
if ident.PrivateKeyPolicy.MFAVerified() {
state.MFAVerified = true
}
state.EnableDeviceVerification = true
state.DeviceVerified = dtauthz.IsTLSDeviceVerified(&ident.DeviceExtensions)
state.IsBot = ident.IsBot()
return state, nil
}
// MCPToolMatcher matches a role against MCP tool.
type MCPToolMatcher struct {
Name string
}
// Match matches MCP tool name against provided role and condition.
func (m *MCPToolMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
mcpSpec := role.GetMCPPermissions(condition)
if mcpSpec == nil {
return false, nil
}
match, err := utils.SliceMatchesRegex(m.Name, mcpSpec.Tools)
return match, trace.Wrap(err)
}
// String returns the matcher's string representation.
func (m *MCPToolMatcher) String() string {
return fmt.Sprintf("MCPToolMatcher(Name=%v)", m.Name)
}