Преглед изворни кода

Merge branch 'nafees/rbac-improvements' of github.com:porter-dev/porter into nico/rbac-crud-operations

jnfrati пре 3 година
родитељ
комит
c76359e824
35 измењених фајлова са 15678 додато и 359 уклоњено
  1. 32 20
      api/server/authz/policy/loader.go
  2. 54 12
      api/server/handlers/infra/forms.go
  3. 12 17
      api/server/handlers/namespace/stream_pod_logs.go
  4. 0 58
      api/server/handlers/policy/delete.go
  5. 0 91
      api/server/handlers/policy/update.go
  6. 68 32
      api/server/handlers/project/create.go
  7. 21 0
      api/server/handlers/project/delete.go
  8. 1 1
      api/server/handlers/project/get_policy.go
  9. 114 0
      api/server/handlers/project_role/create.go
  10. 88 0
      api/server/handlers/project_role/delete.go
  11. 69 0
      api/server/handlers/project_role/get.go
  12. 28 0
      api/server/handlers/project_role/get_scope_hierarchy.go
  13. 67 0
      api/server/handlers/project_role/list.go
  14. 114 0
      api/server/handlers/project_role/update.go
  15. 0 58
      api/server/router/project.go
  16. 233 0
      api/server/router/project_role.go
  17. 3 1
      api/server/router/router.go
  18. 23 15
      api/types/policy.go
  19. 1 1
      api/types/project.go
  20. 22 0
      api/types/project_role.go
  21. 14 3
      cli/cmd/run.go
  22. 28 0
      cmd/migrate/main.go
  23. 184 0
      cmd/migrate/migrate_legacy_rbac/migrate.go
  24. 14275 1
      dashboard/package-lock.json
  25. 2 2
      dashboard/src/main/home/integrations/create-integration/GARForm.tsx
  26. 16 0
      dashboard/src/main/home/onboarding/constants.ts
  27. 0 6
      internal/models/project.go
  28. 27 4
      internal/models/project_role.go
  29. 7 37
      internal/repository/gorm/policy.go
  30. 108 0
      internal/repository/gorm/project_role.go
  31. 6 0
      internal/repository/gorm/repository.go
  32. 14 0
      internal/repository/project_role.go
  33. 1 0
      internal/repository/repository.go
  34. 41 0
      internal/repository/test/project_role.go
  35. 5 0
      internal/repository/test/repository.go

+ 32 - 20
api/server/authz/policy/loader.go

@@ -23,12 +23,12 @@ type PolicyDocumentLoader interface {
 
 
 // RepoPolicyDocumentLoader loads policy documents by reading from the repository database
 // RepoPolicyDocumentLoader loads policy documents by reading from the repository database
 type RepoPolicyDocumentLoader struct {
 type RepoPolicyDocumentLoader struct {
-	projRepo   repository.ProjectRepository
-	policyRepo repository.PolicyRepository
+	projRoleRepo repository.ProjectRoleRepository
+	policyRepo   repository.PolicyRepository
 }
 }
 
 
-func NewBasicPolicyDocumentLoader(projRepo repository.ProjectRepository, policyRepo repository.PolicyRepository) *RepoPolicyDocumentLoader {
-	return &RepoPolicyDocumentLoader{projRepo, policyRepo}
+func NewBasicPolicyDocumentLoader(projRoleRepo repository.ProjectRoleRepository, policyRepo repository.PolicyRepository) *RepoPolicyDocumentLoader {
+	return &RepoPolicyDocumentLoader{projRoleRepo, policyRepo}
 }
 }
 
 
 func (b *RepoPolicyDocumentLoader) LoadPolicyDocuments(
 func (b *RepoPolicyDocumentLoader) LoadPolicyDocuments(
@@ -51,30 +51,42 @@ func (b *RepoPolicyDocumentLoader) LoadPolicyDocuments(
 	} else if opts.ProjectID != 0 && opts.UserID != 0 {
 	} else if opts.ProjectID != 0 && opts.UserID != 0 {
 		userID := opts.UserID
 		userID := opts.UserID
 		projectID := opts.ProjectID
 		projectID := opts.ProjectID
-		// read role and case on role "kind"
-		role, err := b.projRepo.ReadProjectRole(projectID, userID)
 
 
-		if err != nil && err == gorm.ErrRecordNotFound {
+		roles, err := b.projRoleRepo.ListAllRolesForUser(projectID, userID)
+
+		if err != nil {
+			return nil, apierrors.NewErrInternal(err)
+		} else if len(roles) == 0 {
 			return nil, apierrors.NewErrForbidden(
 			return nil, apierrors.NewErrForbidden(
-				fmt.Errorf("user %d does not have a role in project %d", userID, projectID),
+				fmt.Errorf("user does not have any roles assigned in this project"),
 			)
 			)
-		} else if err != nil {
-			return nil, apierrors.NewErrInternal(err)
 		}
 		}
 
 
-		// load role based on role kind
-		switch role.Kind {
-		case types.RoleAdmin:
-			return types.AdminPolicy, nil
-		case types.RoleDeveloper:
-			return types.DeveloperPolicy, nil
-		case types.RoleViewer:
-			return types.ViewerPolicy, nil
-		default:
+		var policies []*types.PolicyDocument
+
+		for _, role := range roles {
+			policy, err := b.policyRepo.ReadPolicy(projectID, role.PolicyUID)
+
+			if err != nil {
+				return nil, apierrors.NewErrInternal(err)
+			}
+
+			policyType, err := policy.ToAPIPolicyType()
+
+			if err != nil {
+				return nil, apierrors.NewErrInternal(err)
+			}
+
+			policies = append(policies, policyType.Policy...)
+		}
+
+		if len(policies) == 0 {
 			return nil, apierrors.NewErrForbidden(
 			return nil, apierrors.NewErrForbidden(
-				fmt.Errorf("%s role not supported for user %d, project %d", string(role.Kind), userID, projectID),
+				fmt.Errorf("user does not have any roles assigned in this project"),
 			)
 			)
 		}
 		}
+
+		return policies, nil
 	}
 	}
 
 
 	return nil, apierrors.NewErrForbidden(
 	return nil, apierrors.NewErrForbidden(

+ 54 - 12
api/server/handlers/infra/forms.go

@@ -396,6 +396,10 @@ tabs:
           value: t3.xlarge
           value: t3.xlarge
         - label: t3.2xlarge
         - label: t3.2xlarge
           value: t3.2xlarge
           value: t3.2xlarge
+        - label: c6i.large
+          value: c6i.large
+        - label: c6i.xlarge
+          value: c6i.xlarge
         - label: c6i.2xlarge
         - label: c6i.2xlarge
           value: c6i.2xlarge
           value: c6i.2xlarge
     - type: string-input
     - type: string-input
@@ -656,12 +660,18 @@ tabs:
           value: asia-northeast3
           value: asia-northeast3
         - label: asia-south1
         - label: asia-south1
           value: asia-south1
           value: asia-south1
+        - label: asia-south2
+          value: asia-south2
         - label: asia-southeast1
         - label: asia-southeast1
           value: asia-southeast1
           value: asia-southeast1
         - label: asia-southeast2
         - label: asia-southeast2
           value: asia-southeast2
           value: asia-southeast2
         - label: australia-southeast1
         - label: australia-southeast1
           value: australia-southeast1
           value: australia-southeast1
+        - label: australia-southeast2
+          value: australia-southeast2
+        - label: europe-central2
+          value: europe-central2
         - label: europe-north1
         - label: europe-north1
           value: europe-north1
           value: europe-north1
         - label: europe-west1
         - label: europe-west1
@@ -674,23 +684,33 @@ tabs:
           value: europe-west4
           value: europe-west4
         - label: europe-west6
         - label: europe-west6
           value: europe-west6
           value: europe-west6
+        - label: europe-west8
+          value: europe-west8
+        - label: europe-west9
+          value: europe-west9
+        - label: europe-southwest1
+          value: europe-southwest1
         - label: northamerica-northeast1
         - label: northamerica-northeast1
           value: northamerica-northeast1
           value: northamerica-northeast1
+        - label: northamerica-northeast2
+          value: northamerica-northeast2
         - label: southamerica-east1
         - label: southamerica-east1
           value: southamerica-east1
           value: southamerica-east1
+        - label: southamerica-west1
+          value: southamerica-west1
         - label: us-central1
         - label: us-central1
           value: us-central1
           value: us-central1
         - label: us-east1
         - label: us-east1
           value: us-east1
           value: us-east1
         - label: us-east4
         - label: us-east4
           value: us-east4
           value: us-east4
-        - label: us-east1
-          value: us-east1
-        - label: us-east1
-          value: us-east1
+        - label: us-east5
+          value: us-east5
+        - label: us-south1
+          value: us-south1
         - label: us-west1
         - label: us-west1
           value: us-west1
           value: us-west1
-        - label: us-east1
+        - label: us-west2
           value: us-west2
           value: us-west2
         - label: us-west3
         - label: us-west3
           value: us-west3
           value: us-west3
@@ -741,8 +761,6 @@ tabs:
           value: europe-central2
           value: europe-central2
         - label: europe-north1
         - label: europe-north1
           value: europe-north1
           value: europe-north1
-        - label: europe-southwest1
-          value: europe-southwest1
         - label: europe-west1
         - label: europe-west1
           value: europe-west1
           value: europe-west1
         - label: europe-west2
         - label: europe-west2
@@ -757,6 +775,8 @@ tabs:
           value: europe-west8
           value: europe-west8
         - label: europe-west9
         - label: europe-west9
           value: europe-west9
           value: europe-west9
+        - label: europe-southwest1
+          value: europe-southwest1
         - label: northamerica-northeast1
         - label: northamerica-northeast1
           value: northamerica-northeast1
           value: northamerica-northeast1
         - label: northamerica-northeast2
         - label: northamerica-northeast2
@@ -783,6 +803,12 @@ tabs:
           value: us-west3
           value: us-west3
         - label: us-west4
         - label: us-west4
           value: us-west4
           value: us-west4
+        - label: us (multi-region)
+          value: us
+        - label: europe (multi-region)
+          value: europe
+        - label: asia (multi-region)
+          value: asia
 `
 `
 
 
 const gkeForm = `name: GKE
 const gkeForm = `name: GKE
@@ -814,12 +840,18 @@ tabs:
           value: asia-northeast3
           value: asia-northeast3
         - label: asia-south1
         - label: asia-south1
           value: asia-south1
           value: asia-south1
+        - label: asia-south2
+          value: asia-south2
         - label: asia-southeast1
         - label: asia-southeast1
           value: asia-southeast1
           value: asia-southeast1
         - label: asia-southeast2
         - label: asia-southeast2
           value: asia-southeast2
           value: asia-southeast2
         - label: australia-southeast1
         - label: australia-southeast1
           value: australia-southeast1
           value: australia-southeast1
+        - label: australia-southeast2
+          value: australia-southeast2
+        - label: europe-central2
+          value: europe-central2
         - label: europe-north1
         - label: europe-north1
           value: europe-north1
           value: europe-north1
         - label: europe-west1
         - label: europe-west1
@@ -832,23 +864,33 @@ tabs:
           value: europe-west4
           value: europe-west4
         - label: europe-west6
         - label: europe-west6
           value: europe-west6
           value: europe-west6
+        - label: europe-west8
+          value: europe-west8
+        - label: europe-west9
+          value: europe-west9
+        - label: europe-southwest1
+          value: europe-southwest1
         - label: northamerica-northeast1
         - label: northamerica-northeast1
           value: northamerica-northeast1
           value: northamerica-northeast1
+        - label: northamerica-northeast2
+          value: northamerica-northeast2
         - label: southamerica-east1
         - label: southamerica-east1
           value: southamerica-east1
           value: southamerica-east1
+        - label: southamerica-west1
+          value: southamerica-west1
         - label: us-central1
         - label: us-central1
           value: us-central1
           value: us-central1
         - label: us-east1
         - label: us-east1
           value: us-east1
           value: us-east1
         - label: us-east4
         - label: us-east4
           value: us-east4
           value: us-east4
-        - label: us-east1
-          value: us-east1
-        - label: us-east1
-          value: us-east1
+        - label: us-east5
+          value: us-east5
+        - label: us-south1
+          value: us-south1
         - label: us-west1
         - label: us-west1
           value: us-west1
           value: us-west1
-        - label: us-east1
+        - label: us-west2
           value: us-west2
           value: us-west2
         - label: us-west3
         - label: us-west3
           value: us-west3
           value: us-west3

+ 12 - 17
api/server/handlers/namespace/stream_pod_logs.go

@@ -55,22 +55,17 @@ func (c *StreamPodLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 
 	err = agent.GetPodLogs(namespace, name, request.Container, safeRW)
 	err = agent.GetPodLogs(namespace, name, request.Container, safeRW)
 
 
-	if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("pod %s/%s was not found", namespace, name),
-			http.StatusNotFound,
-		))
-
-		return
-	} else if brErr := (kubernetes.BadRequestError{}); errors.As(err, &targetErr) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			&brErr,
-			http.StatusBadRequest,
-		))
-
-		return
-	} else if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+	if err != nil {
+		if errors.Is(err, kubernetes.IsNotFoundError) {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("pod %s/%s was not found", namespace, name),
+				http.StatusNotFound))
+			return
+		} else if _, ok := err.(*kubernetes.BadRequestError); ok {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		} else {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 	}
 }
 }

