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

Merge branch '0.5.0-basic-rbac-backend' of github.com:porter-dev/porter into 0.6.0-implement-rbac-settings-invite

jnfrati преди 4 години
родител
ревизия
7aeb2f2301

+ 101 - 0
api/types/policy.go

@@ -0,0 +1,101 @@
+package types
+
+type PermissionScope string
+
+const (
+	UserScope        PermissionScope = "user"
+	ProjectScope     PermissionScope = "project"
+	ClusterScope     PermissionScope = "cluster"
+	NamespaceScope   PermissionScope = "namespace"
+	SettingsScope    PermissionScope = "settings"
+	ApplicationScope PermissionScope = "application"
+)
+
+type NameOrUInt struct {
+	Name string `json:"name"`
+	UInt uint   `json:"uint"`
+}
+
+type PolicyDocument struct {
+	Scope     PermissionScope                     `json:"scope"`
+	Resources []NameOrUInt                        `json:"resources"`
+	Verbs     []APIVerb                           `json:"verbs"`
+	Children  map[PermissionScope]*PolicyDocument `json:"children"`
+}
+
+type ScopeTree map[PermissionScope]ScopeTree
+
+/* ScopeHeirarchy describes the scope tree:
+			Project
+		   /	   \
+		Cluster   Settings
+		/
+	Namespace
+       |
+	 Release
+*/
+var ScopeHeirarchy = ScopeTree{
+	ProjectScope: {
+		ClusterScope: {
+			NamespaceScope: {
+				ApplicationScope: {},
+			},
+		},
+		SettingsScope: {},
+	},
+}
+
+type Policy []*PolicyDocument
+
+type APIVerb string
+
+const (
+	APIVerbGet    APIVerb = "get"
+	APIVerbCreate APIVerb = "create"
+	APIVerbList   APIVerb = "list"
+	APIVerbUpdate APIVerb = "update"
+	APIVerbDelete APIVerb = "delete"
+)
+
+type APIVerbGroup []APIVerb
+
+func ReadVerbGroup() APIVerbGroup {
+	return []APIVerb{APIVerbGet, APIVerbList}
+}
+
+func ReadWriteVerbGroup() APIVerbGroup {
+	return []APIVerb{APIVerbGet, APIVerbList, APIVerbCreate, APIVerbUpdate, APIVerbDelete}
+}
+
+var AdminPolicy = []*PolicyDocument{
+	{
+		Scope: ProjectScope,
+		Verbs: ReadWriteVerbGroup(),
+	},
+}
+
+var DeveloperPolicy = []*PolicyDocument{
+	{
+		Scope: ProjectScope,
+		Verbs: ReadWriteVerbGroup(),
+		Children: map[PermissionScope]*PolicyDocument{
+			SettingsScope: {
+				Scope: SettingsScope,
+				Verbs: ReadVerbGroup(),
+			},
+		},
+	},
+}
+
+var ViewerPolicy = []*PolicyDocument{
+	{
+		Scope: ProjectScope,
+		Verbs: ReadVerbGroup(),
+		Children: map[PermissionScope]*PolicyDocument{
+			SettingsScope: {
+				Scope: SettingsScope,
+				Verbs: []APIVerb{},
+			},
+		},
+	},
+}

+ 2 - 0
internal/forms/invite.go

