Explorar o código

Add project role handlers

Mohammed Nafees %!s(int64=3) %!d(string=hai) anos
pai
achega
71d56a601f

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

@@ -0,0 +1,105 @@
+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
+	}
+
+	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)
+}

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

@@ -0,0 +1,81 @@
+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 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]))
+}

+ 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)
+}

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

@@ -0,0 +1,105 @@
+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 request.Name != "" && request.Name != role.Name {
+		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 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
+		}
+	}
+}

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

@@ -0,0 +1,84 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	project_integration "github.com/porter-dev/porter/api/server/handlers/project_integration"
+	"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_integration.NewListOAuthHandler
+	listOAuthEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/oauth",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listOAuthHandler := project_integration.NewListOAuthHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listOAuthEndpoint,
+		Handler:  listOAuthHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 2 - 0
api/server/router/router.go

@@ -38,6 +38,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	inviteRegisterer := NewInviteScopedRegisterer()
 	projectIntegrationRegisterer := NewProjectIntegrationScopedRegisterer()
 	projectOAuthRegisterer := NewProjectOAuthScopedRegisterer()
+	projectRoleRegisterer := NewProjectRoleScopedRegisterer()
 	slackIntegrationRegisterer := NewSlackIntegrationScopedRegisterer()
 	projRegisterer := NewProjectScopedRegisterer(
 		clusterRegisterer,
@@ -49,6 +50,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 		projectIntegrationRegisterer,
 		projectOAuthRegisterer,
 		slackIntegrationRegisterer,
+		projectRoleRegisterer,
 	)
 	statusRegisterer := NewStatusScopedRegisterer()
 

+ 1 - 1
api/types/project.go

@@ -24,7 +24,7 @@ type CreateProjectRequest struct {
 
 type CreateProjectResponse Project
 
-type CreateProjectRoleRequest struct {
+type CreateLegacyProjectRoleRequest struct {
 	Kind   string `json:"kind" 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"`
+}

+ 0 - 6
internal/models/project.go

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

+ 19 - 4
internal/models/project_role.go

@@ -1,18 +1,33 @@
 package models
 
 import (
+	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 )
 
 type ProjectRole struct {
 	gorm.Model
 
-	ProjectID uint
-
 	UniqueID string `gorm:"unique"`
 
+	ProjectID uint
+	PolicyUID 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
 }

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

@@ -0,0 +1,88 @@
+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.Where("project_id = ?", projectID).Find(&roles).Error; err != nil {
+		return nil, err
+	}
+
+	return roles, 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
 	session                   repository.SessionRepository
 	project                   repository.ProjectRepository
+	projectRole               repository.ProjectRoleRepository
 	cluster                   repository.ClusterRepository
 	database                  repository.DatabaseRepository
 	helmRepo                  repository.HelmRepoRepository
@@ -62,6 +63,10 @@ func (t *GormRepository) Project() repository.ProjectRepository {
 	return t.project
 }
 
+func (t *GormRepository) ProjectRole() repository.ProjectRoleRepository {
+	return t.projectRole
+}
+
 func (t *GormRepository) Cluster() repository.ClusterRepository {
 	return t.cluster
 }
@@ -221,6 +226,7 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		user:                      NewUserRepository(db),
 		session:                   NewSessionRepository(db),
 		project:                   NewProjectRepository(db),
+		projectRole:               NewProjectRoleRepository(db),
 		cluster:                   NewClusterRepository(db, key),
 		database:                  NewDatabaseRepository(db, key),
 		helmRepo:                  NewHelmRepoRepository(db, key),

+ 13 - 0
internal/repository/project_role.go

@@ -0,0 +1,13 @@
+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)
+	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 {
 	User() UserRepository
 	Project() ProjectRepository
+	ProjectRole() ProjectRoleRepository
 	Release() ReleaseRepository
 	Environment() EnvironmentRepository
 	Session() SessionRepository

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

@@ -0,0 +1,37 @@
+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) 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")
+}