mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
* app: Consolidate app name and public_addr validation Consolidate app `name` and `public_addr` validation to fix six issues where Teleport rejects valid app names, accepts invalid ones, or crashes on startup. Preserve backwards compatibility for dynamic and heartbeat paths: clusters with mixed-case records already in the backend keep working. Strict write validation (admin writes + static config): - App name, static: `IsDNS1123Label` - no dots, max 63 chars. - App name, dynamic: `IsDNS1123Subdomain` - dots OK for AWS-OIDC, max 253 chars. - `public_addr`: `IsDNS1123Subdomain` - dots OK (it is a hostname), max 253 chars. All three k8s validators also enforce lowercase. RFC 1123 itself does not -- lowercase is a Teleport convention for interop with Kubernetes object names. `ValidateApp` is the single write-time validator, called from `CreateApp`, `UpdateApp`, `UpsertApplicationServer`, the inventory control stream, and app service startup. Heartbeats run `NormalizeAppServerForHeartbeat` first, which lowercases both `name` and `public_addr` and strips URL schemes and ports, so heartbeats from older agents end up stored lowercase. Link: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ * app: Apply review feedback Address review nits from the PR: - Use t.Context() in new tests; add t.Cleanup for client Close. - Drop double Close() in inventory controller tests. - Use proto getters in identitycenter to avoid panic on nil Spec. - Replace multi-hyphen regex with strings.ReplaceAll loop. - Collapse mockRolesAnywhereClient.profiles into pages. - Retitle and flesh out the CHANGELOG entry. - Revert accidental enterprise submodule bump. * app: Apply review feedback Use proto getters for the remaining `acct.Spec.*` accesses in `IdentityCenterAccountToAppServer` so the function stays nil-safe end to end. Fix grammar in the app name doc bullet ("as is a duplicate name" -> "as are duplicate names"). * app: Preserve Identity Center launch URL Identity Center accounts surface in the unified-resource cache as synthetic AppServers and never traverse `ValidateApp` / `ValidateAppServer`, so the DNS-1123 normalization the previous commits introduced in `IdentityCenterAccountToAppServer` had no write path to satisfy. Keep the raw StartUrl in both `URI` and `PublicAddr`: the web Launch button builds the SSO launch href as `${publicAddr}&role_name=...`, so stripping scheme, path, port, or case breaks every Identity Center app launch. Drop the matching lowercase on `metadata.Name` for the same reason, and restore the delete handler in `processEventsAndUpdateCurrent` to its pre-PR shape (rebuild a `KindAppServer` header from the IC account event, no lowercase). Rewrite `TestIdentityCenterAccountToAppServer` to assert StartUrl passes through verbatim; remove the `ValidateApp` round-trip assertion, which was checking a path the synthetic AppServer never takes in real callers. * app: Apply review feedback * app: Apply review feedback Tighten the "Stricter application validation" CHANGELOG entry to focus on the three backwards-incompatible cases: static-config DNS label, unrecoverable public_addr, and duplicate names within one agent's teleport.yaml. Expand the write-path list to include the Terraform provider, Kubernetes operator, and direct API calls. Note that multi-agent same-name heartbeats remain supported for load balancing. * app: Allow underscores in dynamic app names Swap IsDNS1123Subdomain for IsDNS1123SubdomainWithUnderscore in ValidateApp, the required_apps loop, and ValidateAppServer. The strict variant rejected snake_case names that Teleport Cloud and self-hosted clusters both accept today, breaking the Terraform provider tests as the canary. Static yaml (lib/service/servicecfg/app.go) remains strict, since the rule there is a deliberate breaking change. public_addr also stays strict, since it is the actual hostname used for routing. Document the new rule and the strict-TLS-client caveat in the "Application name" section of connecting-apps.mdx.
1683 lines
53 KiB
Go
1683 lines
53 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"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"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/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/gravitational/teleport/api"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/types/discoveryconfig"
|
|
"github.com/gravitational/teleport/lib/integrations/awsoidc"
|
|
"github.com/gravitational/teleport/lib/integrations/awsoidc/deployserviceconfig"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
libui "github.com/gravitational/teleport/lib/ui"
|
|
"github.com/gravitational/teleport/lib/utils/log/logtest"
|
|
"github.com/gravitational/teleport/lib/web/ui"
|
|
)
|
|
|
|
func TestBuildDeployServiceConfigureIAMScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
|
|
// Unauthenticated client for script downloading.
|
|
publicClt := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"deployservice-iam.sh",
|
|
}
|
|
endpoint := publicClt.Endpoint(pathVars...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"awsAccountID": []string{"123456789012"},
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"taskRole": []string{"taskRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure deployservice-iam " +
|
|
`--cluster=localhost ` +
|
|
`--name=myintegration ` +
|
|
`--aws-region=us-east-1 ` +
|
|
`--role=myRole ` +
|
|
`--task-role=taskRole ` +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"awsAccountID": []string{"123456789012"},
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
"taskRole": []string{"taskRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure deployservice-iam " +
|
|
`--cluster=localhost ` +
|
|
`--name=myintegration ` +
|
|
`--aws-region=us-east-1 ` +
|
|
`--role=Test\+1=2,3.4\@5-6_7 ` +
|
|
`--task-role=taskRole ` +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "missing aws-region",
|
|
reqQuery: url.Values{
|
|
"awsAccountID": []string{"123456789012"},
|
|
"role": []string{"myRole"},
|
|
"taskRole": []string{"taskRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{
|
|
"awsAccountID": []string{"123456789012"},
|
|
"awsRegion": []string{"us-east-1"},
|
|
"taskRole": []string{"taskRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing task role",
|
|
reqQuery: url.Values{
|
|
"awsAccountID": []string{"123456789012"},
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"role"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing integration name",
|
|
reqQuery: url.Values{
|
|
"awsAccountID": []string{"123456789012"},
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"role"},
|
|
"taskRole": []string{"taskRole"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing account id",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"role"},
|
|
"taskRole": []string{"taskRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := publicClt.Get(ctx, endpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildEC2SSMIAMScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
|
|
proxyPublicURL := env.proxies[0].webURL.String()
|
|
|
|
// Unauthenticated client for script downloading.
|
|
publicClt := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"ec2-ssm-iam.sh",
|
|
}
|
|
endpoint := publicClt.Endpoint(pathVars...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
|
|
"integrationName": []string{"my-integration"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure ec2-ssm-iam " +
|
|
"--role=myRole " +
|
|
"--aws-region=us-east-1 " +
|
|
"--ssm-document-name=TeleportDiscoveryInstallerTest " +
|
|
"--proxy-public-url=" + proxyPublicURL + " " +
|
|
"--cluster=localhost " +
|
|
"--name=my-integration " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
|
|
"integrationName": []string{"my-integration"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure ec2-ssm-iam " +
|
|
"--role=Test\\+1=2,3.4\\@5-6_7 " +
|
|
"--aws-region=us-east-1 " +
|
|
"--ssm-document-name=TeleportDiscoveryInstallerTest " +
|
|
"--proxy-public-url=" + proxyPublicURL + " " +
|
|
"--cluster=localhost " +
|
|
"--name=my-integration " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "missing aws-region",
|
|
reqQuery: url.Values{
|
|
"role": []string{"myRole"},
|
|
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing ssm document",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing account id",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "trying to inject escape sequence into query params",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"'; rm -rf /tmp/dir; echo '"},
|
|
"role": []string{"role"},
|
|
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := publicClt.Get(ctx, endpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildAccessGraphCloudSyncIAMScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
env := newWebPack(t, 1)
|
|
|
|
// Unauthenticated client for script downloading.
|
|
anonymousHTTPClient := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"access-graph-cloud-sync-iam.sh",
|
|
}
|
|
endpoint := anonymousHTTPClient.Endpoint(pathVars...)
|
|
|
|
role := "myRole"
|
|
awsAccountID := "123456789012"
|
|
sqsUrl := "https://sqs.us-west-2.amazonaws.com/123456789012/queue-name"
|
|
cloudTrailS3Bucket := "arn:aws:s3:::bucket-name"
|
|
kmsKey1 := "arn:aws:kms:us-west-2:123456789012:key/00000000-1111-2222-3333-444444444444"
|
|
kmsKey2 := "arn:aws:kms:us-west-2:123456789012:key/55555555-6666-7777-8888-999999999999"
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{role},
|
|
"awsAccountID": []string{awsAccountID},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure access-graph aws-iam" +
|
|
" --role=" + role +
|
|
" --aws-account-id=" + awsAccountID,
|
|
},
|
|
{
|
|
name: "valid with cloud trail",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{role},
|
|
"awsAccountID": []string{awsAccountID},
|
|
"sqsUrl": []string{sqsUrl},
|
|
"cloudTrailS3Bucket": []string{cloudTrailS3Bucket},
|
|
"kmsKeysARNs": []string{kmsKey1, kmsKey2},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure access-graph aws-iam" +
|
|
" --role=" + role +
|
|
" --aws-account-id=" + awsAccountID +
|
|
" --sqs-queue-url=" + sqsUrl +
|
|
" --cloud-trail-bucket=" + cloudTrailS3Bucket +
|
|
" --kms-key=" + kmsKey1 +
|
|
" --kms-key=" + kmsKey2,
|
|
},
|
|
{
|
|
name: "valid with eks audit logs",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{role},
|
|
"awsAccountID": []string{awsAccountID},
|
|
"eksAuditLogs": []string{"true"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure access-graph aws-iam" +
|
|
" --role=" + role +
|
|
" --aws-account-id=" + awsAccountID +
|
|
" --eks-audit-logs",
|
|
},
|
|
{
|
|
name: "valid with cloud trail and eks audit logs",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{role},
|
|
"awsAccountID": []string{awsAccountID},
|
|
"sqsUrl": []string{sqsUrl},
|
|
"cloudTrailS3Bucket": []string{cloudTrailS3Bucket},
|
|
"kmsKeysARNs": []string{kmsKey1, kmsKey2},
|
|
"eksAuditLogs": []string{"true"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure access-graph aws-iam" +
|
|
" --role=" + role +
|
|
" --aws-account-id=" + awsAccountID +
|
|
" --sqs-queue-url=" + sqsUrl +
|
|
" --cloud-trail-bucket=" + cloudTrailS3Bucket +
|
|
" --kms-key=" + kmsKey1 +
|
|
" --kms-key=" + kmsKey2 +
|
|
" --eks-audit-logs",
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure access-graph aws-iam " +
|
|
"--role=Test\\+1=2,3.4\\@5-6_7 " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "missing kind",
|
|
reqQuery: url.Values{
|
|
"role": []string{"myRole"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing awsAccountID",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{"myRole"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "trying to inject escape sequence into query params",
|
|
reqQuery: url.Values{
|
|
"kind": []string{"aws-iam"},
|
|
"role": []string{"'; rm -rf /tmp/dir; echo '"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := anonymousHTTPClient.Get(t.Context(), endpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildAWSAppAccessConfigureIAMScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
env := newWebPack(t, 1)
|
|
|
|
// Unauthenticated client for script downloading.
|
|
anonymousHTTPClient := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"aws-app-access-iam.sh",
|
|
}
|
|
endpoint := anonymousHTTPClient.Endpoint(pathVars...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure aws-app-access-iam " +
|
|
"--role=myRole",
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure aws-app-access-iam " +
|
|
"--role=Test\\+1=2,3.4\\@5-6_7",
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "trying to inject escape sequence into query params",
|
|
reqQuery: url.Values{
|
|
"role": []string{"'; rm -rf /tmp/dir; echo '"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := anonymousHTTPClient.Get(t.Context(), endpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildEKSConfigureIAMScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
|
|
// Unauthenticated client for script downloading.
|
|
publicClt := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"eks-iam.sh",
|
|
}
|
|
endpoint := publicClt.Endpoint(pathVars...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure eks-iam " +
|
|
"--aws-region=us-east-1 " +
|
|
"--role=myRole " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure eks-iam " +
|
|
"--aws-region=us-east-1 " +
|
|
"--role=Test\\+1=2,3.4\\@5-6_7 " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "missing aws-region",
|
|
reqQuery: url.Values{
|
|
"role": []string{"myRole"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing account id",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "trying to inject escape sequence into query params",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"'; rm -rf /tmp/dir; echo '"},
|
|
"role": []string{"role"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := publicClt.Get(ctx, endpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
proxyPublicURL := env.proxies[0].webURL
|
|
|
|
// Unauthenticated client for script downloading.
|
|
publicClt := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"awsoidc-idp.sh",
|
|
}
|
|
scriptEndpoint := publicClt.Endpoint(pathVars...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
"policyPreset": []string{""},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
|
"--cluster=localhost " +
|
|
"--name=myintegration " +
|
|
"--role=myRole " +
|
|
"--proxy-public-url=" + proxyPublicURL.String(),
|
|
},
|
|
{
|
|
name: "valid with proxy endpoint",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
|
"--cluster=localhost " +
|
|
"--name=myintegration " +
|
|
"--role=myRole " +
|
|
"--proxy-public-url=" + proxyPublicURL.String(),
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
|
"--cluster=localhost " +
|
|
"--name=myintegration " +
|
|
"--role=Test\\+1=2,3.4\\@5-6_7 " +
|
|
"--proxy-public-url=" + proxyPublicURL.String(),
|
|
},
|
|
{
|
|
name: "valid with supported policy preset",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"integrationName": []string{"myintegration"},
|
|
"policyPreset": []string{"aws-identity-center"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
|
"--cluster=localhost " +
|
|
"--name=myintegration " +
|
|
"--role=myRole " +
|
|
"--proxy-public-url=" + proxyPublicURL.String() + " " +
|
|
"--policy-preset=aws-identity-center",
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{
|
|
"integrationName": []string{"myintegration"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing integration name",
|
|
reqQuery: url.Values{
|
|
"role": []string{"role"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing policy preset",
|
|
reqQuery: url.Values{
|
|
"integrationName": []string{"myintegration"},
|
|
"role": []string{"role"},
|
|
},
|
|
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
|
"--cluster=localhost " +
|
|
"--name=myintegration " +
|
|
"--role=role " +
|
|
"--proxy-public-url=" + proxyPublicURL.String(),
|
|
errCheck: require.NoError,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := publicClt.Get(ctx, scriptEndpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildListDatabasesConfigureIAMScript(t *testing.T) {
|
|
t.Parallel()
|
|
isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
|
|
require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
env := newWebPack(t, 1)
|
|
|
|
// Unauthenticated client for script downloading.
|
|
publicClt := env.proxies[0].newClient(t)
|
|
pathVars := []string{
|
|
"webapi",
|
|
"scripts",
|
|
"integrations",
|
|
"configure",
|
|
"listdatabases-iam.sh",
|
|
}
|
|
endpoint := publicClt.Endpoint(pathVars...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqRelativeURL string
|
|
reqQuery url.Values
|
|
errCheck require.ErrorAssertionFunc
|
|
expectedTeleportArgs string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure listdatabases-iam " +
|
|
"--aws-region=us-east-1 " +
|
|
"--role=myRole " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "valid with symbols in role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"Test+1=2,3.4@5-6_7"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: require.NoError,
|
|
expectedTeleportArgs: "integration configure listdatabases-iam " +
|
|
"--aws-region=us-east-1 " +
|
|
"--role=Test\\+1=2,3.4\\@5-6_7 " +
|
|
"--aws-account-id=123456789012",
|
|
},
|
|
{
|
|
name: "missing aws-region",
|
|
reqQuery: url.Values{
|
|
"role": []string{"myRole"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing account id",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"role": []string{"myRole"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "missing role",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"us-east-1"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
{
|
|
name: "trying to inject escape sequence into query params",
|
|
reqQuery: url.Values{
|
|
"awsRegion": []string{"'; rm -rf /tmp/dir; echo '"},
|
|
"role": []string{"role"},
|
|
"awsAccountID": []string{"123456789012"},
|
|
},
|
|
errCheck: isBadParamErrFn,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, err := publicClt.Get(ctx, endpoint, tc.reqQuery)
|
|
tc.errCheck(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
require.Contains(t, string(resp.Bytes()),
|
|
fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAWSOIDCRequiredVPCSHelper(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
matchRegion := "us-east-1"
|
|
matchAccountId := "123456789012"
|
|
req := ui.AWSOIDCRequiredVPCSRequest{
|
|
Region: matchRegion,
|
|
AccountID: matchAccountId,
|
|
}
|
|
|
|
dbServiceFor := func(vpcId string, matcher []*types.DatabaseResourceMatcher) *types.DatabaseServiceV1 {
|
|
if matcher == nil {
|
|
matcher = []*types.DatabaseResourceMatcher{
|
|
{
|
|
Labels: &types.Labels{
|
|
types.DiscoveryLabelAccountID: []string{matchAccountId},
|
|
types.DiscoveryLabelRegion: []string{matchRegion},
|
|
types.DiscoveryLabelVPCID: []string{vpcId},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
svc, err := types.NewDatabaseServiceV1(types.Metadata{
|
|
Name: uuid.NewString(),
|
|
Labels: map[string]string{types.AWSOIDCAgentLabel: types.True},
|
|
}, types.DatabaseServiceSpecV1{
|
|
ResourceMatchers: matcher,
|
|
})
|
|
require.NoError(t, err)
|
|
return svc
|
|
}
|
|
|
|
extractKeysFn := func(resp *ui.AWSOIDCRequiredVPCSResponse) []string {
|
|
keys := make([]string, 0, len(resp.VPCMapOfSubnets))
|
|
for k := range resp.VPCMapOfSubnets {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
vpcs := []string{"vpc-1", "vpc-2", "vpc-3", "vpc-4", "vpc-5"}
|
|
rdss := []*types.DatabaseV3{}
|
|
for _, vpc := range vpcs {
|
|
rdss = append(rdss,
|
|
mustCreateRDS(t, types.RDS{
|
|
VPCID: vpc,
|
|
}),
|
|
)
|
|
}
|
|
|
|
// Double check we start with 0 db svcs.
|
|
clt := &mockGetResources{
|
|
databaseServices: &proto.ListResourcesResponse{},
|
|
}
|
|
|
|
// All vpc's required.
|
|
resp, err := awsOIDCRequiredVPCSHelper(ctx, clt, req, rdss)
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.VPCMapOfSubnets, 5)
|
|
require.ElementsMatch(t, vpcs, extractKeysFn(resp))
|
|
|
|
// Add some database services.
|
|
// Two valid database services.
|
|
validDBServiceVPC1 := dbServiceFor("vpc-1", nil)
|
|
validDBServiceVPC5 := dbServiceFor("vpc-5", nil)
|
|
|
|
// Two invalid database services.
|
|
invalidDBServiceVPC2 := dbServiceFor("vpc-2", []*types.DatabaseResourceMatcher{
|
|
{
|
|
Labels: &types.Labels{
|
|
types.DiscoveryLabelAccountID: []string{matchAccountId},
|
|
types.DiscoveryLabelRegion: []string{"us-east-2"}, // not matching region
|
|
types.DiscoveryLabelVPCID: []string{"vpc-2"},
|
|
},
|
|
},
|
|
})
|
|
invalidDBServiceVPC2a := dbServiceFor("vpc-2a", []*types.DatabaseResourceMatcher{
|
|
{
|
|
Labels: &types.Labels{
|
|
types.DiscoveryLabelAccountID: []string{matchAccountId},
|
|
types.DiscoveryLabelRegion: []string{matchRegion},
|
|
types.DiscoveryLabelVPCID: []string{"vpc-2"},
|
|
"something": []string{"extra"}, // not matching b/c extra label
|
|
},
|
|
},
|
|
})
|
|
|
|
clt.databaseServices.Resources = append(clt.databaseServices.Resources,
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: validDBServiceVPC1}},
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: validDBServiceVPC5}},
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: invalidDBServiceVPC2}},
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: invalidDBServiceVPC2a}},
|
|
)
|
|
|
|
// Test that only 3 vpcs are required.
|
|
resp, err = awsOIDCRequiredVPCSHelper(ctx, clt, req, rdss)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"vpc-2", "vpc-3", "vpc-4"}, extractKeysFn(resp))
|
|
|
|
// Insert the rest of db services
|
|
validDBServiceVPC2 := dbServiceFor("vpc-2", nil)
|
|
validDBServiceVPC3 := dbServiceFor("vpc-3", nil)
|
|
validDBServiceVPC4 := dbServiceFor("vpc-4", nil)
|
|
clt.databaseServices.Resources = append(clt.databaseServices.Resources,
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: validDBServiceVPC2}},
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: validDBServiceVPC3}},
|
|
&proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: validDBServiceVPC4}},
|
|
)
|
|
|
|
// Test no required vpcs.
|
|
resp, err = awsOIDCRequiredVPCSHelper(ctx, clt, req, rdss)
|
|
require.NoError(t, err)
|
|
require.Empty(t, resp.VPCMapOfSubnets)
|
|
}
|
|
|
|
func TestAWSOIDCRequiredVPCSHelper_CombinedSubnetsForAVpcID(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
rdsVPC1 := mustCreateRDS(t, types.RDS{
|
|
VPCID: "vpc-1",
|
|
Subnets: []string{"subnet1", "subnet2"},
|
|
})
|
|
|
|
rdsVPC1a := mustCreateRDS(t, types.RDS{
|
|
VPCID: "vpc-1",
|
|
Subnets: []string{"subnet2", "subnet3", "subnet4", "subnet1"},
|
|
})
|
|
|
|
rdsVPC2 := mustCreateRDS(t, types.RDS{
|
|
VPCID: "vpc-2",
|
|
Subnets: []string{"subnet8"},
|
|
})
|
|
|
|
rdss := []*types.DatabaseV3{rdsVPC1, rdsVPC1a, rdsVPC2}
|
|
|
|
resp, err := awsOIDCRequiredVPCSHelper(ctx, &mockGetResources{}, ui.AWSOIDCRequiredVPCSRequest{Region: "us-east-1"}, rdss)
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.VPCMapOfSubnets, 2)
|
|
require.ElementsMatch(t, []string{"subnet1", "subnet2", "subnet3", "subnet4"}, resp.VPCMapOfSubnets["vpc-1"])
|
|
require.ElementsMatch(t, []string{"subnet8"}, resp.VPCMapOfSubnets["vpc-2"])
|
|
}
|
|
|
|
type mockGetResources struct {
|
|
databaseServices *proto.ListResourcesResponse
|
|
}
|
|
|
|
func (m *mockGetResources) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
|
|
switch req.ResourceType {
|
|
case types.KindDatabaseService:
|
|
if m.databaseServices != nil {
|
|
return m.databaseServices, nil
|
|
}
|
|
}
|
|
return &proto.ListResourcesResponse{}, nil
|
|
}
|
|
|
|
func mustCreateRDS(t *testing.T, awsRDS types.RDS) *types.DatabaseV3 {
|
|
rdsDB, err := types.NewDatabaseV3(types.Metadata{
|
|
Name: "x",
|
|
}, types.DatabaseSpecV3{
|
|
Protocol: "postgres",
|
|
URI: "endpoint.amazonaws.com:5432",
|
|
AWS: types.AWS{
|
|
RDS: awsRDS,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return rdsDB
|
|
}
|
|
|
|
func TestAWSOIDCSecurityGroupsRulesConverter(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
in []*integrationv1.SecurityGroupRule
|
|
expected []awsoidc.SecurityGroupRule
|
|
}{
|
|
{
|
|
name: "valid",
|
|
in: []*integrationv1.SecurityGroupRule{{
|
|
IpProtocol: "tcp",
|
|
FromPort: 8080,
|
|
ToPort: 8081,
|
|
Cidrs: []*integrationv1.SecurityGroupRuleCIDR{{
|
|
Cidr: "10.10.10.0/24",
|
|
Description: "cidr x",
|
|
}},
|
|
}},
|
|
expected: []awsoidc.SecurityGroupRule{{
|
|
IPProtocol: "tcp",
|
|
FromPort: 8080,
|
|
ToPort: 8081,
|
|
CIDRs: []awsoidc.CIDR{{
|
|
CIDR: "10.10.10.0/24",
|
|
Description: "cidr x",
|
|
}},
|
|
}},
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
out := awsOIDCSecurityGroupsRulesConverter(tt.in)
|
|
require.Equal(t, tt.expected, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAWSOIDCAppAccessAppServerCreationDeletion(t *testing.T) {
|
|
env := newWebPack(t, 1)
|
|
ctx := context.Background()
|
|
|
|
roleTokenCRD, err := types.NewRole(services.RoleNameForUser("my-user"), types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
AppLabels: types.Labels{"*": []string{"*"}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindIntegration, []string{types.VerbRead}),
|
|
types.NewRule(types.KindAppServer, []string{types.VerbCreate, types.VerbUpdate, types.VerbList, types.VerbDelete}),
|
|
types.NewRule(types.KindUserGroup, []string{types.VerbList, types.VerbRead}),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
proxy := env.proxies[0]
|
|
proxy.handler.handler.cfg.PublicProxyAddr = strings.TrimPrefix(proxy.handler.handler.cfg.PublicProxyAddr, "https://")
|
|
proxyPublicAddr := proxy.handler.handler.cfg.PublicProxyAddr
|
|
proxyPublicHost, _, err := net.SplitHostPort(proxyPublicAddr)
|
|
require.NoError(t, err)
|
|
pack := proxy.authPack(t, "foo@example.com", []types.Role{roleTokenCRD})
|
|
|
|
myIntegration, err := types.NewIntegrationAWSOIDC(types.Metadata{
|
|
Name: "my-integration",
|
|
}, &types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/teleport",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = env.server.Auth().CreateIntegration(ctx, myIntegration)
|
|
require.NoError(t, err)
|
|
|
|
// Deleting the AWS App Access should return an error because it was not created yet.
|
|
deleteEndpoint := pack.clt.Endpoint("webapi", "sites", "localhost", "integrations", "aws-oidc", "aws-app-access", "my-integration")
|
|
_, err = pack.clt.Delete(ctx, deleteEndpoint)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "not found")
|
|
|
|
// Create the AWS App Access for the current integration.
|
|
endpoint := pack.clt.Endpoint("webapi", "sites", "localhost", "integrations", "aws-oidc", "my-integration", "aws-app-access")
|
|
_, err = pack.clt.PostJSON(ctx, endpoint, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Ensure the AppServer was correctly created.
|
|
appServers, err := env.server.Auth().GetApplicationServers(ctx, "default")
|
|
require.NoError(t, err)
|
|
require.Len(t, appServers, 1)
|
|
|
|
expectedServer := &types.AppServerV3{
|
|
Kind: types.KindAppServer,
|
|
Version: types.V3,
|
|
Metadata: types.Metadata{
|
|
Name: "my-integration",
|
|
Labels: map[string]string{"aws_account_id": "123456789012"},
|
|
},
|
|
Spec: types.AppServerSpecV3{
|
|
Version: api.Version,
|
|
HostID: "proxy0",
|
|
App: &types.AppV3{
|
|
Kind: types.KindApp,
|
|
Version: types.V3,
|
|
Metadata: types.Metadata{
|
|
Name: "my-integration",
|
|
Labels: map[string]string{"aws_account_id": "123456789012"},
|
|
},
|
|
Spec: types.AppSpecV3{
|
|
URI: "https://console.aws.amazon.com",
|
|
Integration: "my-integration",
|
|
Cloud: "AWS",
|
|
PublicAddr: "my-integration." + proxyPublicHost,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
expectedServer,
|
|
appServers[0],
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
))
|
|
|
|
// After deleting the application, it should be removed from the backend.
|
|
_, err = pack.clt.Delete(ctx, deleteEndpoint)
|
|
require.NoError(t, err)
|
|
appServers, err = env.server.Auth().GetApplicationServers(ctx, "default")
|
|
require.NoError(t, err)
|
|
require.Empty(t, appServers)
|
|
|
|
t.Run("using the account id as name works as expected", func(t *testing.T) {
|
|
// Creating an Integration using the account id as name should not return an error if the proxy is listening at the default HTTPS port
|
|
myIntegrationWithAccountID, err := types.NewIntegrationAWSOIDC(types.Metadata{
|
|
Name: "123456789012",
|
|
}, &types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/teleport",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = env.server.Auth().CreateIntegration(ctx, myIntegrationWithAccountID)
|
|
require.NoError(t, err)
|
|
endpoint = pack.clt.Endpoint("webapi", "sites", "localhost", "integrations", "aws-oidc", "123456789012", "aws-app-access")
|
|
_, err = pack.clt.PostJSON(ctx, endpoint, nil)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("using a period in the name fails when running in cloud", func(t *testing.T) {
|
|
enableCloudFeatureProxy(t, proxy)
|
|
|
|
// Creating an Integration using the account id as name should not return an error if the proxy is listening at the default HTTPS port
|
|
myIntegrationWithAccountID, err := types.NewIntegrationAWSOIDC(types.Metadata{
|
|
Name: "env.prod",
|
|
}, &types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/teleport",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = env.server.Auth().CreateIntegration(ctx, myIntegrationWithAccountID)
|
|
require.NoError(t, err)
|
|
endpoint = pack.clt.Endpoint("webapi", "sites", "localhost", "integrations", "aws-oidc", "env.prod", "aws-app-access")
|
|
_, err = pack.clt.PostJSON(ctx, endpoint, nil)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, `Invalid integration name ("env.prod") for enabling AWS Access.`)
|
|
})
|
|
|
|
t.Run("mixed-case integration name is rejected", func(t *testing.T) {
|
|
mixedCaseIntegration, err := types.NewIntegrationAWSOIDC(types.Metadata{
|
|
Name: "MixedCase",
|
|
}, &types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/teleport",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = env.server.Auth().CreateIntegration(ctx, mixedCaseIntegration)
|
|
require.NoError(t, err)
|
|
endpoint = pack.clt.Endpoint("webapi", "sites", "localhost", "integrations", "aws-oidc", "MixedCase", "aws-app-access")
|
|
_, err = pack.clt.PostJSON(ctx, endpoint, nil)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "contains uppercase characters")
|
|
})
|
|
}
|
|
|
|
func enableCloudFeatureProxy(t *testing.T, proxy *testProxy) {
|
|
t.Helper()
|
|
|
|
existingFeatures := proxy.handler.handler.clusterFeatures
|
|
existingFeatures.Cloud = true
|
|
proxy.handler.handler.clusterFeatures = existingFeatures
|
|
t.Cleanup(func() {
|
|
existingFeatures.Cloud = false
|
|
proxy.handler.handler.clusterFeatures = existingFeatures
|
|
})
|
|
}
|
|
|
|
func TestAWSOIDCAppAccessAppServerCreationWithUserProvidedLabels(t *testing.T) {
|
|
env := newWebPack(t, 1)
|
|
ctx := context.Background()
|
|
|
|
roleTokenCRD, err := types.NewRole(services.RoleNameForUser("my-user"), types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
AppLabels: types.Labels{"*": []string{"*"}},
|
|
Rules: []types.Rule{
|
|
types.NewRule(types.KindIntegration, []string{types.VerbRead}),
|
|
types.NewRule(types.KindAppServer, []string{types.VerbCreate, types.VerbUpdate, types.VerbList, types.VerbDelete}),
|
|
types.NewRule(types.KindUserGroup, []string{types.VerbList, types.VerbRead}),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
proxy := env.proxies[0]
|
|
proxy.handler.handler.cfg.PublicProxyAddr = strings.TrimPrefix(proxy.handler.handler.cfg.PublicProxyAddr, "https://")
|
|
pack := proxy.authPack(t, "foo@example.com", []types.Role{roleTokenCRD})
|
|
|
|
myIntegration, err := types.NewIntegrationAWSOIDC(types.Metadata{
|
|
Name: "my-integration",
|
|
}, &types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/teleport",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = env.server.Auth().CreateIntegration(ctx, myIntegration)
|
|
require.NoError(t, err)
|
|
|
|
// Create the AWS App Access for the current integration.
|
|
endpoint := pack.clt.Endpoint("webapi", "sites", "localhost", "integrations", "aws-oidc", "my-integration", "aws-app-access")
|
|
re, err := pack.clt.PostJSON(ctx, endpoint, ui.AWSOIDCCreateAWSAppAccessRequest{Labels: map[string]string{"env": "testing"}})
|
|
require.NoError(t, err)
|
|
|
|
var app ui.App
|
|
require.NoError(t, json.Unmarshal(re.Bytes(), &app))
|
|
|
|
require.ElementsMatch(t, app.Labels, []libui.Label{{Name: "env", Value: "testing"}, {Name: "aws_account_id", Value: "123456789012"}})
|
|
}
|
|
|
|
type mockDeployedDatabaseServices struct {
|
|
listErr error
|
|
integration string
|
|
servicesPerRegion map[string][]*integrationv1.DeployedDatabaseService
|
|
}
|
|
|
|
func (m *mockDeployedDatabaseServices) ListDeployedDatabaseServices(ctx context.Context, in *integrationv1.ListDeployedDatabaseServicesRequest, opts ...grpc.CallOption) (*integrationv1.ListDeployedDatabaseServicesResponse, error) {
|
|
if m.listErr != nil {
|
|
return nil, m.listErr
|
|
}
|
|
const pageSize = 10
|
|
ret := &integrationv1.ListDeployedDatabaseServicesResponse{}
|
|
if in.Integration != m.integration {
|
|
return ret, nil
|
|
}
|
|
|
|
services := m.servicesPerRegion[in.Region]
|
|
if len(services) == 0 {
|
|
return ret, nil
|
|
}
|
|
|
|
requestedPage := 1
|
|
totalResources := len(services)
|
|
|
|
if in.NextToken != "" {
|
|
currentMarker, err := strconv.Atoi(in.NextToken)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
requestedPage = currentMarker
|
|
}
|
|
|
|
sliceStart := pageSize * (requestedPage - 1)
|
|
sliceEnd := min(pageSize*requestedPage, totalResources)
|
|
|
|
ret.DeployedDatabaseServices = services[sliceStart:sliceEnd]
|
|
if sliceEnd < totalResources {
|
|
ret.NextToken = strconv.Itoa(requestedPage + 1)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func TestAWSOIDCListDeployedDatabaseServices(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger := logtest.NewLogger()
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
integration string
|
|
regions []string
|
|
servicesPerRegion func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService
|
|
expectedServices func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService
|
|
}{
|
|
{
|
|
name: "valid",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}})
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-2": dummyDeployedDatabaseServices(1, command),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService {
|
|
return []ui.AWSOIDCDeployedDatabaseService{{
|
|
Name: "database-service-vpc-0",
|
|
DashboardURL: "url",
|
|
ValidTeleportConfig: true,
|
|
MatchingLabels: []libui.Label{
|
|
{Name: "vpc", Value: "vpc1"},
|
|
{Name: "vpc", Value: "vpc2"},
|
|
},
|
|
}}
|
|
},
|
|
},
|
|
{
|
|
name: "no regions",
|
|
integration: "my-integration",
|
|
regions: []string{},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
return make(map[string][]*integrationv1.DeployedDatabaseService)
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { return nil },
|
|
},
|
|
{
|
|
name: "no services",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
return make(map[string][]*integrationv1.DeployedDatabaseService)
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { return nil },
|
|
},
|
|
{
|
|
name: "services exist but for another region",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-1": dummyDeployedDatabaseServices(1, []string{}),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { return nil },
|
|
},
|
|
{
|
|
name: "services exist for multiple regions",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}})
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-1": dummyDeployedDatabaseServices(1, command),
|
|
"us-west-2": dummyDeployedDatabaseServices(1, command),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService {
|
|
return []ui.AWSOIDCDeployedDatabaseService{{
|
|
Name: "database-service-vpc-0",
|
|
DashboardURL: "url",
|
|
ValidTeleportConfig: true,
|
|
MatchingLabels: []libui.Label{
|
|
{Name: "vpc", Value: "vpc1"},
|
|
{Name: "vpc", Value: "vpc2"},
|
|
},
|
|
}}
|
|
},
|
|
},
|
|
{
|
|
name: "service exist but has invalid configuration",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
command := buildCommandDeployedDatabaseService(t, false, nil)
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-2": dummyDeployedDatabaseServices(1, command),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService {
|
|
return []ui.AWSOIDCDeployedDatabaseService{{
|
|
Name: "database-service-vpc-0",
|
|
DashboardURL: "url",
|
|
ValidTeleportConfig: false,
|
|
}}
|
|
},
|
|
},
|
|
{
|
|
name: "service exist but was changed and --config-string argument is missing",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}})
|
|
command = command[:len(command)-1]
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-2": dummyDeployedDatabaseServices(1, command),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService {
|
|
return []ui.AWSOIDCDeployedDatabaseService{{
|
|
Name: "database-service-vpc-0",
|
|
DashboardURL: "url",
|
|
ValidTeleportConfig: false,
|
|
}}
|
|
},
|
|
},
|
|
{
|
|
name: "service exist but was changed and --config-string flag is missing",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}})
|
|
command[1] = "--no-config-string"
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-2": dummyDeployedDatabaseServices(1, command),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService {
|
|
return []ui.AWSOIDCDeployedDatabaseService{{
|
|
Name: "database-service-vpc-0",
|
|
DashboardURL: "url",
|
|
ValidTeleportConfig: false,
|
|
}}
|
|
},
|
|
},
|
|
{
|
|
name: "supports pagination",
|
|
integration: "my-integration",
|
|
regions: []string{"us-west-2"},
|
|
servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService {
|
|
command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}})
|
|
return map[string][]*integrationv1.DeployedDatabaseService{
|
|
"us-west-2": dummyDeployedDatabaseServices(1_024, command),
|
|
}
|
|
},
|
|
expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService {
|
|
var ret []ui.AWSOIDCDeployedDatabaseService
|
|
for i := range 1_024 {
|
|
ret = append(ret, ui.AWSOIDCDeployedDatabaseService{
|
|
Name: fmt.Sprintf("database-service-vpc-%d", i),
|
|
DashboardURL: "url",
|
|
ValidTeleportConfig: true,
|
|
MatchingLabels: []libui.Label{
|
|
{Name: "vpc", Value: "vpc1"},
|
|
{Name: "vpc", Value: "vpc2"},
|
|
},
|
|
})
|
|
}
|
|
return ret
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
clt := &mockDeployedDatabaseServices{
|
|
integration: tt.integration,
|
|
servicesPerRegion: tt.servicesPerRegion(t),
|
|
}
|
|
actual, err := listDeployedDatabaseServices(ctx, logger, tt.integration, tt.regions, clt)
|
|
require.NoError(t, err)
|
|
expected := tt.expectedServices(t)
|
|
require.Equal(t, expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildCommandDeployedDatabaseService(t *testing.T, valid bool, matchingLabels types.Labels) []string {
|
|
t.Helper()
|
|
if !valid {
|
|
return []string{"not valid"}
|
|
}
|
|
|
|
ret, err := deployserviceconfig.GenerateTeleportConfigString("host", "token", matchingLabels)
|
|
require.NoError(t, err)
|
|
|
|
return []string{"start", "--config-string", ret}
|
|
}
|
|
|
|
func dummyDeployedDatabaseServices(count int, command []string) []*integrationv1.DeployedDatabaseService {
|
|
var ret []*integrationv1.DeployedDatabaseService
|
|
for i := range count {
|
|
ret = append(ret, &integrationv1.DeployedDatabaseService{
|
|
Name: fmt.Sprintf("database-service-vpc-%d", i),
|
|
ServiceDashboardUrl: "url",
|
|
ContainerEntryPoint: []string{"teleport"},
|
|
ContainerCommand: command,
|
|
})
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func TestRegionsForListingDeployedDatabaseService(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("regions query param, returns nil if no internal resources match", func(t *testing.T) {
|
|
clt := &mockRelevantAWSRegionsClient{
|
|
databaseServices: &proto.ListResourcesResponse{
|
|
Resources: []*proto.PaginatedResource{{Resource: &proto.PaginatedResource_DatabaseService{
|
|
DatabaseService: &types.DatabaseServiceV1{Spec: types.DatabaseServiceSpecV1{
|
|
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
|
{Labels: &types.Labels{"region": []string{"af-south-1"}}},
|
|
},
|
|
}},
|
|
}}},
|
|
},
|
|
databases: make([]types.Database, 0),
|
|
discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0),
|
|
}
|
|
r := http.Request{
|
|
URL: &url.URL{RawQuery: "regions=us-east-1®ions=us-east-2"},
|
|
}
|
|
gotRegions, err := regionsForListingDeployedDatabaseService(ctx, &r, clt, clt)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, nil, gotRegions)
|
|
})
|
|
|
|
t.Run("regions query param, returns matches in internal resources", func(t *testing.T) {
|
|
clt := &mockRelevantAWSRegionsClient{
|
|
databaseServices: &proto.ListResourcesResponse{
|
|
Resources: []*proto.PaginatedResource{{Resource: &proto.PaginatedResource_DatabaseService{
|
|
DatabaseService: &types.DatabaseServiceV1{Spec: types.DatabaseServiceSpecV1{
|
|
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
|
{Labels: &types.Labels{"region": []string{"af-south-1"}}}},
|
|
}},
|
|
}}},
|
|
},
|
|
databases: make([]types.Database, 0),
|
|
discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0),
|
|
}
|
|
r := http.Request{
|
|
URL: &url.URL{RawQuery: "regions=af-south-1®ions=us-east-2"},
|
|
}
|
|
gotRegions, err := regionsForListingDeployedDatabaseService(ctx, &r, clt, clt)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"af-south-1"}, gotRegions)
|
|
})
|
|
|
|
t.Run("fallbacks to internal resources when query param is not present", func(t *testing.T) {
|
|
clt := &mockRelevantAWSRegionsClient{
|
|
databaseServices: &proto.ListResourcesResponse{
|
|
Resources: []*proto.PaginatedResource{{Resource: &proto.PaginatedResource_DatabaseService{
|
|
DatabaseService: &types.DatabaseServiceV1{Spec: types.DatabaseServiceSpecV1{
|
|
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
|
{Labels: &types.Labels{"region": []string{"us-east-1"}}},
|
|
{Labels: &types.Labels{"region": []string{"us-east-2"}}},
|
|
},
|
|
}},
|
|
}}},
|
|
},
|
|
databases: make([]types.Database, 0),
|
|
discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0),
|
|
}
|
|
r := http.Request{
|
|
URL: &url.URL{RawQuery: "regions="},
|
|
}
|
|
gotRegions, err := regionsForListingDeployedDatabaseService(ctx, &r, clt, clt)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []string{"us-east-1", "us-east-2"}, gotRegions)
|
|
})
|
|
}
|
|
|
|
func TestFetchRelevantAWSRegions(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("resources do not provide any region", func(t *testing.T) {
|
|
clt := &mockRelevantAWSRegionsClient{
|
|
databaseServices: &proto.ListResourcesResponse{
|
|
Resources: []*proto.PaginatedResource{},
|
|
},
|
|
databases: make([]types.Database, 0),
|
|
discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0),
|
|
}
|
|
gotRegions, err := fetchRelevantAWSRegions(ctx, clt, clt)
|
|
require.NoError(t, err)
|
|
require.Empty(t, gotRegions)
|
|
})
|
|
|
|
t.Run("resources provide multiple regions", func(t *testing.T) {
|
|
clt := &mockRelevantAWSRegionsClient{
|
|
databaseServices: &proto.ListResourcesResponse{
|
|
Resources: []*proto.PaginatedResource{{Resource: &proto.PaginatedResource_DatabaseService{
|
|
DatabaseService: &types.DatabaseServiceV1{Spec: types.DatabaseServiceSpecV1{
|
|
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
|
{Labels: &types.Labels{"region": []string{"us-east-1"}}},
|
|
{Labels: &types.Labels{"region": []string{"us-east-2"}}},
|
|
},
|
|
}},
|
|
}}},
|
|
},
|
|
databases: []types.Database{
|
|
&types.DatabaseV3{Spec: types.DatabaseSpecV3{AWS: types.AWS{Region: "us-west-1"}}},
|
|
&types.DatabaseV3{Metadata: types.Metadata{Labels: map[string]string{"region": "us-west-2"}}},
|
|
},
|
|
discoveryConfigs: []*discoveryconfig.DiscoveryConfig{{
|
|
Spec: discoveryconfig.Spec{AWS: []types.AWSMatcher{{
|
|
Regions: []string{"eu-west-1", "eu-west-2"},
|
|
}}},
|
|
}},
|
|
}
|
|
gotRegions, err := fetchRelevantAWSRegions(ctx, clt, clt)
|
|
require.NoError(t, err)
|
|
expectedRegions := []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "eu-west-1", "eu-west-2"}
|
|
require.ElementsMatch(t, expectedRegions, gotRegions)
|
|
})
|
|
|
|
t.Run("invalid regions are ignored", func(t *testing.T) {
|
|
clt := &mockRelevantAWSRegionsClient{
|
|
databaseServices: &proto.ListResourcesResponse{
|
|
Resources: []*proto.PaginatedResource{},
|
|
},
|
|
databases: []types.Database{
|
|
&types.DatabaseV3{Spec: types.DatabaseSpecV3{AWS: types.AWS{Region: "us-west-1"}}},
|
|
&types.DatabaseV3{Metadata: types.Metadata{Labels: map[string]string{"region": "bad-region"}}},
|
|
},
|
|
discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0),
|
|
}
|
|
gotRegions, err := fetchRelevantAWSRegions(ctx, clt, clt)
|
|
require.NoError(t, err)
|
|
expectedRegions := []string{"us-west-1"}
|
|
require.ElementsMatch(t, expectedRegions, gotRegions)
|
|
})
|
|
}
|
|
|
|
type mockRelevantAWSRegionsClient struct {
|
|
databaseServices *proto.ListResourcesResponse
|
|
databases []types.Database
|
|
discoveryConfigs []*discoveryconfig.DiscoveryConfig
|
|
}
|
|
|
|
func (m *mockRelevantAWSRegionsClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
|
|
return m.databaseServices, nil
|
|
}
|
|
|
|
func (m *mockRelevantAWSRegionsClient) GetDatabases(context.Context) ([]types.Database, error) {
|
|
return m.databases, nil
|
|
}
|
|
|
|
func (m *mockRelevantAWSRegionsClient) ListDiscoveryConfigs(ctx context.Context, pageSize int, nextToken string) ([]*discoveryconfig.DiscoveryConfig, string, error) {
|
|
return m.discoveryConfigs, "", nil
|
|
}
|