mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
Replace the functions in the scopes package to infer if scoped features are enabled with a concrete struct that is injected to consumers. This permits tests to toggle scopes features without having to specify environment variables.
2151 lines
82 KiB
Go
2151 lines
82 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 authz
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/trace"
|
|
"github.com/vulcand/predicate/builder"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/defaults"
|
|
clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
|
|
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
|
|
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
|
|
"github.com/gravitational/teleport/api/mfa"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/utils"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/lib/auth/mfatypes"
|
|
dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz"
|
|
"github.com/gravitational/teleport/lib/scopes"
|
|
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/services/readonly"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
logutils "github.com/gravitational/teleport/lib/utils/log"
|
|
)
|
|
|
|
// NewBuiltinRoleContext create auth context for the provided builtin role.
|
|
func NewBuiltinRoleContext(role types.SystemRole) (*Context, error) {
|
|
authContext, err := ContextForBuiltinRole(BuiltinRole{Role: role, Username: fmt.Sprintf("%v", role)}, nil)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return authContext, nil
|
|
}
|
|
|
|
// NewUnauthenticatedRoleContext create auth context for the provided unauthenticated role.
|
|
func NewUnauthenticatedRoleContext(role types.UnauthenticatedRole) (*Context, error) {
|
|
authContext, err := ContextForUnauthenticatedRole(UnauthenticatedRole{Role: role, Username: string(role)})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return authContext, nil
|
|
}
|
|
|
|
// DeviceAuthorizationOpts captures Device Trust options for [AuthorizerOpts].
|
|
type DeviceAuthorizationOpts struct {
|
|
// DisableGlobalMode disables the global device_trust.mode toggle.
|
|
// See [types.DeviceTrust.Mode].
|
|
DisableGlobalMode bool
|
|
|
|
// DisableRoleMode disables the role-based device trust toggle.
|
|
// See [types.RoleOption.DeviceTrustMode].
|
|
DisableRoleMode bool
|
|
}
|
|
|
|
// AuthorizerOpts holds creation options for [NewAuthorizer].
|
|
type AuthorizerOpts struct {
|
|
ClusterName string
|
|
AccessPoint AuthorizerAccessPoint
|
|
ReadOnlyAccessPoint ReadOnlyAuthorizerAccessPoint
|
|
ScopedRoleReader services.ScopedRoleReader
|
|
MFAAuthenticator MFAAuthenticator
|
|
LockWatcher *services.LockWatcher
|
|
Logger *slog.Logger
|
|
// ScopesFeatures dictates which scoped authorization components are enabled.
|
|
ScopesFeatures scopes.Features
|
|
|
|
// DeviceAuthorization holds Device Trust authorization options.
|
|
//
|
|
// Allows services that either do explicit device authorization or don't (yet)
|
|
// support device trust to disable it.
|
|
// Most services should not set this field.
|
|
DeviceAuthorization DeviceAuthorizationOpts
|
|
// PermitCaching opts into the authorizer setting up its own internal
|
|
// caching when ReadOnlyAccessPoint is not provided.
|
|
PermitCaching bool
|
|
}
|
|
|
|
// NewAuthorizer returns new authorizer using backends
|
|
func NewAuthorizer(opts AuthorizerOpts) (Authorizer, error) {
|
|
return newAuthorizer(opts)
|
|
}
|
|
|
|
func newAuthorizer(opts AuthorizerOpts) (*authorizer, error) {
|
|
if opts.ClusterName == "" {
|
|
return nil, trace.BadParameter("missing parameter clusterName")
|
|
}
|
|
if opts.AccessPoint == nil {
|
|
return nil, trace.BadParameter("missing parameter accessPoint")
|
|
}
|
|
logger := opts.Logger
|
|
if logger == nil {
|
|
logger = slog.With(teleport.ComponentKey, "authorizer")
|
|
}
|
|
|
|
if opts.ReadOnlyAccessPoint == nil {
|
|
// we create the read-only access point if not provided in order to keep our
|
|
// code paths simpler, but the it will not perform ttl-caching unless opts.PermitCaching
|
|
// was set. This is necessary because the vast majority of our test coverage
|
|
// cannot handle caching, and will fail if caching is enabled.
|
|
var err error
|
|
opts.ReadOnlyAccessPoint, err = readonly.NewCache(readonly.CacheConfig{
|
|
Upstream: accessPointWrapper{opts.AccessPoint},
|
|
Disabled: !opts.PermitCaching,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
return &authorizer{
|
|
clusterName: opts.ClusterName,
|
|
accessPoint: opts.AccessPoint,
|
|
readOnlyAccessPoint: opts.ReadOnlyAccessPoint,
|
|
scopedRoleReader: opts.ScopedRoleReader,
|
|
mfaAuthenticator: opts.MFAAuthenticator,
|
|
lockWatcher: opts.LockWatcher,
|
|
logger: logger,
|
|
scopesFeatures: opts.ScopesFeatures,
|
|
disableGlobalDeviceMode: opts.DeviceAuthorization.DisableGlobalMode,
|
|
disableRoleDeviceMode: opts.DeviceAuthorization.DisableRoleMode,
|
|
}, nil
|
|
}
|
|
|
|
type accessPointWrapper struct {
|
|
AuthorizerAccessPoint
|
|
}
|
|
|
|
// GetAccessGraphSettings returns the access graph settings.
|
|
func (accessPointWrapper) GetAccessGraphSettings(ctx context.Context) (*clusterconfigpb.AccessGraphSettings, error) {
|
|
return nil, trace.NotImplemented("GetAccessGraphSettings is not implemented")
|
|
}
|
|
|
|
// Authorizer authorizes identity and returns auth context
|
|
type Authorizer interface {
|
|
// Authorize authorizes user based on identity supplied via context
|
|
Authorize(ctx context.Context) (*Context, error)
|
|
}
|
|
|
|
// The AuthorizerFunc type is an adapter to allow the use of
|
|
// ordinary functions as an Authorizer. If f is a function
|
|
// with the appropriate signature, AuthorizerFunc(f) is a
|
|
// Authorizer that calls f.
|
|
type AuthorizerFunc func(ctx context.Context) (*Context, error)
|
|
|
|
// Authorize calls f(ctx).
|
|
func (f AuthorizerFunc) Authorize(ctx context.Context) (*Context, error) {
|
|
return f(ctx)
|
|
}
|
|
|
|
// AuthorizerAccessPoint is the access point contract required by an Authorizer
|
|
type AuthorizerAccessPoint interface {
|
|
// GetAuthPreference returns the cluster authentication configuration.
|
|
GetAuthPreference(ctx context.Context) (types.AuthPreference, error)
|
|
|
|
// GetRole returns role by name.
|
|
GetRole(ctx context.Context, name string) (types.Role, error)
|
|
|
|
// GetUser returns user by name.
|
|
GetUser(ctx context.Context, name string, withSecrets bool) (types.User, error)
|
|
|
|
// GetCertAuthority returns cert authority by id.
|
|
GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error)
|
|
|
|
// GetClusterNetworkingConfig returns cluster networking configuration.
|
|
GetClusterNetworkingConfig(ctx context.Context) (types.ClusterNetworkingConfig, error)
|
|
|
|
// GetSessionRecordingConfig returns session recording configuration.
|
|
GetSessionRecordingConfig(ctx context.Context) (types.SessionRecordingConfig, error)
|
|
}
|
|
|
|
// ReadOnlyAuthorizerAccessPoint is an additional optional access point interface that permits
|
|
// optimized access-control checks by sharing references to frequently accessed configuration
|
|
// objects across goroutines.
|
|
type ReadOnlyAuthorizerAccessPoint interface {
|
|
// GetReadOnlyAuthPreference returns the cluster authentication configuration.
|
|
GetReadOnlyAuthPreference(ctx context.Context) (readonly.AuthPreference, error)
|
|
|
|
// GetReadOnlyClusterNetworkingConfig returns cluster networking configuration.
|
|
GetReadOnlyClusterNetworkingConfig(ctx context.Context) (readonly.ClusterNetworkingConfig, error)
|
|
|
|
// GetReadOnlySessionRecordingConfig returns session recording configuration.
|
|
GetReadOnlySessionRecordingConfig(ctx context.Context) (readonly.SessionRecordingConfig, error)
|
|
}
|
|
|
|
// MFAAuthenticator authenticates MFA responses.
|
|
type MFAAuthenticator interface {
|
|
// ValidateMFAAuthResponse validates an MFA challenge response.
|
|
ValidateMFAAuthResponse(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, requiredExtensions *mfav1.ChallengeExtensions) (*MFAAuthData, error)
|
|
}
|
|
|
|
// MFAAuthData contains a user's MFA authentication data for a validated MFA response.
|
|
type MFAAuthData struct {
|
|
// User is the authenticated Teleport User.
|
|
User string
|
|
// Device is the user's MFA device used to authenticate.
|
|
Device *types.MFADevice
|
|
// AllowReuse determines whether the MFA challenge response used to authenticate
|
|
// can be reused. AllowReuse MFAAuthData may be denied for specific actions.
|
|
AllowReuse mfav1.ChallengeAllowReuse
|
|
// Payload is the optional session identifying payload attached to the MFA authentication.
|
|
Payload *mfatypes.SessionIdentifyingPayload
|
|
// SourceCluster is the source cluster name associated with this MFA authentication.
|
|
SourceCluster string
|
|
// TargetCluster is the target cluster name associated with this MFA authentication.
|
|
TargetCluster string
|
|
// MFAViaBrowser indicates that this MFA device was used as part of the Browser MFA flow.
|
|
MFAViaBrowser bool
|
|
}
|
|
|
|
// authorizer creates new local authorizer
|
|
type authorizer struct {
|
|
clusterName string
|
|
accessPoint AuthorizerAccessPoint
|
|
readOnlyAccessPoint ReadOnlyAuthorizerAccessPoint
|
|
scopedRoleReader services.ScopedRoleReader
|
|
mfaAuthenticator MFAAuthenticator
|
|
lockWatcher *services.LockWatcher
|
|
logger *slog.Logger
|
|
// scopesFeatures dictates whether scoped authorization is enabled.
|
|
scopesFeatures scopes.Features
|
|
|
|
disableGlobalDeviceMode bool
|
|
disableRoleDeviceMode bool
|
|
}
|
|
|
|
// Context is authorization context
|
|
type Context struct {
|
|
// User is the username
|
|
User types.User
|
|
// Checker is access checker
|
|
Checker services.AccessChecker
|
|
// Identity holds the caller identity:
|
|
// 1. If caller is a user
|
|
// a. local user identity
|
|
// b. remote user identity remapped to local identity based on trusted
|
|
// cluster role mapping.
|
|
// 2. If caller is a teleport instance, Identity holds their identity as-is
|
|
// (because there's no role mapping for non-human roles)
|
|
Identity IdentityGetter
|
|
// UnmappedIdentity holds the original caller identity. If this is a remote
|
|
// user, UnmappedIdentity holds the data before role mapping. Otherwise,
|
|
// it's identical to Identity.
|
|
UnmappedIdentity IdentityGetter
|
|
|
|
// disableDeviceRoleMode disables role-based device verification.
|
|
// Inherited from the authorizer that creates the context.
|
|
disableDeviceRoleMode bool
|
|
|
|
// AdminActionAuthState is the state of admin action authorization for this auth context.
|
|
AdminActionAuthState AdminActionAuthState
|
|
}
|
|
|
|
// AdminActionAuthState is an admin action authorization state.
|
|
type AdminActionAuthState int
|
|
|
|
const (
|
|
// AdminActionAuthUnauthorized admin action is not authorized.
|
|
AdminActionAuthUnauthorized AdminActionAuthState = iota
|
|
// AdminActionAuthNotRequired admin action authorization is not authorized.
|
|
// This state is used for non-user cases, like internal service roles or Machine ID.
|
|
AdminActionAuthNotRequired
|
|
// AdminActionAuthMFAVerified admin action is authorized with MFA verification.
|
|
AdminActionAuthMFAVerified
|
|
// AdminActionAuthMFAVerifiedWithReuse admin action is authorized with MFA verification.
|
|
// The MFA challenged used for verification allows reuse, which may be denied by some
|
|
// admin actions.
|
|
AdminActionAuthMFAVerifiedWithReuse
|
|
)
|
|
|
|
// GetUserMetadata returns information about the authenticated identity
|
|
// to be included in audit events.
|
|
func (c *Context) GetUserMetadata() apievents.UserMetadata {
|
|
return c.Identity.GetIdentity().GetUserMetadata()
|
|
}
|
|
|
|
// LockTargets returns a list of LockTargets inferred from the context's
|
|
// Identity and UnmappedIdentity.
|
|
func (c *Context) LockTargets() []types.LockTarget {
|
|
if _, ok := c.Identity.(UnauthenticatedRole); ok {
|
|
return nil
|
|
}
|
|
lockTargets := services.LockTargetsFromTLSIdentity(c.Identity.GetIdentity())
|
|
Loop:
|
|
for _, unmappedTarget := range services.LockTargetsFromTLSIdentity(c.UnmappedIdentity.GetIdentity()) {
|
|
// Append a lock target from UnmappedIdentity only if it is not already
|
|
// known from Identity.
|
|
for _, knownTarget := range lockTargets {
|
|
if unmappedTarget.Equals(knownTarget) {
|
|
continue Loop
|
|
}
|
|
}
|
|
lockTargets = append(lockTargets, unmappedTarget)
|
|
}
|
|
if r, ok := c.Identity.(BuiltinRole); ok {
|
|
switch r.Role {
|
|
// Node role is a special case because it was previously suported as a
|
|
// lock target that only locked the `ssh_service`. If the same Teleport server
|
|
// had multiple roles, Node lock would only lock the `ssh_service` while
|
|
// other roles would be able to authenticate into Teleport without a problem.
|
|
// To remove the ambiguity, we now lock the entire Teleport server for
|
|
// all roles using the LockTarget.ServerID field and `Node` field is
|
|
// deprecated.
|
|
// In order to support legacy behavior, we need fill in both `ServerID`
|
|
// and `Node` fields if the role is `Node` so that the previous behavior
|
|
// is preserved.
|
|
// This is a legacy behavior that we need to support for backwards compatibility.
|
|
case types.RoleNode:
|
|
lockTargets = append(lockTargets,
|
|
types.LockTarget{ServerID: r.GetServerID()},
|
|
types.LockTarget{ServerID: r.Identity.Username},
|
|
)
|
|
default:
|
|
lockTargets = append(lockTargets,
|
|
types.LockTarget{ServerID: r.GetServerID()},
|
|
types.LockTarget{ServerID: r.Identity.Username},
|
|
)
|
|
}
|
|
}
|
|
return lockTargets
|
|
}
|
|
|
|
// WithExtraRoles returns a shallow copy of [c], where the users roles have been
|
|
// extended with [roles]. It may return [c] unmodified.
|
|
func (c *Context) WithExtraRoles(access services.RoleGetter, clusterName string, roles []string) (*Context, error) {
|
|
var newRoleNames []string
|
|
newRoleNames = append(newRoleNames, c.Checker.RoleNames()...)
|
|
newRoleNames = append(newRoleNames, roles...)
|
|
newRoleNames = utils.Deduplicate(newRoleNames)
|
|
|
|
// Return early if there are no extra roles.
|
|
if len(newRoleNames) == len(c.Checker.RoleNames()) {
|
|
return c, nil
|
|
}
|
|
|
|
accessInfo := &services.AccessInfo{
|
|
Username: c.User.GetName(),
|
|
Roles: newRoleNames,
|
|
Traits: c.User.GetTraits(),
|
|
AllowedResourceAccessIDs: c.Checker.GetAllowedResourceAccessIDs(),
|
|
DelegationSessionID: c.Checker.DelegationSessionID(),
|
|
}
|
|
checker, err := services.NewAccessChecker(accessInfo, clusterName, access)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
newContext := *c
|
|
newContext.Checker = checker
|
|
return &newContext, nil
|
|
}
|
|
|
|
// GetAccessState returns the AccessState based on the underlying
|
|
// [services.AccessChecker] and [tlsca.Identity].
|
|
func (c *Context) GetAccessState(authPref readonly.AuthPreference) services.AccessState {
|
|
state := c.Checker.GetAccessState(authPref)
|
|
identity := c.Identity.GetIdentity()
|
|
|
|
// Builtin services (like proxy_service and kube_service) are not gated
|
|
// on MFA and only need to pass normal RBAC action checks.
|
|
_, isService := c.Identity.(BuiltinRole)
|
|
state.MFAVerified = isService || identity.IsMFAVerified()
|
|
|
|
state.EnableDeviceVerification = !c.disableDeviceRoleMode
|
|
state.DeviceVerified = isService || dtauthz.IsTLSDeviceVerified(&identity.DeviceExtensions)
|
|
state.IsBot = identity.IsBot()
|
|
|
|
return state
|
|
}
|
|
|
|
// GetDisconnectCertExpiry calculates the proper value for DisconnectExpiredCert
|
|
// based on whether a connection is set to disconnect on cert expiry, and whether
|
|
// the cert is a short-lived (<1m) one issued for an MFA verified session. If the session
|
|
// doesn't need to be disconnected on cert expiry, it will return a zero [time.Time].
|
|
func (c *Context) GetDisconnectCertExpiry(authPref readonly.AuthPreference) time.Time {
|
|
// In the case where both disconnect_expired_cert and require_session_mfa are enabled,
|
|
// the PreviousIdentityExpires value of the certificate will be used, which is the
|
|
// expiry of the certificate used to issue the short-lived MFA verified certificate.
|
|
//
|
|
// See https://github.com/gravitational/teleport/issues/18544
|
|
|
|
// If the session doesn't need to be disconnected on cert expiry just return the default value.
|
|
disconnectExpiredCert := authPref.GetDisconnectExpiredCert()
|
|
if c.Checker != nil {
|
|
disconnectExpiredCert = c.Checker.AdjustDisconnectExpiredCert(disconnectExpiredCert)
|
|
}
|
|
|
|
if !disconnectExpiredCert {
|
|
return time.Time{}
|
|
}
|
|
|
|
identity := c.Identity.GetIdentity()
|
|
if !identity.PreviousIdentityExpires.IsZero() {
|
|
// If this is a short-lived mfa verified cert, return the certificate extension
|
|
// that holds its issuing certificates expiry value.
|
|
return identity.PreviousIdentityExpires
|
|
}
|
|
|
|
// Otherwise, return the current certificates expiration
|
|
return identity.Expires
|
|
}
|
|
|
|
// Authorize authorizes user based on identity supplied via context
|
|
func (a *authorizer) Authorize(ctx context.Context) (authCtx *Context, err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
err = a.convertAuthorizerError(err)
|
|
}
|
|
}()
|
|
|
|
if ctx == nil {
|
|
return nil, trace.AccessDenied("missing authentication context")
|
|
}
|
|
userI, err := UserFromContext(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if user, ok := userI.(LocalUser); ok && user.Identity.ScopePin != nil {
|
|
return nil, trace.Errorf("cannot perform standard authz: %w", services.ErrScopedIdentity)
|
|
}
|
|
|
|
if _, ok := userI.(ScopedBuiltinRole); ok {
|
|
return nil, trace.Errorf("cannot perform standard authz: %w", services.ErrScopedIdentity)
|
|
}
|
|
|
|
authContext, err := a.fromUser(ctx, userI)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
var clientAddr string
|
|
if clientSrcAddr, err := ClientSrcAddrFromContext(ctx); err == nil {
|
|
clientAddr = clientSrcAddr.String()
|
|
}
|
|
|
|
if err := CheckIPPinning(ctx, clientAddr, authContext.Identity.GetIdentity().PinnedIP, authContext.Checker.PinSourceIP(), a.logger); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Enforce applicable locks.
|
|
authPref, err := a.readOnlyAccessPoint.GetReadOnlyAuthPreference(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if lockErr := a.lockWatcher.CheckLockInForce(
|
|
authContext.Checker.LockingMode(authPref.GetLockingMode()),
|
|
authContext.LockTargets()...); lockErr != nil {
|
|
return nil, trace.Wrap(lockErr)
|
|
}
|
|
|
|
// Enforce required private key policy if set.
|
|
if err := a.enforcePrivateKeyPolicy(ctx, authContext, authPref); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Device Trust: authorize device extensions.
|
|
if !a.disableGlobalDeviceMode {
|
|
if err := dtauthz.VerifyTLSUser(ctx, authPref.GetDeviceTrust(), authContext.Identity.GetIdentity()); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if err := a.checkAdminActionVerification(ctx, authContext); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return authContext, nil
|
|
}
|
|
|
|
func (a *authorizer) enforcePrivateKeyPolicy(ctx context.Context, authContext *Context, authPref readonly.AuthPreference) error {
|
|
switch authContext.Identity.(type) {
|
|
case BuiltinRole, RemoteBuiltinRole:
|
|
// built in roles do not need to pass private key policies
|
|
return nil
|
|
case UnauthenticatedRole:
|
|
// UnauthenticatedRole won't have the private key policies.
|
|
return nil
|
|
}
|
|
|
|
// Check that the required private key policy, defined by roles and auth pref,
|
|
// is met by this Identity's tls certificate.
|
|
identityPolicy := authContext.Identity.GetIdentity().PrivateKeyPolicy
|
|
requiredPolicy, err := authContext.Checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if !requiredPolicy.IsSatisfiedBy(identityPolicy) {
|
|
return keys.NewPrivateKeyPolicyError(requiredPolicy)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *authorizer) fromUser(ctx context.Context, userI any) (*Context, error) {
|
|
switch user := userI.(type) {
|
|
case LocalUser:
|
|
return a.authorizeLocalUser(ctx, user)
|
|
case RemoteUser:
|
|
return a.authorizeRemoteUser(ctx, user)
|
|
case BuiltinRole:
|
|
return a.authorizeBuiltinRole(ctx, user)
|
|
case RemoteBuiltinRole:
|
|
return a.authorizeRemoteBuiltinRole(user)
|
|
case UnauthenticatedRole:
|
|
return ContextForUnauthenticatedRole(user)
|
|
default:
|
|
return nil, trace.AccessDenied("unsupported context type %T", userI)
|
|
}
|
|
}
|
|
|
|
// checkAdminActionVerification checks if this auth request is verified for admin actions.
|
|
func (a *authorizer) checkAdminActionVerification(ctx context.Context, authContext *Context) error {
|
|
switch authContext.Identity.(type) {
|
|
case BuiltinRole, RemoteBuiltinRole:
|
|
// Builtin roles bypass MFA
|
|
authContext.AdminActionAuthState = AdminActionAuthNotRequired
|
|
return nil
|
|
case UnauthenticatedRole:
|
|
// UnauthenticatedRole is unauthenticated client by default.
|
|
// Mark the AdminActionAuthState as AdminActionAuthUnauthorized.
|
|
authContext.AdminActionAuthState = AdminActionAuthUnauthorized
|
|
return nil
|
|
default:
|
|
required, err := a.isAdminActionAuthorizationRequiredForUsers(ctx, authContext)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if !required {
|
|
authContext.AdminActionAuthState = AdminActionAuthNotRequired
|
|
return nil
|
|
}
|
|
|
|
if err := a.authorizeAdminAction(ctx, authContext); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (a *authorizer) isAdminActionAuthorizationRequiredForUsers(ctx context.Context, authContext *Context) (bool, error) {
|
|
// Provide a way to turn off admin MFA requirements in case expected functionality
|
|
// is disrupted by this requirement, such as for integrations essential to a user
|
|
// which do not yet make use of a machine ID / AdminRole impersonated identity.
|
|
//
|
|
// TODO(Joerger): once we have fully transitioned to requiring machine ID for
|
|
// integrations and ironed out any bugs with admin MFA, this env var should be removed.
|
|
if os.Getenv("TELEPORT_UNSTABLE_DISABLE_MFA_ADMIN_ACTIONS") == "yes" {
|
|
return false, nil
|
|
}
|
|
|
|
authpref, err := a.readOnlyAccessPoint.GetReadOnlyAuthPreference(ctx)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
// Check if this cluster enforces MFA for admin actions.
|
|
if !authpref.IsAdminActionMFAEnforced() {
|
|
return false, nil
|
|
}
|
|
|
|
ident := authContext.Identity.GetIdentity()
|
|
// Skip MFA check if the user is a Bot.
|
|
if user, err := a.accessPoint.GetUser(ctx, ident.Username, false); err == nil && user.IsBot() {
|
|
a.logger.DebugContext(ctx, "Skipping admin action MFA check for bot identity", "identity", ident)
|
|
return false, nil
|
|
}
|
|
|
|
// Skip MFA if the identity is being impersonated by the Bot or Admin built in role.
|
|
if impersonator := ident.Impersonator; impersonator != "" {
|
|
impersonatorUser, err := a.accessPoint.GetUser(ctx, impersonator, false)
|
|
if err == nil && impersonatorUser.IsBot() {
|
|
a.logger.DebugContext(ctx, "Skipping admin action MFA check for bot-impersonated identity", "identity", ident)
|
|
return false, nil
|
|
}
|
|
|
|
// If we don't find a user matching the impersonator, it may be the admin role impersonating.
|
|
// Check that the impersonator matches a host service FQDN - <host-id>.<clustername>
|
|
if trace.IsNotFound(err) {
|
|
hostFQDNParts := strings.SplitN(impersonator, ".", 2)
|
|
if len(hostFQDNParts) > 1 && hostFQDNParts[1] == a.clusterName {
|
|
if _, err := uuid.Parse(hostFQDNParts[0]); err == nil {
|
|
a.logger.DebugContext(ctx, "Skipping admin action MFA check for admin-impersonated identity", "identity", ident)
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (a *authorizer) authorizeAdminAction(ctx context.Context, authContext *Context) error {
|
|
// MFA is required to be passed through the request context.
|
|
mfaResp, err := mfa.CredentialsFromContext(ctx)
|
|
if err != nil {
|
|
if trace.IsNotFound(err) {
|
|
// missing MFA verification should be a noop.
|
|
return nil
|
|
}
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if a.mfaAuthenticator == nil {
|
|
return trace.Errorf("failed to validate MFA auth response, authorizer missing mfaAuthenticator field")
|
|
}
|
|
|
|
requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_ADMIN_ACTION}
|
|
mfaData, err := a.mfaAuthenticator.ValidateMFAAuthResponse(ctx, mfaResp, authContext.User.GetName(), requiredExt)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
authContext.AdminActionAuthState = AdminActionAuthMFAVerified
|
|
if mfaData.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES {
|
|
authContext.AdminActionAuthState = AdminActionAuthMFAVerifiedWithReuse
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// convertAuthorizerError will take an authorizer error and convert it into an error easily
|
|
// handled by gRPC services.
|
|
func (a *authorizer) convertAuthorizerError(err error) error {
|
|
switch {
|
|
case err == nil:
|
|
return nil
|
|
// propagate connection problem error so we can differentiate
|
|
// between connection failed and access denied
|
|
case trace.IsConnectionProblem(err):
|
|
return trace.ConnectionProblem(err, "failed to connect to the database")
|
|
case trace.IsNotFound(err):
|
|
// user not found, wrap error with access denied
|
|
return trace.Wrap(err, "access denied")
|
|
case errors.Is(err, ErrIPPinningMissing) || errors.Is(err, ErrIPPinningMismatch) || errors.Is(err, ErrIPPinningNotAllowed):
|
|
a.logger.WarnContext(context.Background(), "ip pinning requirements not satisfied", "error", err)
|
|
return trace.Wrap(err)
|
|
case errors.Is(err, services.ErrScopedIdentity):
|
|
return trace.Wrap(err)
|
|
case trace.IsAccessDenied(err):
|
|
a.logger.WarnContext(context.Background(), "access denied", "error", err)
|
|
case keys.IsPrivateKeyPolicyError(err):
|
|
// private key policy errors should be returned to the client
|
|
// unaltered so that they know to reauthenticate with a valid key.
|
|
return trace.Unwrap(err)
|
|
default:
|
|
a.logger.WarnContext(context.Background(), "Suppressing unknown authz error", "error", err)
|
|
}
|
|
return trace.AccessDenied("access denied")
|
|
}
|
|
|
|
// ErrIPPinningMissing is returned when user cert should be pinned but isn't.
|
|
var ErrIPPinningMissing = &trace.AccessDeniedError{Message: "pinned IP is required for the user, but is not present on identity"}
|
|
|
|
// ErrIPPinningMismatch is returned when user's pinned IP doesn't match observed IP.
|
|
var ErrIPPinningMismatch = &trace.AccessDeniedError{Message: "pinned IP doesn't match observed client IP"}
|
|
|
|
// ErrIPPinningNotAllowed is returned when user's pinned IP doesn't match observed IP.
|
|
var ErrIPPinningNotAllowed = &trace.AccessDeniedError{Message: "IP pinning is not allowed for connections that have been" +
|
|
"downgraded or are behind a L4 load balancers with PROXY protocol enabled without explicitly setting 'proxy_protocol: on'" +
|
|
"in the proxy_service and/or auth_service config."}
|
|
|
|
// CheckIPPinning verifies IP pinning for the identity, using the provided client addr.
|
|
// Check is considered successful if no error is returned.
|
|
func CheckIPPinning(ctx context.Context, clientAddr string, pinnedIP string, pinSourceIP bool, log *slog.Logger) error {
|
|
if pinnedIP == "" {
|
|
if pinSourceIP {
|
|
return trace.Wrap(ErrIPPinningMissing)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if clientAddr == "" {
|
|
return trace.BadParameter("pinned IP is required for the user, but the client IP was not provided")
|
|
}
|
|
|
|
clientHost, clientPort, err := net.SplitHostPort(clientAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
parsedClientIP := net.ParseIP(clientHost)
|
|
if parsedClientIP == nil {
|
|
return trace.BadParameter("client IP address %q is not a valid IP address", clientHost)
|
|
}
|
|
|
|
parsedPinnedIP := net.ParseIP(pinnedIP)
|
|
if parsedPinnedIP == nil {
|
|
return trace.BadParameter("pinned IP address %q is not a valid IP address", pinnedIP)
|
|
}
|
|
|
|
if !parsedClientIP.Equal(net.ParseIP(pinnedIP)) {
|
|
if log != nil {
|
|
log.DebugContext(ctx, "Pinned IP and client IP mismatch",
|
|
"client_ip", logutils.StringerAttr(parsedClientIP),
|
|
"pinned_ip", logutils.StringerAttr(parsedPinnedIP),
|
|
)
|
|
}
|
|
return trace.Wrap(ErrIPPinningMismatch)
|
|
}
|
|
// If connection has port 0 it means it was marked by multiplexer's 'detect()' function as affected by unexpected PROXY header.
|
|
// For security reason we don't allow such connection for IP pinning because we can't rely on client IP being correct.
|
|
if clientPort == "0" {
|
|
if log != nil {
|
|
log.DebugContext(ctx, "client address is not allowed to use IP pinning", "client_ip", parsedClientIP, "pinned_ip", pinnedIP, "error", ErrIPPinningNotAllowed.Message)
|
|
}
|
|
return trace.Wrap(ErrIPPinningNotAllowed)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// authorizeLocalUser returns authz context based on the username
|
|
func (a *authorizer) authorizeLocalUser(ctx context.Context, u LocalUser) (*Context, error) {
|
|
return ContextForLocalUser(ctx, u, a.accessPoint, a.clusterName, a.disableRoleDeviceMode)
|
|
}
|
|
|
|
// authorizeRemoteUser returns checker based on cert authority roles
|
|
func (a *authorizer) authorizeRemoteUser(ctx context.Context, u RemoteUser) (*Context, error) {
|
|
ca, err := a.accessPoint.GetCertAuthority(ctx, types.CertAuthID{
|
|
Type: types.UserCA,
|
|
DomainName: u.ClusterName,
|
|
}, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
accessInfo, err := services.AccessInfoFromRemoteTLSIdentity(u.Identity, ca.CombinedMapping())
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
checker, err := services.NewAccessChecker(accessInfo, a.clusterName, a.accessPoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// The user is prefixed with "remote-" and suffixed with cluster name with
|
|
// the hope that it does not match a real local user.
|
|
user, err := types.NewUser(services.UsernameForRemoteCluster(u.Username, u.ClusterName))
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
user.SetTraits(accessInfo.Traits)
|
|
user.SetRoles(accessInfo.Roles)
|
|
|
|
// Adjust expiry based on locally mapped roles.
|
|
ttl := time.Until(u.Identity.Expires)
|
|
ttl = checker.AdjustSessionTTL(ttl)
|
|
var previousIdentityExpires time.Time
|
|
if u.Identity.MFAVerified != "" {
|
|
prevIdentityTTL := time.Until(u.Identity.PreviousIdentityExpires)
|
|
prevIdentityTTL = checker.AdjustSessionTTL(prevIdentityTTL)
|
|
previousIdentityExpires = time.Now().Add(prevIdentityTTL)
|
|
}
|
|
|
|
kubeUsers, kubeGroups, err := checker.CheckKubeGroupsAndUsers(ttl, false)
|
|
// IsNotFound means that the user has no k8s users or groups, which is fine
|
|
// in many cases. The downstream k8s handler will ensure that users/groups
|
|
// are set if this is a k8s request.
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
principals, err := checker.CheckLoginDuration(ttl)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// Convert u.Identity into the mapped local identity.
|
|
//
|
|
// This prevents downstream users from accidentally using the unmapped
|
|
// identity information and confusing who's accessing a resource.
|
|
identity := tlsca.Identity{
|
|
Username: user.GetName(),
|
|
Groups: user.GetRoles(),
|
|
Traits: accessInfo.Traits,
|
|
Principals: principals,
|
|
KubernetesGroups: kubeGroups,
|
|
KubernetesUsers: kubeUsers,
|
|
TeleportCluster: a.clusterName,
|
|
Expires: time.Now().Add(ttl),
|
|
PreviousIdentityExpires: previousIdentityExpires,
|
|
|
|
// These fields are for routing and restrictions, safe to re-use from
|
|
// unmapped identity.
|
|
Usage: u.Identity.Usage,
|
|
RouteToCluster: u.Identity.RouteToCluster,
|
|
KubernetesCluster: u.Identity.KubernetesCluster,
|
|
RouteToApp: u.Identity.RouteToApp,
|
|
RouteToDatabase: u.Identity.RouteToDatabase,
|
|
MFAVerified: u.Identity.MFAVerified,
|
|
LoginIP: u.Identity.LoginIP,
|
|
PinnedIP: u.Identity.PinnedIP,
|
|
PrivateKeyPolicy: u.Identity.PrivateKeyPolicy,
|
|
UserType: u.Identity.UserType,
|
|
OriginClusterName: u.Identity.TeleportCluster,
|
|
DeviceExtensions: u.Identity.DeviceExtensions,
|
|
}
|
|
if checker.PinSourceIP() && identity.PinnedIP == "" {
|
|
return nil, trace.Wrap(ErrIPPinningMissing)
|
|
}
|
|
|
|
return &Context{
|
|
User: user,
|
|
Checker: checker,
|
|
Identity: WrapIdentity(identity),
|
|
UnmappedIdentity: u,
|
|
disableDeviceRoleMode: a.disableRoleDeviceMode,
|
|
}, nil
|
|
}
|
|
|
|
// authorizeBuiltinRole authorizes builtin role
|
|
func (a *authorizer) authorizeBuiltinRole(ctx context.Context, r BuiltinRole) (*Context, error) {
|
|
recConfig, err := a.readOnlyAccessPoint.GetReadOnlySessionRecordingConfig(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return ContextForBuiltinRole(r, recConfig)
|
|
}
|
|
|
|
func (a *authorizer) authorizeRemoteBuiltinRole(r RemoteBuiltinRole) (*Context, error) {
|
|
if r.Role != types.RoleProxy {
|
|
return nil, trace.AccessDenied("access denied for remote %v connecting to cluster", r.Role)
|
|
}
|
|
roleSet, err := services.RoleSetFromSpec(
|
|
string(types.RoleRemoteProxy),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
GitHubPermissions: []types.GitHubPermission{{
|
|
Organizations: []string{types.Wildcard},
|
|
}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindNode, services.RO()),
|
|
types.NewRule(types.KindProxy, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindAuthServer, services.RO()),
|
|
types.NewRule(types.KindReverseTunnel, services.RO()),
|
|
types.NewRule(types.KindTunnelConnection, services.RO()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindKubeServer, services.RO()),
|
|
types.NewRule(types.KindInstaller, services.RO()),
|
|
types.NewRule(types.KindUIConfig, services.RO()),
|
|
types.NewRule(types.KindDatabaseService, services.RO()),
|
|
types.NewRule(types.KindGitServer, services.RO()),
|
|
// this rule allows remote proxy to update the cluster's certificate authorities
|
|
// during certificates renewal
|
|
{
|
|
Resources: []string{types.KindCertAuthority},
|
|
// It is important that remote proxy can only rotate
|
|
// existing certificate authority, and not create or update new ones
|
|
Verbs: []string{types.VerbRead, types.VerbRotate},
|
|
// allow administrative access to the certificate authority names
|
|
// matching the cluster name only
|
|
Where: builder.Equals(services.ResourceNameExpr, builder.String(r.ClusterName)).String(),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
user, err := types.NewUser(r.Username)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
roles := []string{string(types.RoleRemoteProxy)}
|
|
user.SetRoles(roles)
|
|
checker := services.NewAccessCheckerWithRoleSet(&services.AccessInfo{
|
|
Roles: roles,
|
|
Traits: nil,
|
|
AllowedResourceAccessIDs: nil,
|
|
}, a.clusterName, roleSet)
|
|
return &Context{
|
|
User: user,
|
|
Checker: checker,
|
|
Identity: r,
|
|
UnmappedIdentity: r,
|
|
disableDeviceRoleMode: a.disableRoleDeviceMode,
|
|
}, nil
|
|
}
|
|
|
|
func roleSpecForProxyWithRecordAtProxy(clusterName string) types.RoleSpecV6 {
|
|
base := roleSpecForProxy(clusterName)
|
|
base.Allow.Rules = append(base.Allow.Rules, types.NewRule(types.KindHostCert, services.RW()))
|
|
return base
|
|
}
|
|
|
|
func roleSpecForProxy(clusterName string) types.RoleSpecV6 {
|
|
return types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
ClusterLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseServiceLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
GitHubPermissions: []types.GitHubPermission{{
|
|
Organizations: []string{types.Wildcard},
|
|
}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindProxy, services.RW()),
|
|
types.NewRule(types.KindOIDCRequest, services.RW()),
|
|
types.NewRule(types.KindSSHSession, services.RW()),
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindSAMLRequest, services.RW()),
|
|
types.NewRule(types.KindOIDC, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindSAML, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindGithub, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindGithubRequest, services.RW()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindNode, services.RO()),
|
|
types.NewRule(types.KindAuthServer, services.RO()),
|
|
types.NewRule(types.KindReverseTunnel, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(scopedaccess.KindScopedRole, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindUIConfig, services.RO()),
|
|
types.NewRule(types.KindStaticTokens, services.RO()),
|
|
types.NewRule(types.KindTunnelConnection, services.RW()),
|
|
types.NewRule(types.KindRemoteCluster, services.RO()),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindAppServer, services.RO()),
|
|
types.NewRule(types.KindWebSession, services.RW()),
|
|
types.NewRule(types.KindWebToken, services.RW()),
|
|
types.NewRule(types.KindKubeServer, services.RW()),
|
|
types.NewRule(types.KindKubeWaitingContainer, services.RW()),
|
|
types.NewRule(types.KindDatabaseServer, services.RO()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindToken, []string{types.VerbRead, types.VerbDelete}),
|
|
types.NewRule(types.KindWindowsDesktopService, services.RO()),
|
|
types.NewRule(types.KindDatabaseCertificate, []string{types.VerbCreate}),
|
|
types.NewRule(types.KindWindowsDesktop, services.RO()),
|
|
types.NewRule(types.KindLinuxDesktop, services.RO()),
|
|
types.NewRule(types.KindInstaller, services.RO()),
|
|
types.NewRule(types.KindConnectionDiagnostic, services.RW()),
|
|
types.NewRule(types.KindDatabaseService, services.RO()),
|
|
types.NewRule(types.KindSAMLIdPServiceProvider, services.RO()),
|
|
types.NewRule(types.KindUserGroup, services.RO()),
|
|
types.NewRule(types.KindClusterMaintenanceConfig, services.RO()),
|
|
types.NewRule(types.KindAutoUpdateConfig, services.RO()),
|
|
types.NewRule(types.KindAutoUpdateVersion, services.RO()),
|
|
types.NewRule(types.KindAutoUpdateAgentRollout, services.RO()),
|
|
types.NewRule(types.KindIntegration, append(services.RO(), types.VerbUse)),
|
|
types.NewRule(types.KindAuditQuery, services.RO()),
|
|
types.NewRule(types.KindSecurityReport, services.RO()),
|
|
types.NewRule(types.KindSecurityReportState, services.RO()),
|
|
types.NewRule(types.KindUserTask, services.RO()),
|
|
types.NewRule(types.KindGitServer, services.RO()),
|
|
types.NewRule(types.KindAccessGraphSettings, services.RO()),
|
|
types.NewRule(types.KindRelayServer, services.RO()),
|
|
types.NewRule(types.KindAccessList, services.RO()),
|
|
types.NewRule(types.KindHealthCheckConfig, services.RO()),
|
|
types.NewRule(types.KindAppAuthConfig, services.RO()),
|
|
types.NewRule(types.KindValidatedMFAChallenge, services.RO()),
|
|
// this rule allows cloud proxies to read
|
|
// plugins of `openai` type, since Assist uses the OpenAI API and runs in Proxy.
|
|
{
|
|
Resources: []string{types.KindPlugin},
|
|
Verbs: []string{types.VerbRead},
|
|
Where: builder.Equals(
|
|
builder.Identifier(`resource.metadata.labels["type"]`),
|
|
builder.String("openai"),
|
|
).String(),
|
|
},
|
|
// this rule allows local proxy to update the remote cluster's host certificate authorities
|
|
// during certificates renewal
|
|
{
|
|
Resources: []string{types.KindCertAuthority},
|
|
Verbs: []string{types.VerbCreate, types.VerbRead, types.VerbUpdate},
|
|
// allow administrative access to the host certificate authorities
|
|
// matching any cluster name except local
|
|
Where: builder.And(
|
|
builder.Equals(services.CertAuthorityTypeExpr, builder.String(string(types.HostCA))),
|
|
builder.Not(
|
|
builder.Equals(
|
|
services.ResourceNameExpr,
|
|
builder.String(clusterName),
|
|
),
|
|
),
|
|
).String(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// RoleSetForUnauthenticatedRoles returns a RoleSet for unauthenticated roles.
|
|
func RoleSetForUnauthenticatedRoles(clusterName string, roles ...types.UnauthenticatedRole) (services.RoleSet, error) {
|
|
var definitions []types.Role
|
|
for _, role := range roles {
|
|
switch role {
|
|
case types.RoleNop:
|
|
rd, err := services.RoleFromSpec(
|
|
string(role),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{},
|
|
Rules: []types.Rule{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
definitions = append(definitions, rd)
|
|
default:
|
|
return nil, trace.NotFound("unauthenticated role %q is not recognized", role)
|
|
}
|
|
}
|
|
return services.NewRoleSet(definitions...), nil
|
|
}
|
|
|
|
// RoleSetForBuiltinRoles returns RoleSet for embedded builtin role
|
|
func RoleSetForBuiltinRoles(clusterName string, recConfig readonly.SessionRecordingConfig, isScoped bool, roles ...types.SystemRole) (services.RoleSet, error) {
|
|
var definitions []types.Role
|
|
for _, role := range roles {
|
|
rd, err := definitionForBuiltinRole(clusterName, recConfig, role, isScoped)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
definitions = append(definitions, rd)
|
|
}
|
|
return services.NewRoleSet(definitions...), nil
|
|
}
|
|
|
|
// definitionForBuiltinRole constructs the appropriate role definition for a given builtin role with
|
|
// consideration for whether the definition be will be used in a scoped context.
|
|
func definitionForBuiltinRole(clusterName string, recConfig readonly.SessionRecordingConfig, role types.SystemRole, isScoped bool) (types.Role, error) {
|
|
if isScoped {
|
|
return scopedDefinitionForBuiltinRole(clusterName, recConfig, role)
|
|
}
|
|
return unscopedDefinitionForBuiltinRole(clusterName, recConfig, role)
|
|
}
|
|
|
|
// scopedDefinitionForBuiltinRole constructs the appropriate role definition for a given builtin role in a scoped context.
|
|
func scopedDefinitionForBuiltinRole(clusterName string, recConfig readonly.SessionRecordingConfig, role types.SystemRole) (types.Role, error) {
|
|
switch role {
|
|
case types.RoleNode:
|
|
return unscopedDefinitionForBuiltinRole(clusterName, recConfig, types.RoleNode)
|
|
case types.RoleKube:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindEvent, services.RW()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindHealthCheckConfig, services.RO()),
|
|
// TODO(fspmarshall/scopes): we eventually want to remove blanket scoped role
|
|
// access in favor of agents only being able to read scoped roles that may affect
|
|
// access decisions for the given agent specifically. this verb grant will need to
|
|
// be revisited as part of that work.
|
|
types.NewRule(scopedaccess.KindScopedRole, services.RO()),
|
|
},
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
return nil, trace.NotFound("builtin role for scoped %q is not recognized", role.String())
|
|
}
|
|
|
|
// unscopedDefinitionForBuiltinRole constructs the appropriate role definition for a given builtin role in an unscoped context.
|
|
func unscopedDefinitionForBuiltinRole(clusterName string, recConfig readonly.SessionRecordingConfig, role types.SystemRole) (types.Role, error) {
|
|
switch role {
|
|
case types.RoleAuth:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindAuthServer, services.RW()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleProvisionToken:
|
|
return services.RoleFromSpec(role.String(), types.RoleSpecV6{})
|
|
case types.RoleNode:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindNode, services.RW()),
|
|
types.NewRule(types.KindSSHSession, services.RW()),
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindProxy, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindAuthServer, services.RO()),
|
|
types.NewRule(types.KindReverseTunnel, services.RW()),
|
|
types.NewRule(types.KindTunnelConnection, services.RO()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindNetworkRestrictions, services.RO()),
|
|
types.NewRule(types.KindConnectionDiagnostic, services.RW()),
|
|
types.NewRule(types.KindStaticHostUser, services.RO()),
|
|
types.NewRule(types.KindStableUNIXUser, []string{types.VerbCreate, types.VerbRead}),
|
|
// TODO(fspmarshall/scopes): we eventually want to remove blanket scoped role
|
|
// access in favor of nodes only being able to read scoped roles that may affect
|
|
// access decisions for the given node specifically. this verb grant will need to
|
|
// be revisited as part of that work.
|
|
types.NewRule(scopedaccess.KindScopedRole, services.RO()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleApp:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindProxy, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindAuthServer, services.RO()),
|
|
types.NewRule(types.KindReverseTunnel, services.RW()),
|
|
types.NewRule(types.KindTunnelConnection, services.RO()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindAppServer, services.RW()),
|
|
types.NewRule(types.KindApp, services.RO()),
|
|
types.NewRule(types.KindJWT, services.RW()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleDatabase:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindProxy, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindAuthServer, services.RO()),
|
|
types.NewRule(types.KindReverseTunnel, services.RW()),
|
|
types.NewRule(types.KindTunnelConnection, services.RO()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindDatabaseServer, services.RW()),
|
|
types.NewRule(types.KindDatabaseService, services.RW()),
|
|
types.NewRule(types.KindDatabase, services.RO()),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindConnectionDiagnostic, services.RW()),
|
|
types.NewRule(types.KindDatabaseObjectImportRule, services.RO()),
|
|
types.NewRule(types.KindDatabaseObject, services.RW()),
|
|
types.NewRule(types.KindHealthCheckConfig, services.RO()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleProxy:
|
|
// to support connecting to Agentless nodes, proxy needs to be
|
|
// able to generate host certificates.
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
roleSpecForProxyWithRecordAtProxy(clusterName),
|
|
)
|
|
case types.RoleRelay:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{
|
|
types.Wildcard,
|
|
},
|
|
NodeLabels: types.Labels{
|
|
types.Wildcard: {types.Wildcard},
|
|
},
|
|
AppLabels: types.Labels{
|
|
types.Wildcard: {types.Wildcard},
|
|
},
|
|
DatabaseLabels: types.Labels{
|
|
types.Wildcard: {types.Wildcard},
|
|
},
|
|
KubernetesLabels: types.Labels{
|
|
types.Wildcard: {types.Wildcard},
|
|
},
|
|
WindowsDesktopLabels: types.Labels{
|
|
types.Wildcard: {types.Wildcard},
|
|
},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindAppServer, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindDatabaseServer, services.RO()),
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindKubeServer, services.RO()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindNode, services.RO()),
|
|
types.NewRule(types.KindRelayServer, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindWindowsDesktop, services.RO()),
|
|
},
|
|
},
|
|
},
|
|
)
|
|
case types.RoleSignup:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindAuthServer, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleAdmin:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Options: types.RoleOptions{
|
|
MaxSessionTTL: types.MaxDuration(),
|
|
},
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Logins: []string{},
|
|
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}},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseServiceLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
ClusterLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
WindowsDesktopLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
GitHubPermissions: []types.GitHubPermission{{
|
|
Organizations: []string{types.Wildcard},
|
|
}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.Wildcard, services.RW()),
|
|
types.NewRule(types.KindDevice, append(services.RW(), types.VerbCreateEnrollToken, types.VerbEnroll)),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleKube:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindKubeServer, services.RW()),
|
|
types.NewRule(types.KindKubeWaitingContainer, services.RW()),
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindKubernetesCluster, services.RO()),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindHealthCheckConfig, services.RO()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleWindowsDesktop:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
WindowsDesktopLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindCertAuthorityOverride, services.RO()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindUser, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindLock, services.RO()),
|
|
types.NewRule(types.KindWindowsDesktopService, services.RW()),
|
|
types.NewRule(types.KindWindowsDesktop, services.RW()),
|
|
types.NewRule(types.KindDynamicWindowsDesktop, services.RW()),
|
|
},
|
|
},
|
|
})
|
|
case types.RoleDiscovery:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindNamespace, services.RO()),
|
|
types.NewRule(types.KindNode, services.RW()),
|
|
types.NewRule(types.KindKubeServer, services.RO()),
|
|
types.NewRule(types.KindKubernetesCluster, services.RW()),
|
|
types.NewRule(types.KindDatabase, services.RW()),
|
|
types.NewRule(types.KindServerInfo, services.RW()),
|
|
types.NewRule(types.KindApp, services.RW()),
|
|
types.NewRule(types.KindDiscoveryConfig, services.RO()),
|
|
types.NewRule(types.KindIntegration, append(services.RO(), types.VerbUse)),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindUserTask, services.RW()),
|
|
},
|
|
// Discovery service should only access kubes/apps/dbs that originated from discovery.
|
|
KubernetesLabels: types.Labels{types.OriginLabel: []string{types.OriginCloud}},
|
|
DatabaseLabels: types.Labels{types.OriginLabel: []string{types.OriginCloud}},
|
|
AppLabels: types.Labels{types.OriginLabel: []string{types.OriginDiscoveryKubernetes}},
|
|
},
|
|
})
|
|
case types.RoleOkta:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
GroupLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindClusterName, services.RO()),
|
|
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
types.NewRule(types.KindEvent, services.WO()),
|
|
types.NewRule(types.KindAppServer, services.RW()),
|
|
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
|
types.NewRule(types.KindUser, services.RW()),
|
|
types.NewRule(types.KindUserGroup, services.RW()),
|
|
types.NewRule(types.KindOktaImportRule, services.RO()),
|
|
types.NewRule(types.KindOktaAssignment, services.RW()),
|
|
types.NewRule(types.KindProxy, services.RO()),
|
|
types.NewRule(types.KindClusterAuthPreference, services.RO()),
|
|
types.NewRule(types.KindRole, services.RO()),
|
|
types.NewRule(types.KindLock, services.RW()),
|
|
types.NewRule(types.KindSAML, services.ReadNoSecrets()),
|
|
types.NewRule(types.KindAccessList, services.RO()),
|
|
types.NewRule(types.KindAccessListMember, services.RO()),
|
|
// Okta can read/write access lists and roles it creates.
|
|
{
|
|
Resources: []string{types.KindRole},
|
|
Verbs: services.RW(),
|
|
Where: builder.Equals(
|
|
builder.Identifier(`resource.metadata.labels["`+types.OriginLabel+`"]`),
|
|
builder.String(types.OriginOkta),
|
|
).String(),
|
|
},
|
|
{
|
|
Resources: []string{types.KindAccessList},
|
|
Verbs: services.RW(),
|
|
Where: builder.Equals(
|
|
builder.Identifier(`resource.metadata.labels["`+types.OriginLabel+`"]`),
|
|
builder.String(types.OriginOkta),
|
|
).String(),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
case types.RoleMDM:
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindDevice, services.RW()),
|
|
},
|
|
},
|
|
})
|
|
|
|
case types.RoleAccessGraphPlugin:
|
|
// RoleAccessGraphPlugin is a special role that is used by the Access Graph plugins
|
|
// to access the semaphore resource.
|
|
return services.RoleFromSpec(
|
|
role.String(),
|
|
types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Namespaces: []string{types.Wildcard},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindSemaphore, services.RW()),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
return nil, trace.NotFound("builtin role %q is not recognized", role.String())
|
|
}
|
|
|
|
// ContextForUnauthenticatedRole returns a context with the unauthenticated role information embedded.
|
|
func ContextForUnauthenticatedRole(r UnauthenticatedRole) (*Context, error) {
|
|
roleSet, err := RoleSetForUnauthenticatedRoles(r.ClusterName, r.Role)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
user, err := types.NewUser(r.Username)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
roles := []string{string(r.Role)}
|
|
user.SetRoles(roles)
|
|
checker := services.NewAccessCheckerWithRoleSet(&services.AccessInfo{
|
|
Roles: roles,
|
|
Traits: nil,
|
|
AllowedResourceAccessIDs: nil,
|
|
}, r.ClusterName, roleSet)
|
|
return &Context{
|
|
User: user,
|
|
Checker: checker,
|
|
Identity: r,
|
|
UnmappedIdentity: r,
|
|
disableDeviceRoleMode: true, // Unauthenticated roles skip device trust.
|
|
AdminActionAuthState: AdminActionAuthUnauthorized, // Unauthenticated won't be able to do admin actions.
|
|
}, nil
|
|
}
|
|
|
|
// ContextForBuiltinRole returns a context with the builtin role information embedded.
|
|
func ContextForBuiltinRole(r BuiltinRole, recConfig readonly.SessionRecordingConfig) (*Context, error) {
|
|
var systemRoles []types.SystemRole
|
|
if r.Role == types.RoleInstance {
|
|
// instance certs encode multiple system roles in a separate field
|
|
systemRoles = r.AdditionalSystemRoles
|
|
if len(systemRoles) == 0 {
|
|
// note: previous parsing skipped unknown roles for this field, so its possible that some
|
|
// system roles were defined, but they were all unknown to us.
|
|
return nil, trace.BadParameter("cannot create instance context, no additional system roles recognized")
|
|
}
|
|
} else {
|
|
// all other certs encode a single system role
|
|
systemRoles = []types.SystemRole{r.Role}
|
|
}
|
|
roleSet, err := RoleSetForBuiltinRoles(r.ClusterName, recConfig, r.GetIdentity().AgentScope != "", systemRoles...)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
user, err := types.NewUser(r.Username)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
var roles []string
|
|
for _, r := range systemRoles {
|
|
roles = append(roles, string(r))
|
|
}
|
|
user.SetRoles(roles)
|
|
checker := services.NewAccessCheckerWithRoleSet(&services.AccessInfo{
|
|
Roles: roles,
|
|
Traits: nil,
|
|
AllowedResourceAccessIDs: nil,
|
|
}, r.ClusterName, roleSet)
|
|
return &Context{
|
|
User: user,
|
|
Checker: checker,
|
|
Identity: r,
|
|
UnmappedIdentity: r,
|
|
disableDeviceRoleMode: true, // Builtin roles skip device trust.
|
|
AdminActionAuthState: AdminActionAuthNotRequired, // builtin roles skip mfa for admin actions.
|
|
}, nil
|
|
}
|
|
|
|
// ContextForLocalUser returns a context with the local user info embedded.
|
|
func ContextForLocalUser(ctx context.Context, u LocalUser, accessPoint AuthorizerAccessPoint, clusterName string, disableDeviceRoleMode bool) (*Context, error) {
|
|
user, accessInfo, err := resolveLocalUser(ctx, u, accessPoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
accessChecker, err := services.NewAccessChecker(accessInfo, clusterName, accessPoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &Context{
|
|
User: user,
|
|
Checker: accessChecker,
|
|
Identity: u,
|
|
UnmappedIdentity: u,
|
|
disableDeviceRoleMode: disableDeviceRoleMode,
|
|
}, nil
|
|
}
|
|
|
|
func resolveLocalUser(ctx context.Context, u LocalUser, accessPoint AuthorizerAccessPoint) (types.User, *services.AccessInfo, error) {
|
|
// User has to be fetched to check if it's a blocked username
|
|
user, err := accessPoint.GetUser(ctx, u.Username, false)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
accessInfo, err := services.AccessInfoFromLocalTLSIdentity(u.Identity)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Override roles and traits from the local user based on the identity roles
|
|
// and traits, this is done to prevent potential conflict. Imagine a scenario
|
|
// when SSO user has left the company, but local user entry remained with old
|
|
// privileged roles. New user with the same name has been onboarded and would
|
|
// have derived the roles from the stale user entry. This code prevents
|
|
// that by extracting up to date identity traits and roles from the user's
|
|
// certificate metadata.
|
|
user.SetRoles(accessInfo.Roles)
|
|
user.SetTraits(accessInfo.Traits)
|
|
|
|
return user, accessInfo, nil
|
|
}
|
|
|
|
type contextKey string
|
|
|
|
const (
|
|
// contextUserCertificate is the X.509 certificate used by the contextUser to
|
|
// establish the mTLS connection.
|
|
// Holds a *x509.Certificate.
|
|
contextUserCertificate contextKey = "teleport-user-cert"
|
|
|
|
// contextUser is a user set in the context of the request
|
|
contextUser contextKey = "teleport-user"
|
|
|
|
// contextClientSrcAddr is a client source address set in the context of the request
|
|
contextClientSrcAddr contextKey = "teleport-client-src-addr"
|
|
|
|
// contextClientDstAddr is a client destination address set in the context of the request
|
|
contextClientDstAddr contextKey = "teleport-client-dst-addr"
|
|
|
|
// contextConn is a connection in the context associated with the request
|
|
contextConn contextKey = "teleport-connection"
|
|
)
|
|
|
|
// WithDelegator alias for backwards compatibility
|
|
var WithDelegator = utils.WithDelegator
|
|
|
|
// ClientUsername returns the username of a remote HTTP client making the call.
|
|
// If ctx didn't pass through auth middleware or did not come from an HTTP
|
|
// request, teleport.UserSystem is returned.
|
|
func ClientUsername(ctx context.Context) string {
|
|
userWithIdentity, err := UserFromContext(ctx)
|
|
if err != nil {
|
|
return teleport.UserSystem
|
|
}
|
|
identity := userWithIdentity.GetIdentity()
|
|
if identity.Username == "" {
|
|
return teleport.UserSystem
|
|
}
|
|
return identity.Username
|
|
}
|
|
|
|
func userIdentityFromContext(ctx context.Context) (*tlsca.Identity, error) {
|
|
userWithIdentity, err := UserFromContext(ctx)
|
|
if err != nil {
|
|
return nil, trace.AccessDenied("missing identity")
|
|
}
|
|
|
|
identity := userWithIdentity.GetIdentity()
|
|
if identity.Username == "" {
|
|
return nil, trace.AccessDenied("missing identity username")
|
|
}
|
|
|
|
return &identity, nil
|
|
}
|
|
|
|
// GetClientUsername returns the username of a remote HTTP client making the call.
|
|
// If ctx didn't pass through auth middleware or did not come from an HTTP
|
|
// request, returns an error.
|
|
func GetClientUsername(ctx context.Context) (string, error) {
|
|
identity, err := userIdentityFromContext(ctx)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
return identity.Username, nil
|
|
}
|
|
|
|
// GetClientUserIsSSO extracts the identity of a remote HTTP client and indicates whether that is an SSO user.
|
|
// If ctx didn't pass through auth middleware or did not come from an HTTP
|
|
// request, returns an error.
|
|
func GetClientUserIsSSO(ctx context.Context) (bool, error) {
|
|
identity, err := userIdentityFromContext(ctx)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
return identity.UserType == types.UserTypeSSO, nil
|
|
}
|
|
|
|
// ClientUserMetadata returns a UserMetadata suitable for events caused by a
|
|
// remote client making a call. If ctx didn't pass through auth middleware or
|
|
// did not come from an HTTP request, metadata for teleport.UserSystem is
|
|
// returned.
|
|
func ClientUserMetadata(ctx context.Context) apievents.UserMetadata {
|
|
identityGetter, err := UserFromContext(ctx)
|
|
if err != nil {
|
|
return apievents.UserMetadata{
|
|
User: teleport.UserSystem,
|
|
}
|
|
}
|
|
meta := identityGetter.GetIdentity().GetUserMetadata()
|
|
if meta.User == "" {
|
|
meta.User = teleport.UserSystem
|
|
}
|
|
return meta
|
|
}
|
|
|
|
// ClientUserMetadataWithUser returns a UserMetadata suitable for events caused
|
|
// by a remote client making a call, with the specified username overriding the one
|
|
// from the remote client.
|
|
func ClientUserMetadataWithUser(ctx context.Context, user string) apievents.UserMetadata {
|
|
meta := ClientUserMetadata(ctx)
|
|
meta.User = user
|
|
return meta
|
|
}
|
|
|
|
// CheckAccessToKind will ensure that the user has access to the given verbs for the given kind.
|
|
func (c *Context) CheckAccessToKind(kind string, verb string, additionalVerbs ...string) error {
|
|
ruleCtx := &services.Context{
|
|
User: c.User,
|
|
}
|
|
|
|
return c.CheckAccessToRule(ruleCtx, kind, verb, additionalVerbs...)
|
|
}
|
|
|
|
// MaybeAccessToKind will check if the user has access to the given verbs for
|
|
// the given kind, ignoring any where clauses configured. This is useful for
|
|
// avoiding work where the user has no chance of having access to the resource.
|
|
// Prefer using CheckAccessToResource where feasible.
|
|
func (c *Context) MaybeAccessToKind(kind string, verb string, additionalVerbs ...string) error {
|
|
ruleCtx := &services.Context{
|
|
User: c.User,
|
|
}
|
|
|
|
var errs []error
|
|
for _, verb := range append(additionalVerbs, verb) {
|
|
if err := c.Checker.GuessIfAccessIsPossible(ruleCtx, defaults.Namespace, kind, verb); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
return trace.NewAggregate(errs...)
|
|
}
|
|
|
|
// CheckAccessToResource will ensure that the user has access to the given verbs for the given resource.
|
|
func (c *Context) CheckAccessToResource(resource types.Resource, verb string, additionalVerbs ...string) error {
|
|
ruleCtx := &services.Context{
|
|
User: c.User,
|
|
Resource: resource,
|
|
}
|
|
|
|
return c.CheckAccessToRule(ruleCtx, resource.GetKind(), verb, additionalVerbs...)
|
|
}
|
|
|
|
// CheckAccessToResource153 will ensure that the user has access to the given verbs for the given resource.
|
|
func (c *Context) CheckAccessToResource153(resource types.Resource153, verb string, additionalVerbs ...string) error {
|
|
ruleCtx := &services.Context{
|
|
User: c.User,
|
|
Resource153: resource,
|
|
}
|
|
|
|
return c.CheckAccessToRule(ruleCtx, resource.GetKind(), verb, additionalVerbs...)
|
|
}
|
|
|
|
// CheckAccessToRule will ensure that the user has access to the given verbs for the given [services.Context] and kind.
|
|
// Prefer to use [Context.CheckAccessToKind] or [Context.CheckAccessToResource] for common checks.
|
|
func (c *Context) CheckAccessToRule(ruleCtx *services.Context, kind string, verb string, additionalVerbs ...string) error {
|
|
var errs []error
|
|
for _, verb := range append(additionalVerbs, verb) {
|
|
if err := c.Checker.CheckAccessToRule(ruleCtx, defaults.Namespace, kind, verb); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
return trace.NewAggregate(errs...)
|
|
}
|
|
|
|
// AuthorizeAdminAction will ensure that the user is authorized to perform admin actions.
|
|
// MFA challenges that allow reuse will not be accepted.
|
|
//
|
|
// In the majority of cases, allowing reuse is ok and can result in better UX. Forbidding
|
|
// reuse should be reserved for critical actions (e.g. CA rotation, cert generation) and
|
|
// other actions that are not expected to be performed in bulk (e.g. access request reviews).
|
|
//
|
|
// See https://github.com/gravitational/teleport/blob/master/rfd/0155-scoped-webauthn-credentials.md#when-to-extend-reuse
|
|
func (c *Context) AuthorizeAdminAction() error {
|
|
switch c.AdminActionAuthState {
|
|
case AdminActionAuthMFAVerified, AdminActionAuthNotRequired:
|
|
return nil
|
|
}
|
|
return trace.Wrap(&mfa.ErrAdminActionMFARequired)
|
|
}
|
|
|
|
// AuthorizeAdminActionAllowReusedMFA will ensure that the user is authorized to perform
|
|
// admin actions. MFA challenges that allow reuse will be accepted.
|
|
//
|
|
// In the majority of cases, allowing reuse is ok and can result in better UX. Forbidding
|
|
// reuse should be reserved for critical actions (e.g. CA rotation, cert generation) and
|
|
// other actions that are not expected to be performed in bulk (e.g. access request reviews).
|
|
//
|
|
// See https://github.com/gravitational/teleport/blob/master/rfd/0155-scoped-webauthn-credentials.md#when-to-extend-reuse
|
|
func (c *Context) AuthorizeAdminActionAllowReusedMFA() error {
|
|
if c.AdminActionAuthState == AdminActionAuthMFAVerifiedWithReuse {
|
|
return nil
|
|
}
|
|
return c.AuthorizeAdminAction()
|
|
}
|
|
|
|
// LocalUser is a local user
|
|
type LocalUser struct {
|
|
// Username is local username
|
|
Username string
|
|
// Identity is x509-derived identity used to build this user
|
|
Identity tlsca.Identity
|
|
}
|
|
|
|
// GetIdentity returns client identity
|
|
func (l LocalUser) GetIdentity() tlsca.Identity {
|
|
return l.Identity
|
|
}
|
|
|
|
// IdentityGetter returns the unmapped client identity.
|
|
//
|
|
// Unmapped means that if the client is a remote cluster user, the returned
|
|
// tlsca.Identity contains data from the remote cluster before role mapping is
|
|
// applied.
|
|
type IdentityGetter interface {
|
|
// GetIdentity returns x509-derived identity of the user
|
|
GetIdentity() tlsca.Identity
|
|
}
|
|
|
|
// WrapIdentity wraps identity to return identity getter function
|
|
type WrapIdentity tlsca.Identity
|
|
|
|
// GetIdentity returns identity
|
|
func (i WrapIdentity) GetIdentity() tlsca.Identity {
|
|
return tlsca.Identity(i)
|
|
}
|
|
|
|
// UnauthenticatedRole is the role given to a client that doesn't present
|
|
// a certificate.
|
|
// It's used for actions that are already using external authz mechanisms
|
|
// e.g. tokens or passwords
|
|
type UnauthenticatedRole struct {
|
|
// Role is the primary role this username is associated with
|
|
Role types.UnauthenticatedRole
|
|
|
|
// Username is for authentication tracking purposes
|
|
Username string
|
|
|
|
// ClusterName is the name of the local cluster
|
|
ClusterName string
|
|
}
|
|
|
|
// GetIdentity returns client identity
|
|
func (r UnauthenticatedRole) GetIdentity() tlsca.Identity {
|
|
return tlsca.Identity{}
|
|
}
|
|
|
|
// BuiltinRole is the role of the Teleport service.
|
|
type BuiltinRole struct {
|
|
// Role is the primary builtin role this username is associated with
|
|
Role types.SystemRole
|
|
|
|
// AdditionalSystemRoles is a collection of additional system roles held by
|
|
// this identity (only currently used by identities with RoleInstance as their
|
|
// primary role).
|
|
AdditionalSystemRoles types.SystemRoles
|
|
|
|
// Username is for authentication tracking purposes
|
|
Username string
|
|
|
|
// ClusterName is the name of the local cluster
|
|
ClusterName string
|
|
|
|
// Identity is source x509 used to build this role
|
|
Identity tlsca.Identity
|
|
}
|
|
|
|
// IsServer returns true if the primary role is either RoleInstance, or one of
|
|
// the local service roles (e.g. proxy).
|
|
func (r BuiltinRole) IsServer() bool {
|
|
return r.Role == types.RoleInstance || r.Role.IsLocalService()
|
|
}
|
|
|
|
// GetServerID extracts the identity from the full name. The username
|
|
// extracted from the node's identity (x.509 certificate) is expected to
|
|
// consist of "<server-id>.<cluster-name>" so strip the cluster name suffix
|
|
// to get the server id.
|
|
//
|
|
// Note that as of right now Teleport expects server id to be a UUID4 but
|
|
// older Gravity clusters used to override it with strings like
|
|
// "192_168_1_1.<cluster-name>" so this code can't rely on it being
|
|
// UUID4 to account for clusters upgraded from older versions.
|
|
func (r BuiltinRole) GetServerID() string {
|
|
return strings.TrimSuffix(r.Identity.Username, "."+r.ClusterName)
|
|
}
|
|
|
|
// GetIdentity returns client identity
|
|
func (r BuiltinRole) GetIdentity() tlsca.Identity {
|
|
return r.Identity
|
|
}
|
|
|
|
// ScopedBuiltinRole is the role of a scoped Teleport service — one whose access is constrained to a specific
|
|
// scope via a scope pin. It is intentionally a distinct type from [BuiltinRole] so that code paths
|
|
// handling builtin roles must explicitly opt into supporting scoped agents.
|
|
type ScopedBuiltinRole struct {
|
|
// ScopePin is the agent scope pin, encoding the target scope and the agent's system roles.
|
|
ScopePin *scopesv1.Pin
|
|
|
|
// ServerFQDN is the host FQDN of the agent, of the form <host-uuid>.<cluster-name>.
|
|
ServerFQDN string
|
|
|
|
// ClusterName is the name of the local cluster.
|
|
ClusterName string
|
|
|
|
// Identity is source x509 used to build this role.
|
|
Identity tlsca.Identity
|
|
}
|
|
|
|
// GetServerID extracts the server UUID from the agent's ServerFQDN.
|
|
func (r ScopedBuiltinRole) GetServerID() string {
|
|
return strings.TrimSuffix(r.ServerFQDN, "."+r.ClusterName)
|
|
}
|
|
|
|
// GetIdentity returns the client identity.
|
|
func (r ScopedBuiltinRole) GetIdentity() tlsca.Identity {
|
|
return r.Identity
|
|
}
|
|
|
|
// RemoteBuiltinRole is the role of the remote (service connecting via trusted cluster link)
|
|
// Teleport service.
|
|
type RemoteBuiltinRole struct {
|
|
// Role is the builtin role of the user
|
|
Role types.SystemRole
|
|
|
|
// Username is for authentication tracking purposes
|
|
Username string
|
|
|
|
// ClusterName is the name of the remote cluster.
|
|
ClusterName string
|
|
|
|
// Identity is source x509 used to build this role
|
|
Identity tlsca.Identity
|
|
}
|
|
|
|
// GetIdentity returns client identity
|
|
func (r RemoteBuiltinRole) GetIdentity() tlsca.Identity {
|
|
return r.Identity
|
|
}
|
|
|
|
// IsRemoteServer returns true if the primary role is either RoleRemoteProxy, or one of
|
|
// the local service roles (e.g. proxy) from the remote cluster.
|
|
func (r RemoteBuiltinRole) IsRemoteServer() bool {
|
|
return r.Role == types.RoleInstance || r.Role == types.RoleRemoteProxy || r.Role.IsLocalService()
|
|
}
|
|
|
|
// RemoteUser defines encoded remote user.
|
|
type RemoteUser struct {
|
|
// Username is a name of the remote user
|
|
Username string `json:"username"`
|
|
|
|
// ClusterName is the name of the remote cluster
|
|
// of the user.
|
|
ClusterName string `json:"cluster_name"`
|
|
|
|
// RemoteRoles is optional list of remote roles
|
|
RemoteRoles []string `json:"remote_roles"`
|
|
|
|
// Principals is a list of Unix logins.
|
|
Principals []string `json:"principals"`
|
|
|
|
// KubernetesGroups is a list of Kubernetes groups
|
|
KubernetesGroups []string `json:"kubernetes_groups"`
|
|
|
|
// KubernetesUsers is a list of Kubernetes users
|
|
KubernetesUsers []string `json:"kubernetes_users"`
|
|
|
|
// DatabaseNames is a list of database names a user can connect to.
|
|
DatabaseNames []string `json:"database_names"`
|
|
|
|
// DatabaseUsers is a list of database users a user can connect as.
|
|
DatabaseUsers []string `json:"database_users"`
|
|
|
|
// Identity is source x509 used to build this role
|
|
Identity tlsca.Identity
|
|
}
|
|
|
|
// GetIdentity returns client identity
|
|
func (r RemoteUser) GetIdentity() tlsca.Identity {
|
|
return r.Identity
|
|
}
|
|
|
|
// ContextWithUserCertificate returns the context with the user certificate embedded.
|
|
func ContextWithUserCertificate(ctx context.Context, cert *x509.Certificate) context.Context {
|
|
return context.WithValue(ctx, contextUserCertificate, cert)
|
|
}
|
|
|
|
// UserCertificateFromContext returns the user certificate from the context.
|
|
func UserCertificateFromContext(ctx context.Context) (*x509.Certificate, error) {
|
|
if ctx == nil {
|
|
return nil, trace.BadParameter("context is nil")
|
|
}
|
|
cert, ok := ctx.Value(contextUserCertificate).(*x509.Certificate)
|
|
if !ok {
|
|
return nil, trace.BadParameter("user certificate was not found in the context")
|
|
}
|
|
return cert, nil
|
|
}
|
|
|
|
// ContextWithClientSrcAddr returns the context with the address embedded.
|
|
func ContextWithClientSrcAddr(ctx context.Context, addr net.Addr) context.Context {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
return context.WithValue(ctx, contextClientSrcAddr, addr)
|
|
}
|
|
|
|
// ClientSrcAddrFromContext returns the client address from the context.
|
|
func ClientSrcAddrFromContext(ctx context.Context) (net.Addr, error) {
|
|
if ctx == nil {
|
|
return nil, trace.BadParameter("context is nil")
|
|
}
|
|
addr, ok := ctx.Value(contextClientSrcAddr).(net.Addr)
|
|
if !ok {
|
|
return nil, trace.BadParameter("client source address was not found in the context")
|
|
}
|
|
return addr, nil
|
|
}
|
|
|
|
// ContextWithClientAddrs returns the context with the client source and destination addresses embedded.
|
|
func ContextWithClientAddrs(ctx context.Context, src, dst net.Addr) context.Context {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
ctx = context.WithValue(ctx, contextClientSrcAddr, src)
|
|
return context.WithValue(ctx, contextClientDstAddr, dst)
|
|
}
|
|
|
|
// ClientAddrsFromContext returns the client address from the context.
|
|
func ClientAddrsFromContext(ctx context.Context) (src net.Addr, dst net.Addr) {
|
|
if ctx == nil {
|
|
return nil, nil
|
|
}
|
|
src, _ = ctx.Value(contextClientSrcAddr).(net.Addr)
|
|
dst, _ = ctx.Value(contextClientDstAddr).(net.Addr)
|
|
return
|
|
}
|
|
|
|
func ContextWithConn(ctx context.Context, conn net.Conn) context.Context {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
return context.WithValue(ctx, contextConn, conn)
|
|
}
|
|
|
|
func ConnFromContext(ctx context.Context) (net.Conn, error) {
|
|
if ctx == nil {
|
|
return nil, trace.BadParameter("context is nil")
|
|
}
|
|
conn, ok := ctx.Value(contextConn).(net.Conn)
|
|
if !ok {
|
|
return nil, trace.NotFound("connection was not found in the context")
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// ContextWithUser returns the context with the user embedded.
|
|
func ContextWithUser(ctx context.Context, user IdentityGetter) context.Context {
|
|
return context.WithValue(ctx, contextUser, user)
|
|
}
|
|
|
|
// UserFromContext returns the user from the context.
|
|
func UserFromContext(ctx context.Context) (IdentityGetter, error) {
|
|
if ctx == nil {
|
|
return nil, trace.BadParameter("context is nil")
|
|
}
|
|
user, ok := ctx.Value(contextUser).(IdentityGetter)
|
|
if !ok {
|
|
return nil, trace.BadParameter("user identity was not found in the context")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// HasBuiltinRole checks if the identity is a builtin role with the matching
|
|
// name.
|
|
func HasBuiltinRole(authContext Context, name string) bool {
|
|
if _, ok := authContext.Identity.(BuiltinRole); !ok {
|
|
return false
|
|
}
|
|
if !authContext.Checker.HasRole(name) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// HasUnauthenticatedRole checks if the identity is a unauthenticated role with the matching
|
|
// name.
|
|
func HasUnauthenticatedRole(authContext Context, name string) bool {
|
|
if _, ok := authContext.Identity.(UnauthenticatedRole); !ok {
|
|
return false
|
|
}
|
|
if !authContext.Checker.HasRole(name) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsLocalUser checks if the identity is a local user.
|
|
func IsLocalUser(authContext Context) bool {
|
|
_, ok := authContext.UnmappedIdentity.(LocalUser)
|
|
return ok
|
|
}
|
|
|
|
// IsLocalOrRemoteUser checks if the identity is either a local or remote user.
|
|
func IsLocalOrRemoteUser(authContext Context) bool {
|
|
switch authContext.UnmappedIdentity.(type) {
|
|
case LocalUser, RemoteUser:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsLocalOrRemoteService checks if the identity is either a local or remote service.
|
|
func IsLocalOrRemoteService(authContext Context) bool {
|
|
switch authContext.UnmappedIdentity.(type) {
|
|
case BuiltinRole, RemoteBuiltinRole:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsCurrentUser checks if the identity is a local user matching the given username
|
|
func IsCurrentUser(authContext Context, username string) bool {
|
|
return IsLocalUser(authContext) && authContext.User.GetName() == username
|
|
}
|
|
|
|
// ScopedIsCurrentUser checks if the scoped identity is a local user matching the given username.
|
|
func ScopedIsCurrentUser(scopedContext *ScopedContext, username string) bool {
|
|
_, isLocal := scopedContext.Identity.(LocalUser)
|
|
return isLocal && scopedContext.User.GetName() == username
|
|
}
|
|
|
|
// IsRemoteUser checks if the identity is a remote user.
|
|
func IsRemoteUser(authContext Context) bool {
|
|
_, ok := authContext.UnmappedIdentity.(RemoteUser)
|
|
return ok
|
|
}
|
|
|
|
// ConnectionMetadata returns a ConnectionMetadata suitable for events caused by
|
|
// a remote client making a call. If ctx didn't pass through auth middleware or
|
|
// did not come from an HTTP request, empty metadata is returned.
|
|
func ConnectionMetadata(ctx context.Context) apievents.ConnectionMetadata {
|
|
remoteAddr, err := ClientSrcAddrFromContext(ctx)
|
|
if err != nil {
|
|
return apievents.ConnectionMetadata{}
|
|
}
|
|
|
|
return apievents.ConnectionMetadata{
|
|
RemoteAddr: remoteAddr.String(),
|
|
}
|
|
}
|