mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 01:03:40 +00:00
d4b3afe9a1
* split recording session events and emitting audit events This is a refactor of how audit events and session events are handled. Previously, all events were emitted using the same interface, api/types/events.Emitter. This lead to event-related code getting to be very confusing, as it was often unclear whether a given event was being recorded as a session event and emitted as an audit event, or only one of the two. Naturally, a few bugs arose due to this. To simplify event handling, a separate interface for recording session events has been created. A api/types/events.Recorder should now only be used to record session events, and an Emitter should now only be used to emit audit events. Instead of using a confusing TeeWriter that would transparently (and confusingly, given its name) hold a few event types that only belonged in session recordings, callers can now explicitly record and/or emit an event when necessary. * ensure e build won't break
496 lines
15 KiB
Go
496 lines
15 KiB
Go
// Copyright 2022 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 srv
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"github.com/gravitational/teleport/lib/events/eventstest"
|
|
"github.com/gravitational/teleport/lib/modules"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
)
|
|
|
|
type mockLockEnforcer struct {
|
|
lockInForceErr error
|
|
}
|
|
|
|
func (m mockLockEnforcer) CheckLockInForce(constants.LockingMode, ...types.LockTarget) error {
|
|
return m.lockInForceErr
|
|
}
|
|
|
|
type mockAccessPoint struct {
|
|
AccessPoint
|
|
|
|
authPreference types.AuthPreference
|
|
clusterName types.ClusterName
|
|
netConfig types.ClusterNetworkingConfig
|
|
}
|
|
|
|
func (m mockAccessPoint) GetAuthPreference(ctx context.Context) (types.AuthPreference, error) {
|
|
return m.authPreference, nil
|
|
}
|
|
|
|
func (m mockAccessPoint) GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error) {
|
|
return m.clusterName, nil
|
|
}
|
|
|
|
func (m mockAccessPoint) GetClusterNetworkingConfig(ctx context.Context, opts ...services.MarshalOption) (types.ClusterNetworkingConfig, error) {
|
|
return m.netConfig, nil
|
|
}
|
|
|
|
type mockSemaphores struct {
|
|
types.Semaphores
|
|
|
|
lease *types.SemaphoreLease
|
|
acquireErr error
|
|
}
|
|
|
|
func (m mockSemaphores) AcquireSemaphore(ctx context.Context, params types.AcquireSemaphoreRequest) (*types.SemaphoreLease, error) {
|
|
return m.lease, m.acquireErr
|
|
}
|
|
|
|
func (m mockSemaphores) CancelSemaphoreLease(ctx context.Context, lease types.SemaphoreLease) error {
|
|
return nil
|
|
}
|
|
|
|
type mockAccessChecker struct {
|
|
services.AccessChecker
|
|
|
|
lockMode constants.LockingMode
|
|
maxConnections int64
|
|
keyPolicy keys.PrivateKeyPolicy
|
|
roleNames []string
|
|
}
|
|
|
|
func (m mockAccessChecker) LockingMode(defaultMode constants.LockingMode) constants.LockingMode {
|
|
return m.lockMode
|
|
}
|
|
|
|
func (m mockAccessChecker) MaxConnections() int64 {
|
|
return m.maxConnections
|
|
}
|
|
|
|
func (m mockAccessChecker) PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) keys.PrivateKeyPolicy {
|
|
return m.keyPolicy
|
|
}
|
|
|
|
func (m mockAccessChecker) RoleNames() []string {
|
|
return m.roleNames
|
|
}
|
|
|
|
func TestSessionController_AcquireSessionContext(t *testing.T) {
|
|
clock := clockwork.NewFakeClock()
|
|
emitter := &eventstest.MockRecorderEmitter{}
|
|
|
|
minimalCfg := SessionControllerConfig{
|
|
Semaphores: mockSemaphores{},
|
|
AccessPoint: mockAccessPoint{
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{},
|
|
},
|
|
clusterName: &types.ClusterNameV2{
|
|
Spec: types.ClusterNameSpecV2{
|
|
ClusterName: "llama",
|
|
},
|
|
},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentNode,
|
|
ServerID: "1234",
|
|
}
|
|
|
|
minimalIdentity := IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
KeyId: "alpaca",
|
|
},
|
|
AccessChecker: &mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyNone,
|
|
},
|
|
}
|
|
|
|
cfgWithDeviceMode := func(mode string) SessionControllerConfig {
|
|
cfg := minimalCfg
|
|
authPref, _ := cfg.AccessPoint.GetAuthPreference(context.Background())
|
|
authPref.(*types.AuthPreferenceV2).Spec.DeviceTrust = &types.DeviceTrust{
|
|
Mode: mode,
|
|
}
|
|
return cfg
|
|
}
|
|
identityWithDeviceExtensions := func() IdentityContext {
|
|
idCtx := minimalIdentity
|
|
idCtx.Certificate = &ssh.Certificate{
|
|
KeyId: "alpaca",
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionDeviceID: "deviceid1",
|
|
teleport.CertExtensionDeviceAssetTag: "assettag1",
|
|
teleport.CertExtensionDeviceCredentialID: "credentialid1",
|
|
},
|
|
},
|
|
}
|
|
return idCtx
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
buildType string // defaults to modules.BuildOSS
|
|
cfg SessionControllerConfig
|
|
identity IdentityContext
|
|
assertion func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter)
|
|
}{
|
|
{
|
|
name: "proxy: access allowed",
|
|
cfg: SessionControllerConfig{
|
|
Semaphores: mockSemaphores{},
|
|
AccessPoint: mockAccessPoint{
|
|
netConfig: &types.ClusterNetworkingConfigV2{},
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{
|
|
LockingMode: constants.LockingModeStrict,
|
|
},
|
|
},
|
|
clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentProxy,
|
|
ServerID: "1234",
|
|
},
|
|
identity: IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionPrivateKeyPolicy: string(keys.PrivateKeyPolicyNone),
|
|
},
|
|
},
|
|
},
|
|
AccessChecker: mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyNone,
|
|
maxConnections: 1,
|
|
},
|
|
},
|
|
assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ctx)
|
|
require.Empty(t, emitter.Events())
|
|
},
|
|
},
|
|
{
|
|
name: "node: access allowed",
|
|
cfg: SessionControllerConfig{
|
|
Clock: clock,
|
|
Semaphores: mockSemaphores{
|
|
lease: &types.SemaphoreLease{
|
|
SemaphoreKind: types.SemaphoreKindConnection,
|
|
SemaphoreName: "test",
|
|
LeaseID: "1",
|
|
Expires: clock.Now().Add(time.Minute),
|
|
},
|
|
},
|
|
AccessPoint: mockAccessPoint{
|
|
netConfig: &types.ClusterNetworkingConfigV2{
|
|
Spec: types.ClusterNetworkingConfigSpecV2{
|
|
SessionControlTimeout: types.NewDuration(time.Minute),
|
|
},
|
|
},
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{
|
|
LockingMode: constants.LockingModeStrict,
|
|
},
|
|
},
|
|
clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentNode,
|
|
ServerID: "1234",
|
|
},
|
|
identity: IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionPrivateKeyPolicy: string(keys.PrivateKeyPolicyNone),
|
|
},
|
|
},
|
|
},
|
|
AccessChecker: mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyNone,
|
|
maxConnections: 1,
|
|
},
|
|
},
|
|
assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ctx)
|
|
require.Empty(t, emitter.Events())
|
|
},
|
|
},
|
|
{
|
|
name: "session rejected due to lock",
|
|
cfg: SessionControllerConfig{
|
|
Clock: clock,
|
|
Semaphores: mockSemaphores{},
|
|
AccessPoint: mockAccessPoint{
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{
|
|
LockingMode: constants.LockingModeStrict,
|
|
},
|
|
},
|
|
clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{
|
|
lockInForceErr: trace.AccessDenied("lock in force"),
|
|
},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentNode,
|
|
ServerID: "1234",
|
|
},
|
|
identity: IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionPrivateKeyPolicy: string(keys.PrivateKeyPolicyNone),
|
|
},
|
|
},
|
|
},
|
|
AccessChecker: mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyNone,
|
|
maxConnections: 1,
|
|
},
|
|
},
|
|
assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) {
|
|
require.ErrorIs(t, err, trace.AccessDenied("lock in force"))
|
|
require.NotNil(t, ctx)
|
|
require.Len(t, emitter.Events(), 1)
|
|
|
|
evt, ok := emitter.Events()[0].(*apievents.SessionReject)
|
|
require.True(t, ok)
|
|
require.Equal(t, events.SessionRejectedEvent, evt.Metadata.Type)
|
|
require.Equal(t, events.SessionRejectedCode, evt.Metadata.Code)
|
|
require.Equal(t, events.EventProtocolSSH, evt.ConnectionMetadata.Protocol)
|
|
require.Equal(t, "lock in force", evt.Reason)
|
|
},
|
|
},
|
|
{
|
|
name: "session rejected due to private key policy",
|
|
cfg: SessionControllerConfig{
|
|
Clock: clock,
|
|
Semaphores: mockSemaphores{},
|
|
AccessPoint: mockAccessPoint{
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{
|
|
LockingMode: constants.LockingModeStrict,
|
|
},
|
|
},
|
|
clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentNode,
|
|
ServerID: "1234",
|
|
},
|
|
identity: IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionPrivateKeyPolicy: string(keys.PrivateKeyPolicyNone),
|
|
},
|
|
},
|
|
},
|
|
AccessChecker: mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyHardwareKey,
|
|
maxConnections: 1,
|
|
},
|
|
},
|
|
assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.NotNil(t, ctx)
|
|
require.Empty(t, emitter.Events())
|
|
},
|
|
},
|
|
{
|
|
name: "session rejected due to connection limit",
|
|
cfg: SessionControllerConfig{
|
|
Clock: clock,
|
|
Semaphores: mockSemaphores{
|
|
acquireErr: trace.LimitExceeded(teleport.MaxLeases),
|
|
},
|
|
AccessPoint: mockAccessPoint{
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{
|
|
LockingMode: constants.LockingModeStrict,
|
|
},
|
|
},
|
|
clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}},
|
|
netConfig: &types.ClusterNetworkingConfigV2{
|
|
Spec: types.ClusterNetworkingConfigSpecV2{
|
|
SessionControlTimeout: types.NewDuration(time.Minute),
|
|
},
|
|
},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentNode,
|
|
ServerID: "1234",
|
|
},
|
|
identity: IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionPrivateKeyPolicy: string(keys.PrivateKeyPolicyNone),
|
|
},
|
|
},
|
|
},
|
|
AccessChecker: mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyNone,
|
|
maxConnections: 1,
|
|
},
|
|
},
|
|
assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
require.NotNil(t, ctx)
|
|
require.Len(t, emitter.Events(), 1)
|
|
|
|
evt, ok := emitter.Events()[0].(*apievents.SessionReject)
|
|
require.True(t, ok)
|
|
require.Equal(t, events.SessionRejectedEvent, evt.Metadata.Type)
|
|
require.Equal(t, events.SessionRejectedCode, evt.Metadata.Code)
|
|
require.Equal(t, events.EventProtocolSSH, evt.ConnectionMetadata.Protocol)
|
|
require.Equal(t, events.SessionRejectedEvent, evt.Reason)
|
|
require.Equal(t, int64(1), evt.Maximum)
|
|
},
|
|
},
|
|
{
|
|
name: "no connection limits prevent acquiring semaphore lock",
|
|
cfg: SessionControllerConfig{
|
|
Clock: clock,
|
|
Semaphores: mockSemaphores{
|
|
acquireErr: trace.LimitExceeded(teleport.MaxLeases),
|
|
},
|
|
AccessPoint: mockAccessPoint{
|
|
authPreference: &types.AuthPreferenceV2{
|
|
Spec: types.AuthPreferenceSpecV2{
|
|
LockingMode: constants.LockingModeStrict,
|
|
},
|
|
},
|
|
clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}},
|
|
netConfig: &types.ClusterNetworkingConfigV2{
|
|
Spec: types.ClusterNetworkingConfigSpecV2{
|
|
SessionControlTimeout: types.NewDuration(time.Minute),
|
|
},
|
|
},
|
|
},
|
|
LockEnforcer: mockLockEnforcer{},
|
|
Emitter: emitter,
|
|
Component: teleport.ComponentNode,
|
|
ServerID: "1234",
|
|
},
|
|
identity: IdentityContext{
|
|
TeleportUser: "alpaca",
|
|
Login: "alpaca",
|
|
Certificate: &ssh.Certificate{
|
|
Permissions: ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
teleport.CertExtensionPrivateKeyPolicy: string(keys.PrivateKeyPolicyNone),
|
|
},
|
|
},
|
|
},
|
|
AccessChecker: mockAccessChecker{
|
|
keyPolicy: keys.PrivateKeyPolicyNone,
|
|
maxConnections: 0,
|
|
},
|
|
},
|
|
assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ctx)
|
|
require.Empty(t, emitter.Events(), 0)
|
|
},
|
|
},
|
|
{
|
|
name: "device extensions not enforced for OSS",
|
|
cfg: cfgWithDeviceMode(constants.DeviceTrustModeRequired),
|
|
identity: minimalIdentity,
|
|
assertion: func(t *testing.T, _ context.Context, err error, _ *eventstest.MockRecorderEmitter) {
|
|
assert.NoError(t, err, "AcquireSessionContext returned an unexpected error")
|
|
},
|
|
},
|
|
{
|
|
name: "device extensions enforced for Enterprise",
|
|
buildType: modules.BuildEnterprise,
|
|
cfg: cfgWithDeviceMode(constants.DeviceTrustModeRequired),
|
|
identity: minimalIdentity,
|
|
assertion: func(t *testing.T, _ context.Context, err error, _ *eventstest.MockRecorderEmitter) {
|
|
assert.ErrorContains(t, err, "device", "AcquireSessionContext returned an unexpected error")
|
|
assert.True(t, trace.IsAccessDenied(err), "AcquireSessionContext returned an error other than trace.AccessDeniedError: %T", err)
|
|
},
|
|
},
|
|
{
|
|
name: "device extensions valid for Enterprise",
|
|
buildType: modules.BuildEnterprise,
|
|
cfg: cfgWithDeviceMode(constants.DeviceTrustModeRequired),
|
|
identity: identityWithDeviceExtensions(),
|
|
assertion: func(t *testing.T, _ context.Context, err error, _ *eventstest.MockRecorderEmitter) {
|
|
assert.NoError(t, err, "AcquireSessionContext returned an unexpected error")
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
buildType := tt.buildType
|
|
if buildType == "" {
|
|
buildType = modules.BuildOSS
|
|
}
|
|
modules.SetTestModules(t, &modules.TestModules{
|
|
TestBuildType: buildType,
|
|
})
|
|
|
|
emitter.Reset()
|
|
ctrl, err := NewSessionController(tt.cfg)
|
|
require.NoError(t, err, "NewSessionController failed")
|
|
|
|
ctx, err := ctrl.AcquireSessionContext(context.Background(), tt.identity, "127.0.0.1:1", "127.0.0.1:2")
|
|
tt.assertion(t, ctx, err, emitter)
|
|
})
|
|
}
|
|
}
|