Skip to content

Commit

Permalink
Allow passing an opt. ARN when health checking an AWSOIDC integration
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa committed Sep 26, 2024
1 parent 117300b commit c402a35
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 123 deletions.
200 changes: 107 additions & 93 deletions api/gen/proto/go/teleport/integration/v1/awsoidc_service.pb.go

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion api/proto/teleport/integration/v1/awsoidc_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,15 @@ message ListEKSClustersResponse {
// PingRequest is a request for doing an health check against the configured integration.
message PingRequest {
// Integration is the AWS OIDC Integration name.
// Required.
// One of integration or arn is required.
// Optional.
string integration = 1;

// The AWS ARN to be used when generating the token.
// This is used to test another AWS ARN before saving the integration.
// One of integration or arn is required.
// Optional.
string arn = 2;
}

// PingResponse contains the response for the Ping operation.
Expand Down
39 changes: 28 additions & 11 deletions lib/auth/integration/integrationv1/awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (s *Service) GenerateAWSOIDCToken(ctx context.Context, req *integrationpb.G

for _, allowedRole := range []types.SystemRole{types.RoleDiscovery, types.RoleAuth, types.RoleProxy} {
if authz.HasBuiltinRole(*authCtx, string(allowedRole)) {
return s.generateAWSOIDCTokenWithoutAuthZ(ctx, req.Integration)
return s.generateAWSOIDCTokenWithoutAuthZ(ctx, req.Integration, "")
}
}

Expand All @@ -54,14 +54,15 @@ func (s *Service) GenerateAWSOIDCToken(ctx context.Context, req *integrationpb.G

// generateAWSOIDCTokenWithoutAuthZ generates a token to be used when executing an AWS OIDC Integration action.
// Bypasses authz and should only be used by other methods that validate AuthZ.
func (s *Service) generateAWSOIDCTokenWithoutAuthZ(ctx context.Context, integrationName string) (*integrationpb.GenerateAWSOIDCTokenResponse, error) {
func (s *Service) generateAWSOIDCTokenWithoutAuthZ(ctx context.Context, integrationName, arn string) (*integrationpb.GenerateAWSOIDCTokenResponse, error) {
username, err := authz.GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

token, err := awsoidc.GenerateAWSOIDCToken(ctx, s.cache, s.keyStoreManager, awsoidc.GenerateAWSOIDCTokenRequest{
Integration: integrationName,
ARN: arn,
Username: username,
Subject: types.IntegrationAWSOIDCSubject,
Clock: s.clock,
Expand Down Expand Up @@ -173,7 +174,7 @@ func (s *AWSOIDCService) awsClientReq(ctx context.Context, integrationName, regi
return nil, trace.BadParameter("missing spec fields for %q (%q) integration", integration.GetName(), integration.GetSubKind())
}

token, err := s.integrationService.generateAWSOIDCTokenWithoutAuthZ(ctx, integrationName)
token, err := s.integrationService.generateAWSOIDCTokenWithoutAuthZ(ctx, integrationName, "")
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -773,15 +774,31 @@ func (s *AWSOIDCService) Ping(ctx context.Context, req *integrationpb.PingReques
return nil, trace.Wrap(err)
}

if req.Integration == "" {
return nil, trace.BadParameter("integration is required")
}
var awsClientReq *awsoidc.AWSClientRequest

// Instead of asking the user for a region (or storing a default region), we use the sentinel value for the global region.
// This improves the UX, because it is one less input we require from the user.
awsClientReq, err := s.awsClientReq(ctx, req.Integration, awsutils.AWSGlobalRegion)
if err != nil {
return nil, trace.Wrap(err)
switch {
case req.Arn != "":
token, err := s.integrationService.generateAWSOIDCTokenWithoutAuthZ(ctx, "", req.Arn)
if err != nil {
return nil, trace.Wrap(err)
}

awsClientReq = &awsoidc.AWSClientRequest{
IntegrationName: "no-integration",
Token: token.Token,
RoleARN: req.Arn,
Region: awsutils.AWSGlobalRegion,
}
case req.Integration != "":
// Instead of asking the user for a region (or storing a default region), we use the sentinel value for the global region.
// This improves the UX, because it is one less input we require from the user.
awsClientReq, err = s.awsClientReq(ctx, req.Integration, awsutils.AWSGlobalRegion)
if err != nil {
return nil, trace.Wrap(err)
}

default:
return nil, trace.BadParameter("integration or arn is required")
}

awsClient, err := awsoidc.NewPingClient(ctx, awsClientReq)
Expand Down
10 changes: 10 additions & 0 deletions lib/auth/integration/integrationv1/awsoidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ func TestRBAC(t *testing.T) {
return err
},
},
{
name: "Ping with arn",
fn: func() error {
_, err := awsoidService.Ping(userCtx, &integrationv1.PingRequest{
Integration: integrationName,
Arn: "some-arn",
})
return err
},
},
} {
t.Run(tt.name, func(t *testing.T) {
err := tt.fn()
Expand Down
51 changes: 33 additions & 18 deletions lib/integrations/awsoidc/token_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ type KeyStoreManager interface {
// GenerateAWSOIDCTokenRequest contains the required elements to generate an AWS OIDC Token (JWT).
type GenerateAWSOIDCTokenRequest struct {
// Integration is the AWS OIDC Integration name.
// Integration or ARN is required.
// Optional.
Integration string
// ARN is the ARN for the role that should be used instead of loading it from the integration.
// Integration or ARN is required.
// Optional.
ARN string
// Username is the JWT Username (on behalf of claim)
Username string
// Subject is the JWT Subject (subject claim)
Expand All @@ -70,8 +76,8 @@ type GenerateAWSOIDCTokenRequest struct {

// CheckAndSetDefaults checks the request params.
func (g *GenerateAWSOIDCTokenRequest) CheckAndSetDefaults() error {
if g.Integration == "" {
return trace.BadParameter("integration missing")
if g.Integration == "" && g.ARN == "" {
return trace.BadParameter("integration and arn are missing")
}
if g.Username == "" {
return trace.BadParameter("username missing")
Expand Down Expand Up @@ -107,22 +113,31 @@ func GenerateAWSOIDCToken(ctx context.Context, cacheClt Cache, keyStoreManager K
return "", trace.Wrap(err)
}

integration, err := cacheClt.GetIntegration(ctx, req.Integration)
if err != nil {
return "", trace.Wrap(err)
}

if integration.GetSubKind() != types.IntegrationSubKindAWSOIDC {
return "", trace.BadParameter("integration subkind (%s) mismatch", integration.GetSubKind())
}

if integration.GetAWSOIDCIntegrationSpec() == nil {
return "", trace.BadParameter("missing spec fields for %q (%q) integration", integration.GetName(), integration.GetSubKind())
}

issuer, err := issuerForIntegration(ctx, integration, cacheClt)
if err != nil {
return "", trace.Wrap(err)
var issuer string
var err error
if req.ARN == "" {
integration, err := cacheClt.GetIntegration(ctx, req.Integration)
if err != nil {
return "", trace.Wrap(err)
}

if integration.GetSubKind() != types.IntegrationSubKindAWSOIDC {
return "", trace.BadParameter("integration subkind (%s) mismatch", integration.GetSubKind())
}

if integration.GetAWSOIDCIntegrationSpec() == nil {
return "", trace.BadParameter("missing spec fields for %q (%q) integration", integration.GetName(), integration.GetSubKind())
}

issuer, err = issuerForIntegration(ctx, integration, cacheClt)
if err != nil {
return "", trace.Wrap(err)
}
} else {
issuer, err = oidc.IssuerForCluster(ctx, cacheClt)
if err != nil {
return "", trace.Wrap(err)
}
}

clusterName, err := cacheClt.GetClusterName()
Expand Down
6 changes: 6 additions & 0 deletions lib/web/integrations_awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1425,13 +1425,19 @@ func (h *Handler) awsOIDCPing(w http.ResponseWriter, r *http.Request, p httprout
return nil, trace.BadParameter("an integration name is required")
}

var req ui.AWSOIDCPingRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}

clt, err := sctx.GetUserClient(ctx, site)
if err != nil {
return nil, trace.Wrap(err)
}

pingResp, err := clt.IntegrationAWSOIDCClient().Ping(ctx, &integrationv1.PingRequest{
Integration: integrationName,
Arn: req.ARN,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
8 changes: 8 additions & 0 deletions lib/web/ui/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,11 @@ type AWSOIDCPingResponse struct {
// UserID is the unique identifier of the calling entity.
UserID string `json:"userId"`
}

// AWSOIDCPingRequest contains ping request fields.
type AWSOIDCPingRequest struct {
// ARN is optional, and used for cases such as
// pinging to check validity before upserting an
// AWS OIDC integration.
ARN string `json:"arn,omitempty"`
}

0 comments on commit c402a35

Please sign in to comment.