Quellcode durchsuchen

full authz testing cases for project

Alexander Belanger vor 5 Jahren
Ursprung
Commit
d6183c9d3f

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

@@ -9,6 +9,7 @@ import (
 	"github.com/go-chi/chi"
 	"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/apitest"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -55,16 +56,7 @@ func TestGetURLUintParamsErrors(t *testing.T) {
 		r := httptest.NewRequest("POST", test.route, nil)
 
 		// 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)
 

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

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

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

+ 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) {
-	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)
 	user := apitest.CreateTestUser(t, config)
@@ -45,9 +50,14 @@ func TestCreateProjectSuccessful(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)
 	user := apitest.CreateTestUser(t, config)
@@ -65,9 +75,14 @@ func TestFailingDecoderValidator(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)
 	user := apitest.CreateTestUser(t, config)
@@ -85,9 +100,14 @@ func TestFailingCreateMethod(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)
 	user := apitest.CreateTestUser(t, config)
@@ -105,9 +125,14 @@ func TestFailingCreateRoleMethod(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)
 	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)
 	}
 
-	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.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)
 	}
 
-	req, rr := apitest.GetRequestAndRecorder(t, nil)
+	req, rr := apitest.GetRequestAndRecorder(t, string(types.HTTPVerbGet), "/api/projects", nil)
 
 	req = apitest.WithAuthenticatedUser(t, req, user)
 
@@ -52,9 +52,12 @@ func TestListProjectsSuccessful(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)
 	user := apitest.CreateTestUser(t, config)

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

@@ -2,7 +2,6 @@ package router
 
 import (
 	"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/shared"
 	"github.com/porter-dev/porter/api/types"
@@ -10,44 +9,50 @@ import (
 
 func NewProjectScopedRegisterer(children ...*Registerer) *Registerer {
 	return &Registerer{
-		Func:     RegisterProjectScopedRoutes,
-		Children: children,
+		GetRoutes: GetProjectScopedRoutes,
+		Children:  children,
 	}
 }
 
-func RegisterProjectScopedRoutes(
+func GetProjectScopedRoutes(
 	r chi.Router,
 	config *shared.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	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 {
 		r.Route(projPath.RelativePath, func(r chi.Router) {
 			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,
 	config *shared.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) *types.Path {
+) ([]*Route, *types.Path) {
 	relPath := "/projects/{project_id}"
 
 	newPath := &types.Path{
@@ -77,9 +82,8 @@ func registerProjectEndpoints(
 	routes = append(routes, &Route{
 		Endpoint: getEndpoint,
 		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)
 
 	r.Route("/api", func(r chi.Router) {
-		userRegisterer.Func(
+		userRoutes := userRegisterer.GetRoutes(
 			r,
 			config,
 			&types.Path{
@@ -25,6 +25,8 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 			endpointFactory,
 			userRegisterer.Children...,
 		)
+
+		registerRoutes(userRoutes)
 	})
 
 	return r
@@ -33,23 +35,24 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 type Route struct {
 	Endpoint *shared.APIEndpoint
 	Handler  http.Handler
+	Router   chi.Router
 }
 
 type Registerer struct {
-	Func func(
+	GetRoutes func(
 		r chi.Router,
 		config *shared.Config,
 		basePath *types.Path,
 		factory shared.APIEndpointFactory,
 		children ...*Registerer,
-	) chi.Router
+	) []*Route
 
 	Children []*Registerer
 }
 
-func registerRoutes(r chi.Router, routes []*Route) {
+func registerRoutes(routes []*Route) {
 	for _, route := range routes {
-		r.Method(
+		route.Router.Method(
 			string(route.Endpoint.Metadata.Method),
 			route.Endpoint.Metadata.Path.RelativePath,
 			route.Handler,

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

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

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

@@ -2,7 +2,6 @@ package router
 
 import (
 	"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/shared"
 	"github.com/porter-dev/porter/api/types"
@@ -10,42 +9,44 @@ import (
 
 func NewUserScopedRegisterer(children ...*Registerer) *Registerer {
 	return &Registerer{
-		Func:     RegisterUserScopedRoutes,
-		Children: children,
+		GetRoutes: GetUserScopedRoutes,
+		Children:  children,
 	}
 }
 
-func RegisterUserScopedRoutes(
+func GetUserScopedRoutes(
 	r chi.Router,
 	config *shared.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	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 {
 		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,
 	config *shared.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
-) {
+) []*Route {
 	routes := make([]*Route, 0)
 
 	// POST /api/projects -> project.NewProjectCreateHandler
@@ -69,6 +70,7 @@ func registerUserRoutes(
 	routes = append(routes, &Route{
 		Endpoint: createEndpoint,
 		Handler:  createHandler,
+		Router:   r,
 	})
 
 	// GET /api/projects -> project.NewProjectListHandler
@@ -91,7 +93,8 @@ func registerUserRoutes(
 	routes = append(routes, &Route{
 		Endpoint: listEndpoint,
 		Handler:  listHandler,
+		Router:   r,
 	})
 
-	registerRoutes(r, routes)
+	return routes
 }

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

@@ -5,6 +5,8 @@ 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/types"
 	"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
 }
+
+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
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -9,11 +10,12 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/shared"
 	"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
 
 	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
-	req, err := http.NewRequest("POST", "/fake-route", reader)
+	req, err := http.NewRequest(method, route, reader)
 
 	if err != nil {
 		t.Fatal(err)
@@ -38,6 +40,21 @@ func GetRequestAndRecorder(t *testing.T, requestObj interface{}) (*http.Request,
 	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 {
 	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, 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
 
 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 {
@@ -39,7 +39,7 @@ var ScopeHeirarchy = ScopeTree{
 	ProjectScope: {
 		ClusterScope: {
 			NamespaceScope: {
-				ReleaseScope: {},
+				ApplicationScope: {},
 			},
 		},
 		SettingsScope: {},

+ 10 - 0
api/types/request.go

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