Prechádzať zdrojové kódy

full authz testing cases for project

Alexander Belanger 5 rokov pred
rodič
commit
d6183c9d3f

+ 2 - 10
api/server/authz/param_test.go

@@ -9,6 +9,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 )
 )
 
 
@@ -55,16 +56,7 @@ func TestGetURLUintParamsErrors(t *testing.T) {
 		r := httptest.NewRequest("POST", test.route, nil)
 		r := httptest.NewRequest("POST", test.route, nil)
 
 
 		// set the context for testing
 		// set the context for testing
-		rctx := chi.NewRouteContext()
-		routeParams := &chi.RouteParams{}
-
-		for key, val := range test.routeParams {
-			routeParams.Add(key, val)
-		}
-
-		rctx.URLParams = *routeParams
-
-		r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
+		r = apitest.WithURLParams(t, r, test.routeParams)
 
 
 		_, err := authz.GetURLParamUint(r, test.paramReq)
 		_, err := authz.GetURLParamUint(r, test.paramReq)
 
 

+ 118 - 0
api/server/authz/policy.go

@@ -0,0 +1,118 @@
+package authz
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz/policy"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type PolicyMiddleware struct {
+	config       *shared.Config
+	endpointMeta types.APIRequestMetadata
+	loader       policy.PolicyDocumentLoader
+}
+
+func NewPolicyMiddleware(
+	config *shared.Config,
+	endpointMeta types.APIRequestMetadata,
+	loader policy.PolicyDocumentLoader,
+) *PolicyMiddleware {
+	return &PolicyMiddleware{config, endpointMeta, loader}
+}
+
+func (p *PolicyMiddleware) Middleware(next http.Handler) http.Handler {
+	return &PolicyHandler{next, p.config, p.endpointMeta, p.loader}
+}
+
+type PolicyHandler struct {
+	next         http.Handler
+	config       *shared.Config
+	endpointMeta types.APIRequestMetadata
+	loader       policy.PolicyDocumentLoader
+}
+
+func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// get the full map of scopes to resource actions
+	reqScopes, reqErr := getRequestActionForEndpoint(r, h.endpointMeta)
+
+	if reqErr != nil {
+		apierrors.HandleAPIError(w, h.config.Logger, reqErr)
+		return
+	}
+
+	// load policy documents for the user + project
+	projID := reqScopes[types.ProjectScope].Resource.UInt
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	policyDocs, reqErr := h.loader.LoadPolicyDocuments(user.ID, projID)
+
+	if reqErr != nil {
+		apierrors.HandleAPIError(w, h.config.Logger, reqErr)
+		return
+	}
+
+	// validate that the policy permits the action
+	hasAccess := policy.HasScopeAccess(policyDocs, reqScopes)
+
+	if !hasAccess {
+		apierrors.HandleAPIError(
+			w,
+			h.config.Logger,
+			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action for user %d in project %d", user.ID, projID)),
+		)
+
+		return
+	}
+
+	// add the set of resource ids to the request context
+	ctx := NewRequestScopeCtx(r.Context(), reqScopes)
+	r = r.WithContext(ctx)
+	h.next.ServeHTTP(w, r)
+}
+
+const RequestScopeCtxKey = "requestscopes"
+
+func NewRequestScopeCtx(ctx context.Context, reqScopes map[types.PermissionScope]*policy.RequestAction) context.Context {
+	return context.WithValue(ctx, RequestScopeCtxKey, reqScopes)
+}
+
+func getRequestActionForEndpoint(
+	r *http.Request,
+	endpointMeta types.APIRequestMetadata,
+) (res map[types.PermissionScope]*policy.RequestAction, reqErr apierrors.RequestError) {
+	res = make(map[types.PermissionScope]*policy.RequestAction)
+
+	// iterate through scopes, attach policies as needed
+	for _, scope := range endpointMeta.Scopes {
+		// find the resource ID and create the resource
+		resource := types.NameOrUInt{}
+
+		switch scope {
+		case types.ProjectScope:
+			resource.UInt, reqErr = GetURLParamUint(r, string(types.URLParamProjectID))
+		case types.ClusterScope:
+			resource.UInt, reqErr = GetURLParamUint(r, string(types.URLParamClusterID))
+		case types.NamespaceScope:
+			resource.Name, reqErr = GetURLParamString(r, string(types.URLParamNamespace))
+		case types.ApplicationScope:
+			resource.Name, reqErr = GetURLParamString(r, string(types.URLParamApplication))
+		}
+
+		if reqErr != nil {
+			return nil, reqErr
+		}
+
+		res[scope] = &policy.RequestAction{
+			Verb:     endpointMeta.Verb,
+			Resource: resource,
+		}
+	}
+
+	return res, nil
+}

+ 2 - 6
api/server/authz/policy/policy.go

@@ -1,8 +1,6 @@
 package policy
 package policy
 
 
 import (
 import (
-	"fmt"
-
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 )
 )
 
 