+ 0 - 58
api/server/handlers/policy/delete.go

@@ -1,58 +0,0 @@
-package policy
-
-import (
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type PolicyDeleteHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewPolicyDeleteHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *PolicyDeleteHandler {
-	return &PolicyDeleteHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (p *PolicyDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	policyID, reqErr := requestutils.GetURLParamString(r, types.URLParamPolicyID)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, reqErr)
-		return
-	}
-
-	policy, err := p.Repo().Policy().ReadPolicy(proj.ID, policyID)
-
-	if err == nil {
-		policy, err = p.Repo().Policy().DeletePolicy(policy)
-
-		if err != nil {
-			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	res, err := policy.ToAPIPolicyType()
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	p.WriteResult(w, r, res)
-}

+ 0 - 91
api/server/handlers/policy/update.go

@@ -1,91 +0,0 @@
-package policy
-
-import (
-	"bytes"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/models"
-	"gorm.io/gorm"
-)
-
-type PolicyUpdateHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewPolicyUpdateHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *PolicyUpdateHandler {
-	return &PolicyUpdateHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (p *PolicyUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-
-	policyID, reqErr := requestutils.GetURLParamString(r, types.URLParamPolicyID)
-
-	if reqErr != nil {
-		p.HandleAPIError(w, r, reqErr)
-		return
-	}
-
-	req := &types.UpdatePolicyRequest{}
-
-	if ok := p.DecodeAndValidate(w, r, req); !ok {
-		return
-	}
-
-	policy, err := p.Repo().Policy().ReadPolicy(proj.ID, policyID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("policy with id %s not found in project", policyID),
-				http.StatusNotFound,
-			))
-			return
-		}
-
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	policyBytes, err := json.Marshal(req.Policy)
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	if !bytes.Equal(policyBytes, policy.PolicyBytes) {
-		policy.PolicyBytes = policyBytes
-
-		policy, err = p.Repo().Policy().UpdatePolicy(policy)
-
-		if err != nil {
-			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
-		}
-	}
-
-	res, err := policy.ToAPIPolicyType()
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	p.WriteResult(w, r, res)
-}

+ 68 - 32
api/server/handlers/project/create.go

@@ -1,6 +1,8 @@
 package project
 package project
 
 
 import (
 import (
+	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/http"
 
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/analytics"
+	"github.com/porter-dev/porter/internal/encryption"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 )
 )
@@ -43,14 +46,23 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Name: request.Name,
 		Name: request.Name,
 	}
 	}
 
 
