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/apierrors" "github.com/porter-dev/porter/api/server/shared/apitest" "github.com/porter-dev/porter/api/server/shared/config" "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, true) _, _, err := project.CreateProjectWithUser(config.Repo.Project(), &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]*types.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.ReleaseScope, }, }, false, false) user := apitest.CreateTestUser(t, config, true) _, _, err := project.CreateProjectWithUser(config.Repo.Project(), &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", "name": "app-1", }) req = apitest.WithAuthenticatedUser(t, req, user) handler.ServeHTTP(rr, req) assertNextHandlerCalled(t, next, rr, map[types.PermissionScope]*types.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.ReleaseScope: { 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, true) _, _, err := project.CreateProjectWithUser(config.Repo.Project(), &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, true) _, _, err := project.CreateProjectWithUser(config.Repo.Project(), &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, true) _, _, err := project.CreateProjectWithUser(config.Repo.Project(), &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, ) (*config.Config, http.Handler, *testHandler) { config := apitest.LoadConfig(t) var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project(), config.Repo.Policy()) 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(opts *policy.PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError) { return nil, apierrors.NewErrInternal(fmt.Errorf("new error internal")) } type viewerDocLoader struct{} func (f *viewerDocLoader) LoadPolicyDocuments(opts *policy.PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError) { return types.ViewerPolicy, nil } type testHandler struct { WasCalled bool ReqScopes map[types.PermissionScope]*types.RequestAction } func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.WasCalled = true t.ReqScopes, _ = r.Context().Value(types.RequestScopeCtxKey).(map[types.PermissionScope]*types.RequestAction) } func assertNextHandlerCalled( t *testing.T, next *testHandler, rr *httptest.ResponseRecorder, expScopes map[types.PermissionScope]*types.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) }