mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Removes any externally visibile notions to end users that a token name and token secret are two different entities. This removes the need to output a token_name and token_secret value from `tctl scoped tokens add` as well as removing the token_secret from teleport.yaml and teleport CLIs. Internally, the name of a scoped_token is still NOT a secret. The secret is a separate field in the scoped_token.status. When the token is presented to a user it is done so as `<token_name>:<base64(token_secret)`. The leading token_name is not encoded to allow users to visually dentify tokens they may have created with a specific name. The `~` separator was chosen because it is neither a valid backend key, scope separator, and does not need any special encoding if included in a URL. The token_secret is base64 encoded so that it appears as opaque text to users and can be included in a URL. Additionally, the tctl scoped ref parsing has been updated such that tctl get/edit/rm honors both `<token_name>` and `<token_name>:<base64(token_secret)>` This does contain a few breaking changes. The token_secret has been removed from the teleport.yaml file config and its equivalent CLI flags have been removed. Since scopes are still under active development and this only impacts scoped_tokens and not traditional tokens the impact of this change should be minimal.
3422 lines
112 KiB
Go
3422 lines
112 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 common
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
"k8s.io/apimachinery/pkg/util/yaml"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
apidefaults "github.com/gravitational/teleport/api/defaults"
|
|
"github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
|
|
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
|
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
|
|
joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
|
|
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apicommon "github.com/gravitational/teleport/api/types/common"
|
|
"github.com/gravitational/teleport/api/types/discoveryconfig"
|
|
"github.com/gravitational/teleport/api/types/header"
|
|
"github.com/gravitational/teleport/entitlements"
|
|
"github.com/gravitational/teleport/integration/helpers"
|
|
"github.com/gravitational/teleport/lib/auth/authclient"
|
|
"github.com/gravitational/teleport/lib/config"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/fixtures"
|
|
"github.com/gravitational/teleport/lib/modules"
|
|
"github.com/gravitational/teleport/lib/modules/modulestest"
|
|
"github.com/gravitational/teleport/lib/scopes"
|
|
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
|
"github.com/gravitational/teleport/lib/service/servicecfg"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/utils/log/logtest"
|
|
"github.com/gravitational/teleport/tool/tctl/common/databaseobject"
|
|
"github.com/gravitational/teleport/tool/tctl/common/databaseobjectimportrule"
|
|
"github.com/gravitational/teleport/tool/teleport/testenv"
|
|
)
|
|
|
|
func TestListKindsStructuredOutput(t *testing.T) {
|
|
rows := resourceKindRows()
|
|
require.NotEmpty(t, rows)
|
|
|
|
var jsonBuf bytes.Buffer
|
|
rc := ResourceCommand{
|
|
Stdout: &jsonBuf,
|
|
format: "json",
|
|
}
|
|
require.NoError(t, rc.listKinds())
|
|
gotJSON := mustDecodeJSON[[]resourceKindRow](t, &jsonBuf)
|
|
require.Equal(t, rows, gotJSON)
|
|
|
|
var yamlBuf bytes.Buffer
|
|
rc = ResourceCommand{
|
|
Stdout: &yamlBuf,
|
|
format: "yaml",
|
|
}
|
|
require.NoError(t, rc.listKinds())
|
|
yamlJSON, err := utils.ToJSON(yamlBuf.Bytes())
|
|
require.NoError(t, err)
|
|
gotYAML := mustDecodeJSON[[]resourceKindRow](t, bytes.NewReader(yamlJSON))
|
|
require.Equal(t, rows, gotYAML)
|
|
}
|
|
|
|
// TestDatabaseServerResource tests tctl db_server rm/get commands.
|
|
func TestDatabaseServerResource(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
caCertFilePath := filepath.Join(t.TempDir(), "ca-cert.pem")
|
|
require.NoError(t, os.WriteFile(caCertFilePath, []byte(fixtures.TLSCACertPEM), 0644))
|
|
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Databases: config.Databases{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
Databases: []*config.Database{
|
|
{
|
|
Name: "example-rds-us-west-1",
|
|
Description: "Example MySQL",
|
|
Protocol: "mysql",
|
|
URI: "localhost:33306",
|
|
StaticLabels: map[string]string{
|
|
// pretend it's been "discovered"
|
|
types.DiscoveredNameLabel: "example",
|
|
},
|
|
TLS: config.DatabaseTLS{
|
|
ServerName: "db.example.com",
|
|
CACertFile: caCertFilePath,
|
|
},
|
|
},
|
|
{
|
|
Name: "example-rds-us-west-2",
|
|
Description: "Example PostgreSQL",
|
|
Protocol: "postgres",
|
|
URI: "localhost:33307",
|
|
AdminUser: config.DatabaseAdminUser{
|
|
Name: "root",
|
|
},
|
|
TLS: config.DatabaseTLS{
|
|
Mode: "verify-ca",
|
|
ServerName: "db.example.com",
|
|
CACertFile: caCertFilePath,
|
|
},
|
|
StaticLabels: map[string]string{
|
|
// pretend it's been "discovered"
|
|
types.DiscoveredNameLabel: "example",
|
|
},
|
|
},
|
|
{
|
|
Name: "db3",
|
|
Description: "Example MySQL",
|
|
Protocol: "mysql",
|
|
URI: "localhost:33308",
|
|
TLS: config.DatabaseTLS{
|
|
ServerName: "db.example.com",
|
|
CACertFile: caCertFilePath,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
db1, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "example-rds-us-west-1",
|
|
Description: "Example MySQL",
|
|
Labels: map[string]string{
|
|
types.OriginLabel: types.OriginConfigFile,
|
|
types.DiscoveredNameLabel: "example",
|
|
},
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: defaults.ProtocolMySQL,
|
|
URI: "localhost:33306",
|
|
CACert: fixtures.TLSCACertPEM,
|
|
AdminUser: &types.DatabaseAdminUser{
|
|
Name: "",
|
|
},
|
|
TLS: types.DatabaseTLS{
|
|
Mode: types.DatabaseTLSMode_VERIFY_FULL,
|
|
ServerName: "db.example.com",
|
|
CACert: fixtures.TLSCACertPEM,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
db2, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "example-rds-us-west-2",
|
|
Description: "Example PostgreSQL",
|
|
Labels: map[string]string{
|
|
types.OriginLabel: types.OriginConfigFile,
|
|
types.DiscoveredNameLabel: "example",
|
|
},
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: defaults.ProtocolPostgres,
|
|
URI: "localhost:33307",
|
|
CACert: fixtures.TLSCACertPEM,
|
|
AdminUser: &types.DatabaseAdminUser{
|
|
Name: "root",
|
|
},
|
|
TLS: types.DatabaseTLS{
|
|
Mode: types.DatabaseTLSMode_VERIFY_CA,
|
|
ServerName: "db.example.com",
|
|
CACert: fixtures.TLSCACertPEM,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
db3, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "db3",
|
|
Description: "Example MySQL",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginConfigFile},
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: defaults.ProtocolMySQL,
|
|
URI: "localhost:33308",
|
|
CACert: fixtures.TLSCACertPEM,
|
|
AdminUser: &types.DatabaseAdminUser{
|
|
Name: "",
|
|
},
|
|
TLS: types.DatabaseTLS{
|
|
Mode: types.DatabaseTLSMode_VERIFY_FULL,
|
|
ServerName: "db.example.com",
|
|
CACert: fixtures.TLSCACertPEM,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
// get all database servers
|
|
buff, err := runResourceCommand(t, clt, []string{"get", types.KindDatabaseServer, "--format=json"})
|
|
require.NoError(t, err)
|
|
requireGotDatabaseServers(t, buff, db1, db2, db3)
|
|
|
|
// get specific database server
|
|
wantServer := fmt.Sprintf("%v/%v", types.KindDatabaseServer, db2.GetName())
|
|
buff, err = runResourceCommand(t, clt, []string{"get", wantServer, "--format=json"})
|
|
require.NoError(t, err)
|
|
requireGotDatabaseServers(t, buff, db2)
|
|
|
|
// get database servers by discovered name
|
|
wantServersDiscoveredName := fmt.Sprintf("%v/%v", types.KindDatabaseServer, "example")
|
|
buff, err = runResourceCommand(t, clt, []string{"get", wantServersDiscoveredName, "--format=json"})
|
|
require.NoError(t, err)
|
|
requireGotDatabaseServers(t, buff, db1, db2)
|
|
|
|
// remove multiple distinct database servers by discovered name is an error
|
|
_, err = runResourceCommand(t, clt, []string{"rm", wantServersDiscoveredName})
|
|
require.ErrorContains(t, err, "db_server/example matches multiple auto-discovered database servers")
|
|
|
|
// remove database server by name
|
|
_, err = runResourceCommand(t, clt, []string{"rm", wantServer})
|
|
require.NoError(t, err)
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"get", wantServer, "--format=json"})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
// remove database server by discovered name.
|
|
_, err = runResourceCommand(t, clt, []string{"rm", wantServersDiscoveredName})
|
|
require.NoError(t, err)
|
|
|
|
buff, err = runResourceCommand(t, clt, []string{"get", "db_server/db3", "--format=json"})
|
|
require.NoError(t, err)
|
|
requireGotDatabaseServers(t, buff, db3)
|
|
}
|
|
|
|
// TestDatabaseServiceResource tests tctl db_services get commands.
|
|
func TestDatabaseServiceResource(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
|
|
ctx := context.Background()
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
auth := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(auth)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
// Add a lot of DatabaseServices to test pagination
|
|
dbS, err := types.NewDatabaseServiceV1(
|
|
types.Metadata{Name: uuid.NewString()},
|
|
types.DatabaseServiceSpecV1{
|
|
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
|
{Labels: &types.Labels{"env": []string{"prod"}}},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
randomDBServiceName := ""
|
|
totalDBServices := apidefaults.DefaultChunkSize*2 + 20 // testing partial pages
|
|
for i := range totalDBServices {
|
|
dbS.SetName(uuid.NewString())
|
|
if i == apidefaults.DefaultChunkSize { // A "random" database service name
|
|
randomDBServiceName = dbS.GetName()
|
|
}
|
|
_, err = auth.GetAuthServer().UpsertDatabaseService(ctx, dbS)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("test pagination of database services ", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", types.KindDatabaseService, "--format=json"})
|
|
require.NoError(t, err)
|
|
out := mustDecodeJSON[[]*types.DatabaseServiceV1](t, buff)
|
|
require.Len(t, out, totalDBServices)
|
|
})
|
|
|
|
service := fmt.Sprintf("%v/%v", types.KindDatabaseService, randomDBServiceName)
|
|
|
|
t.Run("get specific database service", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", service, "--format=json"})
|
|
require.NoError(t, err)
|
|
out := mustDecodeJSON[[]*types.DatabaseServiceV1](t, buff)
|
|
require.Len(t, out, 1)
|
|
require.Equal(t, randomDBServiceName, out[0].GetName())
|
|
})
|
|
|
|
t.Run("get unknown database service", func(t *testing.T) {
|
|
unknownService := fmt.Sprintf("%v/%v", types.KindDatabaseService, "unknown")
|
|
_, err := runResourceCommand(t, clt, []string{"get", unknownService, "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected a NotFound error, got %v", err)
|
|
})
|
|
|
|
t.Run("get specific database service with human output", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", service, "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString := buff.String()
|
|
require.Contains(t, outputString, "env=[prod]")
|
|
require.Contains(t, outputString, randomDBServiceName)
|
|
})
|
|
}
|
|
|
|
func TestScopedRoleAndAssignmentResource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const scopedRoleYAML = `kind: scoped_role
|
|
metadata:
|
|
name: some-role
|
|
scope: "/"
|
|
spec:
|
|
assignable_scopes: ["/foo"]
|
|
version: v1
|
|
`
|
|
const scopedRoleAssignmentYAML = `kind: scoped_role_assignment
|
|
sub_kind: dynamic
|
|
scope: "/"
|
|
spec:
|
|
user: "bob"
|
|
assignments:
|
|
- role: some-role
|
|
scope: /foo
|
|
version: v1
|
|
`
|
|
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
auth := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors), withScopesFeatures(scopes.Features{Enabled: true}))
|
|
clt, err := testenv.NewDefaultAuthClient(auth)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
scopedRoleYAMLPath := filepath.Join(t.TempDir(), "some-role.yaml")
|
|
require.NoError(t, os.WriteFile(scopedRoleYAMLPath, []byte(scopedRoleYAML), 0644))
|
|
|
|
// Create the scoped role
|
|
_, err = runResourceCommand(t, clt, []string{"create", scopedRoleYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// wait for cache propagation
|
|
timeout := time.After(time.Second * 30)
|
|
var raw []byte
|
|
for {
|
|
// Get the scoped role
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "scoped_role", "/::some-role", "--format=json"})
|
|
if err == nil {
|
|
raw = buff.Bytes()
|
|
break
|
|
}
|
|
|
|
require.True(t, trace.IsNotFound(err), "expected a NotFound error, got %v", err)
|
|
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "Timed out waiting for scoped role cache propagation")
|
|
case <-time.After(time.Millisecond * 100):
|
|
}
|
|
}
|
|
|
|
// Unmarshal the response into a ScopedRole object
|
|
rs, err := services.UnmarshalProtoResourceArray[*scopedaccessv1.ScopedRole](raw, services.DisallowUnknown())
|
|
require.NoError(t, err)
|
|
require.Len(t, rs, 1)
|
|
|
|
// Compare with expected value
|
|
expected := scopedaccessv1.ScopedRole_builder{
|
|
Kind: scopedaccess.KindScopedRole,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "some-role",
|
|
}.Build(),
|
|
Scope: "/",
|
|
Spec: scopedaccessv1.ScopedRoleSpec_builder{
|
|
AssignableScopes: []string{"/foo"},
|
|
}.Build(),
|
|
Version: types.V1,
|
|
}.Build()
|
|
|
|
require.Empty(t, cmp.Diff(expected, rs[0], protocmp.Transform(), protocmp.IgnoreFields(&headerv1.Metadata{}, "revision")))
|
|
|
|
// list-all for scoped_role (no SQN)
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "scoped_role", "--format=json"})
|
|
require.NoError(t, err)
|
|
allRoles, err := services.UnmarshalProtoResourceArray[*scopedaccessv1.ScopedRole](buff.Bytes(), services.DisallowUnknown())
|
|
require.NoError(t, err)
|
|
require.Len(t, allRoles, 1)
|
|
require.Equal(t, "some-role", allRoles[0].GetMetadata().GetName())
|
|
|
|
// bare name without SQN separator should require a scope-qualified name
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role", "some-role"})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter for bare name, got %v", err)
|
|
require.ErrorContains(t, err, "scope-qualified name")
|
|
|
|
// sub-kind segment on a type with no sub-kinds should be rejected on get
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role/badsubkind", "/::some-role"})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter for invalid sub-kind on get, got %v", err)
|
|
require.ErrorContains(t, err, "does not support sub-kinds")
|
|
|
|
// sub-kind segment on a type with no sub-kinds should be rejected on delete
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role/badsubkind", "/::some-role"})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter for invalid sub-kind on delete, got %v", err)
|
|
require.ErrorContains(t, err, "does not support sub-kinds")
|
|
|
|
// scope mismatch on delete should return NotFound (role lives at "/", not "/wrong")
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role", "/wrong::some-role"})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound for scope mismatch on delete, got %v", err)
|
|
|
|
// now that a role exists, test commands for assignment creation
|
|
|
|
// Can't create an assignment without a subkind.
|
|
scopedRoleAssignmentYAMLPath := filepath.Join(t.TempDir(), "some-role-assignment.yaml")
|
|
require.NoError(t, os.WriteFile(
|
|
scopedRoleAssignmentYAMLPath,
|
|
[]byte(strings.ReplaceAll(scopedRoleAssignmentYAML, "sub_kind: dynamic\n", "")),
|
|
0644,
|
|
))
|
|
_, err = runResourceCommand(t, clt, []string{"create", scopedRoleAssignmentYAMLPath})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter error, got %v", err)
|
|
require.ErrorContains(t, err, "has empty sub_kind")
|
|
|
|
// Create the valid scoped role assignment
|
|
require.NoError(t, os.WriteFile(scopedRoleAssignmentYAMLPath, []byte(scopedRoleAssignmentYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", scopedRoleAssignmentYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// wait for cache propagation
|
|
timeout = time.After(time.Second * 30)
|
|
var rawAssignment []byte
|
|
var as []*scopedaccessv1.ScopedRoleAssignment
|
|
for {
|
|
// Get the scoped role assignment.
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "scoped_role_assignment", "--format=json"})
|
|
if err == nil {
|
|
rawAssignment = buff.Bytes()
|
|
// Unmarshal the response into a ScopedRoleAssignment object
|
|
as, err = services.UnmarshalProtoResourceArray[*scopedaccessv1.ScopedRoleAssignment](rawAssignment, services.DisallowUnknown())
|
|
require.NoError(t, err)
|
|
if len(as) > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "Timed out waiting for scoped role assignment cache propagation")
|
|
case <-time.After(time.Millisecond * 100):
|
|
}
|
|
}
|
|
|
|
require.Len(t, as, 1)
|
|
assignmentName := as[0].GetMetadata().GetName()
|
|
|
|
// Ensure that retrieving the scoped role assignment with incorrect sub_kind fails.
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role_assignment/materialized", "/::" + assignmentName, "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound error, got %v", err)
|
|
|
|
// Ensure that trying to retrieve the scoped role assignment without a subkind fails.
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role_assignment", "/::" + assignmentName, "--format=json"})
|
|
require.ErrorContains(t, err, "requires a sub-kind")
|
|
|
|
// Ensure that retrieving the scoped role assignment by name with explicit sub_kind works.
|
|
buff, err = runResourceCommand(t, clt, []string{"get", "scoped_role_assignment/dynamic", "/::" + assignmentName, "--format=json"})
|
|
require.NoError(t, err)
|
|
var asByName []*scopedaccessv1.ScopedRoleAssignment
|
|
err = json.Unmarshal(buff.Bytes(), &asByName)
|
|
require.NoError(t, err)
|
|
require.Len(t, asByName, 1)
|
|
require.Equal(t, assignmentName, asByName[0].GetMetadata().GetName())
|
|
|
|
// Compare with expected value
|
|
expectedAssignment := scopedaccessv1.ScopedRoleAssignment_builder{
|
|
Kind: scopedaccess.KindScopedRoleAssignment,
|
|
SubKind: scopedaccess.SubKindDynamic,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: assignmentName,
|
|
}.Build(),
|
|
Scope: "/",
|
|
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
|
|
User: "bob",
|
|
Assignments: []*scopedaccessv1.Assignment{
|
|
scopedaccessv1.Assignment_builder{
|
|
Role: "some-role",
|
|
Scope: "/foo",
|
|
}.Build(),
|
|
},
|
|
}.Build(),
|
|
Version: types.V1,
|
|
}.Build()
|
|
|
|
require.Empty(t, cmp.Diff(expectedAssignment, as[0], protocmp.Transform(), protocmp.IgnoreFields(&headerv1.Metadata{}, "revision")))
|
|
|
|
// a bare segment for a scoped-only resource (previously treated as a name)
|
|
// should now be an explicit error rather than silently returning all resources.
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role_assignment/dynamic"})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter for old-format subkind filter, got %v", err)
|
|
require.ErrorContains(t, err, "scope-qualified name")
|
|
|
|
// scope mismatch on delete should return NotFound (assignment lives at "/", not "/wrong")
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role_assignment/dynamic", "/wrong::" + assignmentName})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound for scope mismatch on assignment delete, got %v", err)
|
|
|
|
// verify delete of assignment fails without subkind
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role_assignment", "/::" + assignmentName})
|
|
require.ErrorContains(t, err, "requires a sub-kind")
|
|
|
|
// verify delete of assignment fails without materialized subkind.
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role_assignment/materialized", "/::" + assignmentName})
|
|
require.ErrorContains(t, err, "cannot be deleted")
|
|
|
|
// verify delete of assignment
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role_assignment/dynamic", "/::" + assignmentName})
|
|
require.NoError(t, err)
|
|
|
|
// wait for delete cache propagation
|
|
timeout = time.After(time.Second * 30)
|
|
for {
|
|
// verify assignment is gone
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role_assignment/dynamic", "/::" + assignmentName, "--format=json"})
|
|
if err != nil {
|
|
require.True(t, trace.IsNotFound(err), "expected a NotFound error, got %v", err)
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "Timed out waiting for scoped role assignment cache propagation")
|
|
case <-time.After(time.Millisecond * 100):
|
|
}
|
|
}
|
|
|
|
// verify delete of role
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_role", "/::some-role"})
|
|
require.NoError(t, err)
|
|
|
|
// wait for delete cache propagation
|
|
timeout = time.After(time.Second * 30)
|
|
for {
|
|
// verify role is gone
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_role", "/::some-role", "--format=json"})
|
|
if err != nil {
|
|
require.True(t, trace.IsNotFound(err), "expected a NotFound error, got %v", err)
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "Timed out waiting for scoped role cache propagation")
|
|
case <-time.After(time.Millisecond * 100):
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScopedToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const scopedTokenYAML = `kind: scoped_token
|
|
version: v1
|
|
metadata:
|
|
name: test-token
|
|
scope: "/"
|
|
spec:
|
|
roles:
|
|
- Node
|
|
assigned_scope: "/foo"
|
|
join_method: "token"
|
|
usage_mode: "unlimited"
|
|
`
|
|
|
|
const gcpScopedTokenYAML = `kind: scoped_token
|
|
version: v1
|
|
metadata:
|
|
name: gcp-test-token
|
|
scope: "/"
|
|
spec:
|
|
roles:
|
|
- Node
|
|
assigned_scope: "/foo"
|
|
join_method: "gcp"
|
|
usage_mode: "unlimited"
|
|
gcp:
|
|
allow:
|
|
- project_ids:
|
|
- example-project-123456
|
|
locations:
|
|
- us-west1
|
|
service_accounts:
|
|
- [email protected]
|
|
`
|
|
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
auth := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors), withScopesFeatures(scopes.Features{Enabled: true}))
|
|
clt, err := testenv.NewDefaultAuthClient(auth)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
scopedTokenYAMLPath := filepath.Join(t.TempDir(), "test-token.yaml")
|
|
require.NoError(t, os.WriteFile(scopedTokenYAMLPath, []byte(scopedTokenYAML), 0644))
|
|
|
|
// Create the scoped token
|
|
_, err = runResourceCommand(t, clt, []string{"create", scopedTokenYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// wait for cache propagation
|
|
var raw []byte
|
|
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
// Get the scoped token by name
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "scoped_token", "/::test-token", "--format=json"})
|
|
require.NoError(t, err)
|
|
raw = buff.Bytes()
|
|
}, time.Second*30, time.Millisecond*100, "Timed out waiting for scoped token cache propagation")
|
|
|
|
tokens, err := services.UnmarshalProtoResourceArray[*joiningv1.ScopedToken](raw, services.DisallowUnknown())
|
|
require.NoError(t, err)
|
|
require.Len(t, tokens, 1)
|
|
|
|
// Compare with expected value
|
|
token := tokens[0]
|
|
require.Equal(t, types.KindScopedToken, token.GetKind())
|
|
require.Equal(t, "test-token", token.GetMetadata().GetName())
|
|
require.Equal(t, "/", token.GetScope())
|
|
require.Equal(t, []string{"Node"}, token.GetSpec().GetRoles())
|
|
require.Equal(t, "/foo", token.GetSpec().GetAssignedScope())
|
|
require.Equal(t, "token", token.GetSpec().GetJoinMethod())
|
|
require.Equal(t, "unlimited", token.GetSpec().GetUsageMode())
|
|
// Secret is stripped server-side when not requested with --with-secrets.
|
|
require.Empty(t, token.GetStatus().GetSecret())
|
|
|
|
// A bare name (no :: separator) should produce a helpful "scope-qualified name" error.
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_token", "test-token"})
|
|
require.ErrorContains(t, err, "scope-qualified name")
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_token", "test-token"})
|
|
require.ErrorContains(t, err, "scope-qualified name")
|
|
|
|
// sub-kind segment on a type with no sub-kinds should be rejected on get
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_token/badsubkind", "/::test-token"})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter for invalid sub-kind on get, got %v", err)
|
|
require.ErrorContains(t, err, "does not support sub-kinds")
|
|
|
|
// sub-kind segment on a type with no sub-kinds should be rejected on delete
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_token/badsubkind", "/::test-token"})
|
|
require.True(t, trace.IsBadParameter(err), "expected BadParameter for invalid sub-kind on delete, got %v", err)
|
|
require.ErrorContains(t, err, "does not support sub-kinds")
|
|
|
|
// scope mismatch on delete should return NotFound (token lives at "/", not "/wrong")
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_token", "/wrong::test-token"})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound for scope mismatch on delete, got %v", err)
|
|
|
|
// Get all scoped tokens
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "scoped_token", "--format=json", "--with-secrets"})
|
|
require.NoError(t, err)
|
|
var allTokens []*joiningv1.ScopedToken
|
|
err = json.Unmarshal(buff.Bytes(), &allTokens)
|
|
require.NoError(t, err)
|
|
require.Len(t, allTokens, 1)
|
|
require.Equal(t, "test-token", allTokens[0].GetMetadata().GetName())
|
|
// Secret should be populated
|
|
require.NotEmpty(t, allTokens[0].GetStatus().GetSecret())
|
|
require.NotContains(t, allTokens[0].GetStatus().GetSecret(), "******")
|
|
|
|
// Overwrite scopedTokenYAMLPath with the retrieved token
|
|
retrievedBytes, err := services.MarshalProtoResource(allTokens[0])
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(scopedTokenYAMLPath, retrievedBytes, 0644))
|
|
|
|
// Create with --force for upsert
|
|
// Duplicate create without --force should fail
|
|
_, err = runResourceCommand(t, clt, []string{"create", scopedTokenYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err), "expected already exists error, got %v", err)
|
|
|
|
// Using --force should succeed and act as an upsert
|
|
allTokens[0].GetMetadata().SetLabels(map[string]string{"env": "staging"})
|
|
allTokens[0].GetSpec().SetAssignedScope("/bar")
|
|
updatedBytes, err := services.MarshalProtoResource(allTokens[0])
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(scopedTokenYAMLPath, updatedBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "--force", scopedTokenYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Wait for cache propagation and verify the update took effect
|
|
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "scoped_token", "/::test-token", "--format=json"})
|
|
require.NoError(t, err)
|
|
updated, err := services.UnmarshalProtoResourceArray[*joiningv1.ScopedToken](buff.Bytes(), services.DisallowUnknown())
|
|
require.NoError(t, err)
|
|
require.Len(t, updated, 1)
|
|
require.Equal(t, "test-token", updated[0].GetMetadata().GetName())
|
|
require.Equal(ct, "/bar", updated[0].GetSpec().GetAssignedScope())
|
|
require.Equal(ct, "staging", updated[0].GetMetadata().GetLabels()["env"])
|
|
}, time.Second*30, time.Millisecond*100, "Timed out waiting for upserted scoped token cache propagation")
|
|
|
|
// verify delete of token
|
|
_, err = runResourceCommand(t, clt, []string{"rm", "scoped_token", "/::test-token"})
|
|
require.NoError(t, err)
|
|
|
|
// wait for delete cache propagation
|
|
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_token", "/::test-token", "--format=json"})
|
|
require.True(ct, trace.IsNotFound(err))
|
|
}, time.Second*30, time.Millisecond*100, "Timed out waiting for scoped token cache propagation")
|
|
|
|
// Create a GCP scoped token and verify it appears in listings.
|
|
gcpScopedTokenYAMLPath := filepath.Join(t.TempDir(), "gcp-test-token.yaml")
|
|
require.NoError(t, os.WriteFile(gcpScopedTokenYAMLPath, []byte(gcpScopedTokenYAML), 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", gcpScopedTokenYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Wait for cache propagation.
|
|
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
_, err := runResourceCommand(t, clt, []string{"get", "scoped_token", "/::gcp-test-token", "--format=json"})
|
|
require.NoError(ct, err)
|
|
}, time.Second*30, time.Millisecond*100, "Timed out waiting for GCP scoped token cache propagation")
|
|
|
|
// Verify GCP token fields outside the retry loop.
|
|
buff, err = runResourceCommand(t, clt, []string{"get", "scoped_token", "--format=json"})
|
|
require.NoError(t, err)
|
|
gcpTokens, err := services.UnmarshalProtoResourceArray[*joiningv1.ScopedToken](buff.Bytes(), services.DisallowUnknown())
|
|
require.NoError(t, err)
|
|
require.Len(t, gcpTokens, 1)
|
|
require.Equal(t, "gcp-test-token", gcpTokens[0].GetMetadata().GetName())
|
|
require.Equal(t, "gcp", gcpTokens[0].GetSpec().GetJoinMethod())
|
|
require.Len(t, gcpTokens[0].GetSpec().GetGcp().GetAllow(), 1)
|
|
require.Equal(t, []string{"example-project-123456"}, gcpTokens[0].GetSpec().GetGcp().GetAllow()[0].GetProjectIds())
|
|
|
|
// Verify scope mismatch: name exists at "/" but a different scope is requested. Must produce
|
|
// a NotFound error to comply with namespacing behavior. Future iterations may not be able
|
|
// to provide hints as namespacing may prevent it.
|
|
_, err = runResourceCommand(t, clt, []string{"get", "scoped_token", "/other-scope::gcp-test-token", "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound for scope mismatch, got: %v", err)
|
|
require.ErrorContains(t, err, "tctl get scoped_token /::gcp-test-token")
|
|
}
|
|
|
|
// TestScopedAndUnscopedNodeResource exercises tctl get/rm on nodes which are double
|
|
// registered as both a scoped and unscoped resource handler in order to support a
|
|
// mix of scope and classic style interactions. This test is intended specifically
|
|
// to help verify that we are handling double-registration sensibly.
|
|
func TestScopedAndUnscopedNodeResource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{DataDir: t.TempDir()},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{EnabledFlag: "true"},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
auth := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(auth)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
// classicNode has no explicit scope (scope == "").
|
|
classicNode, err := types.NewServer("unscoped-node", types.KindNode, types.ServerSpecV2{
|
|
Hostname: "unscoped-host",
|
|
Addr: "127.0.0.1:22",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = clt.UpsertNode(ctx, classicNode)
|
|
require.NoError(t, err)
|
|
|
|
// scopedNode has scope "/" so SQN lookups with "/::scoped-node" succeed.
|
|
scopedNode, err := types.NewServer("scoped-node", types.KindNode, types.ServerSpecV2{
|
|
Hostname: "scoped-host",
|
|
Addr: "127.0.0.1:23",
|
|
})
|
|
require.NoError(t, err)
|
|
scopedNode.(*types.ServerV2).Scope = "/staging"
|
|
_, err = clt.UpsertNode(ctx, scopedNode)
|
|
require.NoError(t, err)
|
|
|
|
// Poll until both nodes appear via the unified-resource list (cache propagation).
|
|
timeout := time.After(30 * time.Second)
|
|
for {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", "node", "--format=json"})
|
|
if err == nil && strings.Contains(buf.String(), "scoped-node") && strings.Contains(buf.String(), "unscoped-node") {
|
|
break
|
|
}
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "timed out waiting for nodes to appear in list")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
t.Run("list all", func(t *testing.T) {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", "node", "--format=json"})
|
|
require.NoError(t, err)
|
|
out := buf.String()
|
|
require.Contains(t, out, "unscoped-node")
|
|
require.Contains(t, out, "scoped-node")
|
|
})
|
|
|
|
t.Run("single-arg unscoped get", func(t *testing.T) {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", "node/unscoped-node", "--format=json"})
|
|
require.NoError(t, err)
|
|
require.Contains(t, buf.String(), "unscoped-node")
|
|
})
|
|
|
|
t.Run("two-arg unscoped get", func(t *testing.T) {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", "node", "unscoped-node", "--format=json"})
|
|
require.NoError(t, err)
|
|
require.Contains(t, buf.String(), "unscoped-node")
|
|
})
|
|
|
|
t.Run("scope-qualified get", func(t *testing.T) {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", "node", "/staging::scoped-node", "--format=json"})
|
|
require.NoError(t, err)
|
|
require.Contains(t, buf.String(), "scoped-node")
|
|
})
|
|
|
|
t.Run("scope-qualified get with scope mismatch", func(t *testing.T) {
|
|
_, err := runResourceCommand(t, clt, []string{"get", "node", "/prod::scoped-node", "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound on scope mismatch, got: %v", err)
|
|
})
|
|
|
|
t.Run("scope-qualified delete with scope mismatch", func(t *testing.T) {
|
|
_, err := runResourceCommand(t, clt, []string{"rm", "node", "/prod::scoped-node"})
|
|
require.True(t, trace.IsNotFound(err), "expected NotFound on scope mismatch delete, got: %v", err)
|
|
})
|
|
|
|
t.Run("edit via single-arg classic form", func(t *testing.T) {
|
|
editor := func(name string) error {
|
|
data, err := os.ReadFile(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data = bytes.ReplaceAll(data, []byte("unscoped-host"), []byte("unscoped-host-edited"))
|
|
return os.WriteFile(name, data, 0600)
|
|
}
|
|
_, err := runEditCommand(t, clt, []string{"edit", "node/unscoped-node"}, withEditor(editor))
|
|
require.NoError(t, err)
|
|
|
|
timeout := time.After(30 * time.Second)
|
|
for {
|
|
node, err := clt.GetNode(ctx, apidefaults.Namespace, "unscoped-node")
|
|
if err == nil && node.GetHostname() == "unscoped-host-edited" {
|
|
break
|
|
}
|
|
require.NoError(t, err, "unexpected error waiting for unscoped-node edit")
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "timed out waiting for unscoped-node hostname edit")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("edit via two-arg SQN form", func(t *testing.T) {
|
|
editor := func(name string) error {
|
|
data, err := os.ReadFile(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data = bytes.ReplaceAll(data, []byte("scoped-host"), []byte("scoped-host-edited"))
|
|
return os.WriteFile(name, data, 0600)
|
|
}
|
|
_, err := runEditCommand(t, clt, []string{"edit", "node", "/staging::scoped-node"}, withEditor(editor))
|
|
require.NoError(t, err)
|
|
|
|
timeout := time.After(30 * time.Second)
|
|
for {
|
|
node, err := clt.GetNode(ctx, apidefaults.Namespace, "scoped-node")
|
|
if err == nil && node.GetHostname() == "scoped-host-edited" {
|
|
break
|
|
}
|
|
require.NoError(t, err, "unexpected error waiting for scoped-node edit")
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "timed out waiting for scoped-node hostname edit")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("scope-qualified delete", func(t *testing.T) {
|
|
_, err := runResourceCommand(t, clt, []string{"rm", "node", "/staging::scoped-node"})
|
|
require.NoError(t, err)
|
|
|
|
timeout := time.After(30 * time.Second)
|
|
for {
|
|
_, err = clt.GetNode(ctx, apidefaults.Namespace, "scoped-node")
|
|
if trace.IsNotFound(err) {
|
|
break
|
|
}
|
|
require.NoError(t, err, "unexpected error waiting for scoped-node deletion")
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "timed out waiting for scoped-node to be deleted")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("two-arg unscoped delete", func(t *testing.T) {
|
|
_, err := runResourceCommand(t, clt, []string{"rm", "node", "unscoped-node"})
|
|
require.NoError(t, err)
|
|
|
|
timeout := time.After(30 * time.Second)
|
|
for {
|
|
_, err = clt.GetNode(ctx, apidefaults.Namespace, "unscoped-node")
|
|
if trace.IsNotFound(err) {
|
|
break
|
|
}
|
|
require.NoError(t, err, "unexpected error waiting for unscoped-node deletion")
|
|
select {
|
|
case <-timeout:
|
|
require.FailNow(t, "timed out waiting for unscoped-node to be deleted")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestIntegrationResource tests tctl integration commands.
|
|
func TestIntegrationResource(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
|
|
ctx := context.Background()
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
auth := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(auth)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
t.Run("get", func(t *testing.T) {
|
|
|
|
// Add a lot of Integrations to test pagination
|
|
ig1, err := types.NewIntegrationAWSOIDC(
|
|
types.Metadata{Name: uuid.NewString()},
|
|
&types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/OpsTeam",
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
randomIntegrationName := ""
|
|
totalIntegrations := apidefaults.DefaultChunkSize*2 + 20 // testing partial pages
|
|
for i := range totalIntegrations {
|
|
ig1.SetName(uuid.NewString())
|
|
if i == apidefaults.DefaultChunkSize { // A "random" integration name
|
|
randomIntegrationName = ig1.GetName()
|
|
}
|
|
_, err = auth.GetAuthServer().CreateIntegration(ctx, ig1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("test pagination of integrations ", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", types.KindIntegration, "--format=json"})
|
|
require.NoError(t, err)
|
|
out := mustDecodeJSON[[]types.IntegrationV1](t, buff)
|
|
require.Len(t, out, totalIntegrations)
|
|
})
|
|
|
|
igName := fmt.Sprintf("%v/%v", types.KindIntegration, randomIntegrationName)
|
|
|
|
t.Run("get specific integration", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", igName, "--format=json"})
|
|
require.NoError(t, err)
|
|
out := mustDecodeJSON[[]types.IntegrationV1](t, buff)
|
|
require.Len(t, out, 1)
|
|
require.Equal(t, randomIntegrationName, out[0].GetName())
|
|
})
|
|
|
|
t.Run("get unknown integration", func(t *testing.T) {
|
|
unknownIntegration := fmt.Sprintf("%v/%v", types.KindIntegration, "unknown")
|
|
_, err := runResourceCommand(t, clt, []string{"get", unknownIntegration, "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected a NotFound error, got %v", err)
|
|
})
|
|
|
|
t.Run("get specific integration with human output", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", igName, "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString := buff.String()
|
|
require.Contains(t, outputString, "RoleARN=arn:aws:iam::123456789012:role/OpsTeam")
|
|
require.Contains(t, outputString, randomIntegrationName)
|
|
})
|
|
})
|
|
|
|
t.Run("create", func(t *testing.T) {
|
|
integrationYAMLPath := filepath.Join(t.TempDir(), "integration.yaml")
|
|
require.NoError(t, os.WriteFile(integrationYAMLPath, []byte(integrationYAML), 0644))
|
|
_, err := runResourceCommand(t, clt, []string{"create", integrationYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "integration/myawsint", "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString := buff.String()
|
|
require.Contains(t, outputString, "RoleARN=arn:aws:iam::123456789012:role/OpsTeam")
|
|
require.Contains(t, outputString, "myawsint")
|
|
|
|
// Update the RoleARN to another role
|
|
integrationYAMLV2 := strings.ReplaceAll(integrationYAML, "OpsTeam", "DevTeam")
|
|
require.NoError(t, os.WriteFile(integrationYAMLPath, []byte(integrationYAMLV2), 0644))
|
|
|
|
// Trying to create it again should return an error
|
|
_, err = runResourceCommand(t, clt, []string{"create", integrationYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err), "expected already exists error, got %v", err)
|
|
|
|
// Using the force should be ok and replace the current object
|
|
_, err = runResourceCommand(t, clt, []string{"create", "--force", integrationYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// The RoleARN must be updated
|
|
buff, err = runResourceCommand(t, clt, []string{"get", "integration/myawsint", "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString = buff.String()
|
|
require.Contains(t, outputString, "RoleARN=arn:aws:iam::123456789012:role/DevTeam")
|
|
})
|
|
}
|
|
|
|
// TestDiscoveryConfigResource tests tctl discoveryConfig commands.
|
|
func TestDiscoveryConfigResource(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
|
|
ctx := context.Background()
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
auth := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(auth)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
t.Run("get", func(t *testing.T) {
|
|
// Add a lot of DiscoveryConfigs to test pagination
|
|
dc, err := discoveryconfig.NewDiscoveryConfig(
|
|
header.Metadata{
|
|
Name: "mydiscoveryconfig",
|
|
},
|
|
discoveryconfig.Spec{
|
|
DiscoveryGroup: "prod-resources",
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
randomDiscoveryConfigName := ""
|
|
totalDiscoveryConfigs := apidefaults.DefaultChunkSize*2 + 20 // testing partial pages
|
|
for i := range totalDiscoveryConfigs {
|
|
dc.SetName(uuid.NewString())
|
|
if i == apidefaults.DefaultChunkSize { // A "random" discoveryConfig name
|
|
randomDiscoveryConfigName = dc.GetName()
|
|
}
|
|
_, err = auth.GetAuthServer().CreateDiscoveryConfig(ctx, dc)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("test pagination of discovery configs ", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", types.KindDiscoveryConfig, "--format=json"})
|
|
require.NoError(t, err)
|
|
out := mustDecodeJSON[[]discoveryconfig.DiscoveryConfig](t, buff)
|
|
require.Len(t, out, totalDiscoveryConfigs)
|
|
})
|
|
|
|
dcName := fmt.Sprintf("%v/%v", types.KindDiscoveryConfig, randomDiscoveryConfigName)
|
|
|
|
t.Run("get specific discovery config", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", dcName, "--format=json"})
|
|
require.NoError(t, err)
|
|
out := mustDecodeJSON[[]discoveryconfig.DiscoveryConfig](t, buff)
|
|
require.Len(t, out, 1)
|
|
require.Equal(t, randomDiscoveryConfigName, out[0].GetName())
|
|
})
|
|
|
|
t.Run("get unknown discovery config", func(t *testing.T) {
|
|
unknownDiscoveryConfig := fmt.Sprintf("%v/%v", types.KindDiscoveryConfig, "unknown")
|
|
_, err := runResourceCommand(t, clt, []string{"get", unknownDiscoveryConfig, "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected a NotFound error, got %v", err)
|
|
})
|
|
|
|
t.Run("get specific discovery config with human output", func(t *testing.T) {
|
|
buff, err := runResourceCommand(t, clt, []string{"get", dcName, "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString := buff.String()
|
|
require.Contains(t, outputString, "prod-resources")
|
|
require.Contains(t, outputString, randomDiscoveryConfigName)
|
|
})
|
|
})
|
|
|
|
t.Run("create", func(t *testing.T) {
|
|
discoveryConfigYAMLPath := filepath.Join(t.TempDir(), "discoveryConfig.yaml")
|
|
require.NoError(t, os.WriteFile(discoveryConfigYAMLPath, []byte(discoveryConfigYAML), 0644))
|
|
_, err := runResourceCommand(t, clt, []string{"create", discoveryConfigYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
buff, err := runResourceCommand(t, clt, []string{"get", "discovery_config/my-discovery-config", "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString := buff.String()
|
|
require.Contains(t, outputString, "my-discovery-config")
|
|
require.Contains(t, outputString, "mydg1")
|
|
|
|
// Update the discovery group to another group
|
|
discoveryConfigYAMLV2 := strings.ReplaceAll(discoveryConfigYAML, "mydg1", "mydg2")
|
|
require.NoError(t, os.WriteFile(discoveryConfigYAMLPath, []byte(discoveryConfigYAMLV2), 0644))
|
|
|
|
// Trying to create it again should return an error
|
|
_, err = runResourceCommand(t, clt, []string{"create", discoveryConfigYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err), "expected already exists error, got %v", err)
|
|
|
|
// Using the force should be ok and replace the current object
|
|
_, err = runResourceCommand(t, clt, []string{"create", "--force", discoveryConfigYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// The DiscoveryGroup must be updated
|
|
buff, err = runResourceCommand(t, clt, []string{"get", "discovery_config/my-discovery-config", "--format=text"})
|
|
require.NoError(t, err)
|
|
outputString = buff.String()
|
|
require.Contains(t, outputString, "mydg2")
|
|
})
|
|
}
|
|
|
|
func TestCreateLock(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
timeNow := time.Now().UTC()
|
|
fakeClock := clockwork.NewFakeClockAt(timeNow)
|
|
process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors), withFakeClock(fakeClock))
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
_, err = types.NewLock("test-lock", types.LockSpecV2{
|
|
Target: types.LockTarget{
|
|
User: "bad@actor",
|
|
},
|
|
Message: "I am a message",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Ensure there are no locks to start
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindLock, "--format=json"})
|
|
require.NoError(t, err)
|
|
locks := mustDecodeJSON[[]*types.LockV2](t, buf)
|
|
require.Empty(t, locks)
|
|
|
|
// Create the locks
|
|
lockYAMLPath := filepath.Join(t.TempDir(), "lock.yaml")
|
|
require.NoError(t, os.WriteFile(lockYAMLPath, []byte(lockYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", lockYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the locks
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindLock, "--format=json"})
|
|
require.NoError(t, err)
|
|
locks = mustDecodeJSON[[]*types.LockV2](t, buf)
|
|
require.Len(t, locks, 1)
|
|
|
|
expected, err := types.NewLock("test-lock", types.LockSpecV2{
|
|
Target: types.LockTarget{
|
|
User: "bad@actor",
|
|
},
|
|
Message: "Come see me",
|
|
})
|
|
require.NoError(t, err)
|
|
expected.SetCreatedBy(string(types.RoleAdmin))
|
|
|
|
expected.SetCreatedAt(timeNow)
|
|
|
|
require.Empty(t, cmp.Diff([]*types.LockV2{expected.(*types.LockV2)}, locks,
|
|
cmpopts.IgnoreFields(types.LockV2{}, "Metadata")))
|
|
}
|
|
|
|
// TestCreateDatabaseInInsecureMode connects to auth server with --insecure mode and creates a DB resource.
|
|
func TestCreateDatabaseInInsecureMode(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Databases: config.Databases{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
// Create the databases yaml file.
|
|
dbYAMLPath := filepath.Join(t.TempDir(), "db.yaml")
|
|
require.NoError(t, os.WriteFile(dbYAMLPath, []byte(dbYAML), 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", dbYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
const (
|
|
dbYAML = `kind: db
|
|
version: v3
|
|
metadata:
|
|
name: foo
|
|
spec:
|
|
protocol: "mysql"
|
|
uri: "localhost:3306"
|
|
tls:
|
|
mode: "verify-ca"
|
|
---
|
|
kind: db
|
|
version: v3
|
|
metadata:
|
|
name: foo-bar-1
|
|
labels:
|
|
teleport.internal/discovered-name: "foo-bar"
|
|
spec:
|
|
protocol: "postgres"
|
|
uri: "localhost:5433"
|
|
tls:
|
|
mode: "verify-full"
|
|
---
|
|
kind: db
|
|
version: v3
|
|
metadata:
|
|
name: foo-bar-2
|
|
labels:
|
|
teleport.internal/discovered-name: "foo-bar"
|
|
spec:
|
|
protocol: "postgres"
|
|
uri: "localhost:5432"`
|
|
|
|
appYAML = `kind: app
|
|
version: v3
|
|
metadata:
|
|
name: foo
|
|
spec:
|
|
uri: "tcp://localhost1"
|
|
tcp_ports:
|
|
- port: 1234
|
|
- port: 30000
|
|
end_port: 30768
|
|
---
|
|
kind: app
|
|
version: v3
|
|
metadata:
|
|
name: foo-bar-1
|
|
labels:
|
|
teleport.internal/discovered-name: "foo-bar"
|
|
spec:
|
|
uri: "localhost2"
|
|
---
|
|
kind: app
|
|
version: v3
|
|
metadata:
|
|
name: foo-bar-2
|
|
labels:
|
|
teleport.internal/discovered-name: "foo-bar"
|
|
spec:
|
|
uri: "localhost3"`
|
|
|
|
kubeYAML = `
|
|
kind: kube_cluster
|
|
version: v3
|
|
metadata:
|
|
name: foo
|
|
spec: {}
|
|
---
|
|
kind: kube_cluster
|
|
version: v3
|
|
metadata:
|
|
name: foo-bar-1
|
|
labels:
|
|
teleport.internal/discovered-name: "foo-bar"
|
|
spec: {}
|
|
---
|
|
kind: kube_cluster
|
|
version: v3
|
|
metadata:
|
|
name: foo-bar-2
|
|
labels:
|
|
teleport.internal/discovered-name: "foo-bar"
|
|
spec: {}`
|
|
|
|
lockYAML = `kind: lock
|
|
version: v2
|
|
metadata:
|
|
name: "test-lock"
|
|
spec:
|
|
target:
|
|
user: "bad@actor"
|
|
message: "Come see me"`
|
|
|
|
integrationYAML = `kind: integration
|
|
sub_kind: aws-oidc
|
|
version: v1
|
|
metadata:
|
|
name: myawsint
|
|
spec:
|
|
aws_oidc:
|
|
role_arn: "arn:aws:iam::123456789012:role/OpsTeam"
|
|
`
|
|
|
|
discoveryConfigYAML = `kind: discovery_config
|
|
version: v1
|
|
metadata:
|
|
name: my-discovery-config
|
|
spec:
|
|
discovery_group: mydg1
|
|
aws:
|
|
- types: ["ec2"]
|
|
regions: ["eu-west-2"]
|
|
`
|
|
)
|
|
|
|
func TestCreateClusterAuthPreference_WithSupportForSecondFactorWithoutQuotes(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
tests := []struct {
|
|
desc string
|
|
input string
|
|
expectError require.ErrorAssertionFunc
|
|
expectSecondFactor require.ValueAssertionFunc
|
|
}{
|
|
{desc: "handle off with quotes", input: `
|
|
kind: cluster_auth_preference
|
|
metadata:
|
|
name: cluster-auth-preference
|
|
spec:
|
|
second_factor: "off"
|
|
type: local
|
|
version: v2`,
|
|
expectError: require.NoError,
|
|
expectSecondFactor: requireEqual(constants.SecondFactorOff)},
|
|
{desc: "handle off without quotes", input: `
|
|
kind: cluster_auth_preference
|
|
metadata:
|
|
name: cluster-auth-preference
|
|
spec:
|
|
second_factor: off
|
|
type: local
|
|
version: v2`,
|
|
expectError: require.NoError,
|
|
expectSecondFactor: requireEqual(constants.SecondFactorOff)},
|
|
{desc: "handle on without quotes", input: `
|
|
kind: cluster_auth_preference
|
|
metadata:
|
|
name: cluster-auth-preference
|
|
spec:
|
|
second_factor: on
|
|
webauthn:
|
|
rp_id: localhost
|
|
type: local
|
|
version: v2`,
|
|
expectError: require.NoError,
|
|
expectSecondFactor: requireEqual(constants.SecondFactorOn)},
|
|
{desc: "unsupported numeric type as second_factor", input: `
|
|
kind: cluster_auth_preference
|
|
metadata:
|
|
name: cluster-auth-preference
|
|
spec:
|
|
second_factor: 4.3
|
|
type: local
|
|
version: v2`,
|
|
expectError: require.Error,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
capYAMLPath := filepath.Join(t.TempDir(), "cap.yaml")
|
|
require.NoError(t, os.WriteFile(capYAMLPath, []byte(tt.input), 0644))
|
|
|
|
_, err := runResourceCommand(t, clt, []string{"create", "-f", capYAMLPath})
|
|
tt.expectError(t, err)
|
|
|
|
if tt.expectSecondFactor != nil {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", "cap", "--format=json"})
|
|
require.NoError(t, err)
|
|
authPreferences := mustDecodeJSON[[]types.AuthPreferenceV2](t, buf)
|
|
require.NotEmpty(t, authPreferences)
|
|
tt.expectSecondFactor(t, authPreferences[0].Spec.SecondFactor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateSAMLIdPServiceProvider(t *testing.T) {
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
|
|
process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
tests := []struct {
|
|
desc string
|
|
input string
|
|
name string
|
|
expectError require.ErrorAssertionFunc
|
|
expectEntityID require.ValueAssertionFunc
|
|
}{
|
|
{
|
|
desc: "handle no supplied entity ID",
|
|
input: `
|
|
kind: saml_idp_service_provider
|
|
version: v1
|
|
metadata:
|
|
name: test1
|
|
spec:
|
|
entity_descriptor: |
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="IAMShowcase" validUntil="2025-12-09T09:13:31.006Z">
|
|
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sptest.iamshowcase.com/acs" index="0" isDefault="true"/>
|
|
</md:SPSSODescriptor>
|
|
</md:EntityDescriptor>
|
|
`,
|
|
name: "test1",
|
|
expectError: require.NoError,
|
|
expectEntityID: requireEqual("IAMShowcase"),
|
|
},
|
|
{
|
|
desc: "handle overwrite entity ID",
|
|
input: `
|
|
kind: saml_idp_service_provider
|
|
version: v1
|
|
metadata:
|
|
name: test1
|
|
spec:
|
|
entity_descriptor: |
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="IAMShowcase" validUntil="2025-12-09T09:13:31.006Z">
|
|
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sptest.iamshowcase.com/acs" index="0" isDefault="true"/>
|
|
</md:SPSSODescriptor>
|
|
</md:EntityDescriptor>
|
|
entity_id: never-seen-entity-id
|
|
`,
|
|
name: "test1",
|
|
expectError: require.Error,
|
|
},
|
|
{
|
|
desc: "handle invalid entity descriptor",
|
|
input: `
|
|
kind: saml_idp_service_provider
|
|
version: v1
|
|
metadata:
|
|
name: test1
|
|
spec:
|
|
entity_descriptor: |
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="IAMShowcase" validUntil="2025-12-09T09:13:31.006Z">
|
|
`,
|
|
name: "test1",
|
|
expectError: require.Error,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
spYAMLPath := filepath.Join(t.TempDir(), "sp.yaml")
|
|
require.NoError(t, os.WriteFile(spYAMLPath, []byte(tt.input), 0644))
|
|
|
|
_, err := runResourceCommand(t, clt, []string{"create", "-f", spYAMLPath})
|
|
tt.expectError(t, err)
|
|
|
|
if tt.expectEntityID != nil {
|
|
buf, err := runResourceCommand(t, clt, []string{"get", fmt.Sprintf("saml_sp/%s", tt.name), "--format=json"})
|
|
require.NoError(t, err)
|
|
sps := mustDecodeJSON[[]*types.SAMLIdPServiceProviderV1](t, buf)
|
|
tt.expectEntityID(t, sps[0].GetEntityID())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpsertVerb(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
exists bool
|
|
force bool
|
|
expected string
|
|
}{
|
|
{
|
|
name: "exists && force",
|
|
exists: true,
|
|
force: true,
|
|
expected: "created",
|
|
},
|
|
{
|
|
name: "!exists && force",
|
|
exists: false,
|
|
force: true,
|
|
expected: "created",
|
|
},
|
|
{
|
|
name: "exists && !force",
|
|
exists: true,
|
|
force: false,
|
|
expected: "updated",
|
|
},
|
|
{
|
|
name: "!exists && !force",
|
|
exists: false,
|
|
force: false,
|
|
expected: "created",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
actual := UpsertVerb(test.exists, test.force)
|
|
require.Equal(t, test.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
type dynamicResourceTest[T types.ResourceWithLabels] struct {
|
|
kind string
|
|
resourceYAML string
|
|
fooResource T
|
|
fooBar1Resource T
|
|
fooBar2Resource T
|
|
runDiscoveredNameChecks bool
|
|
}
|
|
|
|
func (test *dynamicResourceTest[T]) setup(t *testing.T) *authclient.Client {
|
|
t.Helper()
|
|
requireResource := func(t *testing.T, r T, name string) {
|
|
t.Helper()
|
|
require.NotNil(t, r, "dynamicResourceTest requires a resource named %q", name)
|
|
require.Equal(t, r.GetName(), name, "dynamicResourceTest requires a resource named %q", name)
|
|
}
|
|
requireResource(t, test.fooResource, "foo")
|
|
requireResource(t, test.fooBar1Resource, "foo-bar-1")
|
|
requireResource(t, test.fooBar2Resource, "foo-bar-2")
|
|
dynAddr := helpers.NewDynamicServiceAddr(t)
|
|
fileConfig := &config.FileConfig{
|
|
Global: config.Global{
|
|
DataDir: t.TempDir(),
|
|
},
|
|
Proxy: config.Proxy{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
},
|
|
WebAddr: dynAddr.WebAddr,
|
|
TunAddr: dynAddr.TunnelAddr,
|
|
},
|
|
Auth: config.Auth{
|
|
Service: config.Service{
|
|
EnabledFlag: "true",
|
|
ListenAddress: dynAddr.AuthAddr,
|
|
},
|
|
},
|
|
}
|
|
process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors))
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
return clt
|
|
}
|
|
|
|
func (test *dynamicResourceTest[T]) run(t *testing.T) {
|
|
t.Helper()
|
|
clt := test.setup(t)
|
|
|
|
// Initially there are no resources.
|
|
buf, err := runResourceCommand(t, clt, []string{"get", test.kind, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources := mustDecodeJSON[[]T](t, buf)
|
|
require.Empty(t, resources)
|
|
|
|
// Create the resources.
|
|
yamlPath := filepath.Join(t.TempDir(), "resources.yaml")
|
|
require.NoError(t, os.WriteFile(yamlPath, []byte(test.resourceYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", yamlPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch all resources.
|
|
buf, err = runResourceCommand(t, clt, []string{"get", test.kind, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources = mustDecodeJSON[[]T](t, buf)
|
|
require.Len(t, resources, 3)
|
|
require.Empty(t, cmp.Diff([]T{test.fooResource, test.fooBar1Resource, test.fooBar2Resource}, resources,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
|
|
// Fetch specific resource.
|
|
buf, err = runResourceCommand(t, clt,
|
|
[]string{"get", fmt.Sprintf("%v/%v", test.kind, test.fooResource.GetName()), "--format=json"})
|
|
require.NoError(t, err)
|
|
resources = mustDecodeJSON[[]T](t, buf)
|
|
require.Len(t, resources, 1)
|
|
require.Empty(t, cmp.Diff([]T{test.fooResource}, resources,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
|
|
// Remove a resource.
|
|
_, err = runResourceCommand(t, clt, []string{"rm", fmt.Sprintf("%v/%v", test.kind, test.fooResource.GetName())})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch all resources again.
|
|
buf, err = runResourceCommand(t, clt, []string{"get", test.kind, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources = mustDecodeJSON[[]T](t, buf)
|
|
require.Len(t, resources, 2)
|
|
require.Empty(t, cmp.Diff([]T{test.fooBar1Resource, test.fooBar2Resource}, resources,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
|
|
if !test.runDiscoveredNameChecks {
|
|
return
|
|
}
|
|
|
|
// Test discovered name behavior.
|
|
// Fetching multiple resources ("foo-bar-1" and "foo-bar-2") by discovered name is ok.
|
|
buf, err = runResourceCommand(t, clt, []string{"get", fmt.Sprintf("%v/%v", test.kind, "foo-bar"), "--format=json"})
|
|
require.NoError(t, err)
|
|
resources = mustDecodeJSON[[]T](t, buf)
|
|
require.Len(t, resources, 2)
|
|
require.Empty(t, cmp.Diff([]T{test.fooBar1Resource, test.fooBar2Resource}, resources,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
|
|
// Removing multiple resources ("foo-bar-1" and "foo-bar-2") by discovered name is an error.
|
|
_, err = runResourceCommand(t, clt, []string{"rm", fmt.Sprintf("%v/%v", test.kind, "foo-bar")})
|
|
require.ErrorContains(t, err, "matches multiple")
|
|
|
|
// Remove "foo-bar-2" resource by full name.
|
|
_, err = runResourceCommand(t, clt, []string{"rm", fmt.Sprintf("%v/%v", test.kind, test.fooBar2Resource.GetName())})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch all resources again.
|
|
buf, err = runResourceCommand(t, clt, []string{"get", test.kind, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources = mustDecodeJSON[[]T](t, buf)
|
|
require.Len(t, resources, 1)
|
|
require.Empty(t, cmp.Diff([]T{test.fooBar1Resource}, resources,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
}
|
|
|
|
// TestDatabaseResource tests tctl commands that manage database resources.
|
|
func TestDatabaseResource(t *testing.T) {
|
|
t.Parallel()
|
|
dbFoo, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "foo",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: defaults.ProtocolMySQL,
|
|
URI: "localhost:3306",
|
|
TLS: types.DatabaseTLS{
|
|
Mode: types.DatabaseTLSMode_VERIFY_CA,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
dbFooBar, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "foo-bar-1",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic, types.DiscoveredNameLabel: "foo-bar"},
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: defaults.ProtocolPostgres,
|
|
URI: "localhost:5433",
|
|
TLS: types.DatabaseTLS{
|
|
Mode: types.DatabaseTLSMode_VERIFY_FULL,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
dbFooBar2, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "foo-bar-2",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic, types.DiscoveredNameLabel: "foo-bar"},
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: defaults.ProtocolPostgres,
|
|
URI: "localhost:5432",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
test := dynamicResourceTest[*types.DatabaseV3]{
|
|
kind: types.KindDatabase,
|
|
resourceYAML: dbYAML,
|
|
fooResource: dbFoo,
|
|
fooBar1Resource: dbFooBar,
|
|
fooBar2Resource: dbFooBar2,
|
|
runDiscoveredNameChecks: true,
|
|
}
|
|
test.run(t)
|
|
}
|
|
|
|
// TestKubeClusterResource tests tctl commands that manage dynamic kube cluster resources.
|
|
func TestKubeClusterResource(t *testing.T) {
|
|
t.Parallel()
|
|
kubeFoo, err := types.NewKubernetesClusterV3(types.Metadata{
|
|
Name: "foo",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
|
|
}, types.KubernetesClusterSpecV3{})
|
|
require.NoError(t, err)
|
|
kubeFooBar1, err := types.NewKubernetesClusterV3(types.Metadata{
|
|
Name: "foo-bar-1",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic, types.DiscoveredNameLabel: "foo-bar"},
|
|
}, types.KubernetesClusterSpecV3{})
|
|
require.NoError(t, err)
|
|
kubeFooBar2, err := types.NewKubernetesClusterV3(types.Metadata{
|
|
Name: "foo-bar-2",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic, types.DiscoveredNameLabel: "foo-bar"},
|
|
}, types.KubernetesClusterSpecV3{})
|
|
require.NoError(t, err)
|
|
test := dynamicResourceTest[*types.KubernetesClusterV3]{
|
|
kind: types.KindKubernetesCluster,
|
|
resourceYAML: kubeYAML,
|
|
fooResource: kubeFoo,
|
|
fooBar1Resource: kubeFooBar1,
|
|
fooBar2Resource: kubeFooBar2,
|
|
runDiscoveredNameChecks: true,
|
|
}
|
|
test.run(t)
|
|
}
|
|
|
|
// TestAppResource tests tctl commands that manage application resources.
|
|
func TestAppResource(t *testing.T) {
|
|
t.Parallel()
|
|
appFoo, err := types.NewAppV3(types.Metadata{
|
|
Name: "foo",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
|
|
}, types.AppSpecV3{
|
|
URI: "tcp://localhost1",
|
|
TCPPorts: []*types.PortRange{
|
|
&types.PortRange{Port: 1234},
|
|
&types.PortRange{Port: 30000, EndPort: 30768},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
appFooBar1, err := types.NewAppV3(types.Metadata{
|
|
Name: "foo-bar-1",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic, types.DiscoveredNameLabel: "foo-bar"},
|
|
}, types.AppSpecV3{
|
|
URI: "localhost2",
|
|
})
|
|
require.NoError(t, err)
|
|
appFooBar2, err := types.NewAppV3(types.Metadata{
|
|
Name: "foo-bar-2",
|
|
Labels: map[string]string{types.OriginLabel: types.OriginDynamic, types.DiscoveredNameLabel: "foo-bar"},
|
|
}, types.AppSpecV3{
|
|
URI: "localhost3",
|
|
})
|
|
require.NoError(t, err)
|
|
test := dynamicResourceTest[*types.AppV3]{
|
|
kind: types.KindApp,
|
|
resourceYAML: appYAML,
|
|
fooResource: appFoo,
|
|
fooBar1Resource: appFooBar1,
|
|
fooBar2Resource: appFooBar2,
|
|
}
|
|
test.run(t)
|
|
}
|
|
|
|
// requireEqual creates an assertion function with a bound `expected` value
|
|
// for use with table-driven tests
|
|
func requireEqual(expected any) require.ValueAssertionFunc {
|
|
return func(t require.TestingT, actual any, msgAndArgs ...any) {
|
|
require.Equal(t, expected, actual, msgAndArgs...)
|
|
}
|
|
}
|
|
|
|
// helper for decoding the output of runResourceCommand and checking we got
|
|
// the databases expected.
|
|
func requireGotDatabaseServers(t *testing.T, buf *bytes.Buffer, want ...types.Database) {
|
|
t.Helper()
|
|
servers := mustDecodeJSON[[]*types.DatabaseServerV3](t, buf)
|
|
require.Len(t, servers, len(want))
|
|
databases := types.Databases{}
|
|
for _, server := range servers {
|
|
databases = append(databases, server.GetDatabase())
|
|
}
|
|
require.Empty(t, cmp.Diff(types.Databases(want).ToMap(), databases.ToMap(),
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Namespace", "Expires"),
|
|
cmpopts.IgnoreFields(types.DatabaseStatusV3{}, "VNetDNSName"),
|
|
))
|
|
}
|
|
|
|
// TestCreateResources asserts that tctl create and tctl create -f
|
|
// operate as expected when a resource does and does not already exist.
|
|
func TestCreateResources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
process, err := testenv.NewTeleportProcess(t.TempDir(), testenv.WithLogger(logtest.NewLogger()))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, process.Close())
|
|
require.NoError(t, process.Wait())
|
|
})
|
|
rootClient, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = rootClient.Close() })
|
|
|
|
// tctlGetAllValidations allows tests to register post-test validations to validate
|
|
// that their resource is present in "tctl get all" output.
|
|
// This allows running test rows instead of the whole test table.
|
|
var tctlGetAllValidations []func(t *testing.T, out string)
|
|
|
|
tests := []struct {
|
|
kind string
|
|
create func(t *testing.T, clt *authclient.Client)
|
|
getAllCheck func(t *testing.T, out string)
|
|
}{
|
|
{
|
|
kind: types.KindGithubConnector,
|
|
create: testCreateGithubConnector,
|
|
getAllCheck: func(t *testing.T, s string) {
|
|
assert.Contains(t, s, "kind: github")
|
|
},
|
|
},
|
|
{
|
|
kind: types.KindRole,
|
|
create: testCreateRole,
|
|
getAllCheck: func(t *testing.T, s string) {
|
|
assert.Contains(t, s, "kind: role")
|
|
},
|
|
},
|
|
{
|
|
kind: types.KindServerInfo,
|
|
create: testCreateServerInfo,
|
|
},
|
|
{
|
|
kind: types.KindUser,
|
|
create: testCreateUser,
|
|
getAllCheck: func(t *testing.T, s string) {
|
|
assert.Contains(t, s, "kind: user")
|
|
},
|
|
},
|
|
{
|
|
kind: "empty-doc",
|
|
create: testCreateWithEmptyDocument,
|
|
},
|
|
{
|
|
kind: types.KindDatabaseObjectImportRule,
|
|
create: testCreateDatabaseObjectImportRule,
|
|
},
|
|
{
|
|
kind: types.KindDatabaseObject,
|
|
create: testCreateDatabaseObject,
|
|
},
|
|
{
|
|
kind: types.KindClusterNetworkingConfig,
|
|
create: testCreateClusterNetworkingConfig,
|
|
getAllCheck: func(t *testing.T, s string) {
|
|
assert.Contains(t, s, "kind: cluster_networking_config")
|
|
},
|
|
},
|
|
{
|
|
kind: types.KindClusterAuthPreference,
|
|
create: testCreateAuthPreference,
|
|
getAllCheck: func(t *testing.T, s string) {
|
|
assert.Contains(t, s, "kind: cluster_auth_preference")
|
|
},
|
|
},
|
|
{
|
|
kind: types.KindSessionRecordingConfig,
|
|
create: testCreateSessionRecordingConfig,
|
|
},
|
|
{
|
|
kind: types.KindAppServer,
|
|
create: testCreateAppServer,
|
|
},
|
|
{
|
|
kind: types.KindStaticHostUser,
|
|
create: testCreateStaticHostUser,
|
|
},
|
|
{
|
|
kind: types.KindAutoUpdateConfig,
|
|
create: testCreateAutoUpdateConfig,
|
|
},
|
|
{
|
|
kind: types.KindAutoUpdateVersion,
|
|
create: testCreateAutoUpdateVersion,
|
|
},
|
|
{
|
|
kind: types.KindAutoUpdateAgentRollout,
|
|
create: testCreateAutoUpdateAgentRollout,
|
|
},
|
|
{
|
|
kind: types.KindDynamicWindowsDesktop,
|
|
create: testCreateDynamicWindowsDesktop,
|
|
},
|
|
{
|
|
kind: types.KindHealthCheckConfig,
|
|
create: testCreateHealthCheckConfig,
|
|
getAllCheck: func(t *testing.T, s string) {
|
|
assert.Contains(t, s, "kind: health_check_config")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.kind, func(t *testing.T) {
|
|
test.create(t, rootClient)
|
|
if test.getAllCheck != nil {
|
|
tctlGetAllValidations = append(tctlGetAllValidations, test.getAllCheck)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Verify that the created resources appear in tctl get all
|
|
out, err := runResourceCommand(t, rootClient, []string{"get", "all"})
|
|
require.NoError(t, err)
|
|
s := out.String()
|
|
for _, validateGetAll := range tctlGetAllValidations {
|
|
validateGetAll(t, s)
|
|
}
|
|
}
|
|
|
|
func testCreateGithubConnector(t *testing.T, clt *authclient.Client) {
|
|
// Ensure there are no connectors to start
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindGithubConnector, "--format=json"})
|
|
require.NoError(t, err)
|
|
connectors := mustDecodeJSON[[]*types.GithubConnectorV3](t, buf)
|
|
require.Empty(t, connectors)
|
|
|
|
const connectorYAML = `kind: github
|
|
metadata:
|
|
name: github
|
|
spec:
|
|
client_id: "12345"
|
|
client_secret: "678910"
|
|
display: Github
|
|
redirect_url: https://proxy.example.com/v1/webapi/github/callback
|
|
teams_to_roles:
|
|
- organization: acme
|
|
roles:
|
|
- access
|
|
- editor
|
|
- auditor
|
|
team: users
|
|
version: v3`
|
|
|
|
// Create the connector
|
|
connectorYAMLPath := filepath.Join(t.TempDir(), "connector.yaml")
|
|
require.NoError(t, os.WriteFile(connectorYAMLPath, []byte(connectorYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", connectorYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the connector
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindGithubConnector, "--format=json"})
|
|
require.NoError(t, err)
|
|
connectors = mustDecodeJSON[[]*types.GithubConnectorV3](t, buf)
|
|
require.Len(t, connectors, 1)
|
|
|
|
var expected types.GithubConnectorV3
|
|
require.NoError(t, yaml.Unmarshal([]byte(connectorYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*types.GithubConnectorV3{&expected},
|
|
connectors,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
cmpopts.IgnoreFields(types.GithubConnectorSpecV3{}, "ClientSecret"), // get retrieves the connector without secrets
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the user with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
connectorBytes, err := services.MarshalGithubConnector(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(connectorYAMLPath, connectorBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", connectorYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", connectorYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateRole(t *testing.T, clt *authclient.Client) {
|
|
// Ensure that our test role does not exist
|
|
_, err := runResourceCommand(t, clt, []string{"get", types.KindRole + "/test-role", "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected test-role to not exist prior to being created")
|
|
|
|
const roleYAML = `kind: role
|
|
metadata:
|
|
name: test-role
|
|
spec:
|
|
allow:
|
|
app_labels:
|
|
'*': '*'
|
|
db_labels:
|
|
'*': '*'
|
|
kubernetes_labels:
|
|
'*': '*'
|
|
kubernetes_resources:
|
|
- kind: pod
|
|
name: '*'
|
|
namespace: '*'
|
|
logins:
|
|
- test
|
|
node_labels:
|
|
'*': '*'
|
|
deny: {}
|
|
options:
|
|
cert_format: standard
|
|
create_db_user: false
|
|
create_desktop_user: false
|
|
desktop_clipboard: true
|
|
desktop_directory_sharing: true
|
|
enhanced_recording:
|
|
- command
|
|
- network
|
|
forward_agent: false
|
|
idp:
|
|
saml:
|
|
enabled: true
|
|
max_session_ttl: 30h0m0s
|
|
pin_source_ip: false
|
|
port_forwarding: true
|
|
record_session:
|
|
default: best_effort
|
|
desktop: true
|
|
ssh_file_copy: true
|
|
version: v7
|
|
`
|
|
|
|
// Create the role
|
|
roleYAMLPath := filepath.Join(t.TempDir(), "role.yaml")
|
|
require.NoError(t, os.WriteFile(roleYAMLPath, []byte(roleYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", roleYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the role
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindRole + "/test-role", "--format=json"})
|
|
require.NoError(t, err)
|
|
roles := mustDecodeJSON[[]*types.RoleV6](t, buf)
|
|
require.Len(t, roles, 1)
|
|
|
|
var expected types.RoleV6
|
|
require.NoError(t, yaml.Unmarshal([]byte(roleYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*types.RoleV6{&expected},
|
|
roles,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the role with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
connectorBytes, err := services.MarshalRole(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(roleYAMLPath, connectorBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", roleYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", roleYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateServerInfo(t *testing.T, clt *authclient.Client) {
|
|
// Ensure that our test server info does not exist
|
|
_, err := runResourceCommand(t, clt, []string{"get", types.KindServerInfo + "/test-server-info", "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected test-role to not exist prior to being created")
|
|
|
|
const serverInfoYAML = `---
|
|
kind: server_info
|
|
sub_kind: cloud_info
|
|
version: v1
|
|
metadata:
|
|
name: test-server-info
|
|
spec:
|
|
new_labels:
|
|
'a': '1'
|
|
'b': '2'
|
|
`
|
|
|
|
// Create the server info
|
|
serverInfoYAMLPath := filepath.Join(t.TempDir(), "server-info.yaml")
|
|
err = os.WriteFile(serverInfoYAMLPath, []byte(serverInfoYAML), 0644)
|
|
require.NoError(t, err)
|
|
_, err = runResourceCommand(t, clt, []string{"create", serverInfoYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the server info
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindServerInfo + "/test-server-info", "--format=json"})
|
|
require.NoError(t, err)
|
|
serverInfos := mustDecodeJSON[[]*types.ServerInfoV1](t, buf)
|
|
require.Len(t, serverInfos, 1)
|
|
|
|
var expected types.ServerInfoV1
|
|
err = yaml.Unmarshal([]byte(serverInfoYAML), &expected)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*types.ServerInfoV1{&expected},
|
|
serverInfos,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the resource with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
newRevisionServerInfo, err := services.MarshalServerInfo(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(serverInfoYAMLPath, newRevisionServerInfo, 0644)
|
|
require.NoError(t, err)
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", serverInfoYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", serverInfoYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateWithEmptyDocument(t *testing.T, clt *authclient.Client) {
|
|
const userYAML = `
|
|
---
|
|
kind: user
|
|
version: v2
|
|
metadata:
|
|
name: llama2
|
|
spec:
|
|
roles: ["access"]
|
|
`
|
|
userYAMLPath := filepath.Join(t.TempDir(), "user.yaml")
|
|
require.NoError(t, os.WriteFile(userYAMLPath, []byte(userYAML), 0644))
|
|
_, err := runResourceCommand(t, clt, []string{"create", userYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateUser(t *testing.T, clt *authclient.Client) {
|
|
// Ensure that our test user does not exist
|
|
_, err := runResourceCommand(t, clt, []string{"get", types.KindUser + "/llama", "--format=json"})
|
|
require.True(t, trace.IsNotFound(err), "expected llama user to not exist prior to being created")
|
|
|
|
const userYAML = `kind: user
|
|
version: v2
|
|
metadata:
|
|
name: llama
|
|
spec:
|
|
roles: ["access"]
|
|
`
|
|
|
|
// Create the user
|
|
userYAMLPath := filepath.Join(t.TempDir(), "user.yaml")
|
|
require.NoError(t, os.WriteFile(userYAMLPath, []byte(userYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", userYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the user
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindUser + "/llama", "--format=json"})
|
|
require.NoError(t, err)
|
|
users := mustDecodeJSON[[]*types.UserV2](t, buf)
|
|
require.Len(t, users, 1)
|
|
|
|
var expected types.UserV2
|
|
require.NoError(t, yaml.Unmarshal([]byte(userYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*types.UserV2{&expected},
|
|
users,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
cmpopts.IgnoreFields(types.UserSpecV2{}, "CreatedBy"),
|
|
cmpopts.IgnoreFields(types.UserV2{}, "Status"),
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the user with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
connectorBytes, err := services.MarshalUser(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(userYAMLPath, connectorBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", userYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", userYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateDatabaseObjectImportRule(t *testing.T, clt *authclient.Client) {
|
|
const resourceYAML = `kind: db_object_import_rule
|
|
metadata:
|
|
expires: "2034-03-22T18:06:35.161162Z"
|
|
id: 1711129895244889000
|
|
name: import_all_staging_tables
|
|
namespace: default
|
|
spec:
|
|
database_labels:
|
|
- name: env
|
|
values:
|
|
- staging
|
|
- prod
|
|
- name: owner_org
|
|
values:
|
|
- trading
|
|
mappings:
|
|
- add_labels:
|
|
custom_label: my_custom_value
|
|
env: staging
|
|
match:
|
|
procedure_names:
|
|
- aaa
|
|
- bbb
|
|
- ccc
|
|
table_names:
|
|
- '*'
|
|
view_names:
|
|
- "1"
|
|
- "2"
|
|
- "3"
|
|
scope:
|
|
database_names:
|
|
- foo
|
|
- bar
|
|
- baz
|
|
schema_names:
|
|
- public
|
|
priority: 30
|
|
version: v1
|
|
`
|
|
|
|
// Verify there is no matching resource
|
|
const resourceKey = "db_object_import_rule/import_all_staging_tables"
|
|
_, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"})
|
|
require.Error(t, err)
|
|
|
|
// Create the resource
|
|
resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml")
|
|
require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", resourceYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the resource
|
|
buf, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources := mustDecodeJSON[[]databaseobjectimportrule.Resource](t, buf)
|
|
require.Len(t, resources, 1)
|
|
|
|
// Compare with baseline
|
|
cmpOpts := []cmp.Option{
|
|
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
|
protocmp.Transform(),
|
|
}
|
|
|
|
var expected databaseobjectimportrule.Resource
|
|
require.NoError(t, yaml.Unmarshal([]byte(resourceYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(expected, resources[0], cmpOpts...))
|
|
require.Empty(t, cmp.Diff(databaseobjectimportrule.ResourceToProto(&expected), databaseobjectimportrule.ResourceToProto(&resources[0]), cmpOpts...))
|
|
}
|
|
|
|
func testCreateClusterNetworkingConfig(t *testing.T, clt *authclient.Client) {
|
|
// Get the initial cnc.
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindClusterNetworkingConfig, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
cnc := mustDecodeJSON[[]*types.ClusterNetworkingConfigV2](t, buf)
|
|
require.Len(t, cnc, 1)
|
|
initial := cnc[0]
|
|
|
|
const cncYAML = `kind: cluster_networking_config
|
|
metadata:
|
|
name: cluster-networking-config
|
|
spec:
|
|
client_idle_timeout: 0s
|
|
idle_timeout_message: ""
|
|
keep_alive_count_max: 300
|
|
case_insensitive_routing: true
|
|
keep_alive_interval: 5m0s
|
|
proxy_listener_mode: 1
|
|
session_control_timeout: 0s
|
|
tunnel_strategy:
|
|
type: agent_mesh
|
|
web_idle_timeout: 0s
|
|
version: v2
|
|
`
|
|
|
|
// Create the cnc
|
|
cncYAMLPath := filepath.Join(t.TempDir(), "cnc.yaml")
|
|
require.NoError(t, os.WriteFile(cncYAMLPath, []byte(cncYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", cncYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the cnc
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindClusterNetworkingConfig, "--format=json"})
|
|
require.NoError(t, err)
|
|
cnc = mustDecodeJSON[[]*types.ClusterNetworkingConfigV2](t, buf)
|
|
require.Len(t, cnc, 1)
|
|
|
|
var expected types.ClusterNetworkingConfigV2
|
|
require.NoError(t, yaml.Unmarshal([]byte(cncYAML), &expected))
|
|
|
|
require.NotEqual(t, int64(300), initial.GetKeepAliveCountMax())
|
|
require.False(t, initial.GetCaseInsensitiveRouting())
|
|
require.True(t, expected.GetCaseInsensitiveRouting())
|
|
require.Equal(t, int64(300), expected.GetKeepAliveCountMax())
|
|
|
|
// Explicitly change the revision and try creating the cnc with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
raw, err := services.MarshalClusterNetworkingConfig(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(cncYAMLPath, raw, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", cncYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", cncYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateAuthPreference(t *testing.T, clt *authclient.Client) {
|
|
// Get the initial CAP.
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindClusterAuthPreference, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
cap := mustDecodeJSON[[]*types.AuthPreferenceV2](t, buf)
|
|
require.Len(t, cap, 1)
|
|
initial := cap[0]
|
|
|
|
const capYAML = `kind: cluster_auth_preference
|
|
metadata:
|
|
name: cluster-auth-preference
|
|
spec:
|
|
second_factors: [otp, sso]
|
|
type: local
|
|
version: v2
|
|
`
|
|
|
|
// Create the cap
|
|
capYAMLPath := filepath.Join(t.TempDir(), "cap.yaml")
|
|
require.NoError(t, os.WriteFile(capYAMLPath, []byte(capYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", capYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the cap
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindClusterAuthPreference, "--format=json"})
|
|
require.NoError(t, err)
|
|
cap = mustDecodeJSON[[]*types.AuthPreferenceV2](t, buf)
|
|
require.Len(t, cap, 1)
|
|
|
|
expectInitialSecondFactors := []types.SecondFactorType{types.SecondFactorType_SECOND_FACTOR_TYPE_OTP} // second factors defaults to [otp]
|
|
require.Equal(t, expectInitialSecondFactors, initial.GetSecondFactors())
|
|
|
|
var revised types.AuthPreferenceV2
|
|
require.NoError(t, yaml.Unmarshal([]byte(capYAML), &revised))
|
|
expectRevisedSecondFactors := []types.SecondFactorType{types.SecondFactorType_SECOND_FACTOR_TYPE_OTP, types.SecondFactorType_SECOND_FACTOR_TYPE_SSO}
|
|
require.Equal(t, expectRevisedSecondFactors, revised.GetSecondFactors())
|
|
|
|
// Explicitly change the revision and try creating the cap with and without
|
|
// the force flag.
|
|
revised.SetRevision(uuid.NewString())
|
|
raw, err := services.MarshalAuthPreference(&revised, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(capYAMLPath, raw, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", capYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", capYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateSessionRecordingConfig(t *testing.T, clt *authclient.Client) {
|
|
// Get the initial recording config.
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindSessionRecordingConfig, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
src := mustDecodeJSON[[]*types.SessionRecordingConfigV2](t, buf)
|
|
require.Len(t, src, 1)
|
|
initial := src[0]
|
|
|
|
const srcYAML = `kind: session_recording_config
|
|
metadata:
|
|
labels:
|
|
teleport.dev/origin: defaults
|
|
name: session-recording-config
|
|
spec:
|
|
mode: proxy
|
|
version: v2
|
|
`
|
|
|
|
// Create the src
|
|
srcYAMLPath := filepath.Join(t.TempDir(), "src.yaml")
|
|
require.NoError(t, os.WriteFile(srcYAMLPath, []byte(srcYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", srcYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the cap
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindSessionRecordingConfig, "--format=json"})
|
|
require.NoError(t, err)
|
|
src = mustDecodeJSON[[]*types.SessionRecordingConfigV2](t, buf)
|
|
require.Len(t, src, 1)
|
|
|
|
var expected types.SessionRecordingConfigV2
|
|
require.NoError(t, yaml.Unmarshal([]byte(srcYAML), &expected))
|
|
|
|
require.Equal(t, types.RecordOff, initial.GetMode())
|
|
require.Equal(t, types.RecordAtProxy, expected.GetMode())
|
|
|
|
// Explicitly change the revision and try creating the src with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
raw, err := services.MarshalSessionRecordingConfig(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(srcYAMLPath, raw, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", srcYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", srcYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateAppServer(t *testing.T, clt *authclient.Client) {
|
|
const appServerWithIntegrationYAML = `---
|
|
kind: app_server
|
|
metadata:
|
|
name: my-integration
|
|
labels:
|
|
account_id: "123456789012"
|
|
spec:
|
|
app:
|
|
kind: app
|
|
metadata:
|
|
name: my-integration
|
|
labels:
|
|
account_id: "123456789012"
|
|
spec:
|
|
uri: https://console.aws.amazon.com
|
|
integration: my-integration
|
|
public_addr: integration.example.com
|
|
version: v3
|
|
host_id: c6cfe5c2-653f-4e5d-a914-bfac5a7baf38
|
|
version: v3
|
|
`
|
|
|
|
const appServerWithoutIntegrationYAML = `---
|
|
kind: app_server
|
|
metadata:
|
|
name: my-integration
|
|
spec:
|
|
app:
|
|
kind: app
|
|
metadata:
|
|
name: my-integration
|
|
spec:
|
|
uri: https://console.aws.amazon.com
|
|
public_addr: integration.example.com
|
|
version: v3
|
|
host_id: c6cfe5c2-653f-4e5d-a914-bfac5a7baf38
|
|
version: v3
|
|
`
|
|
|
|
// Creating an AppServer with integration is valid.
|
|
srcYAMLPath := filepath.Join(t.TempDir(), "appServerWithIntegrationYAML.yaml")
|
|
require.NoError(t, os.WriteFile(srcYAMLPath, []byte(appServerWithIntegrationYAML), 0644))
|
|
_, err := runResourceCommand(t, clt, []string{"create", srcYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Creating an AppServer without integration is invalid.
|
|
srcYAMLPath = filepath.Join(t.TempDir(), "appServerWithoutIntegrationYAML.yaml")
|
|
require.NoError(t, os.WriteFile(srcYAMLPath, []byte(appServerWithoutIntegrationYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", srcYAMLPath})
|
|
require.ErrorContains(t, err, "integration")
|
|
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindAppServer, "--format=json"})
|
|
require.NoError(t, err)
|
|
appServers := mustDecodeJSON[[]*types.AppServerV3](t, buf)
|
|
require.Len(t, appServers, 1)
|
|
|
|
expectedAppServer, err := types.NewAppServerForAWSOIDCIntegration("my-integration", "c6cfe5c2-653f-4e5d-a914-bfac5a7baf38", "integration.example.com", map[string]string{"account_id": "123456789012"})
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(
|
|
expectedAppServer,
|
|
appServers[0],
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
}
|
|
|
|
func testCreateDatabaseObject(t *testing.T, clt *authclient.Client) {
|
|
const resourceYAML = `kind: db_object
|
|
metadata:
|
|
expires: "2034-03-22T18:06:35.161162Z"
|
|
id: 1711129895244889000
|
|
labels:
|
|
database: foo
|
|
kind: table
|
|
name: page_views
|
|
protocol: postgres
|
|
schema: web_metrics
|
|
service_name: pg-docker
|
|
name: test_table
|
|
revision: 066f87d9-02cf-4062-9419-96523664c082
|
|
spec:
|
|
database: foo
|
|
database_service_name: pg-docker
|
|
name: page_views
|
|
object_kind: table
|
|
protocol: postgres
|
|
schema: web_metrics
|
|
version: v1
|
|
`
|
|
|
|
// Verify there are no pre-existing objects
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindDatabaseObject, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
resources := mustDecodeJSON[[]databaseobject.Resource](t, buf)
|
|
require.Empty(t, resources)
|
|
|
|
// Create the resource
|
|
resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml")
|
|
require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", resourceYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the resource
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindDatabaseObject, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources = mustDecodeJSON[[]databaseobject.Resource](t, buf)
|
|
require.Len(t, resources, 1)
|
|
|
|
// Compare with baseline
|
|
cmpOpts := []cmp.Option{
|
|
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
|
protocmp.Transform(),
|
|
}
|
|
|
|
var expected databaseobject.Resource
|
|
require.NoError(t, yaml.Unmarshal([]byte(resourceYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(expected, resources[0], cmpOpts...))
|
|
require.Empty(t, cmp.Diff(databaseobject.ResourceToProto(&expected), databaseobject.ResourceToProto(&resources[0]), cmpOpts...))
|
|
}
|
|
|
|
// TestCreateEnterpriseResources asserts that tctl create
|
|
// behaves as expected for enterprise resources. The tests are
|
|
// grouped to amortize the cost of creating and auth server since
|
|
// that is the most expensive part of testing editing the resource.
|
|
func TestCreateEnterpriseResources(t *testing.T) {
|
|
t.Parallel()
|
|
process, err := testenv.NewTeleportProcess(t.TempDir(), testenv.WithConfig(func(cfg *servicecfg.Config) {
|
|
cfg.Modules = &modulestest.Modules{
|
|
TestBuildType: modules.BuildEnterprise,
|
|
TestFeatures: modules.Features{
|
|
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
|
|
entitlements.OIDC: {Enabled: true},
|
|
entitlements.SAML: {Enabled: true},
|
|
},
|
|
},
|
|
}
|
|
}))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, process.Close())
|
|
require.NoError(t, process.Wait())
|
|
})
|
|
clt, err := testenv.NewDefaultAuthClient(process)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = clt.Close() })
|
|
|
|
tests := []struct {
|
|
kind string
|
|
create func(t *testing.T, clt *authclient.Client)
|
|
}{
|
|
{
|
|
kind: types.KindOIDCConnector,
|
|
create: testCreateOIDCConnector,
|
|
},
|
|
{
|
|
kind: types.KindSAMLConnector,
|
|
create: testCreateSAMLConnector,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.kind, func(t *testing.T) {
|
|
test.create(t, clt)
|
|
})
|
|
}
|
|
|
|
// Verify that the created resources appear in tctl get all
|
|
out, err := runResourceCommand(t, clt, []string{"get", "all"})
|
|
require.NoError(t, err)
|
|
s := out.String()
|
|
require.NotEmpty(t, s)
|
|
assert.Contains(t, s, "kind: saml")
|
|
assert.Contains(t, s, "kind: oidc")
|
|
}
|
|
|
|
func testCreateOIDCConnector(t *testing.T, clt *authclient.Client) {
|
|
// Ensure there are no connectors to start
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindOIDCConnector, "--format=json"})
|
|
require.NoError(t, err)
|
|
connectors := mustDecodeJSON[[]*types.OIDCConnectorV3](t, buf)
|
|
require.Empty(t, connectors)
|
|
|
|
const connectorYAML = `kind: oidc
|
|
version: v3
|
|
metadata:
|
|
name: oidc
|
|
spec:
|
|
redirect_url: "https://proxy.example.com/v1/webapi/oidc/callback"
|
|
client_id: "12345"
|
|
client_secret: "678910"
|
|
display: OIDC
|
|
pkce_mode: "enabled"
|
|
scope: [roles]
|
|
claims_to_roles:
|
|
- {claim: "test", value: "test", roles: ["access", "editor", "auditor"]}`
|
|
|
|
// Create the connector
|
|
connectorYAMLPath := filepath.Join(t.TempDir(), "connector.yaml")
|
|
require.NoError(t, os.WriteFile(connectorYAMLPath, []byte(connectorYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", connectorYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the connector
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindOIDCConnector, "--format=json"})
|
|
require.NoError(t, err)
|
|
connectors = mustDecodeJSON[[]*types.OIDCConnectorV3](t, buf)
|
|
require.Len(t, connectors, 1)
|
|
|
|
var expected types.OIDCConnectorV3
|
|
require.NoError(t, yaml.Unmarshal([]byte(connectorYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*types.OIDCConnectorV3{&expected},
|
|
connectors,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
cmpopts.IgnoreFields(types.OIDCConnectorSpecV3{}, "ClientSecret"), // get retrieves the connector without secrets
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the user with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
connectorBytes, err := services.MarshalOIDCConnector(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(connectorYAMLPath, connectorBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", connectorYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", connectorYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateSAMLConnector(t *testing.T, clt *authclient.Client) {
|
|
// Ensure there are no connectors to start
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindSAMLConnector, "--format=json"})
|
|
require.NoError(t, err)
|
|
connectors := mustDecodeJSON[[]*types.SAMLConnectorV2](t, buf)
|
|
require.Empty(t, connectors)
|
|
|
|
const connectorYAML = `kind: saml
|
|
version: v2
|
|
metadata:
|
|
name: saml
|
|
spec:
|
|
acs: test
|
|
audience: test
|
|
issuer: test
|
|
sso: https://example.com
|
|
service_provider_issuer: test
|
|
display: SAML
|
|
attributes_to_roles:
|
|
- name: test
|
|
roles:
|
|
- access
|
|
value: test
|
|
entity_descriptor: |
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="test">
|
|
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
<md:KeyDescriptor use="signing">
|
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
<ds:X509Data>
|
|
<ds:X509Certificate></ds:X509Certificate>
|
|
</ds:X509Data>
|
|
</ds:KeyInfo>
|
|
</md:KeyDescriptor>
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
|
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com" />
|
|
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com" />
|
|
</md:IDPSSODescriptor>
|
|
</md:EntityDescriptor>` + "\n"
|
|
|
|
// Create the connector
|
|
connectorYAMLPath := filepath.Join(t.TempDir(), "connector.yaml")
|
|
require.NoError(t, os.WriteFile(connectorYAMLPath, []byte(connectorYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", connectorYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the connector
|
|
buf, err = runResourceCommand(t, clt, []string{"get", types.KindSAMLConnector, "--format=json"})
|
|
require.NoError(t, err)
|
|
connectors = mustDecodeJSON[[]*types.SAMLConnectorV2](t, buf)
|
|
require.Len(t, connectors, 1)
|
|
|
|
var expected types.SAMLConnectorV2
|
|
require.NoError(t, yaml.Unmarshal([]byte(connectorYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*types.SAMLConnectorV2{&expected},
|
|
connectors,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
cmpopts.IgnoreFields(types.SAMLConnectorSpecV2{}, "SigningKeyPair"), // get retrieves the connector without secrets
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the user with and without
|
|
// the force flag.
|
|
expected.SetRevision(uuid.NewString())
|
|
connectorBytes, err := services.MarshalSAMLConnector(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(connectorYAMLPath, connectorBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", connectorYAMLPath})
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", connectorYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateStaticHostUser(t *testing.T, clt *authclient.Client) {
|
|
// Ensure that our test user does not exist
|
|
resourceName := "alice"
|
|
resourceKey := types.KindStaticHostUser + "/" + resourceName
|
|
_, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err), "unexpected error: %v", err)
|
|
|
|
const userYAML = `kind: static_host_user
|
|
version: v2
|
|
metadata:
|
|
name: alice
|
|
spec:
|
|
matchers:
|
|
- node_labels:
|
|
- name: foo
|
|
values: ["bar"]
|
|
groups:
|
|
- foo
|
|
- bar
|
|
uid: 1234
|
|
gid: 5678
|
|
- node_labels_expression: 'labels["foo"] == labels["bar"]'
|
|
groups:
|
|
- baz
|
|
- quux
|
|
sudoers: ["abc1234"]
|
|
`
|
|
|
|
// Create the host user
|
|
userYAMLPath := filepath.Join(t.TempDir(), "host_user.yaml")
|
|
require.NoError(t, os.WriteFile(userYAMLPath, []byte(userYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", userYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the user
|
|
buf, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"})
|
|
require.NoError(t, err)
|
|
hostUsers := mustDecodeJSON[[]*userprovisioningpb.StaticHostUser](t, buf)
|
|
require.Len(t, hostUsers, 1)
|
|
|
|
var expected userprovisioningpb.StaticHostUser
|
|
require.NoError(t, yaml.Unmarshal([]byte(userYAML), &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
[]*userprovisioningpb.StaticHostUser{&expected},
|
|
hostUsers,
|
|
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
|
protocmp.Transform(),
|
|
))
|
|
|
|
// Explicitly change the revision and try creating the user with and without
|
|
// the force flag.
|
|
expected.GetMetadata().SetRevision(uuid.NewString())
|
|
hostUserBytes, err := services.MarshalProtoResource(&expected, services.PreserveRevision())
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(userYAMLPath, hostUserBytes, 0644))
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", userYAMLPath})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAlreadyExists(err), "unexpected error: %v", err)
|
|
|
|
_, err = runResourceCommand(t, clt, []string{"create", "-f", userYAMLPath})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testCreateAutoUpdateConfig(t *testing.T, clt *authclient.Client) {
|
|
const resourceYAML = `kind: autoupdate_config
|
|
metadata:
|
|
name: autoupdate-config
|
|
revision: 3a43b44a-201e-4d7f-aef1-ae2f6d9811ed
|
|
spec:
|
|
tools:
|
|
mode: enabled
|
|
version: v1
|
|
`
|
|
_, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateConfig, "--format=json"})
|
|
require.ErrorContains(t, err, "doesn't exist")
|
|
|
|
// Create the resource.
|
|
resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml")
|
|
require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", resourceYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateConfig, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
rawResources := mustDecodeJSON[[]services.UnknownResource](t, buf)
|
|
require.Len(t, rawResources, 1)
|
|
var resource autoupdate.AutoUpdateConfig
|
|
require.NoError(t, protojson.UnmarshalOptions{}.Unmarshal(rawResources[0].Raw, &resource))
|
|
|
|
var expected autoupdate.AutoUpdateConfig
|
|
expectedJSON := mustTranscodeYAMLToJSON(t, bytes.NewReader([]byte(resourceYAML)))
|
|
require.NoError(t, protojson.UnmarshalOptions{}.Unmarshal(expectedJSON, &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
&expected,
|
|
&resource,
|
|
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
|
protocmp.Transform(),
|
|
))
|
|
|
|
// Delete the resource
|
|
_, err = runResourceCommand(t, clt, []string{"rm", types.KindAutoUpdateConfig})
|
|
require.NoError(t, err)
|
|
_, err = runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateConfig})
|
|
require.ErrorContains(t, err, "autoupdate_config \"autoupdate-config\" doesn't exist")
|
|
}
|
|
|
|
func testCreateAutoUpdateVersion(t *testing.T, clt *authclient.Client) {
|
|
const resourceYAML = `kind: autoupdate_version
|
|
metadata:
|
|
name: autoupdate-version
|
|
revision: 3a43b44a-201e-4d7f-aef1-ae2f6d9811ed
|
|
spec:
|
|
tools:
|
|
target_version: 1.2.3
|
|
version: v1
|
|
`
|
|
_, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateVersion, "--format=json"})
|
|
require.ErrorContains(t, err, "doesn't exist")
|
|
|
|
// Create the resource.
|
|
resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml")
|
|
require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", resourceYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateVersion, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
rawResources := mustDecodeJSON[[]services.UnknownResource](t, buf)
|
|
require.Len(t, rawResources, 1)
|
|
var resource autoupdate.AutoUpdateVersion
|
|
require.NoError(t, protojson.UnmarshalOptions{}.Unmarshal(rawResources[0].Raw, &resource))
|
|
|
|
var expected autoupdate.AutoUpdateVersion
|
|
expectedJSON := mustTranscodeYAMLToJSON(t, bytes.NewReader([]byte(resourceYAML)))
|
|
require.NoError(t, protojson.UnmarshalOptions{}.Unmarshal(expectedJSON, &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
&expected,
|
|
&resource,
|
|
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
|
protocmp.Transform(),
|
|
))
|
|
|
|
// Delete the resource
|
|
_, err = runResourceCommand(t, clt, []string{"rm", types.KindAutoUpdateVersion})
|
|
require.NoError(t, err)
|
|
_, err = runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateVersion})
|
|
require.ErrorContains(t, err, "autoupdate_version \"autoupdate-version\" doesn't exist")
|
|
}
|
|
|
|
func testCreateAutoUpdateAgentRollout(t *testing.T, clt *authclient.Client) {
|
|
const resourceYAML = `kind: autoupdate_agent_rollout
|
|
metadata:
|
|
name: autoupdate-agent-rollout
|
|
revision: 3a43b44a-201e-4d7f-aef1-ae2f6d9811ed
|
|
spec:
|
|
start_version: 1.2.3
|
|
target_version: 1.2.3
|
|
autoupdate_mode: "suspended"
|
|
schedule: "regular"
|
|
strategy: "halt-on-error"
|
|
status:
|
|
groups:
|
|
- name: my-group
|
|
state: 1
|
|
config_days: ["*"]
|
|
config_start_hour: 12
|
|
config_wait_hours: 0
|
|
version: v1
|
|
`
|
|
_, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateAgentRollout, "--format=json"})
|
|
require.ErrorContains(t, err, "doesn't exist")
|
|
|
|
// Create the resource.
|
|
resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml")
|
|
require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644))
|
|
_, err = runResourceCommand(t, clt, []string{"create", resourceYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Get the resource
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateAgentRollout, "--format=json"})
|
|
require.NoError(t, err)
|
|
|
|
rawResources := mustDecodeJSON[[]services.UnknownResource](t, buf)
|
|
require.Len(t, rawResources, 1)
|
|
var resource autoupdate.AutoUpdateAgentRollout
|
|
require.NoError(t, protojson.UnmarshalOptions{}.Unmarshal(rawResources[0].Raw, &resource))
|
|
|
|
var expected autoupdate.AutoUpdateAgentRollout
|
|
expectedJSON := mustTranscodeYAMLToJSON(t, bytes.NewReader([]byte(resourceYAML)))
|
|
require.NoError(t, protojson.UnmarshalOptions{}.Unmarshal(expectedJSON, &expected))
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
&expected,
|
|
&resource,
|
|
protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"),
|
|
protocmp.Transform(),
|
|
))
|
|
|
|
// Delete the resource
|
|
_, err = runResourceCommand(t, clt, []string{"rm", types.KindAutoUpdateAgentRollout})
|
|
require.NoError(t, err)
|
|
_, err = runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateAgentRollout})
|
|
require.ErrorContains(t, err, "autoupdate_agent_rollout \"autoupdate-agent-rollout\" doesn't exist")
|
|
}
|
|
|
|
func testCreateDynamicWindowsDesktop(t *testing.T, clt *authclient.Client) {
|
|
const resourceYAML = `kind: dynamic_windows_desktop
|
|
metadata:
|
|
name: test
|
|
revision: 3a43b44a-201e-4d7f-aef1-ae2f6d9811ed
|
|
spec:
|
|
addr: test
|
|
version: v1
|
|
`
|
|
|
|
// Create the resource.
|
|
resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml")
|
|
require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644))
|
|
_, err := runResourceCommand(t, clt, []string{"create", resourceYAMLPath})
|
|
require.NoError(t, err)
|
|
|
|
// Get the resource
|
|
buf, err := runResourceCommand(t, clt, []string{"get", types.KindDynamicWindowsDesktop, "--format=json"})
|
|
require.NoError(t, err)
|
|
resources := mustDecodeJSON[[]types.DynamicWindowsDesktopV1](t, buf)
|
|
require.Len(t, resources, 1)
|
|
|
|
var expected types.DynamicWindowsDesktopV1
|
|
require.NoError(t, yaml.Unmarshal([]byte(resourceYAML), &expected))
|
|
expected.SetRevision(resources[0].GetRevision())
|
|
|
|
require.Empty(t, cmp.Diff([]types.DynamicWindowsDesktopV1{expected}, resources, protocmp.Transform()))
|
|
}
|
|
|
|
func TestPluginResourceWrapper(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
plugin types.PluginV1
|
|
}{
|
|
{
|
|
name: "okta",
|
|
plugin: types.PluginV1{
|
|
Kind: types.KindPlugin,
|
|
Version: types.V1,
|
|
Metadata: types.Metadata{
|
|
Name: "okta",
|
|
},
|
|
Spec: types.PluginSpecV1{
|
|
Settings: &types.PluginSpecV1_Okta{
|
|
Okta: &types.PluginOktaSettings{
|
|
OrgUrl: "https://oktaorg.okta.com",
|
|
SyncSettings: &types.PluginOktaSyncSettings{
|
|
SyncUsers: true,
|
|
SsoConnectorId: "connectorID",
|
|
SyncAccessLists: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Credentials: &types.PluginCredentialsV1{
|
|
Credentials: &types.PluginCredentialsV1_StaticCredentialsRef{
|
|
StaticCredentialsRef: &types.PluginStaticCredentialsRef{Labels: map[string]string{"label": "value"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "slack",
|
|
plugin: types.PluginV1{
|
|
Kind: types.KindPlugin,
|
|
Version: types.V1,
|
|
Metadata: types.Metadata{
|
|
Name: "okta",
|
|
},
|
|
Spec: types.PluginSpecV1{
|
|
Settings: &types.PluginSpecV1_SlackAccessPlugin{
|
|
SlackAccessPlugin: &types.PluginSlackAccessSettings{
|
|
FallbackChannel: "#channel",
|
|
},
|
|
},
|
|
},
|
|
Credentials: &types.PluginCredentialsV1{
|
|
Credentials: &types.PluginCredentialsV1_Oauth2AccessToken{
|
|
Oauth2AccessToken: &types.PluginOAuth2AccessTokenCredentials{
|
|
AccessToken: "token",
|
|
RefreshToken: "refresh_token",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "identity center",
|
|
plugin: types.PluginV1{
|
|
Kind: types.KindPlugin,
|
|
Version: types.V1,
|
|
Metadata: types.Metadata{
|
|
Name: apicommon.OriginAWSIdentityCenter,
|
|
Labels: map[string]string{
|
|
"teleport.dev/hosted-plugin": "true",
|
|
},
|
|
},
|
|
Spec: types.PluginSpecV1{
|
|
Settings: &types.PluginSpecV1_AwsIc{
|
|
AwsIc: &types.PluginAWSICSettings{
|
|
Credentials: &types.AWSICCredentials{
|
|
Source: &types.AWSICCredentials_System{
|
|
System: &types.AWSICCredentialSourceSystem{},
|
|
},
|
|
},
|
|
Region: "ap-south-2",
|
|
Arn: "some:arn",
|
|
ProvisioningSpec: &types.AWSICProvisioningSpec{
|
|
BaseUrl: "https://scim.example.com/v2",
|
|
},
|
|
AccessListDefaultOwners: []string{"root"},
|
|
UserSyncFilters: []*types.AWSICUserSyncFilter{
|
|
{Labels: map[string]string{types.OriginLabel: types.OriginOkta}},
|
|
{Labels: map[string]string{types.OriginLabel: types.OriginEntraID}},
|
|
},
|
|
GroupSyncFilters: []*types.AWSICResourceFilter{
|
|
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: `^Group #\\d+$`}},
|
|
{Include: &types.AWSICResourceFilter_Id{Id: "42"}},
|
|
},
|
|
AwsAccountsFilters: []*types.AWSICResourceFilter{
|
|
{Include: &types.AWSICResourceFilter_Id{Id: "314159"}},
|
|
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: `^Account #\\d+$`}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: types.PluginStatusV1{
|
|
Code: types.PluginStatusCode_RUNNING,
|
|
Details: &types.PluginStatusV1_AwsIc{
|
|
AwsIc: &types.PluginAWSICStatusV1{
|
|
GroupImportStatus: &types.AWSICGroupImportStatus{
|
|
StatusCode: types.AWSICGroupImportStatusCode_DONE,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "entra_id",
|
|
plugin: types.PluginV1{
|
|
Kind: types.KindPlugin,
|
|
Version: types.V1,
|
|
Metadata: types.Metadata{
|
|
Name: "entra_id",
|
|
},
|
|
Spec: types.PluginSpecV1{
|
|
Settings: &types.PluginSpecV1_EntraId{
|
|
EntraId: &types.PluginEntraIDSettings{},
|
|
},
|
|
},
|
|
Status: types.PluginStatusV1{
|
|
Details: &types.PluginStatusV1_EntraId{
|
|
EntraId: &types.PluginEntraIDStatusV1{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "gitlab",
|
|
plugin: types.PluginV1{
|
|
Kind: types.KindPlugin,
|
|
Version: types.V1,
|
|
Metadata: types.Metadata{
|
|
Name: "gitlab",
|
|
},
|
|
Spec: types.PluginSpecV1{
|
|
Settings: &types.PluginSpecV1_Gitlab{
|
|
Gitlab: &types.PluginGitlabSettings{},
|
|
},
|
|
},
|
|
Status: types.PluginStatusV1{
|
|
Details: &types.PluginStatusV1_Gitlab{
|
|
Gitlab: &types.PluginGitlabStatusV1{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "net_iq",
|
|
plugin: types.PluginV1{
|
|
Kind: types.KindPlugin,
|
|
Version: types.V1,
|
|
Metadata: types.Metadata{
|
|
Name: "net_iq",
|
|
},
|
|
Spec: types.PluginSpecV1{
|
|
Settings: &types.PluginSpecV1_NetIq{
|
|
NetIq: &types.PluginNetIQSettings{},
|
|
},
|
|
},
|
|
Status: types.PluginStatusV1{
|
|
Details: &types.PluginStatusV1_NetIq{
|
|
NetIq: &types.PluginNetIQStatusV1{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
buff, err := json.Marshal(&tc.plugin)
|
|
require.NoError(t, err)
|
|
var failItem types.PluginV1
|
|
err = json.Unmarshal(buff, &failItem)
|
|
require.Error(t, err)
|
|
var item pluginResourceWrapper
|
|
err = json.Unmarshal(buff, &item)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(tc.plugin, item.PluginV1))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseScopedRef(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
ref string
|
|
id string
|
|
want ScopedRef
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "empty id returns ref unchanged",
|
|
ref: "role/myrole",
|
|
id: "",
|
|
want: ScopedRef{Kind: "role", Name: "myrole"},
|
|
},
|
|
{
|
|
name: "bare name, no subkind",
|
|
ref: "scoped_role",
|
|
id: "myrole",
|
|
want: ScopedRef{Kind: "scoped_role", Name: "myrole"},
|
|
},
|
|
{
|
|
name: "bare name with subkind promotion",
|
|
ref: "scoped_role_assignment/dynamic",
|
|
id: "myassignment",
|
|
want: ScopedRef{Kind: "scoped_role_assignment", SubKind: "dynamic", Name: "myassignment"},
|
|
},
|
|
{
|
|
name: "SQN, no subkind",
|
|
ref: "scoped_role",
|
|
id: "/staging/west::myrole",
|
|
want: ScopedRef{Kind: "scoped_role", Scope: "/staging/west", Name: "myrole"},
|
|
},
|
|
{
|
|
name: "SQN with subkind promotion",
|
|
ref: "scoped_role_assignment/dynamic",
|
|
id: "/staging/west::myassignment",
|
|
want: ScopedRef{Kind: "scoped_role_assignment", SubKind: "dynamic", Scope: "/staging/west", Name: "myassignment"},
|
|
},
|
|
{
|
|
name: "root scope SQN",
|
|
ref: "scoped_token",
|
|
id: "/::test-token",
|
|
want: ScopedRef{Kind: "scoped_token", Scope: "/", Name: "test-token"},
|
|
},
|
|
{
|
|
name: "three-segment first arg with second arg is an error",
|
|
ref: "scoped_role_assignment/dynamic/myassignment",
|
|
id: "/staging::extra",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid SQN is an error",
|
|
ref: "scoped_role",
|
|
id: "/staging::my role",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "scoped token with only name",
|
|
ref: "scoped_token",
|
|
id: "/test::test-token",
|
|
want: ScopedRef{Kind: "scoped_token", Scope: "/test", Name: "test-token"},
|
|
},
|
|
{
|
|
name: "scoped token with name and secret",
|
|
ref: "scoped_token",
|
|
id: "/test::test-token:YzZlYzdlYTg1YTZjZTEwZjVjYjAxMTc3Mjc2NTk3NGY",
|
|
want: ScopedRef{Kind: "scoped_token", Scope: "/test", Name: "test-token"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := ParseScopedRef(tt.ref, tt.id)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|