Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moderated Sessions improvements #10991

Merged
merged 39 commits into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3ba7872
remove empty lines bug
xacrimon Mar 4, 2022
cd09adc
update ref doc
xacrimon Mar 4, 2022
73bbdcb
set auth field
xacrimon Mar 2, 2022
2d6d788
add test
xacrimon Mar 5, 2022
9277530
print wait message
xacrimon Mar 8, 2022
e11841a
fix launch message
xacrimon Mar 9, 2022
fd28730
only broadcast when moderated
xacrimon Mar 9, 2022
39948d2
k8s broadcast tty filtering
xacrimon Mar 9, 2022
5243d14
cancel notifier in termmanager
xacrimon Mar 9, 2022
d347e32
respect term request
xacrimon Mar 9, 2022
12b176c
simplify
xacrimon Mar 9, 2022
a41d8ba
rev
xacrimon Mar 9, 2022
7bec12f
add pretty printing requirements list and fix rbac bug being too rest…
xacrimon Mar 9, 2022
e5b2960
add godoc
xacrimon Mar 9, 2022
89ebbdc
improve ismoderated perf
xacrimon Mar 10, 2022
34f3bc7
add verbose requirement printing support to session cores
xacrimon Mar 10, 2022
ee1ddb2
improve printing
xacrimon Mar 10, 2022
49dc0ef
added flag transport for ssh
xacrimon Mar 10, 2022
7c813c1
added transport flag for k8s
xacrimon Mar 10, 2022
e96e467
add ssh flag propagation
xacrimon Mar 10, 2022
e73609a
fixes + test
xacrimon Mar 10, 2022
9fd3785
use eventually
xacrimon Mar 10, 2022
833a0c8
fix broadcast error log
xacrimon Mar 10, 2022
e797c54
fix
xacrimon Mar 10, 2022
9609959
fix double close panic
xacrimon Mar 10, 2022
b3ca7ae
fix feedback
xacrimon Mar 10, 2022
1900815
revert changelog
xacrimon Mar 10, 2022
b3c1554
feedback
xacrimon Mar 10, 2022
b0d01f4
proxy record woes
xacrimon Mar 10, 2022
f4722fc
proxy record early error
xacrimon Mar 10, 2022
f3be0d3
cleanup
xacrimon Mar 10, 2022
891b06f
correct name
xacrimon Mar 10, 2022
efe2349
atomically close channel only once
xacrimon Mar 10, 2022
24bff36
Revert "set auth field"
xacrimon Mar 10, 2022
0ef3d04
fix test
xacrimon Mar 10, 2022
c39596f
Merge branch 'master' into joel/mod-session-fix
xacrimon Mar 10, 2022
318947f
ux fix
xacrimon Mar 10, 2022
68954e1
nlp
xacrimon Mar 10, 2022
81e6d5a
Merge branch 'master' into joel/mod-session-fix
xacrimon Mar 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,10 @@ const (

// EnvSSHSessionInvited is an environment variable listning people invited to a session.
EnvSSHSessionInvited = "TELEPORT_SESSION_JOIN_MODE"

// EnvSSHSessionDisplayParticipantRequirements is set to true or false to indicate if participant
// requirement information should be printed.
EnvSSHSessionDisplayParticipantRequirements = "TELEPORT_SESSION_PARTICIPANT_REQUIREMENTS"
)

