mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:27:24 +08:00
* Move the authorizer into its own package. The Authorizer and associated functions and constants have been moved into their own package `authz`. This will allow us to more easily create separate services for gRPC functionality so that we're not pushing as much into the `lib/auth` package. Though this PR is large, the only actual code change here is making `ContextForLocalUser` exported and changing the `permissions_test.go` so that it doesn't rely on the `lib/auth` package. * Use context functions. * Test and linting fixes. * Add in a temporary alias file to ensure e still compiles. * Use functions instead of var aliases for performance reasons. * Adding in godocs, cleaning up tests. * Fix tests. * Add a few more aliases.
1106 lines
32 KiB
Go
1106 lines
32 KiB
Go
/*
|
|
Copyright 2020 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 app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/gravitational/oxy/forward"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/teleport/api/defaults"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/native"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/authz"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"github.com/gravitational/teleport/lib/labels"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
libsession "github.com/gravitational/teleport/lib/session"
|
|
"github.com/gravitational/teleport/lib/srv/app/common"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
utils.InitLoggerForTests()
|
|
native.PrecomputeTestKeys(m)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
type Suite struct {
|
|
clock clockwork.FakeClock
|
|
dataDir string
|
|
authServer *auth.TestAuthServer
|
|
tlsServer *auth.TestTLSServer
|
|
authClient *auth.Client
|
|
appServer *Server
|
|
hostCertPool *x509.CertPool
|
|
|
|
hostUUID string
|
|
closeContext context.Context
|
|
closeFunc context.CancelFunc
|
|
message string
|
|
hostport string
|
|
testhttp *httptest.Server
|
|
clientCertificate tls.Certificate
|
|
awsConsoleCertificate tls.Certificate
|
|
|
|
user types.User
|
|
role types.Role
|
|
serverPort string
|
|
}
|
|
|
|
func (s *Suite) TearDown(t *testing.T) {
|
|
err := s.appServer.Close()
|
|
require.NoError(t, err)
|
|
|
|
err = s.authClient.Close()
|
|
require.NoError(t, err)
|
|
|
|
s.testhttp.Close()
|
|
|
|
err = s.tlsServer.Auth().DeleteAllApplicationServers(s.closeContext, defaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
s.closeFunc()
|
|
|
|
err = s.tlsServer.Close()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
type suiteConfig struct {
|
|
// ResourceMatchers are resource watcher matchers.
|
|
ResourceMatchers []services.ResourceMatcher
|
|
// OnReconcile sets app resource reconciliation callback.
|
|
OnReconcile func(types.Apps)
|
|
// Apps are the apps to configure.
|
|
Apps types.Apps
|
|
// ServerStreamer is the auth server audit events streamer.
|
|
ServerStreamer events.Streamer
|
|
// ValidateRequest is a function that will validate the request received by the application.
|
|
ValidateRequest func(*Suite, *http.Request)
|
|
// EnableHTTP2 defines if the test server will support HTTP2.
|
|
EnableHTTP2 bool
|
|
// CloudImporter will use the given cloud importer for the app server.
|
|
CloudImporter labels.Importer
|
|
// AppLabels are the labels assigned to the application.
|
|
AppLabels map[string]string
|
|
// RoleAppLabels are the labels set to allow for the user role.
|
|
RoleAppLabels types.Labels
|
|
}
|
|
|
|
func SetUpSuite(t *testing.T) *Suite {
|
|
return SetUpSuiteWithConfig(t, suiteConfig{})
|
|
}
|
|
|
|
func SetUpSuiteWithConfig(t *testing.T, config suiteConfig) *Suite {
|
|
s := &Suite{}
|
|
|
|
s.clock = clockwork.NewFakeClock()
|
|
s.dataDir = t.TempDir()
|
|
s.hostUUID = uuid.New().String()
|
|
|
|
var err error
|
|
// Create Auth Server.
|
|
s.authServer, err = auth.NewTestAuthServer(auth.TestAuthServerConfig{
|
|
ClusterName: "root.example.com",
|
|
Dir: s.dataDir,
|
|
Clock: s.clock,
|
|
Streamer: config.ServerStreamer,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { s.authServer.Close() })
|
|
|
|
if config.ServerStreamer != nil {
|
|
err = s.authServer.AuthServer.SetSessionRecordingConfig(s.closeContext, &types.SessionRecordingConfigV2{
|
|
Spec: types.SessionRecordingConfigSpecV2{Mode: types.RecordAtNodeSync},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
s.tlsServer, err = s.authServer.NewTestTLSServer()
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
s.tlsServer.Close()
|
|
})
|
|
|
|
// Set up the host cert pool.
|
|
rootCA, err := s.tlsServer.Auth().GetCertAuthority(context.Background(), types.CertAuthID{
|
|
Type: types.HostCA,
|
|
DomainName: "root.example.com",
|
|
}, false)
|
|
require.NoError(t, err)
|
|
s.hostCertPool, err = services.CertPool(rootCA)
|
|
require.NoError(t, err)
|
|
|
|
roleAppLabels := config.RoleAppLabels
|
|
// Default to something that matches the staticLabels package level variable.
|
|
if roleAppLabels == nil {
|
|
roleAppLabels = types.Labels{
|
|
"bar": []string{"baz"},
|
|
}
|
|
}
|
|
|
|
// Grant the user's role access to the application label "bar: baz".
|
|
s.role = &types.RoleV6{
|
|
Metadata: types.Metadata{
|
|
Name: "foo",
|
|
},
|
|
Spec: types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
AppLabels: roleAppLabels,
|
|
AWSRoleARNs: []string{"readonly"},
|
|
},
|
|
},
|
|
}
|
|
// Create user for regular tests.
|
|
s.user, err = auth.CreateUser(s.tlsServer.Auth(), "foo", s.role)
|
|
require.NoError(t, err)
|
|
|
|
s.closeContext, s.closeFunc = context.WithCancel(context.Background())
|
|
|
|
// Create a in-memory HTTP server that will respond with a UUID. This value
|
|
// will be checked in the client later to ensure a connection was made.
|
|
s.message = uuid.New().String()
|
|
|
|
s.testhttp = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.ToLower(r.Header.Get("upgrade")) == "websocket" {
|
|
upgrader := websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
}
|
|
ws, err := upgrader.Upgrade(w, r, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = ws.WriteMessage(websocket.TextMessage, []byte(s.message))
|
|
require.NoError(t, err)
|
|
} else {
|
|
fmt.Fprintln(w, s.message)
|
|
}
|
|
|
|
if config.ValidateRequest != nil {
|
|
config.ValidateRequest(s, r)
|
|
}
|
|
}))
|
|
// Add NextProtos to support both protocols: h2, http/1.1
|
|
s.testhttp.Config.TLSConfig = &tls.Config{Time: s.clock.Now}
|
|
if config.EnableHTTP2 {
|
|
s.testhttp.EnableHTTP2 = true
|
|
// Add NextProtos to support both protocols: h2, http/1.1
|
|
s.testhttp.Config.TLSConfig.NextProtos = []string{"h2", "http/1.1"}
|
|
s.testhttp.StartTLS()
|
|
} else {
|
|
s.testhttp.Start()
|
|
}
|
|
|
|
// Extract the hostport that the in-memory HTTP server is running on.
|
|
u, err := url.Parse(s.testhttp.URL)
|
|
require.NoError(t, err)
|
|
s.hostport = u.Host
|
|
s.serverPort = u.Port()
|
|
|
|
// Default to staticLabels.
|
|
appLabels := config.AppLabels
|
|
if appLabels == nil {
|
|
appLabels = staticLabels
|
|
}
|
|
|
|
// Create apps that will be used for each test.
|
|
appFoo, err := types.NewAppV3(types.Metadata{
|
|
Name: "foo",
|
|
Labels: appLabels,
|
|
}, types.AppSpecV3{
|
|
URI: s.testhttp.URL,
|
|
PublicAddr: "foo.example.com",
|
|
InsecureSkipVerify: true,
|
|
DynamicLabels: types.LabelsToV2(dynamicLabels),
|
|
})
|
|
require.NoError(t, err)
|
|
appAWS, err := types.NewAppV3(types.Metadata{
|
|
Name: "awsconsole",
|
|
Labels: staticLabels,
|
|
}, types.AppSpecV3{
|
|
URI: constants.AWSConsoleURL,
|
|
PublicAddr: "aws.example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a client with a machine role of RoleApp.
|
|
s.authClient, err = s.tlsServer.NewClient(auth.TestServerID(types.RoleApp, s.hostUUID))
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
s.authClient.Close()
|
|
})
|
|
|
|
serverIdentity, err := auth.NewServerIdentity(s.authServer.AuthServer, s.hostUUID, types.RoleApp)
|
|
require.NoError(t, err)
|
|
tlsConfig, err := serverIdentity.TLSConfig(nil)
|
|
require.NoError(t, err)
|
|
tlsConfig.Time = s.clock.Now
|
|
|
|
// Generate certificate for user.
|
|
s.clientCertificate = s.generateCertificate(t, s.user, "foo.example.com", "")
|
|
|
|
// Generate certificate for AWS console application.
|
|
s.awsConsoleCertificate = s.generateCertificate(t, s.user, "aws.example.com", "readonly")
|
|
|
|
lockWatcher, err := services.NewLockWatcher(s.closeContext, services.LockWatcherConfig{
|
|
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
|
Component: teleport.ComponentApp,
|
|
Client: s.authClient,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{
|
|
ClusterName: "cluster-name",
|
|
AccessPoint: s.authClient,
|
|
LockWatcher: lockWatcher,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
lockWatcher.Close()
|
|
})
|
|
|
|
apps := types.Apps{appFoo, appAWS}
|
|
if len(config.Apps) > 0 {
|
|
apps = config.Apps
|
|
}
|
|
|
|
discard := events.NewDiscardEmitter()
|
|
|
|
s.appServer, err = New(s.closeContext, &Config{
|
|
Clock: s.clock,
|
|
DataDir: s.dataDir,
|
|
AccessPoint: s.authClient,
|
|
AuthClient: s.authClient,
|
|
TLSConfig: tlsConfig,
|
|
CipherSuites: utils.DefaultCipherSuites(),
|
|
HostID: s.hostUUID,
|
|
Hostname: "test",
|
|
Authorizer: authorizer,
|
|
GetRotation: testRotationGetter,
|
|
Apps: apps,
|
|
OnHeartbeat: func(err error) {},
|
|
Cloud: &testCloud{},
|
|
ResourceMatchers: config.ResourceMatchers,
|
|
OnReconcile: config.OnReconcile,
|
|
LockWatcher: lockWatcher,
|
|
Emitter: discard,
|
|
CloudLabels: config.CloudImporter,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = s.appServer.Start(s.closeContext)
|
|
require.NoError(t, err)
|
|
err = s.appServer.ForceHeartbeat()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
s.appServer.Close()
|
|
|
|
// wait for the server to close before allowing other cleanup
|
|
// actions to proceed
|
|
s.appServer.Wait()
|
|
})
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *Suite) generateCertificate(t *testing.T, user types.User, publicAddr, AWSRoleARN string) tls.Certificate {
|
|
privateKey, publicKey, err := testauthority.New().GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
req := auth.AppTestCertRequest{
|
|
PublicKey: publicKey,
|
|
Username: user.GetName(),
|
|
TTL: 1 * time.Hour,
|
|
PublicAddr: publicAddr,
|
|
ClusterName: "root.example.com",
|
|
}
|
|
if AWSRoleARN != "" {
|
|
req.AWSRoleARN = AWSRoleARN
|
|
}
|
|
certificate, err := s.tlsServer.Auth().GenerateUserAppTestCert(req)
|
|
require.NoError(t, err)
|
|
tlsCertificate, err := tls.X509KeyPair(certificate, privateKey)
|
|
require.NoError(t, err)
|
|
|
|
return tlsCertificate
|
|
}
|
|
|
|
// TestStart makes sure that after the server has started, a correct services.App
|
|
// has been created.
|
|
func TestStart(t *testing.T) {
|
|
s := SetUpSuite(t)
|
|
|
|
// Fetch the services.App that the service heartbeat.
|
|
servers, err := s.authServer.AuthServer.GetApplicationServers(s.closeContext, defaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// Check that the services.Server sent via heartbeat is correct. For example,
|
|
// check that the dynamic labels have been evaluated.
|
|
appFoo, err := types.NewAppV3(types.Metadata{
|
|
Name: "foo",
|
|
Labels: staticLabels,
|
|
}, types.AppSpecV3{
|
|
URI: s.testhttp.URL,
|
|
PublicAddr: "foo.example.com",
|
|
InsecureSkipVerify: true,
|
|
DynamicLabels: map[string]types.CommandLabelV2{
|
|
dynamicLabelName: {
|
|
Period: dynamicLabelPeriod,
|
|
Command: dynamicLabelCommand,
|
|
Result: "4",
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
serverFoo, err := types.NewAppServerV3FromApp(appFoo, "test", s.hostUUID)
|
|
require.NoError(t, err)
|
|
appAWS, err := types.NewAppV3(types.Metadata{
|
|
Name: "awsconsole",
|
|
Labels: staticLabels,
|
|
}, types.AppSpecV3{
|
|
URI: constants.AWSConsoleURL,
|
|
PublicAddr: "aws.example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
serverAWS, err := types.NewAppServerV3FromApp(appAWS, "test", s.hostUUID)
|
|
require.NoError(t, err)
|
|
|
|
sort.Sort(types.AppServers(servers))
|
|
require.Empty(t, cmp.Diff([]types.AppServer{serverAWS, serverFoo}, servers,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID", "Expires")))
|
|
|
|
// Check the expiry time is correct.
|
|
for _, server := range servers {
|
|
require.True(t, s.clock.Now().Before(server.Expiry()))
|
|
require.True(t, s.clock.Now().Add(2*defaults.ServerAnnounceTTL).After(server.Expiry()))
|
|
}
|
|
}
|
|
|
|
// TestWaitStop makes sure the server will block and unlock.
|
|
func TestWaitStop(t *testing.T) {
|
|
s := SetUpSuite(t)
|
|
|
|
// Make sure that wait will block while the server is running.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
s.appServer.Wait()
|
|
cancel()
|
|
}()
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("Wait failed to block.")
|
|
case <-time.After(250 * time.Millisecond):
|
|
}
|
|
|
|
// Close should unblock wait.
|
|
err := s.appServer.Close()
|
|
require.NoError(t, err)
|
|
err = s.appServer.Wait()
|
|
require.Equal(t, err, context.Canceled)
|
|
}
|
|
|
|
func TestShutdown(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hasForkedChild bool
|
|
wantAppServersAfterShutdown bool
|
|
}{
|
|
{
|
|
name: "regular shutdown",
|
|
hasForkedChild: false,
|
|
wantAppServersAfterShutdown: false,
|
|
},
|
|
{
|
|
name: "has forked child",
|
|
hasForkedChild: true,
|
|
wantAppServersAfterShutdown: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Make a static configuration app.
|
|
app0, err := makeStaticApp("app0", nil)
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
Apps: types.Apps{app0},
|
|
})
|
|
|
|
// Validate heartbeat is present after start.
|
|
s.appServer.ForceHeartbeat()
|
|
appServers, err := s.authClient.GetApplicationServers(ctx, defaults.Namespace)
|
|
require.NoError(t, err)
|
|
require.Len(t, appServers, 1)
|
|
require.Equal(t, appServers[0].GetApp(), app0)
|
|
|
|
// Shutdown should not return error.
|
|
shutdownCtx, cancel := context.WithTimeout(ctx, time.Second*5)
|
|
t.Cleanup(cancel)
|
|
if test.hasForkedChild {
|
|
shutdownCtx = services.ProcessForkedContext(shutdownCtx)
|
|
}
|
|
|
|
require.NoError(t, s.appServer.Shutdown(shutdownCtx))
|
|
|
|
// Validate app servers based on the test.
|
|
appServersAfterShutdown, err := s.authClient.GetApplicationServers(ctx, defaults.Namespace)
|
|
require.NoError(t, err)
|
|
if test.wantAppServersAfterShutdown {
|
|
require.Equal(t, appServers, appServersAfterShutdown)
|
|
} else {
|
|
require.Empty(t, appServersAfterShutdown)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppWithUpdatedLabels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
app types.Application
|
|
dynamicLabels map[string]*labels.Dynamic
|
|
cloudLabels labels.Importer
|
|
|
|
expectedDynamicLabels map[string]string
|
|
expectedCloudLabels map[string]string
|
|
}{
|
|
{
|
|
name: "no dynamic labels or cloud labels",
|
|
app: &types.AppV3{
|
|
Metadata: types.Metadata{
|
|
Labels: map[string]string{
|
|
"test1": "value1",
|
|
},
|
|
},
|
|
},
|
|
expectedDynamicLabels: map[string]string{},
|
|
expectedCloudLabels: map[string]string{},
|
|
},
|
|
{
|
|
name: "only dynamic labels",
|
|
app: &types.AppV3{
|
|
Metadata: types.Metadata{
|
|
Name: "app1",
|
|
Labels: map[string]string{
|
|
"test1": "value1",
|
|
},
|
|
},
|
|
Spec: types.AppSpecV3{
|
|
DynamicLabels: map[string]types.CommandLabelV2{
|
|
"something": {
|
|
Command: []string{"echo", "blah"},
|
|
Result: "blah",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedDynamicLabels: map[string]string{
|
|
"something": "blah",
|
|
},
|
|
expectedCloudLabels: map[string]string{},
|
|
},
|
|
{
|
|
name: "dynamic and cloud labels",
|
|
app: &types.AppV3{
|
|
Metadata: types.Metadata{
|
|
Name: "app1",
|
|
Labels: map[string]string{
|
|
"test1": "value1",
|
|
},
|
|
},
|
|
Spec: types.AppSpecV3{
|
|
DynamicLabels: map[string]types.CommandLabelV2{
|
|
"something": {
|
|
Command: []string{"echo", "blah"},
|
|
Result: "blah",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cloudLabels: mustNewCloudImporter(t, &labels.CloudConfig{
|
|
Client: newTestIMClient("1", "host", map[string]string{
|
|
"cloud1": "value1",
|
|
"cloud2": "value2",
|
|
}),
|
|
}),
|
|
expectedDynamicLabels: map[string]string{
|
|
"something": "blah",
|
|
},
|
|
expectedCloudLabels: map[string]string{
|
|
"aws/cloud1": "value1",
|
|
"aws/cloud2": "value2",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
CloudImporter: test.cloudLabels,
|
|
})
|
|
|
|
if test.cloudLabels != nil {
|
|
require.NoError(t, test.cloudLabels.Sync(context.Background()))
|
|
}
|
|
|
|
updatedApp := s.appServer.appWithUpdatedLabels(test.app)
|
|
|
|
for key, value := range test.expectedDynamicLabels {
|
|
require.Equal(t, value, updatedApp.GetDynamicLabels()[key].GetResult())
|
|
}
|
|
|
|
for key, value := range test.expectedCloudLabels {
|
|
require.Equal(t, value, updatedApp.GetStaticLabels()[key])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// testIMClient is a test instance metadata client for exercising cloud labels.
|
|
type testIMClient struct {
|
|
id string
|
|
hostname string
|
|
labels map[string]string
|
|
}
|
|
|
|
func newTestIMClient(id, hostname string, labels map[string]string) *testIMClient {
|
|
return &testIMClient{
|
|
id: id,
|
|
hostname: hostname,
|
|
labels: labels,
|
|
}
|
|
}
|
|
|
|
func (i *testIMClient) IsAvailable(_ context.Context) bool {
|
|
return true
|
|
}
|
|
|
|
func (i *testIMClient) GetTags(_ context.Context) (map[string]string, error) {
|
|
return i.labels, nil
|
|
}
|
|
|
|
func (i *testIMClient) GetHostname(_ context.Context) (string, error) {
|
|
return i.hostname, nil
|
|
}
|
|
|
|
func (i *testIMClient) GetType() types.InstanceMetadataType {
|
|
return types.InstanceMetadataTypeEC2
|
|
}
|
|
|
|
func (i *testIMClient) GetID(ctx context.Context) (string, error) {
|
|
return i.id, nil
|
|
}
|
|
|
|
func mustNewCloudImporter(t *testing.T, config *labels.CloudConfig) labels.Importer {
|
|
importer, err := labels.NewCloudImporter(context.Background(), config)
|
|
require.NoError(t, err)
|
|
|
|
return importer
|
|
}
|
|
|
|
// TestHandleConnection verifies that requests with valid certificates are forwarded and the
|
|
// request had headers rewritten as expected.
|
|
func TestHandleConnection(t *testing.T) {
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
ValidateRequest: func(_ *Suite, r *http.Request) {
|
|
require.Equal(t, "on", r.Header.Get(common.XForwardedSSL))
|
|
require.Equal(t, "443", r.Header.Get(forward.XForwardedPort))
|
|
},
|
|
})
|
|
s.checkHTTPResponse(t, s.clientCertificate, func(resp *http.Response) {
|
|
require.Equal(t, resp.StatusCode, http.StatusOK)
|
|
buf, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, strings.TrimSpace(string(buf)), s.message)
|
|
})
|
|
}
|
|
|
|
// TestHandleConnectionWS verifies that websocket requests with valid certificates are forwarded and the
|
|
// request had headers rewritten as expected.
|
|
func TestHandleConnectionWS(t *testing.T) {
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
ValidateRequest: func(s *Suite, r *http.Request) {
|
|
require.Equal(t, "on", r.Header.Get(common.XForwardedSSL))
|
|
// Websockets will pass on the server port at this level due to the
|
|
// websocket transport header rewriter delegate.
|
|
require.Equal(t, s.serverPort, r.Header.Get(forward.XForwardedPort))
|
|
},
|
|
})
|
|
|
|
s.checkWSResponse(t, s.clientCertificate, func(messageType int, message string) {
|
|
require.Equal(t, websocket.TextMessage, messageType)
|
|
require.Equal(t, s.message, message)
|
|
})
|
|
}
|
|
|
|
// TestHandleConnectionHTTP2WS given a server that supports HTTP2, make a
|
|
// request and then connect to WebSocket, ensuring that both succeed.
|
|
//
|
|
// This test guarantees the server is capable of handing requests and websockets
|
|
// in different HTTP versions.
|
|
func TestHandleConnectionHTTP2WS(t *testing.T) {
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
EnableHTTP2: true,
|
|
ValidateRequest: func(s *Suite, r *http.Request) {
|
|
// Differentiate WebSocket requests.
|
|
if strings.ToLower(r.Header.Get("upgrade")) == "websocket" {
|
|
// Expect WS requests to be using http 1.
|
|
require.Equal(t, 1, r.ProtoMajor)
|
|
return
|
|
}
|
|
|
|
// Expect http requests to be using h2.
|
|
require.Equal(t, 2, r.ProtoMajor)
|
|
},
|
|
})
|
|
|
|
// First, make the request. This will be using HTTP2.
|
|
s.checkHTTPResponse(t, s.clientCertificate, func(resp *http.Response) {
|
|
require.Equal(t, resp.StatusCode, http.StatusOK)
|
|
buf, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, strings.TrimSpace(string(buf)), s.message)
|
|
})
|
|
|
|
// Second, make the WebSocket connection. This will be using HTTP/1.1
|
|
s.checkWSResponse(t, s.clientCertificate, func(messageType int, message string) {
|
|
require.Equal(t, websocket.TextMessage, messageType)
|
|
require.Equal(t, s.message, message)
|
|
})
|
|
}
|
|
|
|
// TestAuthorize verifies that only authorized requests are handled.
|
|
func TestAuthorize(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cloudLabels labels.Importer
|
|
roleAppLabels types.Labels
|
|
appLabels map[string]string
|
|
message string
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "no cloud labels",
|
|
roleAppLabels: types.Labels{
|
|
"bar": []string{"baz"},
|
|
},
|
|
appLabels: map[string]string{
|
|
"bar": "baz",
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "cloud labels",
|
|
cloudLabels: mustNewCloudImporter(t, &labels.CloudConfig{
|
|
Client: newTestIMClient("foo", "host", map[string]string{
|
|
"test": "value",
|
|
}),
|
|
}),
|
|
roleAppLabels: types.Labels{
|
|
"aws/test": []string{"value"},
|
|
},
|
|
appLabels: map[string]string{},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "no access",
|
|
roleAppLabels: types.Labels{},
|
|
appLabels: map[string]string{
|
|
"bar": "baz",
|
|
},
|
|
message: "Not Found",
|
|
expectedStatus: http.StatusNotFound,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
CloudImporter: test.cloudLabels,
|
|
AppLabels: test.appLabels,
|
|
RoleAppLabels: test.roleAppLabels,
|
|
})
|
|
|
|
if test.cloudLabels != nil {
|
|
require.NoError(t, s.appServer.c.CloudLabels.Sync(context.Background()))
|
|
}
|
|
|
|
s.checkHTTPResponse(t, s.clientCertificate, func(resp *http.Response) {
|
|
require.Equal(t, test.expectedStatus, resp.StatusCode)
|
|
buf, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
message := s.message
|
|
if test.message != "" {
|
|
message = test.message
|
|
}
|
|
require.Equal(t, message, strings.TrimSpace(string(buf)))
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAuthorizeWithLocks verifies that requests are forbidden when there is
|
|
// a matching lock in force.
|
|
func TestAuthorizeWithLocks(t *testing.T) {
|
|
s := SetUpSuite(t)
|
|
// Create a lock targeting the user.
|
|
lock, err := types.NewLock("test-lock", types.LockSpecV2{
|
|
Target: types.LockTarget{User: s.user.GetName()},
|
|
})
|
|
require.NoError(t, err)
|
|
s.tlsServer.Auth().UpsertLock(s.closeContext, lock)
|
|
defer func() {
|
|
s.tlsServer.Auth().DeleteLock(s.closeContext, lock.GetName())
|
|
}()
|
|
|
|
s.checkHTTPResponse(t, s.clientCertificate, func(resp *http.Response) {
|
|
require.Equal(t, resp.StatusCode, http.StatusForbidden)
|
|
buf, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, strings.TrimSpace(string(buf)), "Forbidden")
|
|
})
|
|
}
|
|
|
|
// TestGetConfigForClient verifies that only the CAs of the requested cluster are returned.
|
|
func TestGetConfigForClient(t *testing.T) {
|
|
// TODO(r0mant): Implement this.
|
|
t.Skip("Not Implemented")
|
|
}
|
|
|
|
// TestRewriteRequest verifies that requests are rewritten to include JWT headers.
|
|
func TestRewriteRequest(t *testing.T) {
|
|
// TODO(r0mant): Implement this.
|
|
t.Skip("Not Implemented")
|
|
}
|
|
|
|
// TestRewriteResponse verifies that responses are rewritten if rewrite rules are specified.
|
|
func TestRewriteResponse(t *testing.T) {
|
|
// TODO(r0mant): Implement this.
|
|
t.Skip("Not Implemented")
|
|
}
|
|
|
|
// TestSessionClose makes sure sessions are closed after the given session time period.
|
|
func TestSessionClose(t *testing.T) {
|
|
// TODO(r0mant): Implement this.
|
|
t.Skip("Not Implemented")
|
|
}
|
|
|
|
// TestAWSConsoleRedirect verifies AWS management console access.
|
|
func TestAWSConsoleRedirect(t *testing.T) {
|
|
s := SetUpSuite(t)
|
|
s.checkHTTPResponse(t, s.awsConsoleCertificate, func(resp *http.Response) {
|
|
require.Equal(t, http.StatusFound, resp.StatusCode)
|
|
location, err := resp.Location()
|
|
require.NoError(t, err)
|
|
require.Equal(t, location.String(), "https://signin.aws.amazon.com")
|
|
})
|
|
}
|
|
|
|
// TestRequestAuditEvent verifies that audit events regarding application access
|
|
// are being generated with the correct metadata.
|
|
// This test configures the server to record the session sync, meaning that the
|
|
// events will be forwarded to the auth server. On the server-side, it defines a
|
|
// CallbackStreamer, which is going to be used to "intercept" the
|
|
// app.session.request events.
|
|
func TestRequestAuditEvents(t *testing.T) {
|
|
testhttp := httptest.NewUnstartedServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
|
testhttp.Config.TLSConfig = &tls.Config{Time: clockwork.NewFakeClock().Now}
|
|
testhttp.Start()
|
|
|
|
app, err := types.NewAppV3(types.Metadata{
|
|
Name: "foo",
|
|
Labels: staticLabels,
|
|
}, types.AppSpecV3{
|
|
URI: testhttp.URL,
|
|
PublicAddr: "foo.example.com",
|
|
DynamicLabels: types.LabelsToV2(dynamicLabels),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var requestEventsReceived atomic.Uint64
|
|
var chunkEventsReceived atomic.Uint64
|
|
serverStreamer, err := events.NewCallbackStreamer(events.CallbackStreamerConfig{
|
|
Inner: events.NewDiscardEmitter(),
|
|
OnEmitAuditEvent: func(_ context.Context, _ libsession.ID, event apievents.AuditEvent) error {
|
|
switch event.GetType() {
|
|
case events.AppSessionChunkEvent:
|
|
chunkEventsReceived.Add(1)
|
|
expectedEvent := &apievents.AppSessionChunk{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.AppSessionChunkEvent,
|
|
Code: events.AppSessionChunkCode,
|
|
ClusterName: "root.example.com",
|
|
Index: 0,
|
|
},
|
|
AppMetadata: apievents.AppMetadata{
|
|
AppURI: app.Spec.URI,
|
|
AppPublicAddr: app.Spec.PublicAddr,
|
|
AppName: app.Metadata.Name,
|
|
},
|
|
}
|
|
require.Empty(t, cmp.Diff(
|
|
expectedEvent,
|
|
event,
|
|
cmpopts.IgnoreTypes(apievents.ServerMetadata{}, apievents.SessionMetadata{}, apievents.UserMetadata{}, apievents.ConnectionMetadata{}),
|
|
cmpopts.IgnoreFields(apievents.Metadata{}, "ID", "ClusterName", "Time"),
|
|
cmpopts.IgnoreFields(apievents.AppSessionChunk{}, "SessionChunkID"),
|
|
))
|
|
case events.AppSessionRequestEvent:
|
|
requestEventsReceived.Add(1)
|
|
expectedEvent := &apievents.AppSessionRequest{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.AppSessionRequestEvent,
|
|
Code: events.AppSessionRequestCode,
|
|
ClusterName: "root.example.com",
|
|
Index: 1,
|
|
},
|
|
AppMetadata: apievents.AppMetadata{
|
|
AppURI: app.Spec.URI,
|
|
AppPublicAddr: app.Spec.PublicAddr,
|
|
AppName: app.Metadata.Name,
|
|
},
|
|
StatusCode: 200,
|
|
Method: "GET",
|
|
Path: "/",
|
|
}
|
|
require.Empty(t, cmp.Diff(
|
|
expectedEvent,
|
|
event,
|
|
cmpopts.IgnoreTypes(apievents.ServerMetadata{}, apievents.SessionMetadata{}, apievents.UserMetadata{}, apievents.ConnectionMetadata{}),
|
|
cmpopts.IgnoreFields(apievents.Metadata{}, "ID", "ClusterName", "Time"),
|
|
cmpopts.IgnoreFields(apievents.AppSessionChunk{}, "SessionChunkID"),
|
|
))
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s := SetUpSuiteWithConfig(t, suiteConfig{
|
|
ServerStreamer: serverStreamer,
|
|
Apps: types.Apps{app},
|
|
})
|
|
|
|
// make a request to generate events.
|
|
s.checkHTTPResponse(t, s.clientCertificate, func(_ *http.Response) {
|
|
// wait until chunk events are generated before closing the server.
|
|
require.Eventually(t, func() bool {
|
|
return chunkEventsReceived.Load() == 1
|
|
}, 500*time.Millisecond, 50*time.Millisecond, "app.session.chunk event not generated")
|
|
// wait until request events are generated before closing the server.
|
|
require.Eventually(t, func() bool {
|
|
return requestEventsReceived.Load() == 1
|
|
}, 500*time.Millisecond, 50*time.Millisecond, "app.session.request event not generated")
|
|
})
|
|
|
|
searchEvents, _, err := s.authServer.AuditLog.SearchEvents(time.Time{}, time.Now().Add(time.Minute), "", []string{events.AppSessionChunkEvent}, 10, types.EventOrderDescending, "")
|
|
require.NoError(t, err)
|
|
require.Len(t, searchEvents, 1)
|
|
|
|
expectedEvent := &apievents.AppSessionChunk{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.AppSessionChunkEvent,
|
|
Code: events.AppSessionChunkCode,
|
|
},
|
|
AppMetadata: apievents.AppMetadata{
|
|
AppURI: app.Spec.URI,
|
|
AppPublicAddr: app.Spec.PublicAddr,
|
|
AppName: app.Metadata.Name,
|
|
},
|
|
}
|
|
require.Empty(t, cmp.Diff(
|
|
expectedEvent,
|
|
searchEvents[0],
|
|
cmpopts.IgnoreTypes(apievents.ServerMetadata{}, apievents.SessionMetadata{}, apievents.UserMetadata{}, apievents.ConnectionMetadata{}),
|
|
cmpopts.IgnoreFields(apievents.Metadata{}, "ID", "ClusterName", "Time"),
|
|
cmpopts.IgnoreFields(apievents.AppSessionChunk{}, "SessionChunkID"),
|
|
))
|
|
}
|
|
|
|
// checkHTTPResponse checks expected HTTP response.
|
|
func (s *Suite) checkHTTPResponse(t *testing.T, clientCert tls.Certificate, checkResp func(*http.Response)) {
|
|
pr, pw := net.Pipe()
|
|
defer pw.Close()
|
|
defer pr.Close()
|
|
|
|
// Create an HTTP client authenticated with the user's credentials. This acts
|
|
// like the proxy does.
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return pr, nil
|
|
},
|
|
TLSClientConfig: &tls.Config{
|
|
// RootCAs is a pool of host certificates used to verify the identity of
|
|
// the server this client is connecting to.
|
|
RootCAs: s.hostCertPool,
|
|
// Certificates is the user's application specific certificate.
|
|
Certificates: []tls.Certificate{clientCert},
|
|
// Time defines the time anchor for certificate validation
|
|
Time: s.clock.Now,
|
|
},
|
|
},
|
|
// Prevent client from following redirect to be able to test redirect locations.
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
// Handle the connection in another goroutine.
|
|
go func() {
|
|
s.appServer.HandleConnection(pw)
|
|
wg.Done()
|
|
}()
|
|
|
|
// Issue request.
|
|
resp, err := httpClient.Get("https://" + constants.APIDomain)
|
|
require.NoError(t, err)
|
|
|
|
// Check response.
|
|
checkResp(resp)
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
// Close should not trigger an error. Closing the connection is enough to
|
|
// get out of the HandleConnection routine.
|
|
require.NoError(t, pw.Close())
|
|
|
|
// Wait for the application server to actually stop serving before
|
|
// closing the test. This will make sure the server removes the listeners
|
|
wg.Wait()
|
|
}
|
|
|
|
// checkWSResponse checks expected websocket response.
|
|
func (s *Suite) checkWSResponse(t *testing.T, clientCert tls.Certificate, checkMessage func(messageType int, message string)) {
|
|
pr, pw := net.Pipe()
|
|
defer pw.Close()
|
|
defer pr.Close()
|
|
|
|
dialer := websocket.Dialer{
|
|
NetDial: func(_, _ string) (net.Conn, error) {
|
|
return pr, nil
|
|
},
|
|
TLSClientConfig: &tls.Config{
|
|
// RootCAs is a pool of host certificates used to verify the identity of
|
|
// the server this client is connecting to.
|
|
RootCAs: s.hostCertPool,
|
|
// Certificates is the user's application specific certificate.
|
|
Certificates: []tls.Certificate{clientCert},
|
|
// Time defines the time anchor for certificate validation
|
|
Time: s.clock.Now,
|
|
},
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
// Handle the connection in another goroutine.
|
|
go func() {
|
|
s.appServer.HandleConnection(pw)
|
|
wg.Done()
|
|
}()
|
|
|
|
// Issue request.
|
|
ws, resp, err := dialer.Dial("wss://"+constants.APIDomain, http.Header{})
|
|
require.NoError(t, err)
|
|
|
|
// Check response.
|
|
require.Equal(t, resp.StatusCode, http.StatusSwitchingProtocols)
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
// Read websocket message
|
|
messageType, message, err := ws.ReadMessage()
|
|
require.NoError(t, err)
|
|
|
|
// Check message
|
|
checkMessage(messageType, string(message))
|
|
|
|
// This should not trigger an error.
|
|
require.NoError(t, ws.Close())
|
|
|
|
// Close should not trigger an error. Closing the connection is enough to
|
|
// get out of the HandleConnection routine.
|
|
require.NoError(t, pw.Close())
|
|
|
|
// Wait for the application server to actually stop serving before
|
|
// closing the test. This will make sure the server removes the listeners
|
|
wg.Wait()
|
|
}
|
|
|
|
func testRotationGetter(role types.SystemRole) (*types.Rotation, error) {
|
|
return &types.Rotation{}, nil
|
|
}
|
|
|
|
type testCloud struct{}
|
|
|
|
func (c *testCloud) GetAWSSigninURL(_ AWSSigninRequest) (*AWSSigninResponse, error) {
|
|
return &AWSSigninResponse{
|
|
SigninURL: "https://signin.aws.amazon.com",
|
|
}, nil
|
|
}
|
|
|
|
var (
|
|
staticLabels = map[string]string{
|
|
"bar": "baz",
|
|
}
|
|
dynamicLabelName = "qux"
|
|
dynamicLabelPeriod = types.NewDuration(time.Second)
|
|
dynamicLabelCommand = []string{"expr", "1", "+", "3"}
|
|
dynamicLabels = map[string]types.CommandLabel{
|
|
dynamicLabelName: &types.CommandLabelV2{
|
|
Period: dynamicLabelPeriod,
|
|
Command: dynamicLabelCommand,
|
|
},
|
|
}
|
|
)
|