Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions integration/integrations/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
28 changes: 18 additions & 10 deletions lib/web/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
)

Expand Down Expand Up @@ -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 {
Expand Down
63 changes: 45 additions & 18 deletions lib/web/integrations_awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -866,32 +867,58 @@ 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{
"integration", "configure", "awsoidc-idp",
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
Comment on lines 879 to 881
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always serve the oidc configuration from the proxy even if the S3 bucket is enabled, right? This comment kinda makes it seem like it's only one of the two at a time.

//
// 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.",
Expand Down
15 changes: 15 additions & 0 deletions lib/web/integrations_awsoidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand Down
45 changes: 28 additions & 17 deletions lib/web/ui/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down