const (
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/access-controls/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,12 @@ that are more appropriately scoped.

### Role versions

There are currently two supported role versions: `v3` and `v5`. `v5` roles are
There are currently three supported role versions: `v3`, `v4` and `v5`. `v4` and `v5` roles are
completely backwards-compatible with `v3`, the only difference lies in the
default allow labels which will be applied to the role if they are not
explicitly set.
explicitly set. Additionally, `v5` is required to use [Moderated Sessions](./guides/moderated-sessions.mdx).

Label | `v3` Default | `v5` Default
Label | `v3` Default | `v4` and `v5` Default
------------------ | -------------- | ---------------
`node_labels` | `[{"*": "*"}]` if the role has any logins, else `[]` | `[]`
`app_labels` | `[{"*": "*"}]` | `[]`
Expand Down
33 changes: 33 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func TestIntegrations(t *testing.T) {
t.Run("TwoClustersTunnel", suite.bind(testTwoClustersTunnel))
t.Run("UUIDBasedProxy", suite.bind(testUUIDBasedProxy))
t.Run("WindowChange", suite.bind(testWindowChange))
t.Run("SSHTracker", suite.bind(testSSHTracker))
}

// testAuditOn creates a live session, records a bunch of data through it
Expand Down Expand Up @@ -789,6 +790,38 @@ func testUUIDBasedProxy(t *testing.T, suite *integrationTestSuite) {
require.NoError(t, err)
}

// testSSHTracker verifies that an SSH session creates a tracker for sessions.
func testSSHTracker(t *testing.T, suite *integrationTestSuite) {
ctx := context.Background()
teleport := suite.newTeleport(t, nil, true)
defer teleport.StopAll()

site := teleport.GetSiteAPI(Site)
require.NotNil(t, site)

personA := NewTerminal(250)
cl, err := teleport.NewClient(ClientConfig{
Login: suite.me.Username,
Cluster: Site,
Host: Host,
})
require.NoError(t, err)
cl.Stdout = personA
cl.Stdin = personA
personA.Type("\aecho hi\n\r")
go cl.SSH(ctx, []string{}, false)

condition := func() bool {
// verify that the tracker was created
trackers, err := site.GetActiveSessionTrackers(ctx)
require.NoError(t, err)
return len(trackers) == 1
}

// wait for the tracker to be created
require.Eventually(t, condition, time.Minute, time.Millisecond*100)
}

// testInteractive covers SSH into shell and joining the same session from another client
// against a standard teleport node.
func testInteractiveRegular(t *testing.T, suite *integrationTestSuite) {
Expand Down
66 changes: 58 additions & 8 deletions lib/auth/session_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package auth

import (
"fmt"
"regexp"
"strings"

Expand All @@ -36,17 +37,27 @@ import (
// 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
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 {
return SessionAccessEvaluator{
kind,
policySets,
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 {
Expand Down Expand Up @@ -102,8 +113,13 @@ 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(require.Kinds) || !e.matchesKind(allow.Kinds) {
if !e.matchesKind(allow.Kinds) {
return false, nil
}

Expand Down Expand Up @@ -209,6 +225,39 @@ func (e *SessionAccessEvaluator) hasPolicies() bool {
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()
Expand All @@ -227,13 +276,14 @@ func (e *SessionAccessEvaluator) FulfilledFor(participants []SessionAccessContex
// We need every policy set to match to allow the session.
policySetLoop:
for _, policySet := range e.policySets {
if len(policySet.RequireSessionJoin) == 0 {
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 policySet.RequireSessionJoin {
for _, requirePolicy := range policies {
// Count of how many additional participant matches we need to fulfill the policy.
left := requirePolicy.Count

Expand Down
20 changes: 20 additions & 0 deletions lib/auth/session_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ func failCountStartTestCase(t *testing.T) startTestCase {
}
}

func succeedDiscardPolicySetStartTestCase(t *testing.T) startTestCase {
hostRole, err := types.NewRole("host", types.RoleSpecV5{})
require.NoError(t, err)

hostRole.SetSessionRequirePolicies([]*types.SessionRequirePolicy{{
Filter: "contains(user.roles, \"host\")",
Kinds: []string{string(types.KubernetesSessionKind)},
Count: 2,
Modes: []string{"peer"},
}})

return startTestCase{
name: "succeedDiscardPolicySet",
host: hostRole,
sessionKind: types.SSHSessionKind,
expected: true,
}
}

func failFilterStartTestCase(t *testing.T) startTestCase {
hostRole, err := types.NewRole("host", types.RoleSpecV5{})
require.NoError(t, err)
Expand Down Expand Up @@ -154,6 +173,7 @@ func TestSessionAccessStart(t *testing.T) {
successStartTestCase(t),
failCountStartTestCase(t),
failFilterStartTestCase(t),
succeedDiscardPolicySetStartTestCase(t),
}

for _, testCase := range testCases {
Expand Down
6 changes: 5 additions & 1 deletion lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ type Config struct {

// Invited is a list of people invited to a session.
Invited []string

// DisplayParticipantRequirements is set if debug information about participants requirements
// should be printed in moderated sessions.
DisplayParticipantRequirements bool
}

// CachePolicy defines cache policy for local clients
Expand Down Expand Up @@ -2106,7 +2110,7 @@ func (tc *TeleportClient) runShell(ctx context.Context, nodeClient *NodeClient,
env := make(map[string]string)
env[teleport.EnvSSHJoinMode] = string(mode)
env[teleport.EnvSSHSessionReason] = tc.Config.Reason

env[teleport.EnvSSHSessionDisplayParticipantRequirements] = strconv.FormatBool(tc.Config.DisplayParticipantRequirements)
encoded, err := json.Marshal(&tc.Config.Invited)
if err != nil {
return trace.Wrap(err)
Expand Down
Loading