-	var err error
-	proj, _, err = CreateProjectWithUser(p.Repo().Project(), proj, user)
+	proj, err := p.Repo().Project().CreateProject(proj)
 
 
 	if err != nil {
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 
+	err = createDefaultProjectRoles(proj.ID, user.ID, p.Repo())
+
+	if err != nil {
+		// we need to first delete the default project roles we just created
+		deleteAllProjectRoles(proj.ID, p.Repo())
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// create onboarding flow set to the first step
 	// create onboarding flow set to the first step
 	_, err = p.Repo().Onboarding().CreateProjectOnboarding(&models.Onboarding{
 	_, err = p.Repo().Onboarding().CreateProjectOnboarding(&models.Onboarding{
 		ProjectID:   proj.ID,
 		ProjectID:   proj.ID,
@@ -92,36 +104,60 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}))
 	}))
 }
 }
 
 
-func CreateProjectWithUser(
-	projectRepo repository.ProjectRepository,
-	proj *models.Project,
-	user *models.User,
-) (*models.Project, *models.Role, error) {
-	proj, err := projectRepo.CreateProject(proj)
-
-	if err != nil {
-		return nil, nil, err
-	}
-
-	// create a new Role with the user as the admin
-	role, err := projectRepo.CreateProjectRole(proj, &models.Role{
-		Role: types.Role{
-			UserID:    user.ID,
-			ProjectID: proj.ID,
-			Kind:      types.RoleAdmin,
-		},
-	})
-
-	if err != nil {
-		return nil, nil, err
-	}
-
-	// read the project again to get the model with the role attached
-	proj, err = projectRepo.ReadProject(proj.ID)
-
-	if err != nil {
-		return nil, nil, err
+func createDefaultProjectRoles(projectID, userID uint, repo repository.Repository) error {
+	for _, kind := range []types.RoleKind{types.RoleAdmin, types.RoleDeveloper, types.RoleViewer} {
+		uid, err := encryption.GenerateRandomBytes(16)
+
+		if err != nil {
+			return err
+		}
+
+		var policyBytes []byte
+
+		switch kind {
+		case types.RoleAdmin:
+			policyBytes, err = json.Marshal(types.AdminPolicy)
+
+			if err != nil {
+				return err
+			}
+		case types.RoleDeveloper:
+			policyBytes, err = json.Marshal(types.DeveloperPolicy)
+
+			if err != nil {
+				return err
+			}
+		case types.RoleViewer:
+			policyBytes, err = json.Marshal(types.ViewerPolicy)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		policy, err := repo.Policy().CreatePolicy(&models.Policy{
+			UniqueID:        uid,
+			ProjectID:       projectID,
+			CreatedByUserID: userID,
+			Name:            fmt.Sprintf("%s-project-role-policy", kind),
+			PolicyBytes:     policyBytes,
+		})
+
+		if err != nil {
+			return err
+		}
+
+		_, err = repo.ProjectRole().CreateProjectRole(&models.ProjectRole{
+			UniqueID:  fmt.Sprintf("%d-%s", projectID, kind),
+			ProjectID: projectID,
+			PolicyUID: policy.UniqueID,
+			Name:      string(kind),
+		})
+
+		if err != nil {
+			return err
+		}
 	}
 	}
 
 
-	return proj, role, nil
+	return nil
 }
 }

+ 21 - 0
api/server/handlers/project/delete.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 )
 )
 
 
 type ProjectDeleteHandler struct {
 type ProjectDeleteHandler struct {
@@ -28,6 +29,8 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 
+	deleteAllProjectRoles(proj.ID, p.Repo())
+
 	proj, err := p.Repo().Project().DeleteProject(proj)
 	proj, err := p.Repo().Project().DeleteProject(proj)
 
 
 	if err != nil {
 	if err != nil {
@@ -44,3 +47,21 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		p.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
 		p.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
 	}
 	}
 }
 }
+
+func deleteAllProjectRoles(projectID uint, repo repository.Repository) {
+	policies, err := repo.Policy().ListPoliciesByProjectID(projectID)
+
+	if err == nil {
+		for _, policy := range policies {
+			repo.Policy().DeletePolicy(policy)
+		}
+	}
+
+	roles, err := repo.ProjectRole().ListProjectRoles(projectID)
+
+	if err == nil {
+		for _, role := range roles {
+			repo.ProjectRole().DeleteProjectRole(role)
+		}
+	}
+}

+ 1 - 1
api/server/handlers/project/get_policy.go

@@ -30,7 +30,7 @@ func (p *ProjectGetPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 
-	policyDocLoader := policy.NewBasicPolicyDocumentLoader(p.Config().Repo.Project(), p.Config().Repo.Policy())
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(p.Config().Repo.ProjectRole(), p.Config().Repo.Policy())
 
 
 	policyDocs, err := policyDocLoader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
 	policyDocs, err := policyDocLoader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
 		UserID:    user.ID,
 		UserID:    user.ID,

+ 114 - 0
api/server/handlers/project_role/create.go

@@ -0,0 +1,114 @@
+package project_role
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type CreateProjectRoleHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreateProjectRoleHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateProjectRoleHandler {
+	return &CreateProjectRoleHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *CreateProjectRoleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	request := &types.CreateProjectRoleRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	if request.Name == string(types.RoleAdmin) ||
+		request.Name == string(types.RoleDeveloper) ||
+		request.Name == string(types.RoleViewer) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("default role names admin, developer, viewer are not allowed"), http.StatusConflict,
+		))
+		return
+	}
+
+	uid, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policyBytes, err := json.Marshal([]*types.PolicyDocument{request.Policy})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policy, err := c.Repo().Policy().CreatePolicy(&models.Policy{
+		UniqueID:        uid,
+		ProjectID:       project.ID,
+		CreatedByUserID: user.ID,
+		Name:            fmt.Sprintf("%s-project-role-policy", request.Name),
+		PolicyBytes:     policyBytes,
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	uid, err = encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		// we need to delete the policy we just created
+		c.Repo().Policy().DeletePolicy(policy)
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	role, err := c.Repo().ProjectRole().CreateProjectRole(&models.ProjectRole{
+		UniqueID:  uid,
+		ProjectID: project.ID,
+		PolicyUID: policy.UniqueID,
+		Name:      request.Name,
+	})
+
+	if err != nil {
+		// we need to delete the policy we just created
+		c.Repo().Policy().DeletePolicy(policy)
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	err = c.Repo().ProjectRole().UpdateUsersInProjectRole(project.ID, role.UniqueID, request.Users)
+
+	if err != nil {
+		// we need to delete the policy and project role we just created
+		c.Repo().Policy().DeletePolicy(policy)
+		c.Repo().ProjectRole().DeleteProjectRole(role)
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	w.WriteHeader(http.StatusCreated)
+}

+ 88 - 0
api/server/handlers/project_role/delete.go

@@ -0,0 +1,88 @@
+package project_role
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type DeleteProjectRoleHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewDeleteProjectRoleHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *DeleteProjectRoleHandler {
+	return &DeleteProjectRoleHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *DeleteProjectRoleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	roleUID, reqErr := requestutils.GetURLParamString(r, types.URLParamProjectRoleID)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	role, err := c.Repo().ProjectRole().ReadProjectRole(project.ID, roleUID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such project role exists")))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if role.IsDefaultRole() {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("cannot delete default project role"), http.StatusConflict,
+		))
+		return
+	}
+
+	if len(role.Users) > 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("role has one or more users assigned"), http.StatusPreconditionFailed,
+		))
+		return
+	}
+
+	policy, err := c.Repo().Policy().ReadPolicy(project.ID, role.PolicyUID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	_, err = c.Repo().Policy().DeletePolicy(policy)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	_, err = c.Repo().ProjectRole().DeleteProjectRole(role)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+}

+ 69 - 0
api/server/handlers/project_role/get.go