@@ -11,6 +11,7 @@ import (
 // invite to a project
 type CreateInvite struct {
 	Email     string `json:"email" form:"required"`
+	Kind      string `json:"kind" form:"required"`
 	ProjectID uint   `form:"required"`
 }
 
@@ -21,6 +22,7 @@ func (ci *CreateInvite) ToInvite() (*models.Invite, error) {
 
 	return &models.Invite{
 		Email:     ci.Email,
+		Kind:      ci.Kind,
 		Expiry:    &expiry,
 		ProjectID: ci.ProjectID,
 		Token:     oauth.CreateRandomState(),

+ 3 - 16
internal/forms/project.go

@@ -3,7 +3,6 @@ package forms
 import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
 )
 
 // WriteProjectForm is a generic form for write operations to the Project model
@@ -24,20 +23,8 @@ func (cpf *CreateProjectForm) ToProject(_ repository.ProjectRepository) (*models
 	}, nil
 }
 
-// CreateProjectRoleForm represents the accepted values for creating a project
+// UpdateProjectRoleForm represents the accepted values for updating a project
 // role
-type CreateProjectRoleForm struct {
-	WriteProjectForm
-	ID    uint          `json:"project_id" form:"required"`
-	Roles []models.Role `json:"roles"`
-}
-
-// ToProject converts the form to a gorm project model
-func (cprf *CreateProjectRoleForm) ToProject(_ repository.ProjectRepository) (*models.Project, error) {
-	return &models.Project{
-		Model: gorm.Model{
-			ID: cprf.ID,
-		},
-		Roles: cprf.Roles,
-	}, nil
+type UpdateProjectRoleForm struct {
+	Kind string `json:"kind"`
 }

+ 4 - 0
internal/models/invite.go

@@ -14,6 +14,9 @@ type Invite struct {
 	Expiry *time.Time
 	Email  string
 
+	// Kind is the role kind that this refers to
+	Kind string
+
 	ProjectID uint
 	UserID    uint
 }
@@ -25,6 +28,7 @@ type InviteExternal struct {
 	Expired  bool   `json:"expired"`
 	Email    string `json:"email"`
 	Accepted bool   `json:"accepted"`
+	Kind     string `json:"kind"`
 }
 
 // Externalize generates an external Invite to be shared over REST

+ 3 - 2
internal/models/role.go

@@ -6,8 +6,9 @@ import (
 
 // The roles available for a project
 const (
-	RoleAdmin  string = "admin"
-	RoleViewer string = "viewer"
+	RoleAdmin     string = "admin"
+	RoleDeveloper string = "developer"
+	RoleViewer    string = "viewer"
 )
 
 // Role type that extends gorm.Model

+ 43 - 0
internal/repository/gorm/project.go

@@ -41,6 +41,22 @@ func (repo *ProjectRepository) CreateProjectRole(project *models.Project, role *
 	return role, nil
 }
 
+func (repo *ProjectRepository) UpdateProjectRole(projID uint, role *models.Role) (*models.Role, error) {
+	foundRole := &models.Role{}
+
+	if err := repo.db.Where("project_id = ? AND user_id = ?", projID, role.UserID).First(&foundRole).Error; err != nil {
+		return nil, err
+	}
+
+	role.ID = foundRole.ID
+
+	if err := repo.db.Save(&role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}
+
 // ReadProject gets a projects specified by a unique id
 func (repo *ProjectRepository) ReadProject(id uint) (*models.Project, error) {
 	project := &models.Project{}
@@ -52,6 +68,18 @@ func (repo *ProjectRepository) ReadProject(id uint) (*models.Project, error) {
 	return project, nil
 }
 
+// ReadProject gets a projects specified by a unique id
+func (repo *ProjectRepository) ReadProjectRole(projID, userID uint) (*models.Role, error) {
+	// find the role
+	role := &models.Role{}
+
+	if err := repo.db.Where("project_id = ? AND user_id = ?", projID, userID).First(&role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}
+
 // ListProjectsByUserID lists projects where a user has an associated role
 func (repo *ProjectRepository) ListProjectsByUserID(userID uint) ([]*models.Project, error) {
 	projects := make([]*models.Project, 0)
@@ -72,3 +100,18 @@ func (repo *ProjectRepository) DeleteProject(project *models.Project) (*models.P
 	}
 	return project, nil
 }
+
+func (repo *ProjectRepository) DeleteProjectRole(projID, userID uint) (*models.Role, error) {
+	// find the role
+	role := &models.Role{}
+
+	if err := repo.db.Where("project_id = ? AND user_id = ?", projID, userID).First(&role).Error; err != nil {
+		return nil, err
+	}
+
+	if err := repo.db.Delete(&role).Error; err != nil {
+		return nil, err
+	}
+
+	return role, nil
+}

+ 117 - 1
internal/repository/gorm/project_test.go

@@ -108,6 +108,72 @@ func TestCreateProjectRole(t *testing.T) {
 	}
 }
 
+func TestUpdateProjectRole(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_update_proj_role.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initUser(tester, t)
+	initProjectRole(tester, t)
+	defer cleanup(tester, t)
+
+	role := &models.Role{
+		Kind:      models.RoleViewer,
+		UserID:    tester.initUsers[0].Model.ID,
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	role, err := tester.repo.Project.UpdateProjectRole(tester.initProjects[0].Model.ID, role)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	proj, err := tester.repo.Project.ReadProject(tester.initProjects[0].Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure IDs are correct
+	if proj.Model.ID != 1 {
+		t.Errorf("incorrect project ID: expected %d, got %d\n", 1, proj.Model.ID)
+	}
+
+	if len(proj.Roles) != 1 {
+		t.Fatalf("project roles incorrect length: expected %d, got %d\n", 1, len(proj.Roles))
+	}
+
+	if proj.Roles[0].Model.ID != 1 {
+		t.Fatalf("incorrect role ID: expected %d, got %d\n", 1, proj.Roles[0].Model.ID)
+	}
+
+	// make sure data is correct
+	expProj := &models.Project{
+		Name: "project-test",
+		Roles: []models.Role{
+			{
+				Kind:      models.RoleViewer,
+				UserID:    1,
+				ProjectID: 1,
+			},
+		},
+	}
+
+	copyProj := proj
+
+	// reset fields for reflect.DeepEqual
+	copyProj.Model = orm.Model{}
+	copyProj.Roles[0].Model = orm.Model{}
+
+	if diff := deep.Equal(copyProj, expProj); diff != nil {
+		t.Errorf("incorrect project")
+		t.Error(diff)
+	}
+}
+
 func TestListProjectsByUserID(t *testing.T) {
 	tester := &tester{
 		dbFileName: "./list_projects_user_id.db",
@@ -172,7 +238,7 @@ func TestListProjectsByUserID(t *testing.T) {
 
 func TestDeleteProject(t *testing.T) {
 	tester := &tester{
-		dbFileName: "./porter_create_proj_role.db",
+		dbFileName: "./porter_delete_proj.db",
 	}
 
 	setupTestEnv(tester, t)
@@ -192,3 +258,53 @@ func TestDeleteProject(t *testing.T) {
 		t.Fatalf("read should have returned record not found: returned %v\n", err)
 	}
 }
+
+func TestDeleteProjectRole(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_delete_proj_role.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initUser(tester, t)
+	initProjectRole(tester, t)
+	defer cleanup(tester, t)
+
+	_, err := tester.repo.Project.DeleteProjectRole(tester.initProjects[0].Model.ID, tester.initUsers[0].Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// attempt to read the project and ensure that the error is gorm.ErrRecordNotFound
+	proj, err := tester.repo.Project.ReadProject(tester.initProjects[0].Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure IDs are correct
+	if proj.Model.ID != 1 {
+		t.Errorf("incorrect project ID: expected %d, got %d\n", 1, proj.Model.ID)
+	}
+
+	if len(proj.Roles) != 0 {
+		t.Fatalf("project roles incorrect length: expected %d, got %d\n", 0, len(proj.Roles))
+	}
+
+	// make sure data is correct
+	expProj := &models.Project{
+		Name:  "project-test",
+		Roles: []models.Role{},
+	}
+
+	copyProj := proj
+
+	// reset fields for reflect.DeepEqual
+	copyProj.Model = orm.Model{}
+
+	if diff := deep.Equal(copyProj, expProj); diff != nil {
+		t.Errorf("incorrect project")
+		t.Error(diff)
+	}
+}

+ 107 - 0
internal/repository/memory/project.go

@@ -51,6 +51,41 @@ func (repo *ProjectRepository) CreateProjectRole(project *models.Project, role *
 	return role, nil
 }
 
+// CreateProjectRole appends a role to the existing array of roles
+func (repo *ProjectRepository) UpdateProjectRole(projID uint, role *models.Role) (*models.Role, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	var foundProject *models.Project
+
+	// find all roles matching
+	for _, project := range repo.projects {
+		if project.ID == projID {
+			foundProject = project
+		}
+	}
+
+	if foundProject == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	var index int
+
+	for i, _role := range foundProject.Roles {
+		if _role.UserID == role.UserID {
+			index = i
+		}
+	}
+
+	if index == 0 {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	foundProject.Roles[index] = *role
+	return role, nil
+}
+
 // ReadProject gets a projects specified by a unique id
 func (repo *ProjectRepository) ReadProject(id uint) (*models.Project, error) {
 	if !repo.canQuery {
@@ -65,6 +100,42 @@ func (repo *ProjectRepository) ReadProject(id uint) (*models.Project, error) {
 	return repo.projects[index], nil
 }
 
+// ReadProjectRole gets a role specified by a project ID and user ID
+func (repo *ProjectRepository) ReadProjectRole(projID, userID uint) (*models.Role, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	var foundProject *models.Project
+
+	// find all roles matching
+	for _, project := range repo.projects {
+		if project.ID == projID {
+			foundProject = project
+		}
+	}
+
+	if foundProject == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	var index int
+
+	for i, _role := range foundProject.Roles {
+		if _role.UserID == userID {
+			index = i
+		}
+	}
+
+	if index == 0 {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	res := foundProject.Roles[index]
+
+	return &res, nil
+}
+
 // ListProjectsByUserID lists projects where a user has an associated role
 func (repo *ProjectRepository) ListProjectsByUserID(userID uint) ([]*models.Project, error) {
 	if !repo.canQuery {
@@ -100,3 +171,39 @@ func (repo *ProjectRepository) DeleteProject(project *models.Project) (*models.P
 
 	return project, nil
 }
+
+func (repo *ProjectRepository) DeleteProjectRole(projID, userID uint) (*models.Role, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	var foundProject *models.Project
+
+	// find all roles matching
+	for _, project := range repo.projects {
+		if project.ID == projID {
+			foundProject = project
+		}
+	}
+
+	if foundProject == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	var index int
+
+	for i, _role := range foundProject.Roles {
+		if _role.UserID == userID {
+			index = i
+		}
+	}
+
+	if index == 0 {
+		return nil, gorm.ErrRecordNotFound
+	}
+	res := foundProject.Roles[index]
+
+	foundProject.Roles = append(foundProject.Roles[:index], foundProject.Roles[index+1:]...)
+
+	return &res, nil
+}

+ 3 - 0
internal/repository/project.go

@@ -11,7 +11,10 @@ type WriteProject func(project *models.Project) (*models.Project, error)
 type ProjectRepository interface {
 	CreateProject(project *models.Project) (*models.Project, error)
 	CreateProjectRole(project *models.Project, role *models.Role) (*models.Role, error)
+	UpdateProjectRole(projID uint, role *models.Role) (*models.Role, error)
 	ReadProject(id uint) (*models.Project, error)
+	ReadProjectRole(projID, userID uint) (*models.Role, error)
 	ListProjectsByUserID(userID uint) ([]*models.Project, error)
 	DeleteProject(project *models.Project) (*models.Project, error)
+	DeleteProjectRole(projID, userID uint) (*models.Role, error)
 }

+ 1 - 1
server/api/invite_handler.go

@@ -171,7 +171,7 @@ func (app *App) HandleAcceptInvite(w http.ResponseWriter, r *http.Request) {
 	_, err = app.Repo.Project.CreateProjectRole(projModel, &models.Role{
 		UserID:    userID,
 		ProjectID: uint(projID),
-		Kind:      models.RoleAdmin,
+		Kind:      invite.Kind,
 	})
 
 	if err != nil {

+ 137 - 0
server/api/project_handler.go

@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -83,6 +84,19 @@ func (app *App) HandleCreateProject(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleGetProjectRoles lists the roles available to the project. For now, these
+// roles are static.
+func (app *App) HandleGetProjectRoles(w http.ResponseWriter, r *http.Request) {
+	roles := []string{models.RoleAdmin, models.RoleDeveloper, models.RoleViewer}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(&roles); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleReadProject returns an externalized Project (models.ProjectExternal)
 // based on an ID
 func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
@@ -110,6 +124,92 @@ func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleReadProjectPolicy returns the policy document given the current user
+func (app *App) HandleReadProjectPolicy(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	userID, err := app.getUserIDFromRequest(r)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	role, err := app.Repo.Project.ReadProjectRole(uint(id), userID)
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	// case on the role to get the policy document
+	var policy types.Policy
+	switch role.Kind {
+	case models.RoleAdmin:
+		policy = types.AdminPolicy
+	case models.RoleDeveloper:
+		policy = types.DeveloperPolicy
+	case models.RoleViewer:
+		policy = types.ViewerPolicy
+	}
+
+	if err := json.NewEncoder(w).Encode(policy); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleUpdateProjectRole updates a project role with a new "kind"
+func (app *App) HandleUpdateProjectRole(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	userID, err := strconv.ParseUint(chi.URLParam(r, "user_id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	role, err := app.Repo.Project.ReadProjectRole(uint(id), uint(userID))
+
+	if err != nil {
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	}
+
+	form := &forms.UpdateProjectRoleForm{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	role.Kind = form.Kind
+
+	role, err = app.Repo.Project.UpdateProjectRole(uint(id), role)
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(role.Externalize()); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleDeleteProject deletes a project from the db, reading from the project_id
 // in the URL param
 func (app *App) HandleDeleteProject(w http.ResponseWriter, r *http.Request) {
@@ -143,3 +243,40 @@ func (app *App) HandleDeleteProject(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 }
+
+// HandleDeleteProjectRole deletes a project role from the db, reading from the project_id
+// in the URL param
+func (app *App) HandleDeleteProjectRole(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	userID, err := strconv.ParseUint(chi.URLParam(r, "user_id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	role, err := app.Repo.Project.ReadProjectRole(uint(id), uint(userID))
+
+	if err != nil {
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	}
+
+	role, err = app.Repo.Project.DeleteProjectRole(uint(id), uint(userID))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(role.Externalize()); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 10 - 5
server/middleware/auth.go

@@ -163,6 +163,7 @@ type AccessType string
 
 // The various access types
 const (
+	AdminAccess AccessType = "admin"
 	ReadAccess  AccessType = "read"
 	WriteAccess AccessType = "write"
 )
@@ -221,16 +222,20 @@ func (auth *Auth) DoesUserHaveProjectAccess(
 		// look for the user role in the project
 		for _, role := range proj.Roles {
 			if role.UserID == userID {
-				if accessType == ReadAccess {
-					next.ServeHTTP(w, r)
-					return
-				} else if accessType == WriteAccess {
+				if accessType == AdminAccess {
 					if role.Kind == models.RoleAdmin {
 						next.ServeHTTP(w, r)
 						return
 					}
+				} else if accessType == WriteAccess {
+					if role.Kind == models.RoleAdmin || role.Kind == models.RoleDeveloper {
+						next.ServeHTTP(w, r)
+						return
+					}
+				} else if accessType == ReadAccess {
+					next.ServeHTTP(w, r)
+					return
 				}
-
 			}
 		}
 

+ 74 - 48
server/router/router.go

@@ -254,6 +254,36 @@ func New(a *api.App) *chi.Mux {
 				),
 			)
 
+			r.Method(
+				"GET",
+				"/projects/{project_id}/policy",
+				auth.DoesUserHaveProjectAccess(
+					requestlog.NewHandler(a.HandleReadProjectPolicy, l),
+					mw.URLParam,
+					mw.ReadAccess,
+				),
+			)
+
+			r.Method(
+				"GET",
+				"/projects/{project_id}/roles",
+				auth.DoesUserHaveProjectAccess(
+					requestlog.NewHandler(a.HandleGetProjectRoles, l),
+					mw.URLParam,
+					mw.AdminAccess,
+				),
+			)
+
+			r.Method(
+				"POST",
+				"/projects/{project_id}/roles/{user_id}",
+				auth.DoesUserHaveProjectAccess(
+					requestlog.NewHandler(a.HandleUpdateProjectRole, l),
+					mw.URLParam,
+					mw.AdminAccess,
+				),
+			)
+
 			r.Method(
 				"POST",
 				"/projects",
@@ -268,7 +298,17 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleDeleteProject, l),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.AdminAccess,
+				),
+			)
+
+			r.Method(
+				"DELETE",
+				"/projects/{project_id}/role/{user_id}",
+				auth.DoesUserHaveProjectAccess(
+					requestlog.NewHandler(a.HandleDeleteProjectRole, l),
+					mw.URLParam,
+					mw.AdminAccess,
 				),
 			)
 
@@ -283,7 +323,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -294,7 +334,7 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleCreateInvite, l),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.AdminAccess,
 				),
 			)
 
@@ -304,7 +344,7 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleListProjectInvites, l),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.AdminAccess,
 				),
 			)
 
@@ -326,7 +366,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.AdminAccess,
 				),
 			)
 
@@ -348,7 +388,7 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleProvisionTestInfra, l),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -363,7 +403,7 @@ func New(a *api.App) *chi.Mux {
 						false,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -378,7 +418,7 @@ func New(a *api.App) *chi.Mux {
 						false,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -393,7 +433,7 @@ func New(a *api.App) *chi.Mux {
 						false,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -408,7 +448,7 @@ func New(a *api.App) *chi.Mux {
 						false,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -423,7 +463,7 @@ func New(a *api.App) *chi.Mux {
 						false,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -438,7 +478,7 @@ func New(a *api.App) *chi.Mux {
 						false,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -456,20 +496,6 @@ func New(a *api.App) *chi.Mux {
 				),
 			)
 
-			r.Method(
-				"POST",
-				"/projects/{project_id}/provision/{kind}/{infra_id}/logs",
-				auth.DoesUserHaveProjectAccess(
-					auth.DoesUserHaveInfraAccess(
-						requestlog.NewHandler(a.HandleGetProvisioningLogs, l),
-						mw.URLParam,
-						mw.URLParam,
-					),
-					mw.URLParam,
-					mw.ReadAccess,
-				),
-			)
-
 			r.Method(
 				"POST",
 				"/projects/{project_id}/infra/{infra_id}/ecr/destroy",
@@ -480,7 +506,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -494,7 +520,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -508,7 +534,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -522,7 +548,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -536,7 +562,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -550,7 +576,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -773,7 +799,7 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleListProjectHelmRepos, l),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.ReadAccess,
 				),
 			)
 
@@ -783,7 +809,7 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleListHelmRepoCharts, l),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.ReadAccess,
 				),
 			)
 
@@ -819,7 +845,7 @@ func New(a *api.App) *chi.Mux {
 				auth.DoesUserHaveProjectAccess(
 					requestlog.NewHandler(a.HandleListProjectRegistries, l),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.ReadAccess,
 				),
 			)
 
@@ -833,7 +859,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.ReadAccess,
 				),
 			)
 
@@ -933,7 +959,7 @@ func New(a *api.App) *chi.Mux {
 						mw.URLParam,
 					),
 					mw.URLParam,
-					mw.WriteAccess,
+					mw.ReadAccess,
 				),
 			)
 
@@ -1366,7 +1392,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1394,7 +1420,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1408,7 +1434,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1450,7 +1476,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1478,7 +1504,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1493,7 +1519,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1515,7 +1541,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1529,7 +1555,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 		})
@@ -1548,7 +1574,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1568,7 +1594,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)
 
@@ -1582,7 +1608,7 @@ func New(a *api.App) *chi.Mux {
 						mw.QueryParam,
 					),
 					mw.URLParam,
-					mw.ReadAccess,
+					mw.WriteAccess,
 				),
 			)