mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 23:31:08 +08:00
936 lines
31 KiB
Go
936 lines
31 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 web
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/trace"
|
|
"github.com/julienschmidt/httprouter"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"k8s.io/apimachinery/pkg/util/yaml"
|
|
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/web/ui"
|
|
)
|
|
|
|
func TestExtractResourceAndValidate(t *testing.T) {
|
|
goodContent := `kind: role
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
allow:
|
|
logins:
|
|
- testing
|
|
version: v3`
|
|
extractedResource, err := ExtractResourceAndValidate(goodContent)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, extractedResource)
|
|
|
|
// Test missing name.
|
|
invalidContent := `kind: role
|
|
metadata:
|
|
name:`
|
|
extractedResource, err = ExtractResourceAndValidate(invalidContent)
|
|
require.Nil(t, extractedResource)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.Contains(t, err.Error(), "Name")
|
|
}
|
|
|
|
func TestCheckResourceUpsert(t *testing.T) {
|
|
ctx := context.Background()
|
|
tests := []struct {
|
|
desc string
|
|
httpMethod string
|
|
httpParams httprouter.Params
|
|
payloadResourceName string
|
|
get getResource
|
|
assertErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
desc: "creating non-existing resource succeeds",
|
|
httpMethod: "POST",
|
|
httpParams: httprouter.Params{},
|
|
payloadResourceName: "my-resource",
|
|
get: func(ctx context.Context, name string) (types.Resource, error) {
|
|
// Resource does not exist.
|
|
return nil, trace.NotFound("")
|
|
},
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
desc: "creating existing resource fails",
|
|
httpMethod: "POST",
|
|
httpParams: httprouter.Params{},
|
|
payloadResourceName: "my-resource",
|
|
get: func(ctx context.Context, name string) (types.Resource, error) {
|
|
// Resource does exist.
|
|
return nil, nil
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
},
|
|
},
|
|
{
|
|
desc: "updating resource without name HTTP param fails",
|
|
httpMethod: "PUT",
|
|
httpParams: httprouter.Params{},
|
|
payloadResourceName: "my-resource",
|
|
get: func(ctx context.Context, name string) (types.Resource, error) {
|
|
// Resource does exist.
|
|
return nil, nil
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
},
|
|
},
|
|
{
|
|
desc: "updating non-existing resource fails",
|
|
httpMethod: "PUT",
|
|
httpParams: httprouter.Params{httprouter.Param{Key: "name", Value: "my-resource"}},
|
|
payloadResourceName: "my-resource",
|
|
get: func(ctx context.Context, name string) (types.Resource, error) {
|
|
// Resource does not exist.
|
|
return nil, trace.NotFound("")
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
},
|
|
},
|
|
{
|
|
desc: "updating existing resource succeeds",
|
|
httpMethod: "PUT",
|
|
httpParams: httprouter.Params{httprouter.Param{Key: "name", Value: "my-resource"}},
|
|
payloadResourceName: "my-resource",
|
|
get: func(ctx context.Context, name string) (types.Resource, error) {
|
|
// Resource does exist.
|
|
return nil, nil
|
|
},
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
desc: "renaming existing resource fails",
|
|
httpMethod: "PUT",
|
|
httpParams: httprouter.Params{httprouter.Param{Key: "name", Value: "my-resource"}},
|
|
payloadResourceName: "my-resource-new-name",
|
|
get: func(ctx context.Context, name string) (types.Resource, error) {
|
|
// Resource does exist.
|
|
return nil, nil
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
err := CheckResourceUpsert(ctx, tc.httpMethod, tc.httpParams, tc.payloadResourceName, tc.get)
|
|
tc.assertErr(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewResourceItemGithub(t *testing.T) {
|
|
const contents = `kind: github
|
|
metadata:
|
|
name: githubName
|
|
spec:
|
|
api_endpoint_url: ""
|
|
client_id: ""
|
|
client_secret: ""
|
|
display: ""
|
|
endpoint_url: ""
|
|
redirect_url: ""
|
|
teams_to_logins:
|
|
- logins:
|
|
- dummy
|
|
organization: octocats
|
|
team: dummy
|
|
teams_to_roles: null
|
|
version: v3
|
|
`
|
|
githubConn, err := types.NewGithubConnector("githubName", types.GithubConnectorSpecV3{
|
|
TeamsToLogins: []types.TeamMapping{
|
|
{
|
|
Organization: "octocats",
|
|
Team: "dummy",
|
|
Logins: []string{"dummy"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
item, err := ui.NewResourceItem(githubConn)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, &ui.ResourceItem{
|
|
ID: "github:githubName",
|
|
Kind: types.KindGithubConnector,
|
|
Name: "githubName",
|
|
Content: contents,
|
|
}, item)
|
|
}
|
|
|
|
func TestNewResourceItemRole(t *testing.T) {
|
|
const contents = `kind: role
|
|
metadata:
|
|
name: roleName
|
|
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
|
|
record_session:
|
|
default: best_effort
|
|
desktop: true
|
|
ssh_file_copy: true
|
|
version: v7
|
|
`
|
|
role, err := types.NewRole("roleName", types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Logins: []string{"test"},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
KubernetesResources: []types.KubernetesResource{
|
|
{
|
|
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
item, err := ui.NewResourceItem(role)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &ui.ResourceItem{
|
|
ID: "role:roleName",
|
|
Kind: types.KindRole,
|
|
Name: "roleName",
|
|
Content: contents,
|
|
}, item)
|
|
}
|
|
|
|
func TestNewResourceItemTrustedCluster(t *testing.T) {
|
|
const contents = `kind: trusted_cluster
|
|
metadata:
|
|
name: tcName
|
|
spec:
|
|
enabled: false
|
|
token: ""
|
|
tunnel_addr: ""
|
|
web_proxy_addr: ""
|
|
version: v2
|
|
`
|
|
cluster, err := types.NewTrustedCluster("tcName", types.TrustedClusterSpecV2{})
|
|
require.NoError(t, err)
|
|
|
|
item, err := ui.NewResourceItem(cluster)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &ui.ResourceItem{
|
|
ID: "trusted_cluster:tcName",
|
|
Kind: types.KindTrustedCluster,
|
|
Name: "tcName",
|
|
Content: contents,
|
|
}, item)
|
|
}
|
|
|
|
func TestGetRoles(t *testing.T) {
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
m.mockListRoles = func(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error) {
|
|
role, err := types.NewRole("test", types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Logins: []string{"test"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return &proto.ListRolesResponse{
|
|
Roles: []*types.RoleV6{role.(*types.RoleV6)},
|
|
NextKey: "",
|
|
}, nil
|
|
}
|
|
|
|
// Test response is converted to ui objects.
|
|
roles, err := listRoles(m, url.Values{})
|
|
require.NoError(t, err)
|
|
require.Len(t, roles.Items, 1)
|
|
require.Contains(t, roles.Items.([]ui.ResourceItem)[0].Content, "name: test")
|
|
}
|
|
|
|
func TestRoleCRUD(t *testing.T) {
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
|
|
proxy := env.proxies[0]
|
|
|
|
// Authenticate to get a session token and cookies.
|
|
pack := proxy.authPack(t, "test-user@example.com", nil)
|
|
|
|
expected, err := types.NewRole("test-role", types.RoleSpecV6{})
|
|
require.NoError(t, err, "creating initial role resource")
|
|
|
|
createPayload := func(r types.Role) ui.ResourceItem {
|
|
raw, err := services.MarshalRole(r, services.PreserveRevision())
|
|
require.NoError(t, err, "marshaling role")
|
|
|
|
return ui.ResourceItem{
|
|
Kind: types.KindRole,
|
|
Name: r.GetName(),
|
|
Content: string(raw),
|
|
}
|
|
}
|
|
|
|
unmarshalResponse := func(resp []byte) types.Role {
|
|
var item ui.ResourceItem
|
|
require.NoError(t, json.Unmarshal(resp, &item), "response from server contained an invalid resource item")
|
|
|
|
var r types.RoleV6
|
|
require.NoError(t, yaml.Unmarshal([]byte(item.Content), &r), "resource item content was not a role")
|
|
return &r
|
|
}
|
|
|
|
// Create the initial role.
|
|
resp, err := pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "roles"), createPayload(expected))
|
|
require.NoError(t, err, "expected creating the initial role to succeed")
|
|
require.Equal(t, http.StatusOK, resp.Code(), "unexpected status code creating role")
|
|
|
|
created := unmarshalResponse(resp.Bytes())
|
|
|
|
// Validate the role can be retrieved.
|
|
resp, err = pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "roles", expected.GetName()), url.Values{})
|
|
require.NoError(t, err, "unexpected error retrieving a role")
|
|
assert.Equal(t, http.StatusOK, resp.Code(), "unexpected status code retrieving a role")
|
|
retrieved := unmarshalResponse(resp.Bytes())
|
|
assert.Equal(t, created, retrieved, "expected the retrieved role to be equal to the created one")
|
|
|
|
// Validate that creating the role again fails.
|
|
resp, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "roles"), createPayload(expected))
|
|
assert.Error(t, err, "expected an error creating a duplicate role")
|
|
assert.True(t, trace.IsAlreadyExists(err), "expected an already exists error got %T", err)
|
|
assert.Equal(t, http.StatusConflict, resp.Code(), "unexpected status code creating duplicate role")
|
|
|
|
// Update the role.
|
|
created.SetLogins(types.Allow, []string{"test"})
|
|
resp, err = pack.clt.PutJSON(ctx, pack.clt.Endpoint("webapi", "roles", expected.GetName()), createPayload(created))
|
|
require.NoError(t, err, "unexpected error updating the role")
|
|
require.Equal(t, http.StatusOK, resp.Code(), "unexpected status code updating the role")
|
|
|
|
updated := unmarshalResponse(resp.Bytes())
|
|
|
|
require.Empty(t, cmp.Diff(created, updated,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
cmpopts.IgnoreFields(types.RoleConditions{}, "Namespaces"),
|
|
))
|
|
require.NotEqual(t, expected.GetLogins(types.Allow), updated.GetLogins(types.Allow), "expected update to modify the logins")
|
|
require.Equal(t, []string{"test"}, updated.GetLogins(types.Allow), "logins should have been updated to test. got %s", updated.GetLogins(types.Allow))
|
|
|
|
// Validate that a stale revision prevents updates.
|
|
resp, err = pack.clt.PutJSON(ctx, pack.clt.Endpoint("webapi", "roles", expected.GetName()), createPayload(expected))
|
|
assert.Error(t, err, "expected an error updating a role with a stale revision")
|
|
assert.True(t, trace.IsCompareFailed(err), "expected a compare failed error got %T", err)
|
|
assert.Equal(t, http.StatusPreconditionFailed, resp.Code(), "unexpected status code updating the role")
|
|
|
|
// Validate that renaming the role prevents updates.
|
|
updated.SetName(uuid.NewString())
|
|
resp, err = pack.clt.PutJSON(ctx, pack.clt.Endpoint("webapi", "roles", expected.GetName()), createPayload(updated))
|
|
assert.Error(t, err, "expected and error when renaming a role")
|
|
assert.True(t, trace.IsBadParameter(err), "expected a bad parameter error got %T", err)
|
|
assert.Equal(t, http.StatusBadRequest, resp.Code(), "unexpected status code updating the role")
|
|
|
|
// Validate that updating a nonexistent role fails.
|
|
updated.SetName(uuid.NewString())
|
|
resp, err = pack.clt.PutJSON(ctx, pack.clt.Endpoint("webapi", "roles", updated.GetName()), createPayload(updated))
|
|
assert.Error(t, err, "expected updating a nonexistent role to fail")
|
|
assert.True(t, trace.IsCompareFailed(err), "expected a compare failed error got %T", err)
|
|
assert.Equal(t, http.StatusPreconditionFailed, resp.Code(), "unexpected status code updating the role")
|
|
|
|
// Validate that the role can be deleted
|
|
_, err = pack.clt.Delete(ctx, pack.clt.Endpoint("webapi", "roles", expected.GetName()))
|
|
require.NoError(t, err, "unexpected error deleting role")
|
|
|
|
resp, err = pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "roles"), url.Values{"limit": []string{"15"}})
|
|
assert.NoError(t, err, "unexpected error listing role")
|
|
|
|
var getResponse listResourcesWithoutCountGetResponse
|
|
require.NoError(t, json.Unmarshal(resp.Bytes(), &getResponse), "invalid resource item received")
|
|
assert.Equal(t, http.StatusOK, resp.Code(), "unexpected status code getting roles")
|
|
|
|
assert.Equal(t, "", getResponse.StartKey)
|
|
for _, item := range getResponse.Items.([]interface{}) {
|
|
assert.NotEqual(t, "test-role", item.(map[string]interface{})["name"], "expected test-role to be deleted")
|
|
}
|
|
|
|
// Validate that attempting to retrieve a deleted role yields a NotFound error.
|
|
resp, err = pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "roles", expected.GetName()), url.Values{})
|
|
assert.Error(t, err, "expected fetching a nonexistent role to fail")
|
|
assert.True(t, trace.IsNotFound(err), "expected a NotFound error, got %T", err)
|
|
assert.Equal(t, http.StatusNotFound, resp.Code(), "unexpected status code retrieving a role")
|
|
}
|
|
|
|
func TestGithubConnectorsCRUD(t *testing.T) {
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
proxy := env.proxies[0]
|
|
|
|
pack := proxy.authPack(t, "test-user@example.com", nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
connectors []types.GithubConnector
|
|
setDefaultReq *ui.SetDefaultAuthConnectorRequest
|
|
wantConnectorName string
|
|
wantConnectorType string
|
|
}{
|
|
{
|
|
name: "no connectors defaults to local auth",
|
|
connectors: []types.GithubConnector{},
|
|
wantConnectorName: "",
|
|
wantConnectorType: constants.Local,
|
|
},
|
|
{
|
|
name: "default connector exists in list",
|
|
connectors: []types.GithubConnector{
|
|
makeGithubConnector(t, "github-1"),
|
|
},
|
|
setDefaultReq: &ui.SetDefaultAuthConnectorRequest{
|
|
Name: "github-1",
|
|
Type: constants.Github,
|
|
},
|
|
wantConnectorName: "github-1",
|
|
wantConnectorType: constants.Github,
|
|
},
|
|
{
|
|
name: "default connector missing defaults to last in list",
|
|
connectors: []types.GithubConnector{
|
|
makeGithubConnector(t, "github-1"),
|
|
makeGithubConnector(t, "github-2"),
|
|
},
|
|
setDefaultReq: &ui.SetDefaultAuthConnectorRequest{
|
|
Name: "missing",
|
|
Type: constants.Github,
|
|
},
|
|
wantConnectorName: "github-2",
|
|
wantConnectorType: constants.Github,
|
|
},
|
|
{
|
|
name: "local auth type always defaults to local",
|
|
connectors: []types.GithubConnector{
|
|
makeGithubConnector(t, "github-1"),
|
|
},
|
|
setDefaultReq: &ui.SetDefaultAuthConnectorRequest{
|
|
Name: "local",
|
|
Type: constants.Local,
|
|
},
|
|
wantConnectorName: "",
|
|
wantConnectorType: constants.Local,
|
|
},
|
|
{
|
|
name: "missing default with no connectors defaults to local",
|
|
connectors: []types.GithubConnector{},
|
|
setDefaultReq: &ui.SetDefaultAuthConnectorRequest{
|
|
Name: "missing",
|
|
Type: constants.Github,
|
|
},
|
|
wantConnectorName: "",
|
|
wantConnectorType: constants.Local,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup initial connectors
|
|
for _, conn := range tt.connectors {
|
|
raw, err := services.MarshalGithubConnector(conn)
|
|
require.NoError(t, err)
|
|
resp, err := pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "github"), ui.ResourceItem{
|
|
Kind: types.KindGithubConnector,
|
|
Name: conn.GetName(),
|
|
Content: string(raw),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.Code())
|
|
}
|
|
|
|
// Set default connector if specified
|
|
if tt.setDefaultReq != nil {
|
|
resp, err := pack.clt.PutJSON(ctx, pack.clt.Endpoint("webapi", "authconnector", "default"), tt.setDefaultReq)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.Code())
|
|
}
|
|
|
|
// Get connectors and verify response
|
|
resp, err := pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "github"), url.Values{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.Code())
|
|
|
|
var connResponse ui.ListAuthConnectorsResponse
|
|
err = json.Unmarshal(resp.Bytes(), &connResponse)
|
|
require.NoError(t, err)
|
|
|
|
// Verify connector name and type
|
|
assert.Equal(t, tt.wantConnectorName, connResponse.DefaultConnectorName)
|
|
assert.Equal(t, tt.wantConnectorType, connResponse.DefaultConnectorType)
|
|
|
|
// Verify connectors list
|
|
require.Equal(t, len(tt.connectors), len(connResponse.Connectors))
|
|
for i, conn := range tt.connectors {
|
|
expectedItem, err := ui.NewResourceItem(conn)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedItem.Name, connResponse.Connectors[i].Name)
|
|
}
|
|
|
|
// Cleanup connectors
|
|
for _, conn := range tt.connectors {
|
|
_, err := pack.clt.Delete(ctx, pack.clt.Endpoint("webapi", "github", conn.GetName()))
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetTrustedClusters(t *testing.T) {
|
|
ctx := context.Background()
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
m.mockGetTrustedClusters = func(ctx context.Context) ([]types.TrustedCluster, error) {
|
|
cluster, err := types.NewTrustedCluster("test", types.TrustedClusterSpecV2{})
|
|
require.NoError(t, err)
|
|
|
|
return []types.TrustedCluster{cluster}, nil
|
|
}
|
|
|
|
// Test response is converted to ui objects.
|
|
tcs, err := getTrustedClusters(ctx, m)
|
|
require.NoError(t, err)
|
|
require.Len(t, tcs, 1)
|
|
require.Contains(t, tcs[0].Content, "name: test")
|
|
}
|
|
|
|
func TestUpsertTrustedCluster(t *testing.T) {
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
existingTrustedClusters := make(map[string]types.TrustedCluster)
|
|
m.mockUpsertTrustedCluster = func(ctx context.Context, tc types.TrustedCluster) (types.TrustedCluster, error) {
|
|
existingTrustedClusters[tc.GetName()] = tc
|
|
return tc, nil
|
|
}
|
|
m.mockGetTrustedCluster = func(ctx context.Context, name string) (types.TrustedCluster, error) {
|
|
tc, ok := existingTrustedClusters[name]
|
|
if ok {
|
|
return tc, nil
|
|
}
|
|
return nil, trace.NotFound("")
|
|
}
|
|
|
|
// Test bad request kind.
|
|
invalidKind := `kind: invalid-kind
|
|
metadata:
|
|
name: test`
|
|
tc, err := upsertTrustedCluster(context.Background(), m, invalidKind, "", httprouter.Params{})
|
|
require.Nil(t, tc)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.Contains(t, err.Error(), "kind")
|
|
|
|
goodContent := `kind: trusted_cluster
|
|
metadata:
|
|
name: test-goodcontent
|
|
spec:
|
|
role_map:
|
|
- local:
|
|
- admin
|
|
remote: admin
|
|
version: v2`
|
|
|
|
// Updating non-existing trusted cluster fails.
|
|
tc, err = upsertTrustedCluster(context.Background(), m, goodContent, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.Nil(t, tc)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
// Creating non-existing trusted cluster succeeds.
|
|
tc, err = upsertTrustedCluster(context.Background(), m, goodContent, "POST", httprouter.Params{})
|
|
require.NoError(t, err)
|
|
require.Contains(t, tc.Content, "name: test-goodcontent")
|
|
|
|
// Creating existing trusted cluster fails.
|
|
tc, err = upsertTrustedCluster(context.Background(), m, goodContent, "POST", httprouter.Params{})
|
|
require.Nil(t, tc)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
// Updating existing trusted cluster succeeds.
|
|
tc, err = upsertTrustedCluster(context.Background(), m, goodContent, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.NoError(t, err)
|
|
require.Contains(t, tc.Content, "name: test-goodcontent")
|
|
|
|
// Renaming existing trusted cluster fails.
|
|
goodContentRenamed := strings.ReplaceAll(goodContent, "test-goodcontent", "test-goodcontent-new-name")
|
|
tc, err = upsertTrustedCluster(context.Background(), m, goodContentRenamed, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.Nil(t, tc)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
}
|
|
|
|
func TestListResources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test parsing query params.
|
|
testCases := []struct {
|
|
name, url string
|
|
wantBadParamErr bool
|
|
expected proto.ListResourcesRequest
|
|
}{
|
|
{
|
|
name: "decode complex query correctly",
|
|
url: "https://dev:3080/login?query=(labels%5B%60%22test%22%60%5D%20%3D%3D%20%22%2B%3A'%2C%23*~%25%5E%22%20%26%26%20!exists(labels.tier))%20%7C%7C%20resource.spec.description%20!%3D%20%22weird%20example%20https%3A%2F%2Ffoo.dev%3A3080%3Fbar%3Da%2Cb%26baz%3Dbanana%22",
|
|
expected: proto.ListResourcesRequest{
|
|
ResourceType: types.KindNode,
|
|
Limit: defaults.MaxIterationLimit,
|
|
PredicateExpression: "(labels[`\"test\"`] == \"+:',#*~%^\" && !exists(labels.tier)) || resource.spec.description != \"weird example https://foo.dev:3080?bar=a,b&baz=banana\"",
|
|
},
|
|
},
|
|
{
|
|
name: "all param defined and set",
|
|
url: `https://dev:3080/login?searchAsRoles=yes&query=labels.env%20%3D%3D%20%22prod%22&limit=50&startKey=banana&sort=foo:desc&search=foo%2Bbar+baz+foo%2Cbar+%22some%20phrase%22`,
|
|
expected: proto.ListResourcesRequest{
|
|
ResourceType: types.KindNode,
|
|
Limit: 50,
|
|
StartKey: "banana",
|
|
SearchKeywords: []string{"foo+bar", "baz", "foo,bar", "some phrase"},
|
|
PredicateExpression: `labels.env == "prod"`,
|
|
SortBy: types.SortBy{Field: "foo", IsDesc: true},
|
|
UseSearchAsRoles: true,
|
|
},
|
|
},
|
|
{
|
|
name: "all query param defined but empty",
|
|
url: `https://dev:3080/login?query=&startKey=&search=&sort=&limit=&startKey=`,
|
|
expected: proto.ListResourcesRequest{
|
|
ResourceType: types.KindNode,
|
|
Limit: defaults.MaxIterationLimit,
|
|
},
|
|
},
|
|
{
|
|
name: "sort partially defined: fieldName",
|
|
url: `https://dev:3080/login?sort=foo`,
|
|
expected: proto.ListResourcesRequest{
|
|
ResourceType: types.KindNode,
|
|
Limit: defaults.MaxIterationLimit,
|
|
SortBy: types.SortBy{Field: "foo", IsDesc: false},
|
|
},
|
|
},
|
|
{
|
|
name: "sort partially defined: fieldName with colon",
|
|
url: `https://dev:3080/login?sort=foo:`,
|
|
expected: proto.ListResourcesRequest{
|
|
ResourceType: types.KindNode,
|
|
Limit: defaults.MaxIterationLimit,
|
|
SortBy: types.SortBy{Field: "foo", IsDesc: false},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid limit value",
|
|
wantBadParamErr: true,
|
|
url: `https://dev:3080/login?limit=12invalid`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
httpReq, err := http.NewRequest("", tc.url, nil)
|
|
require.NoError(t, err)
|
|
|
|
_, err = convertListResourcesRequest(httpReq, types.KindNode)
|
|
if tc.wantBadParamErr {
|
|
require.True(t, trace.IsBadParameter(err))
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockedResourceAPIGetter struct {
|
|
mockGetRole func(ctx context.Context, name string) (types.Role, error)
|
|
mockGetRoles func(ctx context.Context) ([]types.Role, error)
|
|
mockListRoles func(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error)
|
|
mockUpsertRole func(ctx context.Context, role types.Role) (types.Role, error)
|
|
mockGetGithubConnectors func(ctx context.Context, withSecrets bool) ([]types.GithubConnector, error)
|
|
mockGetGithubConnector func(ctx context.Context, id string, withSecrets bool) (types.GithubConnector, error)
|
|
mockDeleteGithubConnector func(ctx context.Context, id string) error
|
|
mockUpsertTrustedCluster func(ctx context.Context, tc types.TrustedCluster) (types.TrustedCluster, error)
|
|
mockGetTrustedCluster func(ctx context.Context, name string) (types.TrustedCluster, error)
|
|
mockGetTrustedClusters func(ctx context.Context) ([]types.TrustedCluster, error)
|
|
mockDeleteTrustedCluster func(ctx context.Context, name string) error
|
|
mockListResources func(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
|
|
mockGetAuthPreference func(ctx context.Context) (types.AuthPreference, error)
|
|
mockUpsertAuthPreference func(ctx context.Context, pref types.AuthPreference) (types.AuthPreference, error)
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) GetRole(ctx context.Context, name string) (types.Role, error) {
|
|
if m.mockGetRole != nil {
|
|
return m.mockGetRole(ctx, name)
|
|
}
|
|
return nil, trace.NotImplemented("mockGetRole not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) GetRoles(ctx context.Context) ([]types.Role, error) {
|
|
if m.mockGetRoles != nil {
|
|
return m.mockGetRoles(ctx)
|
|
}
|
|
return nil, trace.NotImplemented("mockGetRoles not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) ListRoles(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error) {
|
|
if m.mockListRoles != nil {
|
|
return m.mockListRoles(ctx, req)
|
|
}
|
|
return nil, trace.NotImplemented("mockListRoles not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) UpsertRole(ctx context.Context, role types.Role) (types.Role, error) {
|
|
if m.mockUpsertRole != nil {
|
|
return m.mockUpsertRole(ctx, role)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockUpsertRole not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) GetGithubConnectors(ctx context.Context, withSecrets bool) ([]types.GithubConnector, error) {
|
|
if m.mockGetGithubConnectors != nil {
|
|
return m.mockGetGithubConnectors(ctx, false)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockGetGithubConnectors not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) GetGithubConnector(ctx context.Context, id string, withSecrets bool) (types.GithubConnector, error) {
|
|
if m.mockGetGithubConnector != nil {
|
|
return m.mockGetGithubConnector(ctx, id, false)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockGetGithubConnector not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) DeleteGithubConnector(ctx context.Context, id string) error {
|
|
if m.mockDeleteGithubConnector != nil {
|
|
return m.mockDeleteGithubConnector(ctx, id)
|
|
}
|
|
|
|
return trace.NotImplemented("mockDeleteGithubConnector not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) UpsertTrustedCluster(ctx context.Context, tc types.TrustedCluster) (types.TrustedCluster, error) {
|
|
if m.mockUpsertTrustedCluster != nil {
|
|
return m.mockUpsertTrustedCluster(ctx, tc)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockUpsertTrustedCluster not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) GetTrustedCluster(ctx context.Context, name string) (types.TrustedCluster, error) {
|
|
if m.mockGetTrustedCluster != nil {
|
|
return m.mockGetTrustedCluster(ctx, name)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockGetTrustedCluster not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) GetTrustedClusters(ctx context.Context) ([]types.TrustedCluster, error) {
|
|
if m.mockGetTrustedClusters != nil {
|
|
return m.mockGetTrustedClusters(ctx)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockGetTrustedClusters not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) DeleteTrustedCluster(ctx context.Context, name string) error {
|
|
if m.mockDeleteTrustedCluster != nil {
|
|
return m.mockDeleteTrustedCluster(ctx, name)
|
|
}
|
|
|
|
return trace.NotImplemented("mockDeleteTrustedCluster not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
|
|
if m.mockListResources != nil {
|
|
return m.mockListResources(ctx, req)
|
|
}
|
|
|
|
return nil, trace.NotImplemented("mockListResources not implemented")
|
|
}
|
|
|
|
// Add new mock methods
|
|
func (m *mockedResourceAPIGetter) GetAuthPreference(ctx context.Context) (types.AuthPreference, error) {
|
|
if m.mockGetAuthPreference != nil {
|
|
return m.mockGetAuthPreference(ctx)
|
|
}
|
|
return nil, trace.NotImplemented("mockGetAuthPreference not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) UpsertAuthPreference(ctx context.Context, pref types.AuthPreference) (types.AuthPreference, error) {
|
|
if m.mockUpsertAuthPreference != nil {
|
|
return m.mockUpsertAuthPreference(ctx, pref)
|
|
}
|
|
return nil, trace.NotImplemented("mockUpsertAuthPreference not implemented")
|
|
}
|
|
|
|
func Test_newKubeListRequest(t *testing.T) {
|
|
type args struct {
|
|
query string
|
|
site string
|
|
resourceKind string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *kubeproto.ListKubernetesResourcesRequest
|
|
}{
|
|
{
|
|
name: "list resources",
|
|
args: args{
|
|
query: "kind=kind1",
|
|
site: "site1",
|
|
resourceKind: "kind1",
|
|
},
|
|
want: &kubeproto.ListKubernetesResourcesRequest{
|
|
TeleportCluster: "site1",
|
|
ResourceType: "kind1",
|
|
SortBy: &types.SortBy{},
|
|
Limit: defaults.MaxIterationLimit,
|
|
},
|
|
},
|
|
{
|
|
name: "list resources with sort and query",
|
|
args: args{
|
|
query: "kind=kind1&query=foo&sort=bar:desc&limit=10",
|
|
site: "site1",
|
|
resourceKind: "kind1",
|
|
},
|
|
want: &kubeproto.ListKubernetesResourcesRequest{
|
|
TeleportCluster: "site1",
|
|
ResourceType: "kind1",
|
|
PredicateExpression: "foo",
|
|
SortBy: &types.SortBy{
|
|
Field: "bar",
|
|
IsDesc: true,
|
|
},
|
|
Limit: 10,
|
|
},
|
|
},
|
|
{
|
|
name: "list resources with search as roles",
|
|
args: args{
|
|
query: "startKey=startK1&query=bar&sort=foo:asc&searchAsRoles=yes&limit=10&kubeCluster=cluster&kubeNamespace=namespace",
|
|
site: "site1",
|
|
resourceKind: "kind1",
|
|
},
|
|
want: &kubeproto.ListKubernetesResourcesRequest{
|
|
StartKey: "startK1",
|
|
KubernetesCluster: "cluster",
|
|
KubernetesNamespace: "namespace",
|
|
TeleportCluster: "site1",
|
|
ResourceType: "kind1",
|
|
PredicateExpression: "bar",
|
|
SortBy: &types.SortBy{
|
|
Field: "foo",
|
|
IsDesc: false,
|
|
},
|
|
UseSearchAsRoles: true,
|
|
Limit: 10,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
values, err := url.ParseQuery(tt.args.query)
|
|
require.NoError(t, err)
|
|
got, err := newKubeListRequest(values, tt.args.site, tt.args.resourceKind)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeGithubConnector(t *testing.T, name string) types.GithubConnector {
|
|
connector, err := types.NewGithubConnector(name, types.GithubConnectorSpecV3{
|
|
TeamsToRoles: []types.TeamRolesMapping{
|
|
{
|
|
Organization: "octocats",
|
|
Team: "dummy",
|
|
Roles: []string{"dummy"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return connector
|
|
}
|