@@ -15,7 +13,7 @@ type RequestAction struct {
 // resource (`resource+scope`) according to a `policy`.
 // resource (`resource+scope`) according to a `policy`.
 func HasScopeAccess(
 func HasScopeAccess(
 	policy []*types.PolicyDocument,
 	policy []*types.PolicyDocument,
-	reqScopes map[types.PermissionScope]RequestAction,
+	reqScopes map[types.PermissionScope]*RequestAction,
 ) bool {
 ) bool {
 	// iterate through policy documents until a match is found
 	// iterate through policy documents until a match is found
 	for _, policyDoc := range policy {
 	for _, policyDoc := range policy {
@@ -96,7 +94,7 @@ func populateAndVerifyPolicyDocument(
 	tree types.ScopeTree,
 	tree types.ScopeTree,
 	currScope types.PermissionScope,
 	currScope types.PermissionScope,
 	parentVerbs []types.APIVerb,
 	parentVerbs []types.APIVerb,
-	reqScopes map[types.PermissionScope]RequestAction,
+	reqScopes map[types.PermissionScope]*RequestAction,
 	currMatchDocs map[types.PermissionScope]*types.PolicyDocument,
 	currMatchDocs map[types.PermissionScope]*types.PolicyDocument,
 ) (ok bool, matchDocs map[types.PermissionScope]*types.PolicyDocument) {
 ) (ok bool, matchDocs map[types.PermissionScope]*types.PolicyDocument) {
 	if currMatchDocs == nil {
 	if currMatchDocs == nil {
@@ -118,8 +116,6 @@ func populateAndVerifyPolicyDocument(
 
 
 	subTree, ok := tree[currDoc.Scope]
 	subTree, ok := tree[currDoc.Scope]
 
 
-	fmt.Println(currDoc.Scope, tree, currDoc.Scope, currScope)
-
 	if !ok || currDoc.Scope != currScope {
 	if !ok || currDoc.Scope != currScope {
 		return false, matchDocs
 		return false, matchDocs
 	}
 	}

+ 14 - 14
api/server/authz/policy/policy_test.go

@@ -11,7 +11,7 @@ import (
 type testHasScopeAccess struct {
 type testHasScopeAccess struct {
 	description string
 	description string
 	policy      []*types.PolicyDocument
 	policy      []*types.PolicyDocument
-	reqScopes   map[types.PermissionScope]policy.RequestAction
+	reqScopes   map[types.PermissionScope]*policy.RequestAction
 	expRes      bool
 	expRes      bool
 }
 }
 
 
@@ -19,7 +19,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "admin access to project",
 		description: "admin access to project",
 		policy:      policy.AdminPolicy,
 		policy:      policy.AdminPolicy,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ProjectScope: {
 			types.ProjectScope: {
 				Verb: types.APIVerbGet,
 				Verb: types.APIVerbGet,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -32,7 +32,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "viewer access cannot perform write operation",
 		description: "viewer access cannot perform write operation",
 		policy:      policy.ViewerPolicy,
 		policy:      policy.ViewerPolicy,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ClusterScope: {
 			types.ClusterScope: {
 				Verb: types.APIVerbCreate,
 				Verb: types.APIVerbCreate,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -45,7 +45,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "developer access cannot write settings",
 		description: "developer access cannot write settings",
 		policy:      policy.DeveloperPolicy,
 		policy:      policy.DeveloperPolicy,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.SettingsScope: {
 			types.SettingsScope: {
 				Verb: types.APIVerbUpdate,
 				Verb: types.APIVerbUpdate,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -58,7 +58,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "custom policy for cluster 1 can write cluster 1",
 		description: "custom policy for cluster 1 can write cluster 1",
 		policy:      testPolicySpecificClusters,
 		policy:      testPolicySpecificClusters,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ClusterScope: {
 			types.ClusterScope: {
 				Verb: types.APIVerbUpdate,
 				Verb: types.APIVerbUpdate,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -71,7 +71,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "custom policy for cluster 1 cannot write cluster 2",
 		description: "custom policy for cluster 1 cannot write cluster 2",
 		policy:      testPolicySpecificClusters,
 		policy:      testPolicySpecificClusters,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ClusterScope: {
 			types.ClusterScope: {
 				Verb: types.APIVerbUpdate,
 				Verb: types.APIVerbUpdate,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -84,7 +84,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "cannot access wrong namespace + cluster combination",
 		description: "cannot access wrong namespace + cluster combination",
 		policy:      testPolicyNamespaceSpecific,
 		policy:      testPolicyNamespaceSpecific,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ClusterScope: {
 			types.ClusterScope: {
 				Verb: types.APIVerbGet,
 				Verb: types.APIVerbGet,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -103,7 +103,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "can access set namespace + cluster combination",
 		description: "can access set namespace + cluster combination",
 		policy:      testPolicyNamespaceSpecific,
 		policy:      testPolicyNamespaceSpecific,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ClusterScope: {
 			types.ClusterScope: {
 				Verb: types.APIVerbGet,
 				Verb: types.APIVerbGet,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -122,7 +122,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "cannot write the set namespace + cluster combination",
 		description: "cannot write the set namespace + cluster combination",
 		policy:      testPolicyNamespaceSpecific,
 		policy:      testPolicyNamespaceSpecific,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ClusterScope: {
 			types.ClusterScope: {
 				Verb: types.APIVerbGet,
 				Verb: types.APIVerbGet,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -141,7 +141,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "test invalid policy document",
 		description: "test invalid policy document",
 		policy:      testInvalidPolicyDocument,
 		policy:      testInvalidPolicyDocument,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ProjectScope: {
 			types.ProjectScope: {
 				Verb: types.APIVerbGet,
 				Verb: types.APIVerbGet,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -154,7 +154,7 @@ var hasScopeAccessTests = []testHasScopeAccess{
 	{
 	{
 		description: "test invalid policy document nested",
 		description: "test invalid policy document nested",
 		policy:      testInvalidPolicyDocumentNested,
 		policy:      testInvalidPolicyDocumentNested,
-		reqScopes: map[types.PermissionScope]policy.RequestAction{
+		reqScopes: map[types.PermissionScope]*policy.RequestAction{
 			types.ProjectScope: {
 			types.ProjectScope: {
 				Verb: types.APIVerbGet,
 				Verb: types.APIVerbGet,
 				Resource: types.NameOrUInt{
 				Resource: types.NameOrUInt{
@@ -183,7 +183,7 @@ func BenchmarkSimpleHasScopeAccess(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
 		res := policy.HasScopeAccess(
 		res := policy.HasScopeAccess(
 			testPolicySpecificClusters,
 			testPolicySpecificClusters,
-			map[types.PermissionScope]policy.RequestAction{
+			map[types.PermissionScope]*policy.RequestAction{
 				types.ClusterScope: {
 				types.ClusterScope: {
 					Verb: types.APIVerbCreate,
 					Verb: types.APIVerbCreate,
 					Resource: types.NameOrUInt{
 					Resource: types.NameOrUInt{
@@ -313,8 +313,8 @@ var testInvalidPolicyDocumentNested = []*types.PolicyDocument{
 					},
 					},
 				},
 				},
 				Children: map[types.PermissionScope]*types.PolicyDocument{
 				Children: map[types.PermissionScope]*types.PolicyDocument{
-					types.ReleaseScope: {
-						Scope: types.ReleaseScope,
+					types.ApplicationScope: {
+						Scope: types.ApplicationScope,
 						Verbs: types.ReadWriteVerbGroup(),
 						Verbs: types.ReadWriteVerbGroup(),
 						Resources: []types.NameOrUInt{
 						Resources: []types.NameOrUInt{
 							{
 							{

+ 305 - 0
api/server/authz/policy_test.go

@@ -0,0 +1,305 @@
+package authz_test
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/authz/policy"
+	"github.com/porter-dev/porter/api/server/handlers/project"
+	"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/apitest"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPolicyMiddlewareSuccessfulProjectCluster(t *testing.T) {
+	config, handler, next := loadHandlers(t, types.APIRequestMetadata{
+		Verb:   types.APIVerbCreate,
+		Method: types.HTTPVerbPost,
+		Scopes: []types.PermissionScope{
+			types.ProjectScope,
+			types.ClusterScope,
+		},
+	}, false, false)
+
+	user := apitest.CreateTestUser(t, config)
+	_, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1/clusters/1", nil)
+
+	req = apitest.WithURLParams(t, req, map[string]string{
+		"project_id": "1",
+		"cluster_id": "1",
+	})
+
+	req = apitest.WithAuthenticatedUser(t, req, user)
+
+	handler.ServeHTTP(rr, req)
+
+	assertNextHandlerCalled(t, next, rr, map[types.PermissionScope]*policy.RequestAction{
+		types.ProjectScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				UInt: 1,
+			},
+		},
+		types.ClusterScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				UInt: 1,
+			},
+		},
+	})
+}
+
+func TestPolicyMiddlewareSuccessfulApplication(t *testing.T) {
+	config, handler, next := loadHandlers(t, types.APIRequestMetadata{
+		Verb:   types.APIVerbCreate,
+		Method: types.HTTPVerbPost,
+		Scopes: []types.PermissionScope{
+			types.ProjectScope,
+			types.ClusterScope,
+			types.NamespaceScope,
+			types.ApplicationScope,
+		},
+	}, false, false)
+
+	user := apitest.CreateTestUser(t, config)
+	_, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/projects/1/clusters/1/default/app-1",
+		nil,
+	)
+
+	req = apitest.WithURLParams(t, req, map[string]string{
+		"project_id":  "1",
+		"cluster_id":  "1",
+		"namespace":   "default",
+		"application": "app-1",
+	})
+
+	req = apitest.WithAuthenticatedUser(t, req, user)
+
+	handler.ServeHTTP(rr, req)
+
+	assertNextHandlerCalled(t, next, rr, map[types.PermissionScope]*policy.RequestAction{
+		types.ProjectScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				UInt: 1,
+			},
+		},
+		types.ClusterScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				UInt: 1,
+			},
+		},
+		types.NamespaceScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				Name: "default",
+			},
+		},
+		types.ApplicationScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				Name: "app-1",
+			},
+		},
+	})
+}
+
+func TestPolicyMiddlewareInvalidPermissions(t *testing.T) {
+	config, handler, next := loadHandlers(t, types.APIRequestMetadata{
+		Verb:   types.APIVerbCreate,
+		Method: types.HTTPVerbPost,
+		Scopes: []types.PermissionScope{
+			types.ProjectScope,
+			types.ClusterScope,
+		},
+	}, false, true)
+
+	user := apitest.CreateTestUser(t, config)
+	_, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1/clusters/1", nil)
+
+	req = apitest.WithURLParams(t, req, map[string]string{
+		"project_id": "1",
+		"cluster_id": "1",
+	})
+
+	req = apitest.WithAuthenticatedUser(t, req, user)
+
+	handler.ServeHTTP(rr, req)
+
+	assert.False(t, next.WasCalled, "next handler should not have been called")
+	apitest.AssertResponseForbidden(t, rr)
+}
+
+func TestPolicyMiddlewareFailInvalidLoader(t *testing.T) {
+	config, handler, next := loadHandlers(t, types.APIRequestMetadata{
+		Verb:   types.APIVerbCreate,
+		Method: types.HTTPVerbPost,
+		Scopes: []types.PermissionScope{
+			types.ProjectScope,
+			types.ClusterScope,
+		},
+	}, true, false)
+
+	user := apitest.CreateTestUser(t, config)
+	_, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1/clusters/1", nil)
+
+	req = apitest.WithURLParams(t, req, map[string]string{
+		"project_id": "1",
+		"cluster_id": "1",
+	})
+
+	req = apitest.WithAuthenticatedUser(t, req, user)
+
+	handler.ServeHTTP(rr, req)
+
+	assertInternalError(t, next, rr)
+}
+
+func TestPolicyMiddlewareFailBadParam(t *testing.T) {
+	config, handler, next := loadHandlers(t, types.APIRequestMetadata{
+		Verb:   types.APIVerbCreate,
+		Method: types.HTTPVerbPost,
+		Scopes: []types.PermissionScope{
+			types.ProjectScope,
+			types.ClusterScope,
+		},
+	}, true, false)
+
+	user := apitest.CreateTestUser(t, config)
+	_, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1/clusters/1", nil)
+
+	req = apitest.WithURLParams(t, req, map[string]string{
+		"project_id": "notuint",
+		"cluster_id": "1",
+	})
+
+	req = apitest.WithAuthenticatedUser(t, req, user)
+
+	handler.ServeHTTP(rr, req)
+
+	assert.False(t, next.WasCalled, "next handler should not have been called")
+	apitest.AssertResponseError(t, rr, http.StatusBadRequest, &types.ExternalError{
+		Error: fmt.Sprintf("could not convert url parameter %s to uint, got %s", "project_id", "notuint"),
+	})
+}
+
+func loadHandlers(
+	t *testing.T,
+	endpointMeta types.APIRequestMetadata,
+	shouldLoaderFail bool,
+	shouldLoaderLoadViewer bool,
+) (*shared.Config, http.Handler, *testHandler) {
+	config := apitest.LoadConfig(t)
+	var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
+
+	if shouldLoaderFail {
+		loader = &failingDocLoader{}
+	}
+
+	if shouldLoaderLoadViewer {
+		loader = &viewerDocLoader{}
+	}
+
+	mwFactory := authz.NewPolicyMiddleware(config, endpointMeta, loader)
+
+	next := &testHandler{}
+	handler := mwFactory.Middleware(next)
+
+	return config, handler, next
+}
+
+type failingDocLoader struct{}
+
+func (f *failingDocLoader) LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError) {
+	return nil, apierrors.NewErrInternal(fmt.Errorf("new error internal"))
+}
+
+type viewerDocLoader struct{}
+
+func (f *viewerDocLoader) LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError) {
+	return policy.ViewerPolicy, nil
+}
+
+type testHandler struct {
+	WasCalled bool
+	ReqScopes map[types.PermissionScope]*policy.RequestAction
+}
+
+func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	t.WasCalled = true
+
+	t.ReqScopes, _ = r.Context().Value(authz.RequestScopeCtxKey).(map[types.PermissionScope]*policy.RequestAction)
+}
+
+func assertNextHandlerCalled(
+	t *testing.T,
+	next *testHandler,
+	rr *httptest.ResponseRecorder,
+	expScopes map[types.PermissionScope]*policy.RequestAction,
+) {
+	// make sure the handler was called with the expected user, and resulted in 200 OK
+	assert := assert.New(t)
+
+	assert.True(next.WasCalled, "next handler should have been called")
+	assert.Equal(expScopes, next.ReqScopes, "expected scopes should be equal")
+	assert.Equal(http.StatusOK, rr.Result().StatusCode, "status code should be ok")
+}
+
+func assertInternalError(t *testing.T, next *testHandler, rr *httptest.ResponseRecorder) {
+	assert := assert.New(t)
+
+	// first assert that that the next middleware was not called
+	assert.False(next.WasCalled, "next handler should not have been called")
+
+	apitest.AssertResponseInternalServerError(t, rr)
+}

+ 22 - 26
api/server/authz/project.go

@@ -4,54 +4,50 @@ import (
 	"context"
 	"context"
 	"net/http"
 	"net/http"
 
 
+	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/shared"
 	"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/apierrors"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/models"
 )
 )
 
 
 type ProjectScopedFactory struct {
 type ProjectScopedFactory struct {
-	projectRepo repository.ProjectRepository
-	config      *shared.Config
+	config *shared.Config
 }
 }
 
 
 func NewProjectScopedFactory(
 func NewProjectScopedFactory(
-	projectRepo repository.ProjectRepository,
 	config *shared.Config,
 	config *shared.Config,
 ) *ProjectScopedFactory {
 ) *ProjectScopedFactory {
-	return &ProjectScopedFactory{projectRepo, config}
+	return &ProjectScopedFactory{config}
 }
 }
 
 
-func (f *ProjectScopedFactory) NewProjectScoped(next http.Handler) http.Handler {
-	return &ProjectScoped{next, f.projectRepo, f.config}
+func (p *ProjectScopedFactory) Middleware(next http.Handler) http.Handler {
+	return &ProjectScopedMiddleware{next, p.config}
 }
 }
 
 
-type ProjectScoped struct {
-	next        http.Handler
-	projectRepo repository.ProjectRepository
-	config      *shared.Config
+type ProjectScopedMiddleware struct {
+	next   http.Handler
+	config *shared.Config
 }
 }
 
 
-func (scope *ProjectScoped) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the project id from the request
-	_, reqErr := GetURLParamUint(r, "project_id")
+func (p *ProjectScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// get the project id from the URL param context
+	reqScopes, _ := r.Context().Value(RequestScopeCtxKey).(map[types.PermissionScope]*policy.RequestAction)
 
 
-	if reqErr != nil {
-		apierrors.HandleAPIError(w, scope.config.Logger, reqErr)
-		return
-	}
-
-	// find a set of roles for this user and compute a policy document
+	projID := reqScopes[types.ProjectScope].Resource.UInt
 
 
-	// determine if policy document allows for project scope
+	project, err := p.config.Repo.Project().ReadProject(projID)
 
 
-	project := types.Project{}
+	if err != nil {
+		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
 
 
-	// create a new project-scoped context and serve
-	r = r.WithContext(NewProjectContext(r.Context(), project))
-	scope.next.ServeHTTP(w, r)
+	ctx := NewProjectContext(r.Context(), project)
+	r = r.WithContext(ctx)
+	p.next.ServeHTTP(w, r)
 }
 }
 
 
-func NewProjectContext(ctx context.Context, project types.Project) context.Context {
+func NewProjectContext(ctx context.Context, project *models.Project) context.Context {
 	return context.WithValue(ctx, types.ProjectScope, project)
 	return context.WithValue(ctx, types.ProjectScope, project)
 }
 }

+ 98 - 0
api/server/authz/project_test.go

@@ -0,0 +1,98 @@
+package authz_test
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/authz/policy"
+	"github.com/porter-dev/porter/api/server/handlers/project"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository/test"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProjectMiddlewareSuccessful(t *testing.T) {
+	config, handler, next := loadProjectHandlers(t)
+
+	user := apitest.CreateTestUser(t, config)
+	proj, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1", nil)
+	req = apitest.WithAuthenticatedUser(t, req, user)
+	req = apitest.WithRequestScopes(t, req, map[types.PermissionScope]*policy.RequestAction{
+		types.ProjectScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				UInt: 1,
+			},
+		},
+	})
+
+	handler.ServeHTTP(rr, req)
+	assert.True(t, next.WasCalled, "next handler should have been called")
+	assert.Equal(t, proj, next.Project, "project should be equal")
+}
+
+func TestProjectMiddlewareFailedRead(t *testing.T) {
+	config, _, _ := loadProjectHandlers(t)
+
+	user := apitest.CreateTestUser(t, config)
+	_, err := project.CreateProjectWithUser(config, &models.Project{
+		Name: "test-project",
+	}, user)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	config, handler, next := loadProjectHandlers(t, test.ReadProjectMethod)
+
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1", nil)
+	req = apitest.WithAuthenticatedUser(t, req, user)
+	req = apitest.WithRequestScopes(t, req, map[types.PermissionScope]*policy.RequestAction{
+		types.ProjectScope: {
+			Verb: types.APIVerbCreate,
+			Resource: types.NameOrUInt{
+				UInt: 1,
+			},
+		},
+	})
+
+	handler.ServeHTTP(rr, req)
+	assert.False(t, next.WasCalled, "next handler should not have been called")
+	apitest.AssertResponseInternalServerError(t, rr)
+}
+
+func loadProjectHandlers(
+	t *testing.T,
+	failingRepoMethods ...string,
+) (*shared.Config, http.Handler, *testProjectHandler) {
+	config := apitest.LoadConfig(t, failingRepoMethods...)
+	mwFactory := authz.NewProjectScopedFactory(config)
+
+	next := &testProjectHandler{}
+	handler := mwFactory.Middleware(next)
+
+	return config, handler, next
+}
+
+type testProjectHandler struct {
+	WasCalled bool
+	Project   *models.Project
+}
+
+func (t *testProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	t.WasCalled = true
+
+	t.Project, _ = r.Context().Value(types.ProjectScope).(*models.Project)
+}

+ 40 - 15
api/server/handlers/project/create_test.go

@@ -11,9 +11,14 @@ import (
 )
 )
 
 
 func TestCreateProjectSuccessful(t *testing.T) {
 func TestCreateProjectSuccessful(t *testing.T) {
-	req, rr := apitest.GetRequestAndRecorder(t, &types.CreateProjectRequest{
-		Name: "test-project",
-	})
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/projects",
+		&types.CreateProjectRequest{
+			Name: "test-project",
+		},
+	)
 
 
 	config := apitest.LoadConfig(t)
 	config := apitest.LoadConfig(t)
 	user := apitest.CreateTestUser(t, config)
 	user := apitest.CreateTestUser(t, config)
@@ -45,9 +50,14 @@ func TestCreateProjectSuccessful(t *testing.T) {
 }
 }
 
 
 func TestFailingDecoderValidator(t *testing.T) {
 func TestFailingDecoderValidator(t *testing.T) {
-	req, rr := apitest.GetRequestAndRecorder(t, &types.CreateProjectRequest{
-		Name: "test-project",
-	})
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/projects",
+		&types.CreateProjectRequest{
+			Name: "test-project",
+		},
+	)
 
 
 	config := apitest.LoadConfig(t)
 	config := apitest.LoadConfig(t)
 	user := apitest.CreateTestUser(t, config)
 	user := apitest.CreateTestUser(t, config)
@@ -65,9 +75,14 @@ func TestFailingDecoderValidator(t *testing.T) {
 }
 }
 
 
 func TestFailingCreateMethod(t *testing.T) {
 func TestFailingCreateMethod(t *testing.T) {
-	req, rr := apitest.GetRequestAndRecorder(t, &types.CreateProjectRequest{
-		Name: "test-project",
-	})
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/projects",
+		&types.CreateProjectRequest{
+			Name: "test-project",
+		},
+	)
 
 
 	config := apitest.LoadConfig(t, test.CreateProjectMethod)
 	config := apitest.LoadConfig(t, test.CreateProjectMethod)
 	user := apitest.CreateTestUser(t, config)
 	user := apitest.CreateTestUser(t, config)
@@ -85,9 +100,14 @@ func TestFailingCreateMethod(t *testing.T) {
 }
 }
 
 
 func TestFailingCreateRoleMethod(t *testing.T) {
 func TestFailingCreateRoleMethod(t *testing.T) {
-	req, rr := apitest.GetRequestAndRecorder(t, &types.CreateProjectRequest{
-		Name: "test-project",
-	})
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/projects",
+		&types.CreateProjectRequest{
+			Name: "test-project",
+		},
+	)
 
 
 	config := apitest.LoadConfig(t, test.CreateProjectRoleMethod)
 	config := apitest.LoadConfig(t, test.CreateProjectRoleMethod)
 	user := apitest.CreateTestUser(t, config)
 	user := apitest.CreateTestUser(t, config)
@@ -105,9 +125,14 @@ func TestFailingCreateRoleMethod(t *testing.T) {
 }
 }
 
 
 func TestFailingReadMethod(t *testing.T) {
 func TestFailingReadMethod(t *testing.T) {
-	req, rr := apitest.GetRequestAndRecorder(t, &types.CreateProjectRequest{
-		Name: "test-project",
-	})
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/projects",
+		&types.CreateProjectRequest{
+			Name: "test-project",
+		},
+	)
 
 
 	config := apitest.LoadConfig(t, test.ReadProjectMethod)
 	config := apitest.LoadConfig(t, test.ReadProjectMethod)
 	user := apitest.CreateTestUser(t, config)
 	user := apitest.CreateTestUser(t, config)

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

@@ -22,7 +22,7 @@ func TestGetProjectSuccessful(t *testing.T) {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 
 
-	req, rr := apitest.GetRequestAndRecorder(t, nil)
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbPost), "/api/projects/1", nil)
 
 
 	req = apitest.WithAuthenticatedUser(t, req, user)
 	req = apitest.WithAuthenticatedUser(t, req, user)
 	req = apitest.WithProject(t, req, proj)
 	req = apitest.WithProject(t, req, proj)

+ 7 - 4
api/server/handlers/project/list_test.go

@@ -31,7 +31,7 @@ func TestListProjectsSuccessful(t *testing.T) {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 
 
-	req, rr := apitest.GetRequestAndRecorder(t, nil)
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbGet), "/api/projects", nil)
 
 
 	req = apitest.WithAuthenticatedUser(t, req, user)
 	req = apitest.WithAuthenticatedUser(t, req, user)
 
 
@@ -52,9 +52,12 @@ func TestListProjectsSuccessful(t *testing.T) {
 }
 }
 
 
 func TestFailingListMethod(t *testing.T) {
 func TestFailingListMethod(t *testing.T) {
-	req, rr := apitest.GetRequestAndRecorder(t, &types.CreateProjectRequest{
-		Name: "test-project",
-	})
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbGet),
+		"/api/projects",
+		nil,
+	)
 
 
 	config := apitest.LoadConfig(t, test.ListProjectsByUserIDMethod)
 	config := apitest.LoadConfig(t, test.ListProjectsByUserIDMethod)
 	user := apitest.CreateTestUser(t, config)
 	user := apitest.CreateTestUser(t, config)

+ 24 - 20
api/server/router/project.go

@@ -2,7 +2,6 @@ package router
 
 
 import (
 import (
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
-	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -10,44 +9,50 @@ import (
 
 
 func NewProjectScopedRegisterer(children ...*Registerer) *Registerer {
 func NewProjectScopedRegisterer(children ...*Registerer) *Registerer {
 	return &Registerer{
 	return &Registerer{
-		Func:     RegisterProjectScopedRoutes,
-		Children: children,
+		GetRoutes: GetProjectScopedRoutes,
+		Children:  children,
 	}
 	}
 }
 }
 
 
-func RegisterProjectScopedRoutes(
+func GetProjectScopedRoutes(
 	r chi.Router,
 	r chi.Router,
 	config *shared.Config,
 	config *shared.Config,
 	basePath *types.Path,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
 	children ...*Registerer,
-) chi.Router {
-	// Create a new "project-scoped" factory which will create a new project-scoped request
-	// after authorization. Each subsequent http.Handler can lookup the project in context.
-	projFactory := authz.NewProjectScopedFactory(config.Repo.Project(), config)
-
-	// attach middleware to router
-	r.Use(projFactory.NewProjectScoped)
-
-	projPath := registerProjectEndpoints(r, config, basePath, factory)
+) []*Route {
+	routes, projPath := getProjectRoutes(r, config, basePath, factory)
 
 
 	if len(children) > 0 {
 	if len(children) > 0 {
 		r.Route(projPath.RelativePath, func(r chi.Router) {
 		r.Route(projPath.RelativePath, func(r chi.Router) {
 			for _, child := range children {
 			for _, child := range children {
-				child.Func(r, config, basePath, factory, child.Children...)
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
 			}
 			}
 		})
 		})
 	}
 	}
 
 
-	return r
+	// // Note that we can place the middleware r.Use below
+
+	// // all project-scoped routes
+
+	// // Create a new "project-scoped" factory which will create a new project-scoped request
+	// // after authorization. Each subsequent http.Handler can lookup the project in context.
+	// projFactory := authz.NewProjectScopedFactory(config)
+
+	// // attach middleware to router
+	// r.Use(projFactory.Middleware)
+
+	return routes
 }
 }
 
 
-func registerProjectEndpoints(
+func getProjectRoutes(
 	r chi.Router,
 	r chi.Router,
 	config *shared.Config,
 	config *shared.Config,
 	basePath *types.Path,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	factory shared.APIEndpointFactory,
-) *types.Path {
+) ([]*Route, *types.Path) {
 	relPath := "/projects/{project_id}"
 	relPath := "/projects/{project_id}"
 
 
 	newPath := &types.Path{
 	newPath := &types.Path{
@@ -77,9 +82,8 @@ func registerProjectEndpoints(
 	routes = append(routes, &Route{
 	routes = append(routes, &Route{
 		Endpoint: getEndpoint,
 		Endpoint: getEndpoint,
 		Handler:  createHandler,
 		Handler:  createHandler,
+		Router:   r,
 	})
 	})
 
 
-	registerRoutes(r, routes)
-
-	return newPath
+	return routes, newPath
 }
 }

+ 8 - 5
api/server/router/router.go

@@ -16,7 +16,7 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 	userRegisterer := NewUserScopedRegisterer(projRegisterer)
 	userRegisterer := NewUserScopedRegisterer(projRegisterer)
 
 
 	r.Route("/api", func(r chi.Router) {
 	r.Route("/api", func(r chi.Router) {
-		userRegisterer.Func(
+		userRoutes := userRegisterer.GetRoutes(
 			r,
 			r,
 			config,
 			config,
 			&types.Path{
 			&types.Path{
@@ -25,6 +25,8 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 			endpointFactory,
 			endpointFactory,
 			userRegisterer.Children...,
 			userRegisterer.Children...,
 		)
 		)
+
+		registerRoutes(userRoutes)
 	})
 	})
 
 
 	return r
 	return r
@@ -33,23 +35,24 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 type Route struct {
 type Route struct {
 	Endpoint *shared.APIEndpoint
 	Endpoint *shared.APIEndpoint
 	Handler  http.Handler
 	Handler  http.Handler
+	Router   chi.Router
 }
 }
 
 
 type Registerer struct {
 type Registerer struct {
-	Func func(
+	GetRoutes func(
 		r chi.Router,
 		r chi.Router,
 		config *shared.Config,
 		config *shared.Config,
 		basePath *types.Path,
 		basePath *types.Path,
 		factory shared.APIEndpointFactory,
 		factory shared.APIEndpointFactory,
 		children ...*Registerer,
 		children ...*Registerer,
-	) chi.Router
+	) []*Route
 
 
 	Children []*Registerer
 	Children []*Registerer
 }
 }
 
 
-func registerRoutes(r chi.Router, routes []*Route) {
+func registerRoutes(routes []*Route) {
 	for _, route := range routes {
 	for _, route := range routes {
-		r.Method(
+		route.Router.Method(
 			string(route.Endpoint.Metadata.Method),
 			string(route.Endpoint.Metadata.Method),
 			route.Endpoint.Metadata.Path.RelativePath,
 			route.Endpoint.Metadata.Path.RelativePath,
 			route.Handler,
 			route.Handler,

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

@@ -13,7 +13,7 @@ import (
 func TestRouter(t *testing.T) {
 func TestRouter(t *testing.T) {
 	walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
 	walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
 		route = strings.Replace(route, "/*/", "/", -1)
 		route = strings.Replace(route, "/*/", "/", -1)
-		t.Errorf("%s %s\n", method, route)
+		t.Errorf("%s %s %d\n", method, route, len(middlewares))
 		return nil
 		return nil
 	}
 	}
 
 

+ 19 - 16
api/server/router/user.go

@@ -2,7 +2,6 @@ package router
 
 
 import (
 import (
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
-	"github.com/porter-dev/porter/api/server/authn"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -10,42 +9,44 @@ import (
 
 
 func NewUserScopedRegisterer(children ...*Registerer) *Registerer {
 func NewUserScopedRegisterer(children ...*Registerer) *Registerer {
 	return &Registerer{
 	return &Registerer{
-		Func:     RegisterUserScopedRoutes,
-		Children: children,
+		GetRoutes: GetUserScopedRoutes,
+		Children:  children,
 	}
 	}
 }
 }
 
 
-func RegisterUserScopedRoutes(
+func GetUserScopedRoutes(
 	r chi.Router,
 	r chi.Router,
 	config *shared.Config,
 	config *shared.Config,
 	basePath *types.Path,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
 	children ...*Registerer,
-) chi.Router {
-	// Create a new "user-scoped" factory which will create a new user-scoped request
-	// after authentication. Each subsequent http.Handler can lookup the user in context.
-	authNFactory := authn.NewAuthNFactory(config)
+) []*Route {
+	// // Create a new "user-scoped" factory which will create a new user-scoped request
+	// // after authentication. Each subsequent http.Handler can lookup the user in context.
+	// authNFactory := authn.NewAuthNFactory(config)
 
 
-	// attach middleware to router
-	r.Use(authNFactory.NewAuthenticated)
+	// // attach middleware to router
+	// r.Use(authNFactory.NewAuthenticated)
 
 
-	registerUserRoutes(r, config, basePath, factory)
+	routes := getUserRoutes(r, config, basePath, factory)
 
 
 	for _, child := range children {
 	for _, child := range children {
 		r.Group(func(r chi.Router) {
 		r.Group(func(r chi.Router) {
-			child.Func(r, config, basePath, factory, child.Children...)
+			childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+			routes = append(routes, childRoutes...)
 		})
 		})
 	}
 	}
 
 
-	return r
+	return routes
 }
 }
 
 
-func registerUserRoutes(
+func getUserRoutes(
 	r chi.Router,
 	r chi.Router,
 	config *shared.Config,
 	config *shared.Config,
 	basePath *types.Path,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	factory shared.APIEndpointFactory,
-) {
+) []*Route {
 	routes := make([]*Route, 0)
 	routes := make([]*Route, 0)
 
 
 	// POST /api/projects -> project.NewProjectCreateHandler
 	// POST /api/projects -> project.NewProjectCreateHandler
@@ -69,6 +70,7 @@ func registerUserRoutes(
 	routes = append(routes, &Route{
 	routes = append(routes, &Route{
 		Endpoint: createEndpoint,
 		Endpoint: createEndpoint,
 		Handler:  createHandler,
 		Handler:  createHandler,
+		Router:   r,
 	})
 	})
 
 
 	// GET /api/projects -> project.NewProjectListHandler
 	// GET /api/projects -> project.NewProjectListHandler
@@ -91,7 +93,8 @@ func registerUserRoutes(
 	routes = append(routes, &Route{
 	routes = append(routes, &Route{
 		Endpoint: listEndpoint,
 		Endpoint: listEndpoint,
 		Handler:  listHandler,
 		Handler:  listHandler,
+		Router:   r,
 	})
 	})
 
 
-	registerRoutes(r, routes)
+	return routes
 }
 }

+ 10 - 0
api/server/shared/apitest/authz.go

@@ -5,6 +5,8 @@ import (
 	"net/http"
 	"net/http"
 	"testing"
 	"testing"
 
 
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/authz/policy"
 	"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"
 )
 )
@@ -16,3 +18,11 @@ func WithProject(t *testing.T, req *http.Request, proj *models.Project) *http.Re
 
 
 	return req
 	return req
 }
 }
+
+func WithRequestScopes(t *testing.T, req *http.Request, reqScopes map[types.PermissionScope]*policy.RequestAction) *http.Request {
+	ctx := req.Context()
+	ctx = authz.NewRequestScopeCtx(ctx, reqScopes)
+	req = req.WithContext(ctx)
+
+	return req
+}

+ 19 - 2
api/server/shared/apitest/request.go

@@ -1,6 +1,7 @@
 package apitest
 package apitest
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
@@ -9,11 +10,12 @@ import (
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 
 
+	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/shared"
 	"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/apierrors"
 )
 )
 
 
-func GetRequestAndRecorder(t *testing.T, requestObj interface{}) (*http.Request, *httptest.ResponseRecorder) {
+func GetRequestAndRecorder(t *testing.T, method, route string, requestObj interface{}) (*http.Request, *httptest.ResponseRecorder) {
 	var reader io.Reader = nil
 	var reader io.Reader = nil
 
 
 	if requestObj != nil {
 	if requestObj != nil {
@@ -27,7 +29,7 @@ func GetRequestAndRecorder(t *testing.T, requestObj interface{}) (*http.Request,
 	}
 	}
 
 
 	// method and route don't actually matter since this is meant to test handlers
 	// method and route don't actually matter since this is meant to test handlers
-	req, err := http.NewRequest("POST", "/fake-route", reader)
+	req, err := http.NewRequest(method, route, reader)
 
 
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
@@ -38,6 +40,21 @@ func GetRequestAndRecorder(t *testing.T, requestObj interface{}) (*http.Request,
 	return req, rr
 	return req, rr
 }
 }
 
 
+func WithURLParams(t *testing.T, req *http.Request, params map[string]string) *http.Request {
+	rctx := chi.NewRouteContext()
+	routeParams := &chi.RouteParams{}
+
+	for key, val := range params {
+		routeParams.Add(key, val)
+	}
+
+	rctx.URLParams = *routeParams
+
+	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
+
+	return req
+}
+
 type failingDecoderValidator struct {
 type failingDecoderValidator struct {
 	config *shared.Config
 	config *shared.Config
 }
 }

+ 12 - 0
api/server/shared/apitest/response.go

@@ -56,3 +56,15 @@ func AssertResponseInternalServerError(t *testing.T, rr *httptest.ResponseRecord
 	assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode, "status code should be internal server error")
 	assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode, "status code should be internal server error")
 	assert.Equal(t, expReqErr, reqErr, "body should be internal server error")
 	assert.Equal(t, expReqErr, reqErr, "body should be internal server error")
 }
 }
+
+func AssertResponseError(t *testing.T, rr *httptest.ResponseRecorder, statusCode int, expReqErr *types.ExternalError) {
+	reqErr := &types.ExternalError{}
+	err := json.NewDecoder(rr.Result().Body).Decode(reqErr)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, statusCode, rr.Result().StatusCode, "status code should match")
+	assert.Equal(t, expReqErr, reqErr, "body should be internal server error")
+}

+ 7 - 7
api/types/policy.go

@@ -3,12 +3,12 @@ package types
 type PermissionScope string
 type PermissionScope string
 
 
 const (
 const (
-	UserScope      PermissionScope = "user"
-	ProjectScope   PermissionScope = "project"
-	ClusterScope   PermissionScope = "cluster"
-	NamespaceScope PermissionScope = "namespace"
-	SettingsScope  PermissionScope = "settings"
-	ReleaseScope   PermissionScope = "release"
+	UserScope        PermissionScope = "user"
+	ProjectScope     PermissionScope = "project"
+	ClusterScope     PermissionScope = "cluster"
+	NamespaceScope   PermissionScope = "namespace"
+	SettingsScope    PermissionScope = "settings"
+	ApplicationScope PermissionScope = "application"
 )
 )
 
 
 type NameOrUInt struct {
 type NameOrUInt struct {
@@ -39,7 +39,7 @@ var ScopeHeirarchy = ScopeTree{
 	ProjectScope: {
 	ProjectScope: {
 		ClusterScope: {
 		ClusterScope: {
 			NamespaceScope: {
 			NamespaceScope: {
-				ReleaseScope: {},
+				ApplicationScope: {},
 			},
 			},
 		},
 		},
 		SettingsScope: {},
 		SettingsScope: {},

+ 10 - 0
api/types/request.go

@@ -30,6 +30,15 @@ const (
 	HTTPVerbDelete HTTPVerb = "DELETE"
 	HTTPVerbDelete HTTPVerb = "DELETE"
 )
 )
 
 
+type URLParam string
+
+const (
+	URLParamProjectID   URLParam = "project_id"
+	URLParamClusterID   URLParam = "cluster_id"
+	URLParamNamespace   URLParam = "namespace"
+	URLParamApplication URLParam = "application"
+)
+
 type Path struct {
 type Path struct {
 	Parent       *Path
 	Parent       *Path
 	RelativePath string
 	RelativePath string
@@ -39,4 +48,5 @@ type APIRequestMetadata struct {
 	Verb   APIVerb
 	Verb   APIVerb
 	Method HTTPVerb
 	Method HTTPVerb
 	Path   *Path
 	Path   *Path
+	Scopes []PermissionScope
 }
 }