@@ -0,0 +1,69 @@
+package project_role
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type GetProjectRoleHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetProjectRoleHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetProjectRoleHandler {
+	return &GetProjectRoleHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetProjectRoleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	roleUID, reqErr := requestutils.GetURLParamString(r, types.URLParamProjectRoleID)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	role, err := c.Repo().ProjectRole().ReadProjectRole(project.ID, roleUID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such project role exists")))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policy, err := c.Repo().Policy().ReadPolicy(project.ID, role.PolicyUID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policyType, err := policy.ToAPIPolicyType()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, role.ToProjectRoleType(policyType.Policy[0]))
+}

+ 28 - 0
api/server/handlers/project_role/get_scope_hierarchy.go

@@ -0,0 +1,28 @@
+package project_role
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+type GetProjectRoleScopeHierarchyHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetProjectRoleScopeHierarchyHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetProjectRoleScopeHierarchyHandler {
+	return &GetProjectRoleScopeHierarchyHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetProjectRoleScopeHierarchyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	c.WriteResult(w, r, types.ScopeHeirarchy)
+}

+ 67 - 0
api/server/handlers/project_role/list.go

@@ -0,0 +1,67 @@
+package project_role
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type ListProjectRolesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewListProjectRolesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListProjectRolesHandler {
+	return &ListProjectRolesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *ListProjectRolesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	roles, err := c.Repo().ProjectRole().ListProjectRoles(project.ID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("project has no roles")))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res []*types.ProjectRole
+
+	for _, role := range roles {
+		policy, err := c.Repo().Policy().ReadPolicy(project.ID, role.PolicyUID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		policyType, err := policy.ToAPIPolicyType()
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		res = append(res, role.ToProjectRoleType(policyType.Policy[0]))
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 114 - 0
api/server/handlers/project_role/update.go

@@ -0,0 +1,114 @@
+package project_role
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type UpdateProjectRoleHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewUpdateProjectRoleHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateProjectRoleHandler {
+	return &UpdateProjectRoleHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *UpdateProjectRoleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	roleUID, reqErr := requestutils.GetURLParamString(r, types.URLParamProjectRoleID)
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	request := &types.UpdateProjectRoleRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	role, err := c.Repo().ProjectRole().ReadProjectRole(project.ID, roleUID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such project role exists")))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if !role.IsDefaultRole() && request.Name != "" && request.Name != role.Name {
+		if request.Name == string(types.RoleAdmin) ||
+			request.Name == string(types.RoleDeveloper) ||
+			request.Name == string(types.RoleViewer) {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("default role names admin, developer, viewer are not allowed"), http.StatusConflict,
+			))
+			return
+		}
+
+		role.Name = request.Name
+
+		role, err = c.Repo().ProjectRole().UpdateProjectRole(role)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if len(request.Users) > 0 {
+		err = c.Repo().ProjectRole().UpdateUsersInProjectRole(project.ID, roleUID, request.Users)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if !role.IsDefaultRole() && request.Policy != nil {
+		policy, err := c.Repo().Policy().ReadPolicy(project.ID, role.PolicyUID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		policyBytes, err := json.Marshal([]*types.PolicyDocument{request.Policy})
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		policy.PolicyBytes = policyBytes
+
+		_, err = c.Repo().Policy().UpdatePolicy(policy)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+}

+ 0 - 58
api/server/router/project.go

@@ -1035,64 +1035,6 @@ func getProjectRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
-	// PATCH /api/projects/{project_id}/policies/{policy_id} -> policy.NewPolicyUpdateHandler
-	policyUpdateEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate,
-			Method: types.HTTPVerbPatch,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/policies/{%s}", relPath, types.URLParamPolicyID),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.SettingsScope,
-			},
-		},
-	)
-
-	policyUpdateHandler := policy.NewPolicyUpdateHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: policyUpdateEndpoint,
-		Handler:  policyUpdateHandler,
-		Router:   r,
-	})
-
-	// DELETE /api/projects/{project_id}/policies/{policy_id} -> policy.NewPolicyDeleteHandler
-	policyDeleteEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbDelete,
-			Method: types.HTTPVerbDelete,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/policies/{%s}", relPath, types.URLParamPolicyID),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.SettingsScope,
-			},
-		},
-	)
-
-	policyDeleteHandler := policy.NewPolicyDeleteHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: policyDeleteEndpoint,
-		Handler:  policyDeleteHandler,
-		Router:   r,
-	})
-
 	//  POST /api/projects/{project_id}/api_token -> api_token.NewAPITokenCreateHandler
 	//  POST /api/projects/{project_id}/api_token -> api_token.NewAPITokenCreateHandler
 	apiTokenCreateEndpoint := factory.NewAPIEndpoint(
 	apiTokenCreateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{

+ 233 - 0
api/server/router/project_role.go

@@ -0,0 +1,233 @@
+package router
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/project_role"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewProjectRoleScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetProjectRoleScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetProjectRoleScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getProjectRoleRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getProjectRoleRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/project_roles"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	// POST /api/projects/{project_id}/project_roles -> project_role.NewCreateProjectRoleHandler
+	createProjectRoleEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	createProjectRoleHandler := project_role.NewCreateProjectRoleHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createProjectRoleEndpoint,
+		Handler:  createProjectRoleHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/project_roles/{role_id} -> project_role.NewGetProjectRoleHandler
+	getProjectRoleEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamProjectRoleID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	getProjectRoleHandler := project_role.NewGetProjectRoleHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getProjectRoleEndpoint,
+		Handler:  getProjectRoleHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/project_roles -> project_role.NewListProjectRolesHandler
+	listProjectRolesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	listProjectRolesHandler := project_role.NewListProjectRolesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listProjectRolesEndpoint,
+		Handler:  listProjectRolesHandler,
+		Router:   r,
+	})
+
+	// PATCH /api/projects/{project_id}/project_roles/{role_id} -> project_role.NewUpdateProjectRoleHandler
+	updateProjectRoleEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPatch,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamProjectRoleID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	updateProjectRoleHandler := project_role.NewUpdateProjectRoleHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateProjectRoleEndpoint,
+		Handler:  updateProjectRoleHandler,
+		Router:   r,
+	})
+
+	// DELETE /api/projects/{project_id}/project_roles/{role_id} -> project_role.NewDeleteProjectRoleHandler
+	deleteProjectRoleEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamProjectRoleID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	deleteProjectRoleHandler := project_role.NewDeleteProjectRoleHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteProjectRoleEndpoint,
+		Handler:  deleteProjectRoleHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/project_roles/scope_hierarchy -> project_role.NewGetProjectRoleScopeHierarchyHandler
+	getProjectRoleScopeHierarchyEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/scope_hierarchy", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	getProjectRoleScopeHierarchyHandler := project_role.NewGetProjectRoleScopeHierarchyHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getProjectRoleScopeHierarchyEndpoint,
+		Handler:  getProjectRoleScopeHierarchyHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 3 - 1
api/server/router/router.go

@@ -38,6 +38,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	inviteRegisterer := NewInviteScopedRegisterer()
 	inviteRegisterer := NewInviteScopedRegisterer()
 	projectIntegrationRegisterer := NewProjectIntegrationScopedRegisterer()
 	projectIntegrationRegisterer := NewProjectIntegrationScopedRegisterer()
 	projectOAuthRegisterer := NewProjectOAuthScopedRegisterer()
 	projectOAuthRegisterer := NewProjectOAuthScopedRegisterer()
+	projectRoleRegisterer := NewProjectRoleScopedRegisterer()
 	slackIntegrationRegisterer := NewSlackIntegrationScopedRegisterer()
 	slackIntegrationRegisterer := NewSlackIntegrationScopedRegisterer()
 	projRegisterer := NewProjectScopedRegisterer(
 	projRegisterer := NewProjectScopedRegisterer(
 		clusterRegisterer,
 		clusterRegisterer,
@@ -49,6 +50,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 		projectIntegrationRegisterer,
 		projectIntegrationRegisterer,
 		projectOAuthRegisterer,
 		projectOAuthRegisterer,
 		slackIntegrationRegisterer,
 		slackIntegrationRegisterer,
+		projectRoleRegisterer,
 	)
 	)
 	statusRegisterer := NewStatusScopedRegisterer()
 	statusRegisterer := NewStatusScopedRegisterer()
 
 
@@ -221,7 +223,7 @@ func registerRoutes(config *config.Config, routes []*router.Route) {
 	stackFactory := authz.NewStackScopedFactory(config)
 	stackFactory := authz.NewStackScopedFactory(config)
 
 
 	// Policy doc loader loads the policy documents for a specific project.
 	// Policy doc loader loads the policy documents for a specific project.
-	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.Project(), config.Repo.Policy())
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.ProjectRole(), config.Repo.Policy())
 
 
 	// set up logging middleware to log information about the request
 	// set up logging middleware to log information about the request
 	loggerMw := middleware.NewRequestLoggerMiddleware(config.Logger)
 	loggerMw := middleware.NewRequestLoggerMiddleware(config.Logger)

