Преглед на файлове

Require role access for credential updates (#4139)

Stefan McShane преди 2 години
родител
ревизия
c4a2c529f9

+ 41 - 0
api/server/handlers/project_integration/create_aws.go

@@ -2,6 +2,7 @@ package project_integration
 
 import (
 	"net/http"
+	"strings"
 
 	"connectrpc.com/connect"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
@@ -70,6 +71,46 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 
+			// if a user is changing the external ID, then we need to update the external ID for all projects that use that AWS account.
+			// This is required since the same AWS account can be used across multiple projects. In order to change the external ID for a project,
+			// the user must then have access to all projects that use that AWS account.
+			// If we ever do a higher abstraction about porter projects, then we can tie the ability to access a cloud provider account to that higher abstraction.
+			awsAccountIdPrefix := strings.TrimPrefix(request.TargetArn, "arn:aws:iam::")
+			awsAccountId := strings.TrimSuffix(awsAccountIdPrefix, ":role/porter-manager")
+			assumeRoles, err := p.Repo().AWSAssumeRoleChainer().ListByAwsAccountId(ctx, awsAccountId)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error listing assume role chains")
+				p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError, "error listing assume role chains"))
+				return
+			}
+
+			requiredProjects := make(map[int]bool)
+			for _, role := range assumeRoles {
+				requiredProjects[role.ProjectID] = false
+			}
+
+			usersProject, err := p.Repo().Project().ListProjectsByUserID(user.ID)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error listing projects by user id")
+				p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError, "error listing projects by user id"))
+				return
+			}
+
+			for _, project := range usersProject {
+				if _, ok := requiredProjects[int(project.ID)]; ok {
+					requiredProjects[int(project.ID)] = true
+				}
+			}
+
+			for proj, required := range requiredProjects {
+				if !required {
+					err = telemetry.Error(ctx, span, err, "user does not have access to all projects that use this AWS account")
+					telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "missing-project", Value: proj})
+					p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden, "user does not have access to all projects that use this AWS account"))
+					return
+				}
+			}
+
 			credReq := porterv1.UpdateCloudProviderCredentialsRequest{
 				ProjectId:     int64(project.ID),
 				CloudProvider: porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS,

+ 2 - 0
internal/repository/aws_assume_role_chain.go

@@ -12,4 +12,6 @@ type AWSAssumeRoleChainer interface {
 	// List returns the final hop in an assume role chain, where the ARN accounts
 	// are not owned by Porter
 	List(ctx context.Context, projectID uint) ([]*models.AWSAssumeRoleChain, error)
+	// ListByAwsAccountId returns the final hops in an assume role chain, where the ARN accounts match the supplied AWS account ID
+	ListByAwsAccountId(ctx context.Context, targetAwsAccountId string) ([]*models.AWSAssumeRoleChain, error)
 }

+ 19 - 0
internal/repository/gorm/aws_assume_role_chain.go

@@ -47,3 +47,22 @@ func (cr AWSAssumeRoleChain) List(ctx context.Context, projectID uint) ([]*model
 
 	return confs, nil
 }
+
+// ListByAwsAccountId returns a list of aws assume role chains where the target arn is owned by the supplied AWS account ID.
+func (cr AWSAssumeRoleChain) ListByAwsAccountId(ctx context.Context, awsAccountID string) ([]*models.AWSAssumeRoleChain, error) {
+	var confs []*models.AWSAssumeRoleChain
+	if awsAccountID == "" {
+		return nil, errors.New("must provide an AWS account ID")
+	}
+	if len(awsAccountID) != 12 {
+		return nil, fmt.Errorf("must provide a valid AWS account ID: %s", awsAccountID)
+	}
+
+	targetArn := fmt.Sprintf("arn:aws:iam::%s:role/porter-manager", awsAccountID)
+	tx := cr.db.Where("target_arn = ?", targetArn).Find(&confs)
+	if tx.Error != nil {
+		return nil, tx.Error
+	}
+
+	return confs, nil
+}

+ 5 - 0
internal/repository/test/aws_assume_role_chain.go

@@ -20,3 +20,8 @@ func NewAWSAssumeRoleChainer() repository.AWSAssumeRoleChainer {
 func (cr AWSAssumeRoleChain) List(ctx context.Context, projectID uint) ([]*models.AWSAssumeRoleChain, error) {
 	return nil, errors.New("not implemented")
 }
+
+// ListByAwsAccountId ...
+func (cr AWSAssumeRoleChain) ListByAwsAccountId(ctx context.Context, targetAwsAccountId string) ([]*models.AWSAssumeRoleChain, error) {
+	return nil, errors.New("not implemented")
+}