mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
372 lines
11 KiB
Go
372 lines
11 KiB
Go
/*
|
|
Copyright 2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/trace"
|
|
"github.com/vulcand/predicate"
|
|
)
|
|
|
|
// SessionAccessEvaluator takes a set of policies
|
|
// and uses rules to evaluate them to determine when a session may start
|
|
// and if a user can join a session.
|
|
//
|
|
// The current implementation is very simple and uses a brute-force algorithm.
|
|
// More efficient implementations that run in non O(n^2)-ish time are possible but require complex code
|
|
// that is harder to debug in the case of misconfigured policies or other error and are harder to intuitively follow.
|
|
// In the real world, the number of roles and session are small enough that this doesn't have a meaningful impact.
|
|
type SessionAccessEvaluator struct {
|
|
kind types.SessionKind
|
|
policySets []*types.SessionTrackerPolicySet
|
|
isModerated bool
|
|
}
|
|
|
|
// NewSessionAccessEvaluator creates a new session access evaluator for a given session kind
|
|
// and a set of roles attached to the host user.
|
|
func NewSessionAccessEvaluator(policySets []*types.SessionTrackerPolicySet, kind types.SessionKind) SessionAccessEvaluator {
|
|
e := SessionAccessEvaluator{
|
|
kind: kind,
|
|
policySets: policySets,
|
|
}
|
|
|
|
for _, policySet := range policySets {
|
|
if len(e.extractApplicablePolicies(policySet)) != 0 {
|
|
e.isModerated = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func getAllowPolicies(participant SessionAccessContext) []*types.SessionJoinPolicy {
|
|
var policies []*types.SessionJoinPolicy
|
|
|
|
for _, role := range participant.Roles {
|
|
policies = append(policies, role.GetSessionJoinPolicies()...)
|
|
}
|
|
|
|
return policies
|
|
}
|
|
|
|
func containsKind(s []string, e types.SessionKind) bool {
|
|
for _, a := range s {
|
|
if types.SessionKind(a) == e {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// SessionAccessContext is the context that must be provided per participant in the session.
|
|
type SessionAccessContext struct {
|
|
Username string
|
|
Roles []types.Role
|
|
Mode types.SessionParticipantMode
|
|
}
|
|
|
|
// GetIdentifier is used by the `predicate` library to evaluate variable expressions when
|
|
// evaluating policy filters. It deals with evaluating strings like `participant.name` to the appropriate value.
|
|
func (ctx *SessionAccessContext) GetIdentifier(fields []string) (interface{}, error) {
|
|
if fields[0] == "user" {
|
|
if len(fields) == 2 || len(fields) == 3 {
|
|
switch fields[1] {
|
|
case "name":
|
|
return ctx.Username, nil
|
|
case "roles":
|
|
var roles []string
|
|
for _, role := range ctx.Roles {
|
|
roles = append(roles, role.GetName())
|
|
}
|
|
|
|
return roles, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, trace.NotFound("%v is not defined", strings.Join(fields, "."))
|
|
}
|
|
|
|
func (ctx *SessionAccessContext) GetResource() (types.Resource, error) {
|
|
return nil, trace.BadParameter("resource unsupported")
|
|
}
|
|
|
|
// IsModerated returns true if the session needs moderation.
|
|
func (e *SessionAccessEvaluator) IsModerated() bool {
|
|
return e.isModerated
|
|
}
|
|
|
|
func (e *SessionAccessEvaluator) matchesPredicate(ctx *SessionAccessContext, require *types.SessionRequirePolicy, allow *types.SessionJoinPolicy) (bool, error) {
|
|
if !e.matchesKind(allow.Kinds) {
|
|
return false, nil
|
|
}
|
|
|
|
parser, err := services.NewWhereParser(ctx)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
ifn, err := parser.Parse(require.Filter)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
fn, ok := ifn.(predicate.BoolPredicate)
|
|
if !ok {
|
|
return false, trace.BadParameter("unsupported type: %T", ifn)
|
|
}
|
|
|
|
return fn(), nil
|
|
}
|
|
|
|
func (e *SessionAccessEvaluator) matchesJoin(allow *types.SessionJoinPolicy) bool {
|
|
if !e.matchesKind(allow.Kinds) {
|
|
return false
|
|
}
|
|
|
|
for _, policySet := range e.policySets {
|
|
for _, allowRole := range allow.Roles {
|
|
expr := utils.GlobToRegexp(policySet.Name)
|
|
// GlobToRegexp makes sure this is always a valid regexp.
|
|
matched, _ := regexp.MatchString(expr, allowRole)
|
|
|
|
if matched {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (e *SessionAccessEvaluator) matchesKind(allow []string) bool {
|
|
if containsKind(allow, e.kind) || containsKind(allow, "*") {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// CanJoin returns the modes a user has access to join a session with.
|
|
// If the list is empty, the user doesn't have access to join the session at all.
|
|
func (e *SessionAccessEvaluator) CanJoin(user SessionAccessContext) ([]types.SessionParticipantMode, error) {
|
|
supported, err := e.supportsSessionAccessControls()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// If we don't support session access controls, return the default mode set that was supported prior to Moderated Sessions.
|
|
if !supported {
|
|
return preAccessControlsModes(e.kind), nil
|
|
}
|
|
|
|
var modes []types.SessionParticipantMode
|
|
|
|
// Loop over every allow policy attached the participant and check it's applicability.
|
|
// This code serves to merge the permissions of all applicable join policies.
|
|
for _, allowPolicy := range getAllowPolicies(user) {
|
|
// If the policy is applicable and allows joining the session, add the allowed modes to the list of modes.
|
|
if e.matchesJoin(allowPolicy) {
|
|
for _, modeString := range allowPolicy.Modes {
|
|
mode := types.SessionParticipantMode(modeString)
|
|
if !SliceContainsMode(modes, mode) {
|
|
modes = append(modes, mode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return modes, nil
|
|
}
|
|
|
|
func SliceContainsMode(s []types.SessionParticipantMode, e types.SessionParticipantMode) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// PolicyOptions is a set of settings for the session determined by the matched require policy.
|
|
type PolicyOptions struct {
|
|
TerminateOnLeave bool
|
|
}
|
|
|
|
func (e *SessionAccessEvaluator) hasPolicies() bool {
|
|
for _, policySet := range e.policySets {
|
|
if len(policySet.RequireSessionJoin) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Generate a pretty-printed string of precise requirements for session start suitable for user display.
|
|
func (e *SessionAccessEvaluator) PrettyRequirementsList() string {
|
|
s := new(strings.Builder)
|
|
s.WriteString("require all:")
|
|
|
|
for _, policySet := range e.policySets {
|
|
policies := e.extractApplicablePolicies(policySet)
|
|
if len(policies) == 0 {
|
|
continue
|
|
}
|
|
|
|
fmt.Fprintf(s, "\r\n one of (%v):", policySet.Name)
|
|
for _, require := range policies {
|
|
fmt.Fprintf(s, "\r\n - %vx %v with mode %v", require.Count, require.Filter, strings.Join(require.Modes, " or "))
|
|
}
|
|
}
|
|
|
|
return s.String()
|
|
}
|
|
|
|
// extractApplicablePolicies extracts all policies that match the session kind.
|
|
func (e *SessionAccessEvaluator) extractApplicablePolicies(set *types.SessionTrackerPolicySet) []*types.SessionRequirePolicy {
|
|
var policies []*types.SessionRequirePolicy
|
|
|
|
for _, require := range set.RequireSessionJoin {
|
|
if e.matchesKind(require.Kinds) {
|
|
policies = append(policies, require)
|
|
}
|
|
}
|
|
|
|
return policies
|
|
}
|
|
|
|
// FulfilledFor checks if a given session may run with a list of participants.
|
|
func (e *SessionAccessEvaluator) FulfilledFor(participants []SessionAccessContext) (bool, PolicyOptions, error) {
|
|
supported, err := e.supportsSessionAccessControls()
|
|
if err != nil {
|
|
return false, PolicyOptions{}, trace.Wrap(err)
|
|
}
|
|
|
|
// If advanced access controls are supported or no require policies are defined, we allow by default.
|
|
if !e.hasPolicies() || !supported {
|
|
return true, PolicyOptions{TerminateOnLeave: true}, nil
|
|
}
|
|
|
|
options := PolicyOptions{TerminateOnLeave: true}
|
|
|
|
// Check every policy set to check if it's fulfilled.
|
|
// We need every policy set to match to allow the session.
|
|
policySetLoop:
|
|
for _, policySet := range e.policySets {
|
|
policies := e.extractApplicablePolicies(policySet)
|
|
if len(policies) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Check every require policy to see if it's fulfilled.
|
|
// Only one needs to be checked to pass the policyset.
|
|
for _, requirePolicy := range policies {
|
|
// Count of how many additional participant matches we need to fulfill the policy.
|
|
left := requirePolicy.Count
|
|
|
|
var requireModes []types.SessionParticipantMode
|
|
for _, mode := range requirePolicy.Modes {
|
|
requireModes = append(requireModes, types.SessionParticipantMode(mode))
|
|
}
|
|
|
|
// Check every participant against the policy.
|
|
for _, participant := range participants {
|
|
if !SliceContainsMode(requireModes, participant.Mode) {
|
|
continue
|
|
}
|
|
|
|
// Check the allow polices attached to the participant against the session.
|
|
allowPolicies := getAllowPolicies(participant)
|
|
for _, allowPolicy := range allowPolicies {
|
|
// Evaluate the filter in the require policy against the participant and allow policy.
|
|
matchesPredicate, err := e.matchesPredicate(&participant, requirePolicy, allowPolicy)
|
|
if err != nil {
|
|
return false, PolicyOptions{}, trace.Wrap(err)
|
|
}
|
|
|
|
// If the the filter matches the participant and the allow policy matches the session
|
|
// we conclude that the participant matches against the require policy.
|
|
if matchesPredicate && e.matchesJoin(allowPolicy) {
|
|
left--
|
|
break
|
|
}
|
|
}
|
|
|
|
// If we've matched enough participants against the require policy, we can allow the session.
|
|
if left <= 0 {
|
|
switch requirePolicy.OnLeave {
|
|
case types.OnSessionLeaveTerminate:
|
|
case types.OnSessionLeavePause:
|
|
options.TerminateOnLeave = false
|
|
default:
|
|
}
|
|
|
|
// We matched at least one require policy within the set. Continue ahead.
|
|
continue policySetLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
// We failed to match against any require policy and thus the set.
|
|
// Thus, we can't allow the session.
|
|
return false, options, nil
|
|
}
|
|
|
|
// All policy sets matched, we can allow the session.
|
|
return true, options, nil
|
|
}
|
|
|
|
// supportsSessionAccessControls checks if moderated sessions-style access controls can be applied to the session.
|
|
// If a set only has v4 or earlier roles, we don't want to apply the access checks to SSH sessions.
|
|
//
|
|
// This only applies to SSH sessions since they previously had no access control for joining sessions.
|
|
// We don't need this fallback behaviour for multiparty kubernetes since it's a new feature.
|
|
func (e *SessionAccessEvaluator) supportsSessionAccessControls() (bool, error) {
|
|
if e.kind == types.SSHSessionKind {
|
|
for _, policySet := range e.policySets {
|
|
switch policySet.Version {
|
|
case types.V1, types.V2, types.V3, types.V4:
|
|
return false, nil
|
|
case types.V5:
|
|
return true, nil
|
|
default:
|
|
return false, trace.BadParameter("unsupported role version: %v", policySet.Version)
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func preAccessControlsModes(kind types.SessionKind) []types.SessionParticipantMode {
|
|
switch kind {
|
|
case types.SSHSessionKind:
|
|
return []types.SessionParticipantMode{types.SessionPeerMode}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|