+ 23 - 15
api/types/policy.go

@@ -5,20 +5,22 @@ import "time"
 type PermissionScope string
 type PermissionScope string
 
 
 const (
 const (
-	UserScope              PermissionScope = "user"
-	ProjectScope           PermissionScope = "project"
-	ClusterScope           PermissionScope = "cluster"
-	RegistryScope          PermissionScope = "registry"
-	InviteScope            PermissionScope = "invite"
-	HelmRepoScope          PermissionScope = "helm_repo"
-	InfraScope             PermissionScope = "infra"
-	OperationScope         PermissionScope = "operation"
-	GitInstallationScope   PermissionScope = "git_installation"
-	NamespaceScope         PermissionScope = "namespace"
-	SettingsScope          PermissionScope = "settings"
-	ReleaseScope           PermissionScope = "release"
-	StackScope             PermissionScope = "stack"
-	GitlabIntegrationScope PermissionScope = "gitlab_integration"
+	UserScope               PermissionScope = "user"
+	ProjectScope            PermissionScope = "project"
+	ClusterScope            PermissionScope = "cluster"
+	RegistryScope           PermissionScope = "registry"
+	InviteScope             PermissionScope = "invite"
+	HelmRepoScope           PermissionScope = "helm_repo"
+	InfraScope              PermissionScope = "infra"
+	OperationScope          PermissionScope = "operation"
+	GitInstallationScope    PermissionScope = "git_installation"
+	NamespaceScope          PermissionScope = "namespace"
+	SettingsScope           PermissionScope = "settings"
+	ReleaseScope            PermissionScope = "release"
+	StackScope              PermissionScope = "stack"
+	GitlabIntegrationScope  PermissionScope = "gitlab_integration"
+	PreviewEnvironmentScope PermissionScope = "preview_environment"
+	EnvironmentScope        PermissionScope = "environment"
 )
 )
 
 
 type NameOrUInt struct {
 type NameOrUInt struct {
@@ -52,7 +54,13 @@ var ScopeHeirarchy = ScopeTree{
 		InfraScope: {
 		InfraScope: {
 			OperationScope: {},
 			OperationScope: {},
 		},
 		},
-		SettingsScope: {},
+		SettingsScope: {
+			InviteScope: {},
+		},
+		PreviewEnvironmentScope: {
+			EnvironmentScope: {},
+		},
+		GitlabIntegrationScope: {},
 	},
 	},
 }
 }
 
 

+ 1 - 1
api/types/project.go

@@ -24,7 +24,7 @@ type CreateProjectRequest struct {
 
 
 type CreateProjectResponse Project
 type CreateProjectResponse Project
 
 
-type CreateProjectRoleRequest struct {
+type CreateLegacyProjectRoleRequest struct {
 	Kind   string `json:"kind" form:"required"`
 	Kind   string `json:"kind" form:"required"`
 	UserID uint   `json:"user_id" form:"required"`
 	UserID uint   `json:"user_id" form:"required"`
 }
 }

+ 22 - 0
api/types/project_role.go

@@ -0,0 +1,22 @@
+package types
+
+const URLParamProjectRoleID URLParam = "role_id"
+
+type ProjectRole struct {
+	ID     string          `json:"id" form:"required"`
+	Name   string          `json:"name" form:"required"`
+	Users  []uint          `json:"users"`
+	Policy *PolicyDocument `json:"policy" form:"required"`
+}
+
+type CreateProjectRoleRequest struct {
+	Name   string          `json:"name" form:"required"`
+	Users  []uint          `json:"users"`
+	Policy *PolicyDocument `json:"policy" form:"required"`
+}
+
+type UpdateProjectRoleRequest struct {
+	Name   string          `json:"name"`
+	Users  []uint          `json:"users"`
+	Policy *PolicyDocument `json:"policy"`
+}

+ 14 - 3
cli/cmd/run.go

@@ -33,6 +33,8 @@ import (
 
 
 var namespace string
 var namespace string
 var verbose bool
 var verbose bool
+var existingPod bool
+var nonInteractive bool
 
 
 // runCmd represents the "porter run" base command when called
 // runCmd represents the "porter run" base command when called
 // without any subcommands
 // without any subcommands
@@ -63,8 +65,6 @@ var cleanupCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
-var existingPod bool
-
 func init() {
 func init() {
 	rootCmd.AddCommand(runCmd)
 	rootCmd.AddCommand(runCmd)
 
 
@@ -91,12 +91,23 @@ func init() {
 		"whether to print verbose output",
 		"whether to print verbose output",
 	)
 	)
 
 
+	runCmd.PersistentFlags().BoolVar(
+		&nonInteractive,
+		"non-interactive",
+		false,
+		"whether to run in non-interactive mode",
+	)
+
 	runCmd.AddCommand(cleanupCmd)
 	runCmd.AddCommand(cleanupCmd)
 }
 }
 
 
 func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
 func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
 	color.New(color.FgGreen).Println("Running", strings.Join(args[1:], " "), "for release", args[0])
 	color.New(color.FgGreen).Println("Running", strings.Join(args[1:], " "), "for release", args[0])
 
 
+	if nonInteractive {
+		color.New(color.FgBlue).Println("Using non-interactive mode. The first available pod will be used to run the command.")
+	}
+
 	podsSimple, err := getPods(client, namespace, args[0])
 	podsSimple, err := getPods(client, namespace, args[0])
 
 
 	if err != nil {
 	if err != nil {
@@ -108,7 +119,7 @@ func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 
 
 	if len(podsSimple) == 0 {
 	if len(podsSimple) == 0 {
 		return fmt.Errorf("At least one pod must exist in this deployment.")
 		return fmt.Errorf("At least one pod must exist in this deployment.")
-	} else if len(podsSimple) == 1 || !existingPod {
+	} else if nonInteractive || len(podsSimple) == 1 || !existingPod {
 		selectedPod = podsSimple[0]
 		selectedPod = podsSimple[0]
 	} else {
 	} else {
 		podNames := make([]string, 0)
 		podNames := make([]string, 0)

+ 28 - 0
cmd/migrate/main.go

@@ -5,6 +5,7 @@ import (
 
 
 	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
+	migratelegacyrbac "github.com/porter-dev/porter/cmd/migrate/migrate_legacy_rbac"
 	"github.com/porter-dev/porter/cmd/migrate/populate_source_config_display_name"
 	"github.com/porter-dev/porter/cmd/migrate/populate_source_config_display_name"
 
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	adapter "github.com/porter-dev/porter/internal/adapter"
@@ -70,6 +71,14 @@ func main() {
 		}
 		}
 	}
 	}
 
 
+	if shouldMigrateFromLegacyRBAC() {
+		err := migratelegacyrbac.MigrateFromLegacyRBAC(db, logger)
+
+		if err != nil {
+			logger.Fatal().Err(err).Msg("failed to migrate legacy RBAC")
+		}
+	}
+
 	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
 	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
 		logger.Fatal().Err(err).Msg("vault migration failed")
 		logger.Fatal().Err(err).Msg("vault migration failed")
 	}
 	}
@@ -111,3 +120,22 @@ func shouldPopulateSourceConfigDisplayName() bool {
 
 
 	return c.PopulateSourceConfigDisplayName
 	return c.PopulateSourceConfigDisplayName
 }
 }
+
+type MigrateLegacyRBACConf struct {
+	// we add a dummy field to avoid empty struct issue with envdecode
+	DummyField string `env:"ASDF,default=asdf"`
+
+	// if true, will migrate away from legacy RBAC to advanced RBAC
+	MigrateLegacyRBAC bool `env:"MIGRATE_LEGACY_RBAC"`
+}
+
+func shouldMigrateFromLegacyRBAC() bool {
+	var c MigrateLegacyRBACConf
+
+	if err := envdecode.StrictDecode(&c); err != nil {
+		log.Fatalf("Failed to decode migration conf: %s", err)
+		return false
+	}
+
+	return c.MigrateLegacyRBAC
+}

