mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
* Add LinuxDesktop gRPC and backend * Remove CloneResource * Review comments * Fix logins * Update lib/auth/linuxdesktop/linuxdesktopv1/service.go Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com> * Fix role --------- Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com>
1700 lines
66 KiB
Go
1700 lines
66 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 (
|
|
"cmp"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"maps"
|
|
"net"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
|
|
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
apiutils "github.com/gravitational/teleport/api/utils"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/lib/services/readonly"
|
|
"github.com/gravitational/teleport/lib/sshca"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
logutils "github.com/gravitational/teleport/lib/utils/log"
|
|
"github.com/gravitational/teleport/lib/utils/set"
|
|
)
|
|
|
|
// AccessChecker interface checks access to resources based on roles, traits,
|
|
// and allowed resources
|
|
type AccessChecker interface {
|
|
// HasRole checks if the checker includes the role
|
|
HasRole(role string) bool
|
|
|
|
// RoleNames returns a list of role names
|
|
RoleNames() []string
|
|
|
|
// Traits returns the set of user traits
|
|
Traits() wrappers.Traits
|
|
|
|
// Roles returns the list underlying roles this AccessChecker is based on.
|
|
Roles() []types.Role
|
|
|
|
// CheckAccess checks access to the specified resource.
|
|
CheckAccess(r AccessCheckable, state AccessState, matchers ...RoleMatcher) error
|
|
|
|
// CheckConditionalAccess checks conditional access to the specified resource. If access is granted, it returns
|
|
// preconditions that must be satisfied. If access is denied, it returns an error. An empty list of preconditions
|
|
// and a nil error indicates that no additional preconditions are required for access.
|
|
CheckConditionalAccess(r AccessCheckable, state AccessState, matchers ...RoleMatcher) ([]*decisionpb.Precondition, error)
|
|
|
|
// CheckAccessToRemoteCluster checks access to remote cluster
|
|
CheckAccessToRemoteCluster(cluster types.RemoteCluster) error
|
|
|
|
// CheckAccessToRule checks access to a rule within a namespace.
|
|
CheckAccessToRule(context RuleContext, namespace string, rule string, verb string) error
|
|
|
|
// 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.
|
|
GuessIfAccessIsPossible(ctx RuleContext, namespace string, resource string, verb string) error
|
|
|
|
// CheckLoginDuration checks if role set can login up to given duration and
|
|
// returns a combined list of allowed logins.
|
|
CheckLoginDuration(ttl time.Duration) ([]string, error)
|
|
|
|
// CheckKubeGroupsAndUsers check if role can login into kubernetes
|
|
// and returns two lists of combined allowed groups and users
|
|
CheckKubeGroupsAndUsers(ttl time.Duration, overrideTTL bool, matchers ...RoleMatcher) (groups []string, users []string, err error)
|
|
|
|
// CheckAWSRoleARNs returns a list of AWS role ARNs role is allowed to assume.
|
|
CheckAWSRoleARNs(ttl time.Duration, overrideTTL bool) ([]string, error)
|
|
|
|
// CheckAzureIdentities returns a list of Azure identities the user is allowed to assume.
|
|
CheckAzureIdentities(ttl time.Duration, overrideTTL bool) ([]string, error)
|
|
|
|
// CheckGCPServiceAccounts returns a list of GCP service accounts the user is allowed to assume.
|
|
CheckGCPServiceAccounts(ttl time.Duration, overrideTTL bool) ([]string, error)
|
|
|
|
// CheckAccessToSAMLIdP checks access to SAML IdP service provider resource.
|
|
// It checks for both the legacy RBAC (role v7 and below) that checks for IDP
|
|
// role option and MFA, as well as non-legacy RBAC (role v8 and above) that checks
|
|
// for labels, MFA and Device Trust.
|
|
CheckAccessToSAMLIdP(r AccessCheckable, authPref readonly.AuthPreference, state AccessState, matchers ...RoleMatcher) error
|
|
|
|
// AdjustSessionTTL will reduce the requested ttl to lowest max allowed TTL
|
|
// for this role set, otherwise it returns ttl unchanged
|
|
AdjustSessionTTL(ttl time.Duration) time.Duration
|
|
|
|
// AdjustClientIdleTimeout adjusts requested idle timeout
|
|
// to the lowest max allowed timeout, the most restrictive
|
|
// option will be picked
|
|
AdjustClientIdleTimeout(ttl time.Duration) time.Duration
|
|
|
|
// AdjustDisconnectExpiredCert adjusts the value based on the role set
|
|
// the most restrictive option will be picked
|
|
AdjustDisconnectExpiredCert(disconnect bool) bool
|
|
|
|
// CheckAgentForward checks if the role can request agent forward for this
|
|
// user.
|
|
CheckAgentForward(login string) error
|
|
|
|
// CanForwardAgents returns true if this role set offers capability to forward
|
|
// agents.
|
|
CanForwardAgents() bool
|
|
|
|
// CanPortForward returns true if this RoleSet can forward ports.
|
|
CanPortForward() bool
|
|
|
|
// SSHPortForwardMode returns the SSHPortForwardMode that the RoleSet allows.
|
|
SSHPortForwardMode() decisionpb.SSHPortForwardMode
|
|
|
|
// 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.
|
|
DesktopClipboard() bool
|
|
// RecordDesktopSession returns true if a role in the role set has enabled
|
|
// desktop session recoring.
|
|
RecordDesktopSession() bool
|
|
// DesktopDirectorySharing returns true if the role set has directory sharing
|
|
// enabled. This setting is enabled if one or more of the roles in the set has
|
|
// enabled it.
|
|
DesktopDirectorySharing() bool
|
|
|
|
// 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.
|
|
MaybeCanReviewRequests() bool
|
|
|
|
// PermitX11Forwarding returns true if this RoleSet allows X11 Forwarding.
|
|
PermitX11Forwarding() bool
|
|
|
|
// 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.
|
|
CanCopyFiles() bool
|
|
|
|
// CertificateFormat returns the most permissive certificate format in a
|
|
// RoleSet.
|
|
CertificateFormat() string
|
|
|
|
// EnhancedRecordingSet returns a set of events that will be recorded
|
|
// for enhanced session recording.
|
|
EnhancedRecordingSet() map[string]bool
|
|
|
|
// CheckDatabaseNamesAndUsers returns database names and users this role
|
|
// is allowed to use.
|
|
CheckDatabaseNamesAndUsers(ttl time.Duration, overrideTTL bool) (names []string, users []string, err error)
|
|
|
|
// DatabaseAutoUserMode returns whether a user should be auto-created in
|
|
// the database.
|
|
DatabaseAutoUserMode(types.Database) (types.CreateDatabaseUserMode, error)
|
|
|
|
// CheckDatabaseRoles returns a list of database roles to assign, when
|
|
// auto-user provisioning is enabled. If no user-requested roles, all
|
|
// allowed roles are returned.
|
|
CheckDatabaseRoles(database types.Database, userRequestedRoles []string) (roles []string, err error)
|
|
|
|
// GetDatabasePermissions returns a set of database permissions applicable for the user.
|
|
GetDatabasePermissions(database types.Database) (allow types.DatabasePermissions, deny types.DatabasePermissions, err error)
|
|
|
|
// CheckImpersonate checks whether current user is allowed to impersonate
|
|
// users and roles
|
|
CheckImpersonate(currentUser, impersonateUser types.User, impersonateRoles []types.Role) error
|
|
|
|
// CheckImpersonateRoles checks whether the current user is allowed to
|
|
// perform roles-only impersonation.
|
|
CheckImpersonateRoles(currentUser types.User, impersonateRoles []types.Role) error
|
|
|
|
// CanImpersonateSomeone returns true if this checker has any impersonation rules
|
|
CanImpersonateSomeone() bool
|
|
|
|
// LockingMode returns the locking mode to apply with this checker.
|
|
LockingMode(defaultMode constants.LockingMode) constants.LockingMode
|
|
|
|
// ExtractConditionForIdentifier returns a restrictive filter expression
|
|
// for list queries based on the rules' `where` conditions.
|
|
ExtractConditionForIdentifier(ctx RuleContext, namespace, resource, verb, identifier string) (*types.WhereExpr, error)
|
|
|
|
// CertificateExtensions returns the list of extensions for each role in the RoleSet
|
|
CertificateExtensions() []*types.CertExtension
|
|
|
|
// GetAllowedSearchAsRoles returns all of the allowed SearchAsRoles.
|
|
GetAllowedSearchAsRoles(allowFilters ...SearchAsRolesOption) []string
|
|
|
|
// GetAllowedSearchAsRolesForKubeResourceKind returns all of the allowed SearchAsRoles
|
|
// that allowed requesting to the requested Kubernetes resource kind.
|
|
GetAllowedSearchAsRolesForKubeResourceKind(requestedKubeResourceKind string) []string
|
|
|
|
// GetAllowedPreviewAsRoles returns all of the allowed PreviewAsRoles.
|
|
GetAllowedPreviewAsRoles() []string
|
|
|
|
// 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.
|
|
MaxConnections() int64
|
|
|
|
// 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.
|
|
MaxSessions() int64
|
|
|
|
// SessionPolicySets returns the list of SessionPolicySets for all roles.
|
|
SessionPolicySets() []*types.SessionTrackerPolicySet
|
|
|
|
// GetAllLogins returns all valid unix logins for the AccessChecker.
|
|
GetAllLogins() []string
|
|
|
|
// GetAllowedResourceAccessIDs returns the list of allowed resources the identity for
|
|
// the AccessChecker is allowed to access. An empty or nil list indicates that
|
|
// there are no resource-specific restrictions.
|
|
GetAllowedResourceAccessIDs() []types.ResourceAccessID
|
|
|
|
// SessionRecordingMode returns the recording mode for a specific service.
|
|
SessionRecordingMode(service constants.SessionRecordingService) constants.SessionRecordingMode
|
|
|
|
// HostUsers returns host user information matching a server or nil if
|
|
// a role disallows host user creation
|
|
HostUsers(types.Server) (*HostUsersDecision, error)
|
|
|
|
// HostSudoers returns host sudoers entries matching a server
|
|
HostSudoers(types.Server) ([]string, error)
|
|
|
|
// DesktopGroups returns the desktop groups a user is allowed to create or an access denied error if a role disallows desktop user creation
|
|
DesktopGroups(types.WindowsDesktop) ([]string, error)
|
|
|
|
// PinSourceIP forces the same client IP for certificate generation and SSH usage
|
|
PinSourceIP() bool
|
|
|
|
// GetAccessState returns the AccessState for the user given their roles, the
|
|
// cluster auth preference, and whether MFA and the user's device were
|
|
// verified.
|
|
GetAccessState(authPref readonly.AuthPreference) AccessState
|
|
// PrivateKeyPolicy returns the enforced private key policy for this role set,
|
|
// or the provided defaultPolicy - whichever is stricter.
|
|
PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) (keys.PrivateKeyPolicy, error)
|
|
|
|
// GetKubeResources returns the allowed and denied Kubernetes Resources configured
|
|
// for a user.
|
|
GetKubeResources(cluster types.KubeCluster) (allowed, denied []types.KubernetesResource)
|
|
|
|
// EnumerateEntities works on a given role set to return a minimal description
|
|
// of allowed set of entities (db_users, db_names, etc). It is biased towards
|
|
// *allowed* entities; It is meant to describe what the user can do, rather than
|
|
// cannot do. For that reason if the user isn't allowed to pick *any* entities,
|
|
// the output will be empty.
|
|
//
|
|
// In cases where * is listed in set of allowed entities, it may be hard for
|
|
// users to figure out the expected entity to use. For this reason the parameter
|
|
// extraEntities provides an extra set of entities to be checked against
|
|
// RoleSet. This extra set of entities may be sourced e.g. from user connection
|
|
// history.
|
|
EnumerateEntities(resource AccessCheckable, listFn roleEntitiesListFn, newMatcher roleMatcherFactoryFn, extraEntities ...string) EnumerationResult
|
|
|
|
// EnumerateDatabaseUsers specializes EnumerateEntities to enumerate db_users.
|
|
EnumerateDatabaseUsers(database types.Database, extraUsers ...string) (EnumerationResult, error)
|
|
|
|
// EnumerateDatabaseNames specializes EnumerateEntities to enumerate db_names.
|
|
EnumerateDatabaseNames(database types.Database, extraNames ...string) EnumerationResult
|
|
|
|
// EnumerateMCPTools specializes EnumerateEntities to enumerate mcp.tools.
|
|
// mcp.tools support regexes and blobs so those expressions are returned.
|
|
EnumerateMCPTools(app types.Application) EnumerationResult
|
|
|
|
// GetAllowedLoginsForResource returns all of the allowed logins for the passed resource.
|
|
//
|
|
// Supports the following resource types:
|
|
//
|
|
// - types.Server with GetKind() == types.KindNode
|
|
// - types.KindWindowsDesktop
|
|
// - types.KindApp with IsAWSConsole() == true
|
|
GetAllowedLoginsForResource(resource AccessCheckable) ([]string, error)
|
|
|
|
// 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.
|
|
CheckSPIFFESVID(spiffeIDPath string, dnsSANs []string, ipSANs []net.IP) error
|
|
|
|
// AccessInfo returns the AccessInfo that this access checker is based on.
|
|
AccessInfo() *AccessInfo
|
|
|
|
// DelegationSessionID returns the ID of the current Delegation Session.
|
|
DelegationSessionID() string
|
|
}
|
|
|
|
// AccessInfo hold information about an identity necessary to check whether that
|
|
// identity has access to cluster resources. This info can come from a user or
|
|
// host SSH certificate, TLS certificate, or user information stored in the
|
|
// backend.
|
|
type AccessInfo struct {
|
|
// ScopePin is an optional pin that ties an identity to a specific scope and set of scoped roles. When
|
|
// set, the Roles field must not be set.
|
|
ScopePin *scopesv1.Pin
|
|
// Roles is the list of cluster local roles for the identity.
|
|
Roles []string
|
|
// Traits is the set of traits for the identity.
|
|
Traits wrappers.Traits
|
|
// AllowedResourceAccessIDs is the list of resource IDs the identity is allowed to
|
|
// access. A nil or empty list indicates that no resource-specific
|
|
// access restrictions should be applied. Used for search-based access
|
|
// requests.
|
|
AllowedResourceAccessIDs []types.ResourceAccessID
|
|
// DelegationSessionID is the ID of the Delegation Session this identity was
|
|
// created for, if any.
|
|
DelegationSessionID string
|
|
// Username is the Teleport username.
|
|
Username string
|
|
}
|
|
|
|
// accessChecker implements the AccessChecker interface.
|
|
type accessChecker struct {
|
|
info *AccessInfo
|
|
localCluster string
|
|
|
|
// RoleSet is embedded to use the existing implementation for most
|
|
// AccessChecker methods. Methods which require AllowedResourceAccessIDs (relevant
|
|
// to search-based access requests) will be implemented by
|
|
// accessChecker.
|
|
RoleSet
|
|
}
|
|
|
|
// NewAccessChecker returns a new AccessChecker which can be used to check
|
|
// access to resources.
|
|
// Args:
|
|
// - `info *AccessInfo` should hold the roles, traits, and allowed resource IDs
|
|
// for the identity.
|
|
// - `localCluster string` should be the name of the local cluster in which
|
|
// access will be checked. You cannot check for access to resources in remote
|
|
// clusters.
|
|
// - `access RoleGetter` should be a RoleGetter which will be used to fetch the
|
|
// full RoleSet
|
|
func NewAccessChecker(info *AccessInfo, localCluster string, access RoleGetter) (AccessChecker, error) {
|
|
if info.ScopePin != nil {
|
|
return nil, trace.Errorf("cannot create standard access checker: %w", ErrScopedIdentity)
|
|
}
|
|
roleSet, err := FetchRolesWithContext(info.Roles, access, RoleTemplateContext{
|
|
Username: info.Username,
|
|
Traits: info.Traits,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return newAccessChecker(info, localCluster, roleSet), nil
|
|
}
|
|
|
|
// NewAccessCheckerForUserSession is an alternative to NewAccessChecker that includes a UserSessionRoleNotFoundErrorMsg if
|
|
// a role from the user's session is not found during the access check. 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.
|
|
func NewAccessCheckerForUserSession(info *AccessInfo, localCluster string, access RoleGetter) (AccessChecker, error) {
|
|
roleSet, err := FetchRolesWithContext(info.Roles, access, RoleTemplateContext{
|
|
Username: info.Username,
|
|
Traits: info.Traits,
|
|
})
|
|
if err != nil {
|
|
if trace.IsNotFound(err) {
|
|
// Add the UserSessionRoleNotFoundErrorMsg message to indicate this role not found error was encountered fetching
|
|
// the user's session roles. This can only happen if the user's session certificate contains a role that no longer exists.
|
|
return nil, trace.Wrap(err, UserSessionRoleNotFoundErrorMsg)
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return &accessChecker{
|
|
info: info,
|
|
localCluster: localCluster,
|
|
RoleSet: roleSet,
|
|
}, nil
|
|
}
|
|
|
|
// NewAccessCheckerWithRoleSet is similar to NewAccessChecker, but accepts the
|
|
// full RoleSet rather than a RoleGetter.
|
|
func NewAccessCheckerWithRoleSet(info *AccessInfo, localCluster string, roleSet RoleSet) AccessChecker {
|
|
return newAccessChecker(info, localCluster, roleSet)
|
|
}
|
|
|
|
func newAccessChecker(info *AccessInfo, localCluster string, roleSet RoleSet) *accessChecker {
|
|
return &accessChecker{
|
|
info: info,
|
|
localCluster: localCluster,
|
|
RoleSet: roleSet,
|
|
}
|
|
}
|
|
|
|
// CurrentUserRoleGetter limits the interface of auth.ClientI to methods needed
|
|
// by NewAccessCheckerForRemoteCluster.
|
|
type CurrentUserRoleGetter interface {
|
|
// GetCurrentUserRoles returns the remote cluster roles for the current
|
|
// user, traits have not been applied.
|
|
GetCurrentUserRoles(context.Context) ([]types.Role, error)
|
|
// GetCurrentUser returns the remote cluster's view of the current user.
|
|
GetCurrentUser(context.Context) (types.User, error)
|
|
}
|
|
|
|
// NewAccessCheckerForRemoteCluster returns an AccessChecker that can check
|
|
// user's access to resources that may be located in remote/leaf Teleport
|
|
// clusters.
|
|
func NewAccessCheckerForRemoteCluster(ctx context.Context, localAccessInfo *AccessInfo, clusterName string, access CurrentUserRoleGetter) (AccessChecker, error) {
|
|
if localAccessInfo.ScopePin != nil {
|
|
return nil, trace.BadParameter("cannot create unscoped remote cluster AccessChecker based on scoped identity")
|
|
}
|
|
|
|
// Fetch the remote cluster's view of the current user's roles.
|
|
remoteRoles, err := access.GetCurrentUserRoles(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Fetch the remote cluster's view of the current user's traits.
|
|
// These can technically be different than the local user's traits, see
|
|
// AccessInfoFromRemote(Certificate|Identity).
|
|
remoteUser, err := access.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
remoteAccessInfo := &AccessInfo{
|
|
Username: remoteUser.GetName(),
|
|
Traits: remoteUser.GetTraits(),
|
|
// Will fill this in with the names of the remote/mapped roles we got
|
|
// from GetCurrentUserRoles.
|
|
Roles: make([]string, 0, len(remoteRoles)),
|
|
// AllowedResourceAccessIDs are always the same across clusters.
|
|
AllowedResourceAccessIDs: localAccessInfo.AllowedResourceAccessIDs,
|
|
DelegationSessionID: localAccessInfo.DelegationSessionID,
|
|
}
|
|
|
|
for i := range remoteRoles {
|
|
remoteRoles[i], err = ApplyTraitsWithContext(remoteRoles[i], RoleTemplateContext{
|
|
Username: remoteAccessInfo.Username,
|
|
Traits: remoteAccessInfo.Traits,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
remoteAccessInfo.Roles = append(remoteAccessInfo.Roles, remoteRoles[i].GetName())
|
|
}
|
|
roleSet := NewRoleSet(remoteRoles...)
|
|
|
|
return &accessChecker{
|
|
info: remoteAccessInfo,
|
|
// localCluster is a bit of a misnomer here, but it means the local
|
|
// cluster of the resources to which access will be checked, which in
|
|
// this case may be a remote cluster. localCluster is used for access
|
|
// checks involving Resource Access Requests, the cluster name is
|
|
// included in the unique ID of the resource, the accessChecker can only
|
|
// check access to resources in that cluster.
|
|
localCluster: clusterName,
|
|
RoleSet: roleSet,
|
|
}, nil
|
|
}
|
|
|
|
type allowedResourceMatch struct {
|
|
Match *types.ResourceAccessID
|
|
}
|
|
|
|
// checkAllowedResources enforces AllowedResourceAccessIDs if present on the identity.
|
|
func (a *accessChecker) checkAllowedResources(r AccessCheckable) (allowedResourceMatch, error) {
|
|
if len(a.info.AllowedResourceAccessIDs) == 0 {
|
|
// certificate does not contain a list of specifically allowed
|
|
// resources, only role-based access control is used
|
|
return allowedResourceMatch{}, nil
|
|
}
|
|
|
|
// 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()
|
|
isLoggingEnabled := rbacLogger.Enabled(ctx, logutils.TraceLevel)
|
|
|
|
for _, resourceID := range a.info.AllowedResourceAccessIDs {
|
|
if id := resourceID.GetResourceID(); id.ClusterName == a.localCluster && matchesUCRResource(resourceID, r) {
|
|
// Allowed to access this resource by resource ID, move on to role checks.
|
|
if isLoggingEnabled {
|
|
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Matched allowed resource ID",
|
|
slog.String("resource_id", types.ResourceIDToString(id)),
|
|
)
|
|
}
|
|
|
|
return allowedResourceMatch{&resourceID}, nil
|
|
}
|
|
}
|
|
|
|
if isLoggingEnabled {
|
|
// We just want to log allowed IDs here; discarding additional info is ok.
|
|
allowedResources, err := types.ResourceIDsToString(types.RiskyExtractResourceIDs(a.info.AllowedResourceAccessIDs))
|
|
if err != nil {
|
|
return allowedResourceMatch{}, trace.Wrap(err)
|
|
}
|
|
|
|
slog.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, not in allowed resource IDs",
|
|
slog.String("resource_kind", r.GetKind()),
|
|
slog.String("resource_name", r.GetName()),
|
|
slog.Any("allowed_resources", allowedResources),
|
|
)
|
|
|
|
return allowedResourceMatch{}, trace.AccessDenied("access to %v denied, %q not in allowed resource IDs %s",
|
|
r.GetKind(), r.GetName(), allowedResources)
|
|
}
|
|
|
|
return allowedResourceMatch{}, trace.AccessDenied("access to %v denied, not in allowed resource IDs", r.GetKind())
|
|
}
|
|
|
|
// matchesUCRResource matches requested resource with its respective
|
|
// resource type stored in the unified resource cache.
|
|
func matchesUCRResource(requestedR types.ResourceAccessID, r AccessCheckable) bool {
|
|
if requestedR.GetResourceID().Name != r.GetName() {
|
|
return false
|
|
}
|
|
// If the allowed resource has `Kind=types.KindKubePod` or any other
|
|
// Kubernetes supported kinds - types.KubernetesResourcesKinds-, we allow the user to
|
|
// access the Kubernetes cluster that it belongs to.
|
|
// At this point, we do not verify that the accessed resource matches the
|
|
// allowed resources, but that verification happens in the caller function.
|
|
if slices.Contains(types.KubernetesResourcesKinds, requestedR.GetResourceID().Kind) || strings.HasPrefix(requestedR.GetResourceID().Kind, types.AccessRequestPrefixKindKube) {
|
|
return r.GetKind() == types.KindKubernetesCluster
|
|
}
|
|
|
|
// Identity Center account is stored as KindApp kind and
|
|
// KindIdentityCenterAccount subKind in the unified resource cache.
|
|
if requestedR.GetResourceID().Kind == types.KindIdentityCenterAccount {
|
|
return r.GetKind() == types.KindApp && r.GetSubKind() == types.KindIdentityCenterAccount
|
|
}
|
|
|
|
return requestedR.GetResourceID().Kind == r.GetKind()
|
|
}
|
|
|
|
// AccessInfo returns the AccessInfo that this access checker is based on.
|
|
func (a *accessChecker) AccessInfo() *AccessInfo {
|
|
return a.info
|
|
}
|
|
|
|
// DelegationSessionID returns the ID of the current Delegation Session.
|
|
func (a *accessChecker) DelegationSessionID() string {
|
|
return a.info.DelegationSessionID
|
|
}
|
|
|
|
// blockedInDelegationSession checks whether the given action is disallowed
|
|
// because the caller is in a Delegation Session with restricted access to
|
|
// specific resources only.
|
|
//
|
|
// Without this check, the `AllowedResourceAccessIDs` would only restrict
|
|
// regular access (e.g. SSH-ing into a node), not administrative actions,
|
|
// so if the delegating user has a role that allows them to mutate resources,
|
|
// the session user would also be able to do this on their behalf.
|
|
//
|
|
// If the Delegation Session has a "wildcard" resource selector, the user
|
|
// has explicitly allowed the session user to take on *all* of their
|
|
// permissions, including destructive administrative actions.
|
|
func (a *accessChecker) blockedInDelegationSession(kind, verb string) bool {
|
|
if a.DelegationSessionID() == "" || len(a.GetAllowedResourceAccessIDs()) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Collect all the resource kinds the session has access to.
|
|
allowedKinds := set.New[string]()
|
|
for _, id := range a.GetAllowedResourceAccessIDs() {
|
|
allowedKinds.Add(id.GetResourceID().Kind)
|
|
}
|
|
|
|
// Also add the implied resource kinds (e.g. app -> app_server).
|
|
impliedKinds := map[string][]string{
|
|
types.KindApp: []string{types.KindAppServer},
|
|
types.KindDatabase: []string{types.KindDatabaseServer},
|
|
types.KindKubernetesCluster: []string{types.KindKubeServer},
|
|
types.KindWindowsDesktop: []string{types.KindWindowsDesktopService},
|
|
}
|
|
for parent, children := range impliedKinds {
|
|
if allowedKinds.Contains(parent) {
|
|
allowedKinds.Add(children...)
|
|
}
|
|
}
|
|
|
|
// These verbs are allowed to enable `tsh ls`, etc.
|
|
allowedVerbs := set.New(types.VerbList, types.VerbRead, types.VerbReadNoSecrets)
|
|
return !allowedKinds.Contains(kind) || !allowedVerbs.Contains(verb)
|
|
}
|
|
|
|
// CheckAccessToRule checks access to a rule within a namespace.
|
|
//
|
|
// It extends [RoleSet.CheckAccessToRule] to prevent Delegation Sessions with
|
|
// restricted access to specific resources from inheriting the user's destructive
|
|
// admin/rule based privileges
|
|
func (a *accessChecker) CheckAccessToRule(ctx RuleContext, namespace string, resource string, verb string) error {
|
|
if a.blockedInDelegationSession(resource, verb) {
|
|
return trace.AccessDenied("access denied to perform action %q on %q", verb, resource)
|
|
}
|
|
return a.RoleSet.CheckAccessToRule(ctx, namespace, resource, verb)
|
|
}
|
|
|
|
// GuessIfAccessIsPossible guesses if access is possible for an entire category
|
|
// of resources.
|
|
func (a *accessChecker) GuessIfAccessIsPossible(ctx RuleContext, namespace string, resource string, verb string) error {
|
|
if a.blockedInDelegationSession(resource, verb) {
|
|
return trace.AccessDenied("access denied to perform action %q on %q", verb, resource)
|
|
}
|
|
return a.RoleSet.GuessIfAccessIsPossible(ctx, namespace, resource, verb)
|
|
}
|
|
|
|
// ExtractConditionForIdentifier returns a restrictive filter expression
|
|
// for list queries based on the rules' `where` conditions.
|
|
func (a *accessChecker) ExtractConditionForIdentifier(ctx RuleContext, namespace, resource, verb, identifier string) (*types.WhereExpr, error) {
|
|
if a.blockedInDelegationSession(resource, verb) {
|
|
return nil, trace.AccessDenied("access denied to perform action %q on %q", verb, resource)
|
|
}
|
|
return a.RoleSet.ExtractConditionForIdentifier(ctx, namespace, resource, verb, identifier)
|
|
}
|
|
|
|
// CheckAccess checks if the identity for this AccessChecker has access to the given resource.
|
|
func (a *accessChecker) CheckAccess(r AccessCheckable, state AccessState, matchers ...RoleMatcher) error {
|
|
// Immediately return an error regardless of potential preconditions. This is to maintain backwards compatibility
|
|
// with existing callers of CheckAccess which expect an error when access is denied.
|
|
state.ReturnPreconditions = false
|
|
|
|
_, err := a.validateAccessConditions(r, state, matchers...)
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// CheckConditionalAccess checks if the identity for this AccessChecker has conditional access to the given resource.
|
|
func (a *accessChecker) CheckConditionalAccess(r AccessCheckable, state AccessState, matchers ...RoleMatcher) ([]*decisionpb.Precondition, error) {
|
|
// Indicate that we want preconditions to be returned if access is granted rather than an error.
|
|
state.ReturnPreconditions = true
|
|
|
|
return a.validateAccessConditions(r, state, matchers...)
|
|
}
|
|
|
|
func (a *accessChecker) validateAccessConditions(r AccessCheckable, state AccessState, matchers ...RoleMatcher) ([]*decisionpb.Precondition, error) {
|
|
// Enforce AllowedResourceAccessIDs if present; capture match
|
|
res, err := a.checkAllowedResources(r)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
switch rr := r.(type) {
|
|
case types.Resource153UnwrapperT[IdentityCenterAccount]:
|
|
matchers = append(matchers, NewIdentityCenterAccountMatcher(rr.UnwrapT()))
|
|
case types.Resource153UnwrapperT[IdentityCenterAccountAssignment]:
|
|
matchers = append(matchers, NewIdentityCenterAccountAssignmentMatcher(rr.UnwrapT()))
|
|
}
|
|
|
|
// If matched RID has ResourceConstraints, guard any principal-bearing matcher(s)
|
|
if res.Match != nil && res.Match.GetConstraints() != nil {
|
|
guard := WithConstraints(res.Match.GetConstraints())
|
|
for i := range matchers {
|
|
matchers[i] = guard(matchers[i])
|
|
}
|
|
}
|
|
|
|
preconds, err := a.checkAccess(r, a.info.Username, a.info.Traits, state, matchers...)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return preconds, nil
|
|
}
|
|
|
|
// CheckAccessToSAMLIdP checks access to SAML IdP service provider resource.
|
|
// It checks for both the legacy RBAC (role v7 and below) that checks for IDP
|
|
// role option and MFA, as well as non-legacy RBAC (role v8 and above) that checks
|
|
// for labels, MFA and Device Trust.
|
|
func (a *accessChecker) CheckAccessToSAMLIdP(r AccessCheckable, authPref readonly.AuthPreference, state AccessState, matchers ...RoleMatcher) error {
|
|
if _, err := a.checkAllowedResources(r); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return trace.Wrap(a.RoleSet.CheckAccessToSAMLIdP(r, a.info.Username, a.info.Traits, authPref, state, matchers...))
|
|
}
|
|
|
|
// GetKubeResources returns the allowed and denied Kubernetes Resources configured
|
|
// for a user.
|
|
func (a *accessChecker) GetKubeResources(cluster types.KubeCluster) (allowed, denied []types.KubernetesResource) {
|
|
if len(a.info.AllowedResourceAccessIDs) == 0 {
|
|
return a.RoleSet.GetKubeResources(cluster, a.info.Username, a.info.Traits)
|
|
}
|
|
var err error
|
|
rolesAllowed, rolesDenied := a.RoleSet.GetKubeResources(cluster, a.info.Username, a.info.Traits)
|
|
|
|
// If we have a legacy 'namespace' in the allowedResourceIDs, we need to add the new 'namespaces' one.
|
|
// The old one will get mapped to wildcard later.
|
|
allowedResourceAccessIDs := slices.Clone(a.info.AllowedResourceAccessIDs)
|
|
for _, elem := range a.info.AllowedResourceAccessIDs {
|
|
if rid := elem.GetResourceID(); rid.Kind == types.KindKubeNamespace {
|
|
allowedResourceAccessIDs = append(allowedResourceAccessIDs, types.ResourceAccessID{Id: types.ResourceID{
|
|
ClusterName: rid.ClusterName,
|
|
Kind: types.AccessRequestPrefixKindKubeClusterWide + "namespaces",
|
|
SubResourceName: rid.SubResourceName,
|
|
Name: rid.Name,
|
|
}})
|
|
}
|
|
}
|
|
|
|
// Allways append the denied resources from the roles. This is because
|
|
// the denied resources from the roles take precedence over the allowed
|
|
// resources from the certificate.
|
|
denied = rolesDenied
|
|
for _, wr := range allowedResourceAccessIDs {
|
|
r := wr.GetResourceID()
|
|
if r.Name != cluster.GetName() || r.ClusterName != a.localCluster {
|
|
continue
|
|
}
|
|
switch {
|
|
case slices.Contains(types.KubernetesResourcesKinds, r.Kind) || strings.HasPrefix(r.Kind, types.AccessRequestPrefixKindKube):
|
|
namespace := ""
|
|
name := ""
|
|
if slices.Contains(types.KubernetesClusterWideResourceKinds, r.Kind) || strings.HasPrefix(r.Kind, types.AccessRequestPrefixKindKubeClusterWide) {
|
|
// Cluster wide resources do not have a namespace.
|
|
name = r.SubResourceName
|
|
r.Kind = strings.TrimPrefix(r.Kind, types.AccessRequestPrefixKindKubeClusterWide)
|
|
} else {
|
|
r.Kind = strings.TrimPrefix(r.Kind, types.AccessRequestPrefixKindKubeNamespaced)
|
|
splitted := strings.SplitN(r.SubResourceName, "/", 3)
|
|
// This condition should never happen since SubResourceName is validated
|
|
// but it's better to validate it.
|
|
if len(splitted) != 2 {
|
|
continue
|
|
}
|
|
namespace = splitted[0]
|
|
// namespace * would also include cluster-wide resources, if we
|
|
// have a wildcard with a known namespaced resource, use a pattern
|
|
// that will not match cluster-wide resources.
|
|
if namespace == types.Wildcard {
|
|
namespace = "^.+$"
|
|
}
|
|
name = splitted[1]
|
|
}
|
|
|
|
// Map legacy names to the new ones.
|
|
kind := types.KubernetesResourcesKindsPlurals[r.Kind]
|
|
if kind == "" {
|
|
kind = r.Kind
|
|
}
|
|
// NOTE: The kind 'namespace' behavior changed, to maintain backwards compatibility,
|
|
// map the legacy value to wildcard.
|
|
if r.Kind == types.KindKubeNamespace {
|
|
// When requesting the legacy "namespace" kind, we include all api groups.
|
|
kind = types.Wildcard + "." + types.Wildcard
|
|
namespace = name
|
|
// namespace * would also include cluster-wide resources, if we
|
|
// have a wildcard with the legacy "namespace" kind, use a pattern
|
|
// that will not match cluster-wide resources.
|
|
if namespace == types.Wildcard {
|
|
namespace = "^.+$"
|
|
}
|
|
name = types.Wildcard
|
|
}
|
|
|
|
gk := schema.ParseGroupKind(kind)
|
|
if gk.Group == "" {
|
|
gk.Group = types.KubernetesResourcesV7KindGroups[r.Kind]
|
|
}
|
|
r := types.KubernetesResource{
|
|
Kind: gk.Kind,
|
|
Namespace: namespace,
|
|
Name: name,
|
|
APIGroup: gk.Group,
|
|
}
|
|
// matchKubernetesResource checks if the Kubernetes Resource matches the tuple
|
|
// (kind, namespace, kame) from the allowed/denied list and does not match the resource
|
|
// verbs. Verbs are not checked here because they are not included in the
|
|
// ResourceID but we collect them and set them in the returned KubernetesResource
|
|
// so that they can be matched when the resource is accessed.
|
|
if r.Verbs, err = matchKubernetesResource(r, rolesAllowed, rolesDenied); err == nil {
|
|
allowed = append(allowed, r)
|
|
}
|
|
case r.Kind == types.KindKubernetesCluster:
|
|
// When a user has access to a Kubernetes cluster through Resource Access request,
|
|
// he has access to all resources in that cluster that he has access to through his roles.
|
|
// In that case, we append the allowed and denied resources from the roles.
|
|
return rolesAllowed, rolesDenied
|
|
}
|
|
}
|
|
return append(allowed, types.KubernetesResourceSelfSubjectAccessReview), denied
|
|
}
|
|
|
|
// matchKubernetesResource checks if the Kubernetes Resource does not match any
|
|
// entry from the deny list and matches at least one entry from the allowed list.
|
|
func matchKubernetesResource(resource types.KubernetesResource, allowed, denied []types.KubernetesResource) ([]string, error) {
|
|
// utils.KubeResourceMatchesRegex checks if the resource.Kind is strictly equal
|
|
// to each entry and validates if the Name and Namespace fields matches the
|
|
// regex allowed by each entry.
|
|
result, _, err := utils.KubeResourceMatchesRegexWithVerbsCollector(resource, denied)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
} else if result {
|
|
return nil, trace.AccessDenied("access to %s %q denied", resource.Kind, resource.ClusterResource())
|
|
}
|
|
|
|
result, verbs, err := utils.KubeResourceMatchesRegexWithVerbsCollector(resource, allowed)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
} else if !result {
|
|
return nil, trace.AccessDenied("access to %s %q denied", resource.Kind, resource.ClusterResource())
|
|
}
|
|
return verbs, nil
|
|
}
|
|
|
|
// GetAllowedResourceAccessIDs returns the list of allowed resources the identity for
|
|
// the AccessChecker is allowed to access. An empty or nil list indicates that
|
|
// there are no resource-specific restrictions.
|
|
func (a *accessChecker) GetAllowedResourceAccessIDs() []types.ResourceAccessID {
|
|
return a.info.AllowedResourceAccessIDs
|
|
}
|
|
|
|
// Traits returns the set of user traits
|
|
func (a *accessChecker) Traits() wrappers.Traits {
|
|
return a.info.Traits
|
|
}
|
|
|
|
// DatabaseAutoUserMode returns whether a user should be auto-created in
|
|
// the database.
|
|
func (a *accessChecker) DatabaseAutoUserMode(database types.Database) (types.CreateDatabaseUserMode, error) {
|
|
result, err := a.checkDatabaseRoles(database)
|
|
return result.createDatabaseUserMode(), trace.Wrap(err)
|
|
}
|
|
|
|
// CheckDatabaseRoles returns whether a user should be auto-created in the
|
|
// database and a list of database roles to assign.
|
|
func (a *accessChecker) CheckDatabaseRoles(database types.Database, userRequestedRoles []string) ([]string, error) {
|
|
result, err := a.checkDatabaseRoles(database)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
switch {
|
|
case !result.createDatabaseUserMode().IsEnabled():
|
|
return []string{}, nil
|
|
|
|
// If user requested a list of roles, make sure all requested roles are
|
|
// allowed.
|
|
case len(userRequestedRoles) > 0:
|
|
for _, requestedRole := range userRequestedRoles {
|
|
if !slices.Contains(result.allowedRoles(), requestedRole) {
|
|
return nil, trace.AccessDenied("access to database role %q denied", requestedRole)
|
|
}
|
|
}
|
|
return userRequestedRoles, nil
|
|
|
|
// If user does not provide any roles, use all allowed roles from roleset.
|
|
default:
|
|
return result.allowedRoles(), nil
|
|
}
|
|
}
|
|
|
|
type checkDatabaseRolesResult struct {
|
|
allowedRoleSet RoleSet
|
|
deniedRoleSet RoleSet
|
|
}
|
|
|
|
func (result *checkDatabaseRolesResult) createDatabaseUserMode() types.CreateDatabaseUserMode {
|
|
if result == nil {
|
|
return types.CreateDatabaseUserMode_DB_USER_MODE_UNSPECIFIED
|
|
}
|
|
return result.allowedRoleSet.GetCreateDatabaseUserMode()
|
|
}
|
|
|
|
func (result *checkDatabaseRolesResult) allowedRoles() []string {
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
|
|
rolesMap := set.New[string]()
|
|
for _, role := range result.allowedRoleSet {
|
|
for _, dbRole := range role.GetDatabaseRoles(types.Allow) {
|
|
rolesMap.Add(dbRole)
|
|
}
|
|
}
|
|
for _, role := range result.deniedRoleSet {
|
|
for _, dbRole := range role.GetDatabaseRoles(types.Deny) {
|
|
rolesMap.Remove(dbRole)
|
|
}
|
|
}
|
|
// The database user provisioning code is picky - it requires a non-nil
|
|
// slice of roles, because this value is passed directly to a SQL query.
|
|
return rolesMap.ElementsNotNil()
|
|
}
|
|
|
|
func (a *accessChecker) checkDatabaseRoles(database types.Database) (*checkDatabaseRolesResult, error) {
|
|
// First, collect roles from this roleset that have create database user mode set.
|
|
var autoCreateRoles RoleSet
|
|
for _, role := range a.RoleSet {
|
|
if role.GetCreateDatabaseUserMode().IsEnabled() {
|
|
autoCreateRoles = append(autoCreateRoles, role)
|
|
}
|
|
}
|
|
// If there are no "auto-create user" roles, nothing to do.
|
|
if len(autoCreateRoles) == 0 {
|
|
return nil, nil
|
|
}
|
|
// Otherwise, iterate over auto-create roles matching the database user
|
|
// is connecting to and compile a list of roles database user should be
|
|
// assigned.
|
|
var allowedRoleSet RoleSet
|
|
for _, role := range autoCreateRoles {
|
|
match, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Username, a.info.Traits, database, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !match {
|
|
continue
|
|
}
|
|
allowedRoleSet = append(allowedRoleSet, role)
|
|
|
|
}
|
|
var deniedRoleSet RoleSet
|
|
for _, role := range autoCreateRoles {
|
|
match, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Username, a.info.Traits, database, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !match {
|
|
continue
|
|
}
|
|
deniedRoleSet = append(deniedRoleSet, role)
|
|
}
|
|
|
|
// The collected role list can be empty and that should be ok, we want to
|
|
// leave the behavior of what happens when a user is created with default
|
|
// "no roles" configuration up to the target database.
|
|
result := checkDatabaseRolesResult{
|
|
allowedRoleSet: allowedRoleSet,
|
|
deniedRoleSet: deniedRoleSet,
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// GetDatabasePermissions returns a set of database permissions applicable for the user in the context of particular database.
|
|
func (a *accessChecker) GetDatabasePermissions(database types.Database) (allow types.DatabasePermissions, deny types.DatabasePermissions, err error) {
|
|
result, err := a.checkDatabaseRoles(database)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
if !result.createDatabaseUserMode().IsEnabled() {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
for _, role := range result.allowedRoleSet {
|
|
allow = append(allow, role.GetDatabasePermissions(types.Allow)...)
|
|
}
|
|
for _, role := range result.deniedRoleSet {
|
|
deny = append(deny, role.GetDatabasePermissions(types.Deny)...)
|
|
}
|
|
return allow, deny, nil
|
|
}
|
|
|
|
// EnumerateDatabaseUsers specializes EnumerateEntities to enumerate db_users.
|
|
func (a *accessChecker) EnumerateDatabaseUsers(database types.Database, extraUsers ...string) (EnumerationResult, error) {
|
|
// When auto-user provisioning is enabled, only Teleport username is allowed.
|
|
if database.IsAutoUsersEnabled() {
|
|
result := NewEnumerationResult()
|
|
autoUser, err := a.DatabaseAutoUserMode(database)
|
|
if err != nil {
|
|
return result, trace.Wrap(err)
|
|
} else if autoUser.IsEnabled() {
|
|
result.allowedDeniedMap[a.info.Username] = true
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
listFn := func(role types.Role, condition types.RoleConditionType) []string {
|
|
return role.GetDatabaseUsers(condition)
|
|
}
|
|
newMatcher := func(user string) RoleMatcher {
|
|
return NewDatabaseUserMatcher(database, user)
|
|
}
|
|
return a.EnumerateEntities(database, listFn, newMatcher, extraUsers...), nil
|
|
}
|
|
|
|
// EnumerateDatabaseNames specializes EnumerateEntities to enumerate db_names.
|
|
func (a *accessChecker) EnumerateDatabaseNames(database types.Database, extraNames ...string) EnumerationResult {
|
|
listFn := func(role types.Role, condition types.RoleConditionType) []string {
|
|
return role.GetDatabaseNames(condition)
|
|
}
|
|
newMatcher := func(dbName string) RoleMatcher {
|
|
return &DatabaseNameMatcher{Name: dbName}
|
|
}
|
|
return a.EnumerateEntities(database, listFn, newMatcher, extraNames...)
|
|
}
|
|
|
|
// EnumerateMCPTools specializes EnumerateEntities to enumerate mcp.tools.
|
|
func (a *accessChecker) EnumerateMCPTools(app types.Application) EnumerationResult {
|
|
listFn := func(role types.Role, condition types.RoleConditionType) []string {
|
|
if mcpSpec := role.GetMCPPermissions(condition); mcpSpec != nil {
|
|
return mcpSpec.Tools
|
|
}
|
|
return nil
|
|
}
|
|
// Do not use MCPToolMatcher. We are enumerating the expressions.
|
|
newMatcher := func(toolRegex string) RoleMatcher {
|
|
return RoleMatcherFunc(func(role types.Role, condition types.RoleConditionType) (bool, error) {
|
|
if mcpSpec := role.GetMCPPermissions(condition); mcpSpec != nil {
|
|
return slices.Contains(mcpSpec.Tools, toolRegex), nil
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
return a.EnumerateEntities(app, listFn, newMatcher)
|
|
}
|
|
|
|
// roleEntitiesListFn is used for listing a role's allowed/denied entities.
|
|
type roleEntitiesListFn func(types.Role, types.RoleConditionType) []string
|
|
|
|
// roleMatcherFactoryFn is used for making a role matcher for a given entity.
|
|
type roleMatcherFactoryFn func(entity string) RoleMatcher
|
|
|
|
// EnumerateEntities works on a given role set to return a minimal description
|
|
// of allowed set of entities (db_users, db_names, etc). It is biased towards
|
|
// *allowed* entities; It is meant to describe what the user can do, rather than
|
|
// cannot do. For that reason if the user isn't allowed to pick *any* entities,
|
|
// the output will be empty.
|
|
//
|
|
// In cases where * is listed in set of allowed entities, it may be hard for
|
|
// users to figure out the expected entity to use. For this reason the parameter
|
|
// extraEntities provides an extra set of entities to be checked against
|
|
// RoleSet. This extra set of entities may be sourced e.g. from user connection
|
|
// history.
|
|
func (a *accessChecker) EnumerateEntities(resource AccessCheckable, listFn roleEntitiesListFn, newMatcher roleMatcherFactoryFn, extraEntities ...string) EnumerationResult {
|
|
result := NewEnumerationResult()
|
|
|
|
// gather entities for checking from the roles, check wildcards.
|
|
var entities []string
|
|
for _, role := range a.RoleSet {
|
|
wildcardAllowed := false
|
|
wildcardDenied := false
|
|
|
|
// Only append allowed entries and update wildcardAllowed if the role
|
|
// allows the resource without any matcher. In the real CheckAccess,
|
|
// RoleMatchers(matchers).MatchAll(role, types.Allow) is only run when
|
|
// namespace and label matching passes on this resource. Checking
|
|
// if the role allows the resource without any matcher confirms
|
|
// namespace and label matching has passed.
|
|
var resourceAllowedByRole bool
|
|
if _, err := NewRoleSet(role).checkAccess(resource, a.info.Username, a.info.Traits, AccessState{MFAVerified: true}); err == nil {
|
|
resourceAllowedByRole = true
|
|
}
|
|
|
|
for _, e := range listFn(role, types.Allow) {
|
|
if e == types.Wildcard {
|
|
wildcardAllowed = true
|
|
} else if resourceAllowedByRole {
|
|
entities = append(entities, e)
|
|
}
|
|
}
|
|
|
|
for _, e := range listFn(role, types.Deny) {
|
|
if e == types.Wildcard {
|
|
wildcardDenied = true
|
|
} else {
|
|
entities = append(entities, e)
|
|
}
|
|
}
|
|
|
|
result.wildcardDenied = result.wildcardDenied || wildcardDenied
|
|
|
|
if resourceAllowedByRole {
|
|
result.wildcardAllowed = result.wildcardAllowed || wildcardAllowed
|
|
}
|
|
}
|
|
|
|
entities = apiutils.Deduplicate(append(entities, extraEntities...))
|
|
|
|
// check each individual role spec entity against the resource.
|
|
for _, e := range entities {
|
|
err := a.CheckAccess(resource, AccessState{MFAVerified: true}, newMatcher(e))
|
|
result.allowedDeniedMap[e] = err == nil
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetAllowedLoginsForResource returns all of the allowed logins for the passed resource.
|
|
//
|
|
// Supports the following resource types:
|
|
//
|
|
// - types.Server with GetKind() == types.KindNode
|
|
// - types.KindWindowsDesktop
|
|
// - types.KindApp with IsAWSConsole() == true
|
|
func (a *accessChecker) GetAllowedLoginsForResource(resource AccessCheckable) ([]string, error) {
|
|
// Create a map indexed by all logins in the RoleSet,
|
|
// mapped to false if any role has it in its deny section,
|
|
// true otherwise.
|
|
mapped := make(map[string]bool)
|
|
|
|
resourceAsApp, resourceIsApp := resource.(interface{ IsAWSConsole() bool })
|
|
|
|
for _, role := range a.RoleSet {
|
|
var loginGetter func(types.RoleConditionType) []string
|
|
|
|
switch resource.GetKind() {
|
|
case types.KindNode:
|
|
loginGetter = role.GetLogins
|
|
case types.KindWindowsDesktop:
|
|
loginGetter = role.GetWindowsLogins
|
|
case types.KindLinuxDesktop:
|
|
loginGetter = role.GetLinuxDesktopLogins
|
|
case types.KindApp:
|
|
if !resourceIsApp {
|
|
return nil, trace.BadParameter("received unsupported resource type for Application kind: %T", resource)
|
|
}
|
|
// For Apps, only AWS currently supports listing the possible logins.
|
|
if !resourceAsApp.IsAWSConsole() {
|
|
return nil, nil
|
|
}
|
|
|
|
loginGetter = role.GetAWSRoleARNs
|
|
default:
|
|
return nil, trace.BadParameter("received unsupported resource kind: %s", resource.GetKind())
|
|
}
|
|
|
|
for _, login := range loginGetter(types.Allow) {
|
|
// Only set to true if not already set, the login is denied if any
|
|
// role denies it.
|
|
if _, alreadySet := mapped[login]; !alreadySet {
|
|
mapped[login] = true
|
|
}
|
|
}
|
|
for _, login := range loginGetter(types.Deny) {
|
|
mapped[login] = false
|
|
}
|
|
}
|
|
|
|
// Create a list of only the logins not denied by a role in the set.
|
|
var notDenied []string
|
|
for login, isNotDenied := range mapped {
|
|
if isNotDenied {
|
|
notDenied = append(notDenied, login)
|
|
}
|
|
}
|
|
|
|
var newLoginMatcher func(login string) RoleMatcher
|
|
switch resource.GetKind() {
|
|
case types.KindNode:
|
|
newLoginMatcher = NewLoginMatcher
|
|
case types.KindWindowsDesktop:
|
|
newLoginMatcher = NewWindowsLoginMatcher
|
|
case types.KindLinuxDesktop:
|
|
newLoginMatcher = NewLinuxDesktopLoginMatcher
|
|
case types.KindApp:
|
|
if !resourceIsApp || !resourceAsApp.IsAWSConsole() {
|
|
return nil, trace.BadParameter("received unsupported resource type for Application: %T", resource)
|
|
}
|
|
|
|
newLoginMatcher = NewAppAWSLoginMatcher
|
|
default:
|
|
return nil, trace.BadParameter("received unsupported resource kind: %s", resource.GetKind())
|
|
}
|
|
|
|
// Filter the not-denied logins for those allowed to be used with the given resource.
|
|
var allowed []string
|
|
for _, login := range notDenied {
|
|
err := a.CheckAccess(resource, AccessState{MFAVerified: true}, newLoginMatcher(login))
|
|
if err == nil {
|
|
allowed = append(allowed, login)
|
|
}
|
|
}
|
|
|
|
return allowed, nil
|
|
}
|
|
|
|
// CheckAccessToRemoteCluster checks if a role has access to remote cluster. Deny rules are
|
|
// checked first then allow rules. Access to a cluster is determined by
|
|
// namespaces, labels, and logins.
|
|
func (a *accessChecker) CheckAccessToRemoteCluster(rc types.RemoteCluster) error {
|
|
if len(a.RoleSet) == 0 {
|
|
return trace.AccessDenied("access to cluster denied")
|
|
}
|
|
|
|
// Note: logging in this function only happens in trace mode, this is because
|
|
// adding logging to this function (which is called on every server returned
|
|
// by GetRemoteClusters) can slow down this function by 50x for large clusters!
|
|
ctx := context.Background()
|
|
isLoggingEnabled := rbacLogger.Enabled(ctx, logutils.TraceLevel)
|
|
|
|
rcLabels := rc.GetMetadata().Labels
|
|
|
|
// For backwards compatibility, if there is no role in the set with label
|
|
// matchers and the cluster has no labels, assume that the role set has
|
|
// access to the cluster.
|
|
usesLabels := false
|
|
for _, role := range a.RoleSet {
|
|
unset, err := labelMatchersUnset(role, types.KindRemoteCluster)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if !unset {
|
|
usesLabels = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !usesLabels && len(rcLabels) == 0 {
|
|
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Grant access to cluster - no role uses cluster labels and the cluster is not labeled",
|
|
slog.String("cluster_name", rc.GetName()),
|
|
slog.Any("roles", a.RoleNames()),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// Check deny rules first: a single matching label from
|
|
// the deny role set prohibits access.
|
|
var errs []error
|
|
for _, role := range a.RoleSet {
|
|
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, a.info.Username, a.info.Traits, rc, isLoggingEnabled)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matchLabels {
|
|
// This condition avoids formatting calls on large scale.
|
|
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Access to cluster denied, deny rule matched",
|
|
slog.String("cluster", rc.GetName()),
|
|
slog.String("role", role.GetName()),
|
|
slog.String("label_message", labelsMessage),
|
|
)
|
|
return trace.AccessDenied("access to cluster denied")
|
|
}
|
|
}
|
|
|
|
// Check allow rules: label has to match in any role in the role set to be granted access.
|
|
for _, role := range a.RoleSet {
|
|
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, a.info.Username, a.info.Traits, rc, isLoggingEnabled)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
labelMatchers, err := role.GetLabelMatchers(types.Allow, types.KindRemoteCluster)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Check access to role",
|
|
slog.String("role", role.GetName()),
|
|
slog.String("cluster", rc.GetName()),
|
|
slog.Any("cluster_labels", rcLabels),
|
|
slog.Any("match_labels", matchLabels),
|
|
slog.String("labels_message", labelsMessage),
|
|
slog.Any("error", err),
|
|
slog.Any("allow", labelMatchers),
|
|
)
|
|
|
|
if matchLabels {
|
|
return nil
|
|
}
|
|
if isLoggingEnabled {
|
|
deniedError := trace.AccessDenied("role=%v, match(%s)",
|
|
role.GetName(), labelsMessage)
|
|
errs = append(errs, deniedError)
|
|
}
|
|
}
|
|
|
|
rbacLogger.LogAttrs(ctx, logutils.TraceLevel, "Access to cluster denied, no allow rule matched",
|
|
slog.String("cluster", rc.GetName()),
|
|
slog.Any("error", errs),
|
|
)
|
|
return trace.AccessDenied("access to cluster denied")
|
|
}
|
|
|
|
// DesktopGroups returns the desktop groups a user is allowed to create or an access denied error if a role disallows desktop user creation
|
|
func (a *accessChecker) DesktopGroups(s types.WindowsDesktop) ([]string, error) {
|
|
groups := set.New[string]()
|
|
for _, role := range a.RoleSet {
|
|
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Username, a.info.Traits, s, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// skip nodes that dont have matching labels
|
|
if !result {
|
|
continue
|
|
}
|
|
createDesktopUser := role.GetOptions().CreateDesktopUser
|
|
// if any of the matching roles do not enable create host
|
|
// user, the user should not be allowed on
|
|
if createDesktopUser == nil || !createDesktopUser.Value {
|
|
return nil, trace.AccessDenied("user is not allowed to create host users")
|
|
}
|
|
for _, group := range role.GetDesktopGroups(types.Allow) {
|
|
groups.Add(group)
|
|
}
|
|
}
|
|
for _, role := range a.RoleSet {
|
|
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Username, a.info.Traits, s, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !result {
|
|
continue
|
|
}
|
|
for _, group := range role.GetDesktopGroups(types.Deny) {
|
|
groups.Remove(group)
|
|
}
|
|
}
|
|
|
|
// These groups get encoded into a certificate that's parsed by
|
|
// Rust code on Windows. That code expects an empty JSON array,
|
|
// not a null value.
|
|
return groups.ElementsNotNil(), nil
|
|
}
|
|
|
|
func convertHostUserMode(mode types.CreateHostUserMode) decisionpb.HostUserMode {
|
|
switch mode {
|
|
case types.CreateHostUserMode_HOST_USER_MODE_KEEP:
|
|
return decisionpb.HostUserMode_HOST_USER_MODE_KEEP
|
|
case types.CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP:
|
|
return decisionpb.HostUserMode_HOST_USER_MODE_DROP
|
|
default:
|
|
return decisionpb.HostUserMode_HOST_USER_MODE_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
// HostUsersDecision is a decision to allow or disallow host user creation.
|
|
type HostUsersDecision struct {
|
|
// Info is host users information. If host users creation is disallowed, this will be nil.
|
|
Info *decisionpb.HostUsersInfo
|
|
// AllowedBy is a list of determinants that allow host user creation.
|
|
AllowedBy []*decisionpb.Determinant
|
|
// DeniedBy is a list of determinants that disallow host user creation.
|
|
DeniedBy []*decisionpb.Determinant
|
|
}
|
|
|
|
// HostUsers returns host user decision matching a server.
|
|
func (a *accessChecker) HostUsers(s types.Server) (*HostUsersDecision, error) {
|
|
groups := set.New[string]()
|
|
shellToRoles := make(map[string][]string)
|
|
var shell string
|
|
var mode types.CreateHostUserMode
|
|
|
|
decision := new(HostUsersDecision)
|
|
|
|
for _, role := range a.RoleSet {
|
|
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Username, a.info.Traits, s, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// skip roles that don't have matching labels
|
|
if !result {
|
|
continue
|
|
}
|
|
|
|
createHostUserMode := role.GetOptions().CreateHostUserMode
|
|
//nolint:staticcheck // this field is preserved for existing deployments, but shouldn't be used going forward
|
|
createHostUser := role.GetOptions().CreateHostUser
|
|
if createHostUserMode == types.CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED {
|
|
createHostUserMode = types.CreateHostUserMode_HOST_USER_MODE_OFF
|
|
if createHostUser != nil && createHostUser.Value {
|
|
createHostUserMode = types.CreateHostUserMode_HOST_USER_MODE_KEEP
|
|
}
|
|
}
|
|
|
|
if createHostUserMode == types.CreateHostUserMode_HOST_USER_MODE_OFF {
|
|
decision.DeniedBy = append(decision.DeniedBy, &decisionpb.Determinant{
|
|
Kind: role.GetKind(),
|
|
Name: role.GetName(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
decision.AllowedBy = append(decision.AllowedBy, &decisionpb.Determinant{
|
|
Kind: role.GetKind(),
|
|
Name: role.GetName(),
|
|
})
|
|
|
|
if mode == types.CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED {
|
|
mode = createHostUserMode
|
|
}
|
|
// prefer to use HostUserModeKeep over InsecureDrop if mode has already been set.
|
|
if mode == types.CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP &&
|
|
createHostUserMode == types.CreateHostUserMode_HOST_USER_MODE_KEEP {
|
|
mode = types.CreateHostUserMode_HOST_USER_MODE_KEEP
|
|
}
|
|
|
|
hostUserShell := role.GetOptions().CreateHostUserDefaultShell
|
|
shell = cmp.Or(shell, hostUserShell)
|
|
if hostUserShell != "" {
|
|
shellToRoles[hostUserShell] = append(shellToRoles[hostUserShell], role.GetName())
|
|
}
|
|
|
|
for _, group := range role.GetHostGroups(types.Allow) {
|
|
groups.Add(group)
|
|
}
|
|
}
|
|
|
|
// if any of the matching roles do not enable create host user, the user should not be allowed on
|
|
// Represent denial by returning the decision with a nil info field.
|
|
if len(decision.DeniedBy) > 0 {
|
|
decision.Info = nil
|
|
return decision, nil
|
|
}
|
|
|
|
if len(shellToRoles) > 1 {
|
|
b := &strings.Builder{}
|
|
for shell, roles := range shellToRoles {
|
|
fmt.Fprintf(b, "%s=%v ", shell, roles)
|
|
}
|
|
|
|
slog.WarnContext(context.Background(), "Host user shell resolution is ambiguous due to conflicting roles, consider unifying roles around a single shell",
|
|
"selected_shell", shell,
|
|
"shell_assignments", b,
|
|
)
|
|
}
|
|
|
|
for _, role := range a.RoleSet {
|
|
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Username, a.info.Traits, s, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !result {
|
|
continue
|
|
}
|
|
for _, group := range role.GetHostGroups(types.Deny) {
|
|
groups.Remove(group)
|
|
}
|
|
}
|
|
|
|
traits := a.Traits()
|
|
var gid string
|
|
gidL := traits[constants.TraitHostUserGID]
|
|
if len(gidL) >= 1 {
|
|
gid = gidL[0]
|
|
}
|
|
var uid string
|
|
uidL := traits[constants.TraitHostUserUID]
|
|
if len(uidL) >= 1 {
|
|
uid = uidL[0]
|
|
}
|
|
|
|
decision.Info = &decisionpb.HostUsersInfo{
|
|
Groups: groups.Elements(),
|
|
Mode: convertHostUserMode(mode),
|
|
Uid: uid,
|
|
Gid: gid,
|
|
Shell: shell,
|
|
}
|
|
|
|
return decision, nil
|
|
}
|
|
|
|
// HostSudoers returns host sudoers entries matching a server
|
|
func (a *accessChecker) HostSudoers(s types.Server) ([]string, error) {
|
|
var sudoers []string
|
|
|
|
roleSet := slices.Clone(a.RoleSet)
|
|
slices.SortFunc(roleSet, func(a types.Role, b types.Role) int {
|
|
return strings.Compare(a.GetName(), b.GetName())
|
|
})
|
|
|
|
seenSudoers := make(map[string]struct{})
|
|
for _, role := range roleSet {
|
|
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Username, a.info.Traits, s, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// skip nodes that dont have matching labels
|
|
if !result {
|
|
continue
|
|
}
|
|
|
|
for _, sudoer := range role.GetHostSudoers(types.Allow) {
|
|
if _, ok := seenSudoers[sudoer]; ok {
|
|
continue
|
|
}
|
|
seenSudoers[sudoer] = struct{}{}
|
|
sudoers = append(sudoers, sudoer)
|
|
}
|
|
}
|
|
|
|
var finalSudoers []string
|
|
for _, role := range roleSet {
|
|
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Username, a.info.Traits, s, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !result {
|
|
continue
|
|
}
|
|
|
|
outer:
|
|
for _, sudoer := range sudoers {
|
|
for _, deniedSudoer := range role.GetHostSudoers(types.Deny) {
|
|
if deniedSudoer == "*" {
|
|
finalSudoers = nil
|
|
break outer
|
|
}
|
|
if sudoer != deniedSudoer {
|
|
finalSudoers = append(finalSudoers, sudoer)
|
|
}
|
|
}
|
|
}
|
|
sudoers = finalSudoers
|
|
}
|
|
|
|
return sudoers, nil
|
|
}
|
|
|
|
// AccessInfoFromLocalSSHIdentity returns a new AccessInfo populated from the
|
|
// given sshca.Identity. Should only be used for cluster local users as roles
|
|
// will not be mapped.
|
|
func AccessInfoFromLocalSSHIdentity(ident *sshca.Identity) *AccessInfo {
|
|
return &AccessInfo{
|
|
Username: ident.Username,
|
|
ScopePin: ident.ScopePin,
|
|
Roles: ident.Roles,
|
|
Traits: ident.Traits,
|
|
AllowedResourceAccessIDs: ident.AllowedResourceAccessIDs,
|
|
DelegationSessionID: ident.DelegationSessionID,
|
|
}
|
|
}
|
|
|
|
// AccessInfoFromRemoteSSHIdentity returns a new AccessInfo populated from the
|
|
// given remote cluster user's ssh identity. Remote roles will be mapped to
|
|
// local roles based on the given roleMap.
|
|
func AccessInfoFromRemoteSSHIdentity(unmappedIdentity *sshca.Identity, roleMap types.RoleMap) (*AccessInfo, error) {
|
|
if unmappedIdentity.ScopePin != nil {
|
|
return nil, trace.BadParameter("scope pinning is not supported for remote SSH identities")
|
|
}
|
|
|
|
// make a shallow copy of traits to avoid modifying the original
|
|
// (don't use maps.Clone, as we want to ensure the result is an empty, but not nil, map)
|
|
traits := make(map[string][]string, len(unmappedIdentity.Traits)+1)
|
|
maps.Copy(traits, unmappedIdentity.Traits)
|
|
|
|
// Prior to Teleport 6.2 the only trait passed to the remote cluster
|
|
// was the "logins" trait set to the SSH certificate principals.
|
|
//
|
|
// Keep backwards-compatible behavior and set it in addition to the
|
|
// traits extracted from the certificate.
|
|
traits[constants.TraitLogins] = unmappedIdentity.Principals
|
|
|
|
roles, err := MapRoles(roleMap, unmappedIdentity.Roles)
|
|
if err != nil {
|
|
return nil, trace.AccessDenied("failed to map roles for user with remote roles %v: %v", unmappedIdentity.Roles, err)
|
|
}
|
|
if len(roles) == 0 {
|
|
return nil, trace.AccessDenied("no roles mapped for user with remote roles %v", unmappedIdentity.Roles)
|
|
}
|
|
slog.DebugContext(context.Background(), "Mapped remote roles to local roles and traits",
|
|
"remote_roles", unmappedIdentity.Roles,
|
|
"local_roles", roles,
|
|
"traits", traits,
|
|
)
|
|
|
|
return &AccessInfo{
|
|
Username: unmappedIdentity.Username,
|
|
Roles: roles,
|
|
Traits: traits,
|
|
AllowedResourceAccessIDs: unmappedIdentity.AllowedResourceAccessIDs,
|
|
DelegationSessionID: unmappedIdentity.DelegationSessionID,
|
|
}, nil
|
|
}
|
|
|
|
// AccessInfoFromLocalTLSIdentity returns a new AccessInfo populated from the given
|
|
// tlsca.Identity. Should only be used for cluster local users as roles will not
|
|
// be mapped.
|
|
func AccessInfoFromLocalTLSIdentity(identity tlsca.Identity) (*AccessInfo, error) {
|
|
if len(identity.Groups) == 0 && identity.ScopePin == nil {
|
|
return nil, trace.BadParameter("tls identity %q has no roles or scope pin, this may indicate a malformed certificate or one that was issued by an incompatible teleport version", identity.Username)
|
|
}
|
|
|
|
return &AccessInfo{
|
|
Username: identity.Username,
|
|
ScopePin: identity.ScopePin,
|
|
Roles: identity.Groups,
|
|
Traits: identity.Traits,
|
|
AllowedResourceAccessIDs: identity.AllowedResourceAccessIDs,
|
|
DelegationSessionID: identity.DelegationSessionID,
|
|
}, nil
|
|
}
|
|
|
|
// AccessInfoFromRemoteTLSIdentity returns a new AccessInfo populated from the
|
|
// given remote cluster user's tlsca.Identity. Remote roles will be mapped to
|
|
// local roles based on the given roleMap.
|
|
func AccessInfoFromRemoteTLSIdentity(identity tlsca.Identity, roleMap types.RoleMap) (*AccessInfo, error) {
|
|
if identity.ScopePin != nil {
|
|
return nil, trace.BadParameter("scope pinning is not supported for remote TLS identities")
|
|
}
|
|
|
|
// Set internal traits for the remote user. This allows Teleport to work by
|
|
// passing exact logins, Kubernetes users/groups, database users/names, and
|
|
// AWS Role ARNs to the remote cluster.
|
|
traits := map[string][]string{
|
|
constants.TraitLogins: identity.Principals,
|
|
constants.TraitKubeGroups: identity.KubernetesGroups,
|
|
constants.TraitKubeUsers: identity.KubernetesUsers,
|
|
constants.TraitDBNames: identity.DatabaseNames,
|
|
constants.TraitDBUsers: identity.DatabaseUsers,
|
|
constants.TraitAWSRoleARNs: identity.AWSRoleARNs,
|
|
}
|
|
// Prior to Teleport 6.2 no user traits were passed to remote clusters
|
|
// except for the internal ones specified above.
|
|
//
|
|
// To preserve backwards compatible behavior, when applying traits from user
|
|
// identity, make sure to filter out those already present in the map above.
|
|
//
|
|
// This ensures that if e.g. there's a "logins" trait in the root user's
|
|
// identity, it won't overwrite the internal "logins" trait set above
|
|
// causing behavior change.
|
|
for k, v := range identity.Traits {
|
|
if _, ok := traits[k]; !ok {
|
|
traits[k] = v
|
|
}
|
|
}
|
|
|
|
unmappedRoles := identity.Groups
|
|
roles, err := MapRoles(roleMap, unmappedRoles)
|
|
if err != nil {
|
|
return nil, trace.AccessDenied("failed to map roles for remote user %q from cluster %q with remote roles %v: %v", identity.Username, identity.TeleportCluster, unmappedRoles, err)
|
|
}
|
|
if len(roles) == 0 {
|
|
return nil, trace.AccessDenied("no roles mapped for remote user %q from cluster %q with remote roles %v", identity.Username, identity.TeleportCluster, unmappedRoles)
|
|
}
|
|
slog.DebugContext(context.Background(), "Mapped roles of remote user to local roles and traits",
|
|
"remote_roles", unmappedRoles,
|
|
"user", identity.Username,
|
|
"local_roles", roles,
|
|
"traits", traits,
|
|
)
|
|
|
|
return &AccessInfo{
|
|
Username: identity.Username,
|
|
Roles: roles,
|
|
Traits: traits,
|
|
AllowedResourceAccessIDs: identity.AllowedResourceAccessIDs,
|
|
DelegationSessionID: identity.DelegationSessionID,
|
|
}, nil
|
|
}
|
|
|
|
// UserAccessState is a representation of a user's current state required for calculating access.
|
|
type UserAccessState interface {
|
|
// GetName returns the username associated with the user state.
|
|
GetName() string
|
|
|
|
// GetRoles returns the roles associated with the user's current state.
|
|
GetRoles() []string
|
|
|
|
// GetTraits returns the traits associated with the user's current sate.
|
|
GetTraits() map[string][]string
|
|
}
|
|
|
|
// UserState is a representation of a user's current state.
|
|
type UserState interface {
|
|
UserAccessState
|
|
|
|
// GetUserType returns the user type for the user login state.
|
|
GetUserType() types.UserType
|
|
|
|
// GetLabel fetches the given user label.
|
|
GetLabel(key string) (value string, ok bool)
|
|
|
|
// IsBot returns true if the user belongs to a bot.
|
|
IsBot() bool
|
|
|
|
// GetGithubIdentities returns a list of connected GitHub identities
|
|
GetGithubIdentities() []types.ExternalIdentity
|
|
// SetGithubIdentities sets the list of connected GitHub identities
|
|
SetGithubIdentities(identities []types.ExternalIdentity)
|
|
}
|
|
|
|
// AccessInfoFromUserState return a new AccessInfo populated from the roles and
|
|
// traits held be the given user state. This should only be used in cases where the
|
|
// user does not have any active access requests (initial web login, initial
|
|
// tbot certs, tests).
|
|
func AccessInfoFromUserState(user UserAccessState) *AccessInfo {
|
|
return accessInfoFromUserState(user, user.GetRoles(), nil)
|
|
}
|
|
|
|
// ScopePinnedAccessInfoFromUserState returns a new AccessInfo populated from the
|
|
// traits held by the user and the provided scope pin. Population/verification of the
|
|
// scope pin must be performed prior to calling this function.
|
|
func ScopePinnedAccessInfoFromUserState(user UserAccessState, pin *scopesv1.Pin) *AccessInfo {
|
|
return accessInfoFromUserState(user, nil, pin)
|
|
}
|
|
|
|
func accessInfoFromUserState(user UserAccessState, roles []string, pin *scopesv1.Pin) *AccessInfo {
|
|
return &AccessInfo{
|
|
Username: user.GetName(),
|
|
Roles: roles,
|
|
ScopePin: pin,
|
|
Traits: user.GetTraits(),
|
|
}
|
|
}
|