mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
This PR introduces a web API for users to search, from the UI, for Kubernetes pods whose access is only granted by their `search_as_roles`.
The new API can be used to create access requests for those resources.
The new API is located
```bash
curl 'https://<proxy_endpoint>:3080/v1/webapi/sites/<teleport_cluster>/pods?kubeCluster=<kube_cluster>&kubeNamespace=<kube_namespace|empty>&searchAsRoles=yes&limit=10&startKey=&query=&search=&sort=name:asc' \
| jq .
```
The JSON response:
```json
{
"items": [
{
"kind": "pod",
"name": "coredns-6d4b75cb6d-k84h9",
"labels": [
{
"name": "k8s-app",
"value": "kube-dns"
},
{
"name": "pod-template-hash",
"value": "6d4b75cb6d"
}
],
"namespace": "kube-system",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "etcd-minikube",
"labels": [
{
"name": "component",
"value": "etcd"
},
{
"name": "tier",
"value": "control-plane"
}
],
"namespace": "kube-system",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "kube-apiserver-minikube",
"labels": [
{
"name": "component",
"value": "kube-apiserver"
},
{
"name": "tier",
"value": "control-plane"
}
],
"namespace": "kube-system",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "kube-controller-manager-minikube",
"labels": [
{
"name": "component",
"value": "kube-controller-manager"
},
{
"name": "tier",
"value": "control-plane"
}
],
"namespace": "kube-system",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "kube-proxy-g7j5r",
"labels": [
{
"name": "controller-revision-hash",
"value": "94985b49"
},
{
"name": "k8s-app",
"value": "kube-proxy"
},
{
"name": "pod-template-generation",
"value": "1"
}
],
"namespace": "kube-system",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "kube-scheduler-minikube",
"labels": [
{
"name": "component",
"value": "kube-scheduler"
},
{
"name": "tier",
"value": "control-plane"
}
],
"namespace": "kube-system",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "nginx-deployment-6595874d85-6j2zm",
"labels": [
{
"name": "app",
"value": "nginx"
},
{
"name": "pod-template-hash",
"value": "6595874d85"
}
],
"namespace": "default",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "nginx-deployment-6595874d85-7xgmb",
"labels": [
{
"name": "app",
"value": "nginx"
},
{
"name": "pod-template-hash",
"value": "6595874d85"
}
],
"namespace": "default",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "nginx-deployment-6595874d85-c4kz8",
"labels": [
{
"name": "app",
"value": "nginx"
},
{
"name": "pod-template-hash",
"value": "6595874d85"
}
],
"namespace": "default",
"cluster": "minikube"
},
{
"kind": "pod",
"name": "storage-provisioner",
"labels": [
{
"name": "addonmanager.kubernetes.io/mode",
"value": "Reconcile"
},
{
"name": "integration-test",
"value": "storage-provisioner"
}
],
"namespace": "kube-system",
"cluster": "minikube"
}
],
"startKey": "teleport-agent-0",
"totalCount": 11
}
```
Part of #20324 and #19573
Updates #19573
831 lines
26 KiB
Go
831 lines
26 KiB
Go
/**
|
|
* Copyright 2021 Gravitational, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/julienschmidt/httprouter"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
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/web/ui"
|
|
)
|
|
|
|
func TestExtractResourceAndValidate(t *testing.T) {
|
|
goodContent := `kind: role
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
allow:
|
|
logins:
|
|
- testing
|
|
version: v3`
|
|
extractedResource, err := ExtractResourceAndValidate(goodContent)
|
|
require.Nil(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:
|
|
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_host_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: v6
|
|
`
|
|
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.Nil(t, err)
|
|
|
|
item, err := ui.NewResourceItem(cluster)
|
|
require.Nil(t, err)
|
|
require.Equal(t, item, &ui.ResourceItem{
|
|
ID: "trusted_cluster:tcName",
|
|
Kind: types.KindTrustedCluster,
|
|
Name: "tcName",
|
|
Content: contents,
|
|
})
|
|
}
|
|
|
|
func TestGetRoles(t *testing.T) {
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
m.mockGetRoles = func(ctx context.Context) ([]types.Role, error) {
|
|
role, err := types.NewRole("test", types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Logins: []string{"test"},
|
|
},
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
return []types.Role{role}, nil
|
|
}
|
|
|
|
// Test response is converted to ui objects.
|
|
roles, err := getRoles(m)
|
|
require.Nil(t, err)
|
|
require.Len(t, roles, 1)
|
|
require.Contains(t, roles[0].Content, "name: test")
|
|
}
|
|
|
|
func TestUpsertRole(t *testing.T) {
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
existingRoles := make(map[string]types.Role)
|
|
m.mockUpsertRole = func(ctx context.Context, role types.Role) error {
|
|
existingRoles[role.GetName()] = role
|
|
return nil
|
|
}
|
|
m.mockGetRole = func(ctx context.Context, name string) (types.Role, error) {
|
|
role, ok := existingRoles[name]
|
|
if ok {
|
|
return role, nil
|
|
}
|
|
return nil, trace.NotFound("")
|
|
}
|
|
|
|
// Test bad request kind.
|
|
invalidKind := `kind: invalid-kind
|
|
metadata:
|
|
name: test`
|
|
role, err := upsertRole(context.Background(), m, invalidKind, "", httprouter.Params{})
|
|
require.Nil(t, role)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.Contains(t, err.Error(), "kind")
|
|
|
|
goodContent := `kind: role
|
|
metadata:
|
|
name: test-goodcontent
|
|
spec:
|
|
allow:
|
|
logins:
|
|
- testing
|
|
version: v3`
|
|
|
|
// Updating non-existing role fails.
|
|
role, err = upsertRole(context.Background(), m, goodContent, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.Nil(t, role)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
// Creating non-existing role succeeds.
|
|
role, err = upsertRole(context.Background(), m, goodContent, "POST", httprouter.Params{})
|
|
require.NoError(t, err)
|
|
require.Contains(t, role.Content, "name: test-goodcontent")
|
|
|
|
// Creating existing role fails.
|
|
role, err = upsertRole(context.Background(), m, goodContent, "POST", httprouter.Params{})
|
|
require.Nil(t, role)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
// Updating existing role succeeds.
|
|
role, err = upsertRole(context.Background(), m, goodContent, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.NoError(t, err)
|
|
require.Contains(t, role.Content, "name: test-goodcontent")
|
|
|
|
// Renaming existing role fails.
|
|
goodContentRenamed := strings.ReplaceAll(goodContent, "test-goodcontent", "test-goodcontent-new-name")
|
|
role, err = upsertRole(context.Background(), m, goodContentRenamed, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.Nil(t, role)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
}
|
|
|
|
func TestGetGithubConnectors(t *testing.T) {
|
|
ctx := context.Background()
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
m.mockGetGithubConnectors = func(ctx context.Context, withSecrets bool) ([]types.GithubConnector, error) {
|
|
connector, err := types.NewGithubConnector("test", types.GithubConnectorSpecV3{
|
|
TeamsToLogins: []types.TeamMapping{
|
|
{
|
|
Organization: "octocats",
|
|
Team: "dummy",
|
|
Logins: []string{"dummy"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return []types.GithubConnector{connector}, nil
|
|
}
|
|
|
|
// Test response is converted to ui objects.
|
|
connectors, err := getGithubConnectors(ctx, m)
|
|
require.Nil(t, err)
|
|
require.Len(t, connectors, 1)
|
|
require.Contains(t, connectors[0].Content, "name: test")
|
|
}
|
|
|
|
func TestUpsertGithubConnector(t *testing.T) {
|
|
m := &mockedResourceAPIGetter{}
|
|
|
|
existingConnectors := make(map[string]types.GithubConnector)
|
|
m.mockUpsertGithubConnector = func(ctx context.Context, connector types.GithubConnector) error {
|
|
existingConnectors[connector.GetName()] = connector
|
|
return nil
|
|
}
|
|
m.mockGetGithubConnector = func(ctx context.Context, name string, withSecrets bool) (types.GithubConnector, error) {
|
|
connector, ok := existingConnectors[name]
|
|
if ok {
|
|
return connector, nil
|
|
}
|
|
return nil, trace.NotFound("")
|
|
}
|
|
|
|
// Test bad request kind.
|
|
invalidKind := `kind: invalid-kind
|
|
metadata:
|
|
name: test`
|
|
connector, err := upsertGithubConnector(context.Background(), m, invalidKind, "", httprouter.Params{})
|
|
require.Nil(t, connector)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.Contains(t, err.Error(), "kind")
|
|
|
|
goodContent := `kind: github
|
|
metadata:
|
|
name: test-goodcontent
|
|
spec:
|
|
client_id: <client-id>
|
|
client_secret: <client-secret>
|
|
display: Github
|
|
redirect_url: https://<cluster-url>/v1/webapi/github/callback
|
|
teams_to_logins:
|
|
- logins:
|
|
- admins
|
|
organization: <github-org>
|
|
team: admins
|
|
version: v3`
|
|
|
|
// Updating non-existing connector fails.
|
|
connector, err = upsertGithubConnector(context.Background(), m, goodContent, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.Nil(t, connector)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
// Creating non-existing connector succeeds.
|
|
connector, err = upsertGithubConnector(context.Background(), m, goodContent, "POST", httprouter.Params{})
|
|
require.NoError(t, err)
|
|
require.Contains(t, connector.Content, "name: test-goodcontent")
|
|
|
|
// Creating existing connector fails.
|
|
connector, err = upsertGithubConnector(context.Background(), m, goodContent, "POST", httprouter.Params{})
|
|
require.Nil(t, connector)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
|
|
// Updating existing connector succeeds.
|
|
connector, err = upsertGithubConnector(context.Background(), m, goodContent, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.NoError(t, err)
|
|
require.Contains(t, connector.Content, "name: test-goodcontent")
|
|
|
|
// Renaming existing connector fails.
|
|
goodContentRenamed := strings.ReplaceAll(goodContent, "test-goodcontent", "test-goodcontent-new-name")
|
|
connector, err = upsertGithubConnector(context.Background(), m, goodContentRenamed, "PUT", httprouter.Params{httprouter.Param{Key: "name", Value: "test-goodcontent"}})
|
|
require.Nil(t, connector)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsBadParameter(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.Nil(t, err)
|
|
|
|
return []types.TrustedCluster{cluster}, nil
|
|
}
|
|
|
|
// Test response is converted to ui objects.
|
|
tcs, err := getTrustedClusters(ctx, m)
|
|
require.Nil(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)
|
|
|
|
m := &mockedResourceAPIGetter{}
|
|
m.mockListResources = func(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
|
|
if !tc.wantBadParamErr {
|
|
require.Equal(t, tc.expected, req)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
_, err = listResources(m, 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)
|
|
mockUpsertRole func(ctx context.Context, role types.Role) error
|
|
mockUpsertGithubConnector func(ctx context.Context, connector types.GithubConnector) 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)
|
|
}
|
|
|
|
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) UpsertRole(ctx context.Context, role types.Role) error {
|
|
if m.mockUpsertRole != nil {
|
|
return m.mockUpsertRole(ctx, role)
|
|
}
|
|
|
|
return trace.NotImplemented("mockUpsertRole not implemented")
|
|
}
|
|
|
|
func (m *mockedResourceAPIGetter) UpsertGithubConnector(ctx context.Context, connector types.GithubConnector) error {
|
|
if m.mockUpsertGithubConnector != nil {
|
|
return m.mockUpsertGithubConnector(ctx, connector)
|
|
}
|
|
|
|
return trace.NotImplemented("mockUpsertGithubConnector 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")
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|