+ 184 - 0
cmd/migrate/migrate_legacy_rbac/migrate.go

@@ -0,0 +1,184 @@
+package migrate_legacy_rbac
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	gorm "github.com/porter-dev/porter/internal/repository/gorm"
+	lr "github.com/porter-dev/porter/pkg/logger"
+
+	_gorm "gorm.io/gorm"
+)
+
+// process 100 records at a time
+const stepSize = 100
+
+func MigrateFromLegacyRBAC(db *_gorm.DB, logger *lr.Logger) error {
+	logger.Info().Msg("initiated migration from legacy RBAC")
+
+	var count int64
+
+	if err := db.Model(&models.Project{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	projectRepo := gorm.NewProjectRepository(db).(*gorm.ProjectRepository)
+	projectRoleRepo := gorm.NewProjectRoleRepository(db).(*gorm.ProjectRoleRepository)
+	policyRepo := gorm.NewPolicyRepository(db).(*gorm.PolicyRepository)
+
+	logger.Info().Msgf("found %d projects", count)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		projects := []*models.Project{}
+
+		if err := db.Preload("Roles").Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&projects).Error; err != nil {
+			return err
+		}
+
+		for _, project := range projects {
+			logger.Info().Msgf("starting migration for project ID %d", project.ID)
+
+			var legacyRoleCount int64
+
+			if err := db.Where("unique_id = ?", fmt.Sprintf("%d-%s", project.ID, types.RoleAdmin)).
+				Find(&models.ProjectRole{}).Count(&legacyRoleCount).Error; err != nil {
+				return err
+			} else if legacyRoleCount == 0 {
+				err := createNewRole(project.ID, types.RoleAdmin, projectRoleRepo, policyRepo)
+
+				if err != nil {
+					return err
+				}
+			}
+
+			if err := db.Where("unique_id = ?", fmt.Sprintf("%d-%s", project.ID, types.RoleDeveloper)).
+				Find(&models.ProjectRole{}).Count(&legacyRoleCount).Error; err != nil {
+				return err
+			} else if legacyRoleCount == 0 {
+				err := createNewRole(project.ID, types.RoleDeveloper, projectRoleRepo, policyRepo)
+
+				if err != nil {
+					return err
+				}
+			}
+
+			if err := db.Where("unique_id = ?", fmt.Sprintf("%d-%s", project.ID, types.RoleViewer)).
+				Find(&models.ProjectRole{}).Count(&legacyRoleCount).Error; err != nil {
+				return err
+			} else if legacyRoleCount == 0 {
+				err := createNewRole(project.ID, types.RoleViewer, projectRoleRepo, policyRepo)
+
+				if err != nil {
+					return err
+				}
+			}
+
+			legacyRoleKindUsersMap := map[types.RoleKind][]uint{
+				types.RoleAdmin:     make([]uint, 0),
+				types.RoleDeveloper: make([]uint, 0),
+				types.RoleViewer:    make([]uint, 0),
+				types.RoleCustom:    make([]uint, 0), // added this for possible cases of custom roles in the DB?
+			}
+
+			for _, legacyRole := range project.Roles {
+				legacyRoleKindUsersMap[legacyRole.Kind] = append(legacyRoleKindUsersMap[legacyRole.Kind], legacyRole.UserID)
+			}
+
+			delete(legacyRoleKindUsersMap, types.RoleCustom) // added just to make sure nothing goes wrong from here
+
+			for roleKind, users := range legacyRoleKindUsersMap {
+				if len(users) > 0 {
+					err := projectRoleRepo.UpdateUsersInProjectRole(project.ID, fmt.Sprintf("%d-%s", project.ID, roleKind), users)
+
+					if err != nil {
+						return err
+					}
+				}
+			}
+
+			for _, legacyRole := range project.Roles {
+				// delete legacy role from project
+				if _, err := projectRepo.DeleteProjectRole(project.ID, legacyRole.UserID); err != nil {
+					return fmt.Errorf("error encountered while deleting legacy project role: %w", err)
+				}
+			}
+
+			logger.Info().Msgf("finished migration for project ID %d", project.ID)
+		}
+	}
+
+	logger.Info().Msg("legacy RBAC migration completed")
+	return nil
+}
+
+func createNewRole(
+	projectID uint,
+	kind types.RoleKind,
+	projectRoleRepo repository.ProjectRoleRepository,
+	policyRepo repository.PolicyRepository,
+) error {
+	// for legacy roles - admin, developer, viewer (kinds)
+	// default role name such as <project ID>-<kind> for uniqueness
+	// similarly, create policy for each new default role as <project ID>-<kind>-project-role-policy
+
+	uid, err := encryption.GenerateRandomBytes(16)
+
+	if err != nil {
+		return err
+	}
+
+	var policyBytes []byte
+
+	switch kind {
+	case types.RoleAdmin:
+		policyBytes, err = json.Marshal(types.AdminPolicy)
+
+		if err != nil {
+			return err
+		}
+	case types.RoleDeveloper:
+		policyBytes, err = json.Marshal(types.DeveloperPolicy)
+
+		if err != nil {
+			return err
+		}
+	case types.RoleViewer:
+		policyBytes, err = json.Marshal(types.ViewerPolicy)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	newPolicy, err := policyRepo.CreatePolicy(&models.Policy{
+		UniqueID:    uid,
+		ProjectID:   projectID,
+		Name:        fmt.Sprintf("%s-project-role-policy", kind),
+		PolicyBytes: policyBytes,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	_, err = projectRoleRepo.CreateProjectRole(&models.ProjectRole{
+		UniqueID:  fmt.Sprintf("%d-%s", projectID, kind),
+		ProjectID: projectID,
+		PolicyUID: newPolicy.UniqueID,
+		Name:      string(kind),
+	})
+
+	if err != nil {
+		// delete newly created policy first
+		policyRepo.DeletePolicy(newPolicy)
+
+		return err
+	}
+
+	return nil
+}

Разлика између датотеке није приказан због своје велике величине
+ 14275 - 1
dashboard/package-lock.json


+ 2 - 2
dashboard/src/main/home/integrations/create-integration/GARForm.tsx

@@ -4,7 +4,7 @@ import InputRow from "components/form-components/InputRow";
 import SelectRow from "components/form-components/SelectRow";
 import SelectRow from "components/form-components/SelectRow";
 import UploadArea from "components/form-components/UploadArea";
 import UploadArea from "components/form-components/UploadArea";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
-import { GCP_REGION_OPTIONS } from "main/home/onboarding/constants";
+import { GAR_REGION_OPTIONS } from "main/home/onboarding/constants";
 import React, { useContext, useState } from "react";
 import React, { useContext, useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
@@ -109,7 +109,7 @@ const GARForm = (props: { closeForm: () => void }) => {
         />
         />
         <Helper>GAR Region</Helper>
         <Helper>GAR Region</Helper>
         <SelectRow
         <SelectRow
-          options={GCP_REGION_OPTIONS}
+          options={GAR_REGION_OPTIONS}
           width="100%"
           width="100%"
           value={region}
           value={region}
           scrollBuffer={true}
           scrollBuffer={true}

+ 16 - 0
dashboard/src/main/home/onboarding/constants.ts

@@ -5,22 +5,38 @@ export const GCP_REGION_OPTIONS = [
   { value: "asia-northeast2", label: "asia-northeast2" },
   { value: "asia-northeast2", label: "asia-northeast2" },
   { value: "asia-northeast3", label: "asia-northeast3" },
   { value: "asia-northeast3", label: "asia-northeast3" },
   { value: "asia-south1", label: "asia-south1" },
   { value: "asia-south1", label: "asia-south1" },
+  { value: "asia-south2", label: "asia-south2" },
   { value: "asia-southeast1", label: "asia-southeast1" },
   { value: "asia-southeast1", label: "asia-southeast1" },
   { value: "asia-southeast2", label: "asia-southeast2" },
   { value: "asia-southeast2", label: "asia-southeast2" },
   { value: "australia-southeast1", label: "australia-southeast1" },
   { value: "australia-southeast1", label: "australia-southeast1" },
+  { value: "australia-southeast2", label: "australia-southeast2" },
+  { value: "europe-central2", label: "europe-central2" },
   { value: "europe-north1", label: "europe-north1" },
   { value: "europe-north1", label: "europe-north1" },
   { value: "europe-west1", label: "europe-west1" },
   { value: "europe-west1", label: "europe-west1" },
   { value: "europe-west2", label: "europe-west2" },
   { value: "europe-west2", label: "europe-west2" },
   { value: "europe-west3", label: "europe-west3" },
   { value: "europe-west3", label: "europe-west3" },
   { value: "europe-west4", label: "europe-west4" },
   { value: "europe-west4", label: "europe-west4" },
   { value: "europe-west6", label: "europe-west6" },
   { value: "europe-west6", label: "europe-west6" },
+  { value: "europe-west8", label: "europe-west8" },
+  { value: "europe-west9", label: "europe-west9" },
+  { value: "europe-southwest1", label: "europe-southwest1" },
   { value: "northamerica-northeast1", label: "northamerica-northeast1" },
   { value: "northamerica-northeast1", label: "northamerica-northeast1" },
+  { value: "northamerica-northeast2", label: "northamerica-northeast2" },
   { value: "southamerica-east1", label: "southamerica-east1" },
   { value: "southamerica-east1", label: "southamerica-east1" },
+  { value: "southamerica-west1", label: "southamerica-west1" },
   { value: "us-central1", label: "us-central1" },
   { value: "us-central1", label: "us-central1" },
   { value: "us-east1", label: "us-east1" },
   { value: "us-east1", label: "us-east1" },
   { value: "us-east4", label: "us-east4" },
   { value: "us-east4", label: "us-east4" },
+  { value: "us-east5", label: "us-east5" },
   { value: "us-west1", label: "us-west1" },
   { value: "us-west1", label: "us-west1" },
   { value: "us-west2", label: "us-west2" },
   { value: "us-west2", label: "us-west2" },
   { value: "us-west3", label: "us-west3" },
   { value: "us-west3", label: "us-west3" },
   { value: "us-west4", label: "us-west4" },
   { value: "us-west4", label: "us-west4" },
+  { value: "us-south1", label: "us-south1" },
 ];
 ];
+
+export const GAR_REGION_OPTIONS = GCP_REGION_OPTIONS.concat([
+  { value: "us", label: "us (multi-region)" },
+  { value: "europe", label: "europe (multi-region)" },
+  { value: "asia", label: "asia (multi-region)" },
+]);

+ 0 - 6
internal/models/project.go

@@ -48,12 +48,6 @@ type Project struct {
 	// provisioned aws infra
 	// provisioned aws infra
 	Infras []Infra `json:"infras"`
 	Infras []Infra `json:"infras"`
 
 
-	// linked policy documents
-	ProjectPolicies []Policy
-
-	// project roles
-	ProjectRoles []ProjectRole
-
 	// auth mechanisms
 	// auth mechanisms
 	KubeIntegrations   []ints.KubeIntegration   `json:"kube_integrations"`
 	KubeIntegrations   []ints.KubeIntegration   `json:"kube_integrations"`
 	BasicIntegrations  []ints.BasicIntegration  `json:"basic_integrations"`
 	BasicIntegrations  []ints.BasicIntegration  `json:"basic_integrations"`

+ 27 - 4
internal/models/project_role.go

@@ -1,18 +1,41 @@
 package models
 package models
 
 
 import (
 import (
+	"fmt"
+
+	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
 
 
 type ProjectRole struct {
 type ProjectRole struct {
 	gorm.Model
 	gorm.Model
 
 
-	ProjectID uint
-
 	UniqueID string `gorm:"unique"`
 	UniqueID string `gorm:"unique"`
 
 
+	ProjectID uint
+	PolicyUID string
+
 	Name string
 	Name string
 
 
-	Policies []Policy `gorm:"many2many:role_policies"`
-	Users    []User   `gorm:"many2many:user_roles"`
+	Users []User `gorm:"many2many:user_roles"`
+}
+
+func (role *ProjectRole) ToProjectRoleType(policy *types.PolicyDocument) *types.ProjectRole {
+	res := &types.ProjectRole{
+		ID:     role.UniqueID,
+		Name:   role.Name,
+		Policy: policy,
+	}
+
+	for _, user := range role.Users {
+		res.Users = append(res.Users, user.ID)
+	}
+
+	return res
+}
+
+func (role *ProjectRole) IsDefaultRole() bool {
+	return role.UniqueID == fmt.Sprintf("%d-%s", role.ProjectID, types.RoleAdmin) ||
+		role.UniqueID == fmt.Sprintf("%d-%s", role.ProjectID, types.RoleDeveloper) ||
+		role.UniqueID == fmt.Sprintf("%d-%s", role.ProjectID, types.RoleViewer)
 }
 }

+ 7 - 37
internal/repository/gorm/policy.go

@@ -17,34 +17,21 @@ func NewPolicyRepository(db *gorm.DB) repository.PolicyRepository {
 	return &PolicyRepository{db}
 	return &PolicyRepository{db}
 }
 }
 
 
-func (repo *PolicyRepository) CreatePolicy(policy *models.Policy) (*models.Policy, error) {
-	var project models.Project
-
-	if err := repo.db.Where("id = ?", policy.ProjectID).First(&project).Error; err != nil {
-		return nil, err
-	}
-
-	assoc := repo.db.Model(&project).Association("ProjectPolicies")
-
-	if assoc.Error != nil {
-		return nil, assoc.Error
-	}
-
-	if err := assoc.Append(policy); err != nil {
+func (repo *PolicyRepository) CreatePolicy(a *models.Policy) (*models.Policy, error) {
+	if err := repo.db.Create(a).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-
-	return policy, nil
+	return a, nil
 }
 }
 
 
 func (repo *PolicyRepository) ListPoliciesByProjectID(projectID uint) ([]*models.Policy, error) {
 func (repo *PolicyRepository) ListPoliciesByProjectID(projectID uint) ([]*models.Policy, error) {
-	var policies []*models.Policy
+	policys := []*models.Policy{}
 
 
-	if err := repo.db.Where("project_id = ?", projectID).Find(&policies).Error; err != nil {
+	if err := repo.db.Where("project_id = ?", projectID).Find(&policys).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return policies, nil
+	return policys, nil
 }
 }
 
 
 func (repo *PolicyRepository) ReadPolicy(projectID uint, uid string) (*models.Policy, error) {
 func (repo *PolicyRepository) ReadPolicy(projectID uint, uid string) (*models.Policy, error) {
@@ -70,25 +57,8 @@ func (repo *PolicyRepository) UpdatePolicy(
 func (repo *PolicyRepository) DeletePolicy(
 func (repo *PolicyRepository) DeletePolicy(
 	policy *models.Policy,
 	policy *models.Policy,
 ) (*models.Policy, error) {
 ) (*models.Policy, error) {
-	var project models.Project
-
-	if err := repo.db.Where("id = ?", policy.ProjectID).First(&project).Error; err != nil {
+	if err := repo.db.Delete(&policy).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-
-	assoc := repo.db.Model(&project).Association("ProjectPolicies")
-
-	if assoc.Error != nil {
-		return nil, assoc.Error
-	}
-
-	if err := assoc.Delete(policy); err != nil {
-		return nil, err
-	}
-
-	if err := repo.db.Delete(policy).Error; err != nil {
-		return nil, err
-	}
-
 	return policy, nil
 	return policy, nil
 }
 }

+ 108 - 0
internal/repository/gorm/project_role.go

@@ -0,0 +1,108 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ProjectRoleRepository uses gorm.DB for querying the database
+type ProjectRoleRepository struct {
+	db *gorm.DB
+}
+
+// NewProjectRoleRepository returns a ProjectRoleRepository which uses
+// gorm.DB for querying the database
+func NewProjectRoleRepository(db *gorm.DB) repository.ProjectRoleRepository {
+	return &ProjectRoleRepository{db}
+}
+
+func (repo *ProjectRoleRepository) CreateProjectRole(role *models.ProjectRole) (*models.ProjectRole, error) {
+	if err := repo.db.Create(role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}
+
+func (repo *ProjectRoleRepository) ReadProjectRole(projectID uint, roleUID string) (*models.ProjectRole, error) {
+	role := &models.ProjectRole{}
+
+	if err := repo.db.Where("project_id = ? AND unique_id = ?", projectID, roleUID).First(role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}
+
+func (repo *ProjectRoleRepository) ListProjectRoles(projectID uint) ([]*models.ProjectRole, error) {
+	roles := []*models.ProjectRole{}
+
+	if err := repo.db.Preload("Users").Where("project_id = ?", projectID).Find(&roles).Error; err != nil {
+		return nil, err
+	}
+
+	return roles, nil
+}
+
+func (repo *ProjectRoleRepository) ListAllRolesForUser(projectID, userID uint) ([]*models.ProjectRole, error) {
+	projectRoles := []*models.ProjectRole{}
+
+	if err := repo.db.Where("project_id = ?", userID).Find(&projectRoles).Error; err != nil {
+		return nil, err
+	}
+
+	var res []*models.ProjectRole
+
+	for _, role := range projectRoles {
+		if count := repo.db.Model(role).Where("id = ?", userID).Association("Users").Count(); count == 0 {
+			continue
+		} else {
+			res = append(res, role)
+		}
+	}
+
+	return res, nil
+}
+
+func (repo *ProjectRoleRepository) UpdateUsersInProjectRole(projectID uint, roleUID string, userIDs []uint) error {
+	users := []*models.User{}
+
+	if err := repo.db.Find(&users, userIDs).Error; err != nil {
+		return err
+	}
+
+	role := &models.ProjectRole{}
+
+	if err := repo.db.Where("project_id = ? AND unique_id = ?", projectID, roleUID).First(role).Error; err != nil {
+		return err
+	}
+
+	assoc := repo.db.Model(&role).Association("Users")
+
+	if assoc.Error != nil {
+		return assoc.Error
+	}
+
+	if err := assoc.Replace(users); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo *ProjectRoleRepository) UpdateProjectRole(role *models.ProjectRole) (*models.ProjectRole, error) {
+	if err := repo.db.Save(role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}
+
+func (repo *ProjectRoleRepository) DeleteProjectRole(role *models.ProjectRole) (*models.ProjectRole, error) {
+	if err := repo.db.Delete(role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}

+ 6 - 0
internal/repository/gorm/repository.go

@@ -10,6 +10,7 @@ type GormRepository struct {
 	user                      repository.UserRepository
 	user                      repository.UserRepository
 	session                   repository.SessionRepository
 	session                   repository.SessionRepository
 	project                   repository.ProjectRepository
 	project                   repository.ProjectRepository
+	projectRole               repository.ProjectRoleRepository
 	cluster                   repository.ClusterRepository
 	cluster                   repository.ClusterRepository
 	database                  repository.DatabaseRepository
 	database                  repository.DatabaseRepository
 	helmRepo                  repository.HelmRepoRepository
 	helmRepo                  repository.HelmRepoRepository
@@ -62,6 +63,10 @@ func (t *GormRepository) Project() repository.ProjectRepository {
 	return t.project
 	return t.project
 }
 }
 
 
+func (t *GormRepository) ProjectRole() repository.ProjectRoleRepository {
+	return t.projectRole
+}
+
 func (t *GormRepository) Cluster() repository.ClusterRepository {
 func (t *GormRepository) Cluster() repository.ClusterRepository {
 	return t.cluster
 	return t.cluster
 }
 }
@@ -221,6 +226,7 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		user:                      NewUserRepository(db),
 		user:                      NewUserRepository(db),
 		session:                   NewSessionRepository(db),
 		session:                   NewSessionRepository(db),
 		project:                   NewProjectRepository(db),
 		project:                   NewProjectRepository(db),
+		projectRole:               NewProjectRoleRepository(db),
 		cluster:                   NewClusterRepository(db, key),
 		cluster:                   NewClusterRepository(db, key),
 		database:                  NewDatabaseRepository(db, key),
 		database:                  NewDatabaseRepository(db, key),
 		helmRepo:                  NewHelmRepoRepository(db, key),
 		helmRepo:                  NewHelmRepoRepository(db, key),

+ 14 - 0
internal/repository/project_role.go

@@ -0,0 +1,14 @@
+package repository
+
+import "github.com/porter-dev/porter/internal/models"
+
+// ProjectRoleRepository represents the set of queries on the ProjectRole model
+type ProjectRoleRepository interface {
+	CreateProjectRole(role *models.ProjectRole) (*models.ProjectRole, error)
+	ReadProjectRole(projectID uint, roleUID string) (*models.ProjectRole, error)
+	ListProjectRoles(projectID uint) ([]*models.ProjectRole, error)
+	ListAllRolesForUser(projectID, userID uint) ([]*models.ProjectRole, error)
+	UpdateUsersInProjectRole(projectID uint, roleUID string, userIDs []uint) error
+	UpdateProjectRole(role *models.ProjectRole) (*models.ProjectRole, error)
+	DeleteProjectRole(role *models.ProjectRole) (*models.ProjectRole, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -3,6 +3,7 @@ package repository
 type Repository interface {
 type Repository interface {
 	User() UserRepository
 	User() UserRepository
 	Project() ProjectRepository
 	Project() ProjectRepository
+	ProjectRole() ProjectRoleRepository
 	Release() ReleaseRepository
 	Release() ReleaseRepository
 	Environment() EnvironmentRepository
 	Environment() EnvironmentRepository
 	Session() SessionRepository
 	Session() SessionRepository

+ 41 - 0
internal/repository/test/project_role.go

@@ -0,0 +1,41 @@
+package test
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type ProjectRoleRepository struct {
+}
+
+func NewProjectRoleRepository() repository.ProjectRoleRepository {
+	return &ProjectRoleRepository{}
+}
+
+func (repo *ProjectRoleRepository) CreateProjectRole(role *models.ProjectRole) (*models.ProjectRole, error) {
+	panic("not implemented")
+}
+
+func (repo *ProjectRoleRepository) ReadProjectRole(projectID uint, roleUID string) (*models.ProjectRole, error) {
+	panic("not implemented")
+}
+
+func (repo *ProjectRoleRepository) ListProjectRoles(projectID uint) ([]*models.ProjectRole, error) {
+	panic("not implemented")
+}
+
+func (repo *ProjectRoleRepository) ListAllRolesForUser(projectID, userID uint) ([]*models.ProjectRole, error) {
+	panic("not implemented")
+}
+
+func (repo *ProjectRoleRepository) UpdateUsersInProjectRole(projectID uint, roleUID string, userIDs []uint) error {
+	panic("not implemented")
+}
+
+func (repo *ProjectRoleRepository) UpdateProjectRole(role *models.ProjectRole) (*models.ProjectRole, error) {
+	panic("not implemented")
+}
+
+func (repo *ProjectRoleRepository) DeleteProjectRole(role *models.ProjectRole) (*models.ProjectRole, error) {
+	panic("not implemented")
+}

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

@@ -8,6 +8,7 @@ type TestRepository struct {
 	user                      repository.UserRepository
 	user                      repository.UserRepository
 	session                   repository.SessionRepository
 	session                   repository.SessionRepository
 	project                   repository.ProjectRepository
 	project                   repository.ProjectRepository
+	projectRole               repository.ProjectRoleRepository
 	cluster                   repository.ClusterRepository
 	cluster                   repository.ClusterRepository
 	helmRepo                  repository.HelmRepoRepository
 	helmRepo                  repository.HelmRepoRepository
 	registry                  repository.RegistryRepository
 	registry                  repository.RegistryRepository
@@ -60,6 +61,10 @@ func (t *TestRepository) Project() repository.ProjectRepository {
 	return t.project
 	return t.project
 }
 }
 
 
+func (t *TestRepository) ProjectRole() repository.ProjectRoleRepository {
+	return t.projectRole
+}
+
 func (t *TestRepository) Cluster() repository.ClusterRepository {
 func (t *TestRepository) Cluster() repository.ClusterRepository {
 	return t.cluster
 	return t.cluster
 }
 }

Неке датотеке нису приказане због велике количине промена