Skip to content

Commit

Permalink
AWS OIDC: DeployService service (#38511)
Browse files Browse the repository at this point in the history
* AWS OIDC: DeployService service

This PR creates a new method on the AWS OIDC gRPC service that deploys
an ECS Service.

This is part of a refactor that moves the API calls behind the Auth
Service.

* add rbac test and rename teleport config string

* revert e
  • Loading branch information
marcoandredinis authored Feb 23, 2024
1 parent 82e2b5f commit c51cebe
Show file tree
Hide file tree
Showing 10 changed files with 609 additions and 179 deletions.
433 changes: 357 additions & 76 deletions api/gen/proto/go/teleport/integration/v1/awsoidc_service.pb.go

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions api/proto/teleport/integration/v1/awsoidc_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ service AWSOIDCService {
// DeployDatabaseService deploys a Database Services to Amazon ECS.
rpc DeployDatabaseService(DeployDatabaseServiceRequest) returns (DeployDatabaseServiceResponse);

// DeployService deploys an ECS Service to Amazon ECS.
rpc DeployService(DeployServiceRequest) returns (DeployServiceResponse);

// ListEC2 lists the EC2 instances of the AWS account per region.
// It uses the following API:
// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
Expand Down Expand Up @@ -236,6 +239,53 @@ message DeployDatabaseServiceResponse {
string cluster_dashboard_url = 2;
}

// DeployServiceRequest is a request to deploy .
message DeployServiceRequest {
// Integration is the AWS OIDC Integration name.
// Required.
string integration = 1;
// Region is the AWS Region
// Required.
string region = 2;
// DeploymentMode is the deployment name that should be applied when creating the ECS Service.
// Allowed modes: database-service
// Required.
string deployment_mode = 3;
// SecurityGroups to apply to the service's network configuration.
// If empty, the default security group for the VPC is going to be used.
repeated string security_groups = 4;
// SubnetIds are the subnets for the network configuration.
// Required.
repeated string subnet_ids = 5;
// TaskRoleARN is the AWS IAM Role received by the deployed service.
// Required.
string task_role_arn = 6;
// TeleportVersion is the teleport version to be deployed.
// This is used to fetch the correct tag for the teleport container image.
// Eg, 14.3.4 (no "v" prefix)
// Required.
string teleport_version = 7;
// DeploymentJoinTokenName is the Teleport IAM Join Token to be used by the deployed
// service to join the cluster.
// Required.
string deployment_join_token_name = 8;
// TeleportConfigString is the teleport.yaml configuration (base64 encoded) used by teleport.
// Required.
string teleport_config_string = 9;
}

// DeployServiceResponse contains information about the deployed service.
message DeployServiceResponse {
// ClusterArn identifies the cluster where the deployment was made.
string cluster_arn = 1;
// ServiceARN is the Amazon ECS Cluster Service ARN created to run the task.
string service_arn = 2;
// TaskDefinitionARN is the Amazon ECS Task Definition ARN created to run the Service.
string task_definition_arn = 3;
// ServiceDashboardURL is a link to the service's Dashboard URL in Amazon Console.
string service_dashboard_url = 4;
}

// ListEC2Request is a request for a paginated list of AWS EC2 instances.
message ListEC2Request {
// Integration is the AWS OIDC Integration name.
Expand Down
50 changes: 50 additions & 0 deletions lib/auth/integration/integrationv1/awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,56 @@ func (s *AWSOIDCService) DeployDatabaseService(ctx context.Context, req *integra
}, nil
}

// DeployService deploys Services into Amazon ECS.
func (s *AWSOIDCService) DeployService(ctx context.Context, req *integrationpb.DeployServiceRequest) (*integrationpb.DeployServiceResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.CheckAccessToKind(types.KindIntegration, types.VerbUse); err != nil {
return nil, trace.Wrap(err)
}

clusterName, err := s.cache.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
}

awsClientReq, err := s.awsClientReq(ctx, req.Integration, req.Region)
if err != nil {
return nil, trace.Wrap(err)
}

deployServiceClient, err := awsoidc.NewDeployServiceClient(ctx, awsClientReq, s.cache)
if err != nil {
return nil, trace.Wrap(err)
}

deployServiceResp, err := awsoidc.DeployService(ctx, deployServiceClient, awsoidc.DeployServiceRequest{
DeploymentJoinTokenName: req.DeploymentJoinTokenName,
DeploymentMode: req.DeploymentMode,
TeleportConfigString: req.TeleportConfigString,
IntegrationName: req.Integration,
Region: req.Region,
SecurityGroups: req.SecurityGroups,
SubnetIDs: req.SubnetIds,
TaskRoleARN: req.TaskRoleArn,
TeleportClusterName: clusterName.GetClusterName(),
TeleportVersionTag: req.TeleportVersion,
})
if err != nil {
return nil, trace.Wrap(err)
}

return &integrationpb.DeployServiceResponse{
ClusterArn: deployServiceResp.ClusterARN,
ServiceArn: deployServiceResp.ServiceARN,
TaskDefinitionArn: deployServiceResp.TaskDefinitionARN,
ServiceDashboardUrl: deployServiceResp.ServiceDashboardURL,
}, nil
}

// ListEC2 returns a paginated list of AWS EC2 instances.
func (s *AWSOIDCService) ListEC2(ctx context.Context, req *integrationpb.ListEC2Request) (*integrationpb.ListEC2Response, error) {
authCtx, err := s.authorizer.Authorize(ctx)
Expand Down
61 changes: 61 additions & 0 deletions lib/auth/integration/integrationv1/awsoidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,64 @@ func TestListEICE(t *testing.T) {
require.True(t, trace.IsBadParameter(err), "expected BadParameter error, but got %T", err)
})
}

