diff --git a/integration/integrations/integration_test.go b/integration/integrations/integration_test.go index 5688b395af640..1152dfbc5924e 100644 --- a/integration/integrations/integration_test.go +++ b/integration/integrations/integration_test.go @@ -99,6 +99,18 @@ func TestIntegrationCRUD(t *testing.T) { respStatusCode, respBody = webPack.DoRequest(t, http.MethodPost, integrationsEndpoint, createIntegrationReq) require.Equal(t, http.StatusOK, respStatusCode, string(respBody)) + // Create Integration without S3 location + createIntegrationWithoutS3LocationReq := ui.Integration{ + Name: "MyAWSAccountWithoutS3", + SubKind: types.IntegrationSubKindAWSOIDC, + AWSOIDC: &ui.IntegrationAWSOIDCSpec{ + RoleARN: "arn:aws:iam::123456789012:role/DevTeam", + }, + } + + respStatusCode, respBody = webPack.DoRequest(t, http.MethodPost, integrationsEndpoint, createIntegrationWithoutS3LocationReq) + require.Equal(t, http.StatusOK, respStatusCode, string(respBody)) + // Get One Integration by name respStatusCode, respBody = webPack.DoRequest(t, http.MethodGet, integrationsEndpoint+"/MyAWSAccount", nil) require.Equal(t, http.StatusOK, respStatusCode, string(respBody)) @@ -139,6 +151,25 @@ func TestIntegrationCRUD(t *testing.T) { }, }, integrationResp, string(respBody)) + // Update the integration to remove the S3 Location + respStatusCode, respBody = webPack.DoRequest(t, http.MethodPut, integrationsEndpoint+"/MyAWSAccount", ui.UpdateIntegrationRequest{ + AWSOIDC: &ui.IntegrationAWSOIDCSpec{ + RoleARN: "arn:aws:iam::123456789012:role/OpsTeam2", + }, + }) + require.Equal(t, http.StatusOK, respStatusCode, string(respBody)) + + integrationResp = ui.Integration{} + require.NoError(t, json.Unmarshal(respBody, &integrationResp)) + + require.Equal(t, ui.Integration{ + Name: "MyAWSAccount", + SubKind: types.IntegrationSubKindAWSOIDC, + AWSOIDC: &ui.IntegrationAWSOIDCSpec{ + RoleARN: "arn:aws:iam::123456789012:role/OpsTeam2", + }, + }, integrationResp, string(respBody)) + // Delete resource respStatusCode, respBody = webPack.DoRequest(t, http.MethodDelete, integrationsEndpoint+"/MyAWSAccount", nil) require.Equal(t, http.StatusOK, respStatusCode, string(respBody)) @@ -180,13 +211,13 @@ func TestIntegrationCRUD(t *testing.T) { require.Len(t, listResp.Items, pageSize) - // Requesting the 3rd page should return a single item and empty StartKey + // Requesting the 3rd page should return two items and empty StartKey respStatusCode, respBody = webPack.DoRequest(t, http.MethodGet, integrationsEndpoint+"?limit=10&startKey="+listResp.NextKey, nil) require.Equal(t, http.StatusOK, respStatusCode, string(respBody)) listResp = ui.IntegrationsListResponse{} require.NoError(t, json.Unmarshal(respBody, &listResp)) - require.Len(t, listResp.Items, 1) + require.Len(t, listResp.Items, 2) require.Empty(t, listResp.NextKey) } diff --git a/lib/web/integrations.go b/lib/web/integrations.go index 088f74fce6ee6..d708a0606afce 100644 --- a/lib/web/integrations.go +++ b/lib/web/integrations.go @@ -48,16 +48,20 @@ func (h *Handler) integrationsCreate(w http.ResponseWriter, r *http.Request, p h switch req.SubKind { case types.IntegrationSubKindAWSOIDC: - issuerS3URI := url.URL{ - Scheme: "s3", - Host: req.AWSOIDC.IssuerS3Bucket, - Path: req.AWSOIDC.IssuerS3Prefix, + var s3Location string + if req.AWSOIDC.IssuerS3Bucket != "" { + issuerS3URI := url.URL{ + Scheme: "s3", + Host: req.AWSOIDC.IssuerS3Bucket, + Path: req.AWSOIDC.IssuerS3Prefix, + } + s3Location = issuerS3URI.String() } ig, err = types.NewIntegrationAWSOIDC( types.Metadata{Name: req.Name}, &types.AWSOIDCIntegrationSpecV1{ RoleARN: req.AWSOIDC.RoleARN, - IssuerS3URI: issuerS3URI.String(), + IssuerS3URI: s3Location, }, ) @@ -121,13 +125,17 @@ func (h *Handler) integrationsUpdate(w http.ResponseWriter, r *http.Request, p h return nil, trace.BadParameter("cannot update %q fields for a %q integration", types.IntegrationSubKindAWSOIDC, integration.GetSubKind()) } - issuerS3URI := url.URL{ - Scheme: "s3", - Host: req.AWSOIDC.IssuerS3Bucket, - Path: req.AWSOIDC.IssuerS3Prefix, + var s3Location string + if req.AWSOIDC.IssuerS3Bucket != "" { + issuerS3URI := url.URL{ + Scheme: "s3", + Host: req.AWSOIDC.IssuerS3Bucket, + Path: req.AWSOIDC.IssuerS3Prefix, + } + s3Location = issuerS3URI.String() } + integration.SetAWSOIDCIssuerS3URI(s3Location) integration.SetAWSOIDCRoleARN(req.AWSOIDC.RoleARN) - integration.SetAWSOIDCIssuerS3URI(issuerS3URI.String()) } if _, err := clt.UpdateIntegration(r.Context(), integration); err != nil { diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index ac4579ccb472a..048007d1a997e 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -46,6 +46,7 @@ import ( "github.com/gravitational/teleport/lib/integrations/awsoidc/deployserviceconfig" "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/utils/oidc" "github.com/gravitational/teleport/lib/web/scripts/oneoff" "github.com/gravitational/teleport/lib/web/ui" ) @@ -866,22 +867,6 @@ func (h *Handler) awsOIDCConfigureIdP(w http.ResponseWriter, r *http.Request, p return nil, trace.BadParameter("invalid role %q", role) } - s3Bucket := queryParams.Get("s3Bucket") - s3Prefix := queryParams.Get("s3Prefix") - if s3Bucket == "" || s3Prefix == "" { - return nil, trace.BadParameter("s3Bucket and s3Prefix query params are required") - } - s3URI := url.URL{Scheme: "s3", Host: s3Bucket, Path: s3Prefix} - - jwksContents, err := h.jwks(r.Context(), types.OIDCIdPCA) - if err != nil { - return nil, trace.Wrap(err) - } - jwksJSON, err := json.Marshal(jwksContents) - if err != nil { - return nil, trace.Wrap(err) - } - // The script must execute the following command: // teleport integration configure awsoidc-idp argsList := []string{ @@ -889,9 +874,51 @@ func (h *Handler) awsOIDCConfigureIdP(w http.ResponseWriter, r *http.Request, p fmt.Sprintf("--cluster=%s", shsprintf.EscapeDefaultContext(clusterName)), fmt.Sprintf("--name=%s", shsprintf.EscapeDefaultContext(integrationName)), fmt.Sprintf("--role=%s", shsprintf.EscapeDefaultContext(role)), - fmt.Sprintf("--s3-bucket-uri=%s", shsprintf.EscapeDefaultContext(s3URI.String())), - fmt.Sprintf("--s3-jwks-base64=%s", base64.StdEncoding.EncodeToString(jwksJSON)), } + + // We have two set up modes: + // - use the Proxy HTTP endpoint as Identity Provider + // - use an S3 Bucket for storing the public keys + // + // The script will pick a mode depending on the query params received here. + // If the S3 location was defined, then it will use that mode and upload the Public Keys to the S3 Bucket. + // Otherwise, it will create an IdP pointing to the current Cluster. + // + // Whatever the chosen mode, the Proxy HTTP endpoint will always return the public keys. + s3Bucket := queryParams.Get("s3Bucket") + s3Prefix := queryParams.Get("s3Prefix") + + switch { + case s3Bucket == "" && s3Prefix == "": + proxyAddr, err := oidc.IssuerFromPublicAddress(h.cfg.PublicProxyAddr) + if err != nil { + return nil, trace.Wrap(err) + } + + argsList = append(argsList, + fmt.Sprintf("--proxy-public-url=%s", shsprintf.EscapeDefaultContext(proxyAddr)), + ) + + default: + if s3Bucket == "" || s3Prefix == "" { + return nil, trace.BadParameter("s3Bucket and s3Prefix query params are required") + } + s3URI := url.URL{Scheme: "s3", Host: s3Bucket, Path: s3Prefix} + + jwksContents, err := h.jwks(r.Context(), types.OIDCIdPCA) + if err != nil { + return nil, trace.Wrap(err) + } + jwksJSON, err := json.Marshal(jwksContents) + if err != nil { + return nil, trace.Wrap(err) + } + argsList = append(argsList, + fmt.Sprintf("--s3-bucket-uri=%s", shsprintf.EscapeDefaultContext(s3URI.String())), + fmt.Sprintf("--s3-jwks-base64=%s", base64.StdEncoding.EncodeToString(jwksJSON)), + ) + } + script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ TeleportArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the browser to use the integration with AWS.", diff --git a/lib/web/integrations_awsoidc_test.go b/lib/web/integrations_awsoidc_test.go index 539eec69e37ca..ba32ac9856fe8 100644 --- a/lib/web/integrations_awsoidc_test.go +++ b/lib/web/integrations_awsoidc_test.go @@ -344,6 +344,7 @@ func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) { ctx := context.Background() env := newWebPack(t, 1) + proxyPublicURL := env.proxies[0].webURL // Unauthenticated client for script downloading. publicClt := env.proxies[0].newClient(t) @@ -385,6 +386,20 @@ func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) { `--s3-bucket-uri=s3://my-bucket/prefix ` + "--s3-jwks-base64=" + jwksBase64, }, + { + 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{ diff --git a/lib/web/ui/integration.go b/lib/web/ui/integration.go index 316cbe045b907..9856194ac9eef 100644 --- a/lib/web/ui/integration.go +++ b/lib/web/ui/integration.go @@ -44,11 +44,10 @@ func (r *IntegrationAWSOIDCSpec) CheckAndSetDefaults() error { if r.RoleARN == "" { return trace.BadParameter("missing awsoidc.roleArn field") } - if r.IssuerS3Bucket == "" { - return trace.BadParameter("missing awsoidc.issuerS3Bucket field") - } - if r.IssuerS3Prefix == "" { - return trace.BadParameter("missing awsoidc.issuerS3Prefix field") + + // Either both empty or both are filled. + if (r.IssuerS3Bucket == "") != (r.IssuerS3Prefix == "") { + return trace.BadParameter("missing awsoidc s3 fields") } return nil @@ -128,21 +127,33 @@ func MakeIntegrations(igs []types.Integration) ([]*Integration, error) { // MakeIntegration creates a UI Integration representation. func MakeIntegration(ig types.Integration) (*Integration, error) { - issuerS3BucketURL, err := url.Parse(ig.GetAWSOIDCIntegrationSpec().IssuerS3URI) - if err != nil { - return nil, trace.Wrap(err) - } - prefix := strings.TrimLeft(issuerS3BucketURL.Path, "/") - - return &Integration{ + ret := &Integration{ Name: ig.GetName(), SubKind: ig.GetSubKind(), - AWSOIDC: &IntegrationAWSOIDCSpec{ + } + + switch ig.GetSubKind() { + case types.IntegrationSubKindAWSOIDC: + var s3Bucket string + var s3Prefix string + + if s3Location := ig.GetAWSOIDCIntegrationSpec().IssuerS3URI; s3Location != "" { + issuerS3BucketURL, err := url.Parse(s3Location) + if err != nil { + return nil, trace.Wrap(err) + } + s3Bucket = issuerS3BucketURL.Host + s3Prefix = strings.TrimLeft(issuerS3BucketURL.Path, "/") + } + + ret.AWSOIDC = &IntegrationAWSOIDCSpec{ RoleARN: ig.GetAWSOIDCIntegrationSpec().RoleARN, - IssuerS3Bucket: issuerS3BucketURL.Host, - IssuerS3Prefix: prefix, - }, - }, nil + IssuerS3Bucket: s3Bucket, + IssuerS3Prefix: s3Prefix, + } + } + + return ret, nil } // AWSOIDCListDatabasesRequest is a request to ListDatabases using the AWS OIDC Integration.