func TestDeployService(t *testing.T) {
t.Parallel()

clusterName := "test-cluster"
proxyPublicAddr := "127.0.0.1.nip.io"
integrationName := "my-awsoidc-integration"
ig, err := types.NewIntegrationAWSOIDC(
types.Metadata{Name: integrationName},
&types.AWSOIDCIntegrationSpecV1{
RoleARN: "arn:aws:iam::123456789012:role/OpsTeam",
},
)
require.NoError(t, err)

ca := newCertAuthority(t, types.HostCA, clusterName)
ctx, localClient, resourceSvc := initSvc(t, ca, clusterName, proxyPublicAddr)

_, err = localClient.CreateIntegration(ctx, ig)
require.NoError(t, err)

awsoidService, err := NewAWSOIDCService(&AWSOIDCServiceConfig{
IntegrationService: resourceSvc,
Authorizer: resourceSvc.authorizer,
Cache: &mockCache{},
})
require.NoError(t, err)

t.Run("fails when user doesn't have access to integration.use", func(t *testing.T) {
role := types.RoleSpecV6{
Allow: types.RoleConditions{Rules: []types.Rule{{
Resources: []string{types.KindIntegration},
Verbs: []string{types.VerbRead},
}}},
}

userCtx := authorizerForDummyUser(t, ctx, role, localClient)

_, err = awsoidService.DeployService(userCtx, &integrationv1.DeployServiceRequest{
Integration: integrationName,
Region: "my-region",
})
require.True(t, trace.IsAccessDenied(err), "expected AccessDenied error, but got %T", err)
})
t.Run("calls awsoidc package when user has access to integration.use/read", func(t *testing.T) {
role := types.RoleSpecV6{
Allow: types.RoleConditions{Rules: []types.Rule{{
Resources: []string{types.KindIntegration},
Verbs: []string{types.VerbRead, types.VerbUse},
}}},
}

userCtx := authorizerForDummyUser(t, ctx, role, localClient)

_, err = awsoidService.DeployService(userCtx, &integrationv1.DeployServiceRequest{
Integration: integrationName,
Region: "my-region",
})
require.True(t, trace.IsBadParameter(err), "expected BadParameter error, but got %T", err)
})
}
32 changes: 6 additions & 26 deletions lib/integrations/awsoidc/deployservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ type DeployServiceRequest struct {
// DeploymentJoinTokenName is the Teleport IAM Token to use in the deployed Service.
DeploymentJoinTokenName string

// ProxyServerHostPort is the Teleport Proxy's Public.
ProxyServerHostPort string

// IntegrationName is the integration name.
// Used for resource tagging when creating resources in AWS.
IntegrationName string
Expand All @@ -148,20 +145,16 @@ type DeployServiceRequest struct {
// DeploymentMode is the identifier of a deployment mode - which Teleport Services to enable and their configuration.
DeploymentMode string

// DatabaseResourceMatcherLabels contains the set of labels to be used by the DatabaseService.
// This is used when the deployment mode creates a Database Service.
DatabaseResourceMatcherLabels types.Labels

// TeleportVersionTag is the version of teleport to install.
// Ensure the tag exists in:
// public.ecr.aws/gravitational/teleport-distroless:<TeleportVersionTag>
// Eg, 13.2.0
// Optional. Defaults to the current version.
TeleportVersionTag string

// DeployServiceConfigString creates a teleport.yaml configuration that the agent
// deployed in a ECS Cluster (using Fargate) will use.
DeployServiceConfigString func(proxyHostPort, iamToken string, resourceMatcherLabels types.Labels) (string, error)
// TeleportConfigString is the `teleport.yaml` configuration for the service to be deployed.
// It should be base64 encoded as is expected by the `--config-string` param of `teleport start`.
TeleportConfigString string
}

// normalizeECSResourceName converts a name into a valid ECS Resource Name.
Expand Down Expand Up @@ -251,10 +244,6 @@ func (r *DeployServiceRequest) CheckAndSetDefaults() error {
r.TaskName = &taskName
}

if r.ProxyServerHostPort == "" {
return trace.BadParameter("proxy address is required")
}

if r.IntegrationName == "" {
return trace.BadParameter("integration name is required")
}
Expand All @@ -263,12 +252,8 @@ func (r *DeployServiceRequest) CheckAndSetDefaults() error {
r.ResourceCreationTags = defaultResourceCreationTags(r.TeleportClusterName, r.IntegrationName)
}

if len(r.DatabaseResourceMatcherLabels) == 0 {
return trace.BadParameter("at least one agent matcher label is required")
}

if r.DeployServiceConfigString == nil {
return trace.BadParameter("deploy service config is required")
if r.TeleportConfigString == "" {
return trace.BadParameter("teleport config string is required")
}

return nil
Expand Down Expand Up @@ -436,11 +421,6 @@ func DeployService(ctx context.Context, clt DeployServiceClient, req DeployServi
return nil, trace.Wrap(err)
}

teleportConfigString, err := req.DeployServiceConfigString(req.ProxyServerHostPort, req.DeploymentJoinTokenName, req.DatabaseResourceMatcherLabels)
if err != nil {
return nil, trace.Wrap(err)
}

upsertTaskReq := upsertTaskRequest{
TaskName: aws.ToString(req.TaskName),
TaskRoleARN: req.TaskRoleARN,
Expand All @@ -449,7 +429,7 @@ func DeployService(ctx context.Context, clt DeployServiceClient, req DeployServi
TeleportVersionTag: req.TeleportVersionTag,
ResourceCreationTags: req.ResourceCreationTags,
Region: req.Region,
TeleportConfigB64: teleportConfigString,
TeleportConfigB64: req.TeleportConfigString,
}
taskDefinition, err := upsertTask(ctx, clt, upsertTaskReq)
if err != nil {
Expand Down
Loading

0 comments on commit c51cebe

Please sign in to comment.