瀏覽代碼

fix merge conflicts

Alexander Belanger 4 年之前
父節點
當前提交
a1956e9782

+ 32 - 7
api/server/authn/handler.go

@@ -11,6 +11,8 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/models"
+	"golang.org/x/crypto/bcrypt"
 )
 
 // AuthNFactory generates a middleware handler `AuthN`
@@ -62,7 +64,7 @@ func (authn *AuthN) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		authn.sendForbiddenError(err, w, r)
 		return
 	} else if err == nil && tok != nil {
-		authn.nextWithToken(w, r, tok)
+		authn.verifyTokenWithNext(w, r, tok)
 		return
 	}
 
@@ -120,13 +122,36 @@ func (authn *AuthN) handleForbiddenForSession(
 	return
 }
 
-// nextWithToken calls the next handler with either the service account or user corresponding
-// to the token set in context.
-func (authn *AuthN) nextWithToken(w http.ResponseWriter, r *http.Request, tok *token.Token) {
-	// TODO: add section to get service account for server-side token
+func (authn *AuthN) verifyTokenWithNext(w http.ResponseWriter, r *http.Request, tok *token.Token) {
+	// if the token has a stored token id and secret we check that the token is valid in the database
+	if tok.Secret != "" && tok.TokenID != "" {
+		apiToken, err := authn.config.Repo.APIToken().ReadAPIToken(tok.ProjectID, tok.TokenID)
 
-	// for now, we just use nextWithUser using the `iby` field for the token
-	authn.nextWithUserID(w, r, tok.IBy)
+		if err != nil {
+			authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
+			return
+		}
+
+		// compare the secret against the hashed version
+		if err := bcrypt.CompareHashAndPassword([]byte(apiToken.SecretKey), []byte(tok.Secret)); err != nil {
+			authn.sendForbiddenError(fmt.Errorf("incorrect secret key for token %s", tok.TokenID), w, r)
+			return
+		}
+
+		authn.nextWithAPIToken(w, r, apiToken)
+	} else {
+		// otherwise we just use nextWithUser using the `iby` field for the token
+		authn.nextWithUserID(w, r, tok.IBy)
+	}
+}
+
+// nextWithAPIToken sets the token in context
+func (authn *AuthN) nextWithAPIToken(w http.ResponseWriter, r *http.Request, tok *models.APIToken) {
+	ctx := r.Context()
+	ctx = context.WithValue(ctx, "api_token", tok)
+
+	r = r.Clone(ctx)
+	authn.next.ServeHTTP(w, r)
 }
 
 // nextWithUserID calls the next handler with the user set in the context with key

+ 17 - 5
api/server/authz/policy.go

@@ -47,11 +47,23 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// load policy documents for the user + project
-	projID := reqScopes[types.ProjectScope].Resource.UInt
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	policyLoaderOpts := &policy.PolicyLoaderOpts{}
+
+	// first check if an api token exists in context
+	if r.Context().Value("api_token") != nil {
+		apiToken, _ := r.Context().Value("api_token").(*models.APIToken)
+		policyLoaderOpts.Token = apiToken
+		policyLoaderOpts.ProjectID = apiToken.ProjectID
+	} else {
+		projID := reqScopes[types.ProjectScope].Resource.UInt
+		user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+		policyLoaderOpts.ProjectID = projID
+		policyLoaderOpts.UserID = user.ID
+	}
 
-	policyDocs, reqErr := h.loader.LoadPolicyDocuments(user.ID, projID)
+	// load policy documents for the user + project
+	policyDocs, reqErr := h.loader.LoadPolicyDocuments(policyLoaderOpts)
 
 	if reqErr != nil {
 		apierrors.HandleAPIError(h.config.Logger, h.config.Alerter, w, r, reqErr, true)
@@ -67,7 +79,7 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			h.config.Alerter,
 			w,
 			r,
-			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action for user %d in project %d", user.ID, projID)),
+			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action in project %d", policyLoaderOpts.ProjectID)),
 			true,
 		)
 

+ 60 - 30
api/server/authz/policy/loader.go

@@ -5,50 +5,80 @@ import (
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 )
 
+type PolicyLoaderOpts struct {
+	ProjectID, UserID uint
+	Token             *models.APIToken
+}
+
 type PolicyDocumentLoader interface {
-	LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError)
+	LoadPolicyDocuments(opts *PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError)
 }
 
-// BasicPolicyDocumentLoader loads policy documents simply depending on the
-type BasicPolicyDocumentLoader struct {
-	projRepo repository.ProjectRepository
+// RepoPolicyDocumentLoader loads policy documents by reading from the repository database
+type RepoPolicyDocumentLoader struct {
+	projRepo   repository.ProjectRepository
+	policyRepo repository.PolicyRepository
 }
 
-func NewBasicPolicyDocumentLoader(projRepo repository.ProjectRepository) *BasicPolicyDocumentLoader {
-	return &BasicPolicyDocumentLoader{projRepo}
+func NewBasicPolicyDocumentLoader(projRepo repository.ProjectRepository, policyRepo repository.PolicyRepository) *RepoPolicyDocumentLoader {
+	return &RepoPolicyDocumentLoader{projRepo, policyRepo}
 }
 
-func (b *BasicPolicyDocumentLoader) LoadPolicyDocuments(
-	userID, projectID uint,
+func (b *RepoPolicyDocumentLoader) LoadPolicyDocuments(
+	opts *PolicyLoaderOpts,
 ) ([]*types.PolicyDocument, apierrors.RequestError) {
-	// read role and case on role "kind"
-	role, err := b.projRepo.ReadProjectRole(projectID, userID)
-
-	if err != nil && err == gorm.ErrRecordNotFound {
-		return nil, apierrors.NewErrForbidden(
-			fmt.Errorf("user %d does not have a role in project %d", userID, projectID),
-		)
-	} else if err != nil {
-		return nil, apierrors.NewErrInternal(err)
-	}
+	if opts.Token != nil {
+		// load the policy from the repo
+		policy, err := b.policyRepo.ReadPolicy(opts.Token.ProjectID, opts.Token.PolicyUID)
+
+		if err != nil {
+			return nil, apierrors.NewErrInternal(err)
+		}
+
+		apiPolicy, err := policy.ToAPIPolicyType()
 
-	// load role based on role kind
-	switch role.Kind {
-	case types.RoleAdmin:
-		return AdminPolicy, nil
-	case types.RoleDeveloper:
-		return DeveloperPolicy, nil
-	case types.RoleViewer:
-		return ViewerPolicy, nil
-	default:
-		return nil, apierrors.NewErrForbidden(
-			fmt.Errorf("%s role not supported for user %d, project %d", string(role.Kind), userID, projectID),
-		)
+		if err != nil {
+			return nil, apierrors.NewErrInternal(err)
+		}
+
+		return apiPolicy.Policy, nil
+	} else if opts.ProjectID != 0 && opts.UserID != 0 {
+		userID := opts.UserID
+		projectID := opts.ProjectID
+		// read role and case on role "kind"
+		role, err := b.projRepo.ReadProjectRole(projectID, userID)
+
+		if err != nil && err == gorm.ErrRecordNotFound {
+			return nil, apierrors.NewErrForbidden(
+				fmt.Errorf("user %d does not have a role in project %d", userID, projectID),
+			)
+		} else if err != nil {
+			return nil, apierrors.NewErrInternal(err)
+		}
+
+		// load role based on role kind
+		switch role.Kind {
+		case types.RoleAdmin:
+			return AdminPolicy, nil
+		case types.RoleDeveloper:
+			return DeveloperPolicy, nil
+		case types.RoleViewer:
+			return ViewerPolicy, nil
+		default:
+			return nil, apierrors.NewErrForbidden(
+				fmt.Errorf("%s role not supported for user %d, project %d", string(role.Kind), userID, projectID),
+			)
+		}
 	}
+
+	return nil, apierrors.NewErrForbidden(
+		fmt.Errorf("policy loader called with invalid arguments"),
+	)
 }
 
 var AdminPolicy = []*types.PolicyDocument{

+ 15 - 6
api/server/authz/policy/loader_test.go

@@ -53,7 +53,7 @@ func TestBasicPolicyDocumentLoader(t *testing.T) {
 	for _, basicTest := range basicLoaderTests {
 		// use the in-memory project repo
 		projRepo := test.NewProjectRepository(true)
-		loader := policy.NewBasicPolicyDocumentLoader(projRepo)
+		loader := policy.NewBasicPolicyDocumentLoader(projRepo, nil)
 
 		project := &models.Project{
 			Name: "test-project",
@@ -79,7 +79,10 @@ func TestBasicPolicyDocumentLoader(t *testing.T) {
 			t.Fatalf("%v", err)
 		}
 
-		docs, reqErr := loader.LoadPolicyDocuments(1, 1)
+		docs, reqErr := loader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+			ProjectID: 1,
+			UserID:    1,
+		})
 
 		assert.Equal(
 			reqErr != nil,
@@ -123,7 +126,7 @@ func TestErrorForbiddenInvalidRole(t *testing.T) {
 
 	// use the in-memory project repo
 	projRepo := test.NewProjectRepository(true)
-	loader := policy.NewBasicPolicyDocumentLoader(projRepo)
+	loader := policy.NewBasicPolicyDocumentLoader(projRepo, nil)
 
 	project := &models.Project{
 		Name: "test-project",
@@ -149,7 +152,10 @@ func TestErrorForbiddenInvalidRole(t *testing.T) {
 		t.Fatalf("%v", err)
 	}
 
-	_, reqErr := loader.LoadPolicyDocuments(2, 1)
+	_, reqErr := loader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+		ProjectID: 1,
+		UserID:    2,
+	})
 
 	if reqErr == nil {
 		t.Fatalf("Expected forbidden error for invalid project role")
@@ -174,9 +180,12 @@ func TestErrorCannotQuery(t *testing.T) {
 
 	// use the in-memory project repo
 	projRepo := test.NewProjectRepository(false)
-	loader := policy.NewBasicPolicyDocumentLoader(projRepo)
+	loader := policy.NewBasicPolicyDocumentLoader(projRepo, nil)
 
-	_, reqErr := loader.LoadPolicyDocuments(2, 1)
+	_, reqErr := loader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+		ProjectID: 2,
+		UserID:    1,
+	})
 
 	if reqErr == nil {
 		t.Fatalf("Expected internal error for failing to query")

+ 3 - 3
api/server/authz/policy_test.go

@@ -240,7 +240,7 @@ func loadHandlers(
 	shouldLoaderLoadViewer bool,
 ) (*config.Config, http.Handler, *testHandler) {
 	config := apitest.LoadConfig(t)
-	var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
+	var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project(), config.Repo.Policy())
 
 	if shouldLoaderFail {
 		loader = &failingDocLoader{}
@@ -260,13 +260,13 @@ func loadHandlers(
 
 type failingDocLoader struct{}
 
-func (f *failingDocLoader) LoadPolicyDocuments(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError) {
+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(userID, projectID uint) ([]*types.PolicyDocument, apierrors.RequestError) {
+func (f *viewerDocLoader) LoadPolicyDocuments(opts *policy.PolicyLoaderOpts) ([]*types.PolicyDocument, apierrors.RequestError) {
 	return policy.ViewerPolicy, nil
 }
 

+ 124 - 0
api/server/handlers/api_token/create.go

@@ -0,0 +1,124 @@
+package api_token
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
+)
+
+type APITokenCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewAPITokenCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *APITokenCreateHandler {
+	return &APITokenCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *APITokenCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	req := &types.CreateAPIToken{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	// look up the policy and make sure it exists
+	policy, err := p.Repo().Policy().ReadPolicy(proj.ID, req.PolicyUID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("policy not found in project"),
+				http.StatusBadRequest,
+			))
+			return
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	uid, err := repository.GenerateRandomBytes(16)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	secretKey, err := repository.GenerateRandomBytes(16)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// hash the secret key for storage in the db
+	hashedToken, err := bcrypt.GenerateFromPassword([]byte(secretKey), 8)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	apiToken := &models.APIToken{
+		UniqueID:        uid,
+		ProjectID:       proj.ID,
+		CreatedByUserID: user.ID,
+		Expiry:          &req.ExpiresAt,
+		Revoked:         false,
+		PolicyUID:       policy.UniqueID,
+		PolicyName:      policy.Name,
+		Name:            req.Name,
+		SecretKey:       hashedToken,
+	}
+
+	apiToken, err = p.Repo().APIToken().CreateAPIToken(apiToken)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	apiPolicy, err := policy.ToAPIPolicyType()
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// generate porter jwt token
+	jwt, err := token.GetStoredTokenForAPI(user.ID, proj.ID, apiToken.UniqueID, secretKey)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	encoded, err := jwt.EncodeToken(p.Config().TokenConf)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, apiToken.ToAPITokenType(apiPolicy.Policy, encoded))
+}

+ 77 - 0
api/server/handlers/policy/create.go

@@ -0,0 +1,77 @@
+package policy
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type PolicyCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewPolicyCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *PolicyCreateHandler {
+	return &PolicyCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *PolicyCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	req := &types.CreatePolicy{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	uid, err := repository.GenerateRandomBytes(16)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policyBytes, err := json.Marshal(req.Policy)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	policy := &models.Policy{
+		ProjectID:       proj.ID,
+		UniqueID:        uid,
+		CreatedByUserID: user.ID,
+		Name:            req.Name,
+		PolicyBytes:     policyBytes,
+	}
+
+	policy, err = p.Repo().Policy().CreatePolicy(policy)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res, err := policy.ToAPIPolicyType()
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 5 - 2
api/server/handlers/project/get_policy.go

@@ -30,9 +30,12 @@ func (p *ProjectGetPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	policyDocLoader := policy.NewBasicPolicyDocumentLoader(p.Config().Repo.Project())
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(p.Config().Repo.Project(), p.Config().Repo.Policy())
 
-	policyDocs, err := policyDocLoader.LoadPolicyDocuments(user.ID, proj.ID)
+	policyDocs, err := policyDocLoader.LoadPolicyDocuments(&policy.PolicyLoaderOpts{
+		UserID:    user.ID,
+		ProjectID: proj.ID,
+	})
 
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 60 - 0
api/server/router/project.go

@@ -4,10 +4,12 @@ import (
 	"fmt"
 
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/api_token"
 	"github.com/porter-dev/porter/api/server/handlers/billing"
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/infra"
+	"github.com/porter-dev/porter/api/server/handlers/policy"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -890,5 +892,63 @@ func getProjectRoutes(
 	// 	Router:   r,
 	// })
 
+	//  POST /api/projects/{project_id}/policy -> policy.NewPolicyCreateHandler
+	policyCreateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/policy",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	policyCreateHandler := policy.NewPolicyCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: policyCreateEndpoint,
+		Handler:  policyCreateHandler,
+		Router:   r,
+	})
+
+	//  POST /api/projects/{project_id}/api_token -> api_token.NewAPITokenCreateHandler
+	apiTokenCreateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/api_token",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.SettingsScope,
+			},
+		},
+	)
+
+	apiTokenCreateHandler := api_token.NewAPITokenCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: apiTokenCreateEndpoint,
+		Handler:  apiTokenCreateHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

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

@@ -192,7 +192,7 @@ func registerRoutes(config *config.Config, routes []*Route) {
 	releaseFactory := authz.NewReleaseScopedFactory(config)
 
 	// Policy doc loader loads the policy documents for a specific project.
-	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
+	policyDocLoader := policy.NewBasicPolicyDocumentLoader(config.Repo.Project(), config.Repo.Policy())
 
 	// set up logging middleware to log information about the request
 	loggerMw := middleware.NewRequestLoggerMiddleware(config.Logger)

+ 25 - 0
api/types/api_token.go

@@ -0,0 +1,25 @@
+package types
+
+import "time"
+
+type APITokenMeta struct {
+	CreatedAt time.Time `json:"created_at"`
+	ExpiresAt time.Time `json:"expires_at"`
+
+	PolicyName string `json:"policy_name"`
+	PolicyUID  string `json:"policy_uid"`
+	Name       string `json:"name"`
+}
+
+type APIToken struct {
+	*APITokenMeta
+
+	Policy []*PolicyDocument `json:"policy"`
+	Token  string            `json:"token"`
+}
+
+type CreateAPIToken struct {
+	PolicyUID string    `json:"policy_uid" form:"required"`
+	ExpiresAt time.Time `json:"expires_at"`
+	Name      string    `json:"name" form:"required"`
+}

+ 20 - 0
api/types/policy.go

@@ -1,5 +1,7 @@
 package types
 
+import "time"
+
 type PermissionScope string
 
 const (
@@ -85,3 +87,21 @@ var ViewerPolicy = []*PolicyDocument{
 		},
 	},
 }
+
+type CreatePolicy struct {
+	Name   string            `json:"name" form:"required"`
+	Policy []*PolicyDocument `json:"policy" form:"required"`
+}
+
+type APIPolicyMeta struct {
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
+	ProjectID uint      `json:"project_id"`
+	UID       string    `json:"uid"`
+	Name      string    `json:"name"`
+}
+
+type APIPolicy struct {
+	*APIPolicyMeta
+	Policy []*PolicyDocument `json:"policy"`
+}

+ 44 - 2
internal/auth/token/token.go

@@ -25,6 +25,10 @@ type Token struct {
 	ProjectID uint       `json:"project_id"`
 	IBy       uint       `json:"iby"`
 	IAt       *time.Time `json:"iat"`
+
+	// Additional fields that may or may not be set
+	TokenID string `json:"token_id"`
+	Secret  string `json:"secret"`
 }
 
 func GetTokenForUser(userID uint) (*Token, error) {
@@ -58,6 +62,24 @@ func GetTokenForAPI(userID, projID uint) (*Token, error) {
 	}, nil
 }
 
+func GetStoredTokenForAPI(userID, projID uint, tokenID, secret string) (*Token, error) {
+	if userID == 0 || projID == 0 {
+		return nil, fmt.Errorf("id cannot be 0")
+	}
+
+	iat := time.Now()
+
+	return &Token{
+		SubKind:   API,
+		Sub:       string(API),
+		ProjectID: projID,
+		IBy:       userID,
+		IAt:       &iat,
+		TokenID:   tokenID,
+		Secret:    secret,
+	}, nil
+}
+
 func (t *Token) EncodeToken(conf *TokenGeneratorConf) (string, error) {
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 		"sub_kind":   t.SubKind,
@@ -65,6 +87,8 @@ func (t *Token) EncodeToken(conf *TokenGeneratorConf) (string, error) {
 		"iby":        t.IBy,
 		"iat":        fmt.Sprintf("%d", t.IAt.Unix()),
 		"project_id": t.ProjectID,
+		"token_id":   t.TokenID,
+		"secret":     t.Secret,
 	})
 
 	// Sign and get the complete encoded token as a string using the secret
@@ -105,13 +129,31 @@ func GetTokenFromEncoded(tokenString string, conf *TokenGeneratorConf) (*Token,
 
 		iat := time.Unix(iatUnix, 0)
 
-		return &Token{
+		res := &Token{
 			SubKind:   Subject(fmt.Sprintf("%v", claims["sub_kind"])),
 			Sub:       fmt.Sprintf("%v", claims["sub"]),
 			IBy:       uint(iby),
 			IAt:       &iat,
 			ProjectID: uint(projID),
-		}, nil
+		}
+
+		if tokenIDInter, ok := claims["token_id"]; ok {
+			tokenID, ok := tokenIDInter.(string)
+
+			if ok {
+				res.TokenID = tokenID
+			}
+		}
+
+		if secretInter, ok := claims["secret"]; ok {
+			secret, ok := secretInter.(string)
+
+			if ok {
+				res.Secret = secret
+			}
+		}
+
+		return res, nil
 	}
 
 	return nil, fmt.Errorf("invalid token")

+ 22 - 1
internal/models/api_token.go

@@ -3,6 +3,7 @@ package models
 import (
 	"time"
 
+	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 )
 
@@ -15,13 +16,33 @@ type APIToken struct {
 	CreatedByUserID uint
 	Expiry          *time.Time
 	Revoked         bool
+	PolicyUID       string
 	PolicyName      string
+	Name            string
 
 	// SecretKey is hashed like a password before storage
-	SecretKey string
+	SecretKey []byte
 }
 
 func (p *APIToken) IsExpired() bool {
 	timeLeft := p.Expiry.Sub(time.Now())
 	return timeLeft < 0
 }
+
+func (p *APIToken) ToAPITokenMetaType() *types.APITokenMeta {
+	return &types.APITokenMeta{
+		CreatedAt:  p.CreatedAt,
+		ExpiresAt:  *p.Expiry,
+		PolicyName: p.PolicyName,
+		PolicyUID:  p.PolicyUID,
+		Name:       p.Name,
+	}
+}
+
+func (p *APIToken) ToAPITokenType(policy []*types.PolicyDocument, token string) *types.APIToken {
+	return &types.APIToken{
+		APITokenMeta: p.ToAPITokenMetaType(),
+		Policy:       policy,
+		Token:        token,
+	}
+}

+ 44 - 0
internal/models/policy.go

@@ -0,0 +1,44 @@
+package models
+
+import (
+	"encoding/json"
+
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+type Policy struct {
+	gorm.Model
+
+	UniqueID string `gorm:"unique"`
+
+	ProjectID       uint
+	CreatedByUserID uint
+	Name            string
+	PolicyBytes     []byte
+}
+
+func (p *Policy) ToAPIPolicyTypeMeta() *types.APIPolicyMeta {
+	return &types.APIPolicyMeta{
+		CreatedAt: p.CreatedAt,
+		UpdatedAt: p.UpdatedAt,
+		UID:       p.UniqueID,
+		ProjectID: p.ProjectID,
+		Name:      p.Name,
+	}
+}
+
+func (p *Policy) ToAPIPolicyType() (*types.APIPolicy, error) {
+	policy := []*types.PolicyDocument{}
+
+	err := json.Unmarshal(p.PolicyBytes, &policy)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.APIPolicy{
+		APIPolicyMeta: p.ToAPIPolicyTypeMeta(),
+		Policy:        policy,
+	}, nil
+}

+ 1 - 1
internal/repository/api_token.go

@@ -8,6 +8,6 @@ import (
 type APITokenRepository interface {
 	CreateAPIToken(token *models.APIToken) (*models.APIToken, error)
 	ListAPITokensByProjectID(projectID uint) ([]*models.APIToken, error)
-	ReadAPIToken(uid string) (*models.APIToken, error)
+	ReadAPIToken(projectID uint, uid string) (*models.APIToken, error)
 	UpdateAPIToken(token *models.APIToken) (*models.APIToken, error)
 }

+ 2 - 2
internal/repository/gorm/api_token.go

@@ -34,10 +34,10 @@ func (repo *APITokenRepository) ListAPITokensByProjectID(projectID uint) ([]*mod
 	return tokens, nil
 }
 
-func (repo *APITokenRepository) ReadAPIToken(uid string) (*models.APIToken, error) {
+func (repo *APITokenRepository) ReadAPIToken(projectID uint, uid string) (*models.APIToken, error) {
 	token := &models.APIToken{}
 
-	if err := repo.db.Where("id = ?", uid).First(&token).Error; err != nil {
+	if err := repo.db.Where("project_id = ? AND unique_id = ?", projectID, uid).First(&token).Error; err != nil {
 		return nil, err
 	}
 

+ 2 - 0
internal/repository/gorm/migrate.go

@@ -48,6 +48,8 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&models.CredentialsExchangeToken{},
 		&models.BuildConfig{},
 		&models.Allowlist{},
+		&models.APIToken{},
+		&models.Policy{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 64 - 0
internal/repository/gorm/policy.go

@@ -0,0 +1,64 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// PolicyRepository uses gorm.DB for querying the database
+type PolicyRepository struct {
+	db *gorm.DB
+}
+
+// NewPolicyRepository returns a PolicyRepository which uses
+// gorm.DB for querying the database
+func NewPolicyRepository(db *gorm.DB) repository.PolicyRepository {
+	return &PolicyRepository{db}
+}
+
+func (repo *PolicyRepository) CreatePolicy(a *models.Policy) (*models.Policy, error) {
+	if err := repo.db.Create(a).Error; err != nil {
+		return nil, err
+	}
+	return a, nil
+}
+
+func (repo *PolicyRepository) ListPoliciesByProjectID(projectID uint) ([]*models.Policy, error) {
+	policys := []*models.Policy{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&policys).Error; err != nil {
+		return nil, err
+	}
+
+	return policys, nil
+}
+
+func (repo *PolicyRepository) ReadPolicy(projectID uint, uid string) (*models.Policy, error) {
+	policy := &models.Policy{}
+
+	if err := repo.db.Where("project_id = ? AND unique_id = ?", projectID, uid).First(&policy).Error; err != nil {
+		return nil, err
+	}
+
+	return policy, nil
+}
+
+func (repo *PolicyRepository) UpdatePolicy(
+	policy *models.Policy,
+) (*models.Policy, error) {
+	if err := repo.db.Save(policy).Error; err != nil {
+		return nil, err
+	}
+
+	return policy, nil
+}
+
+func (repo *PolicyRepository) DeletePolicy(
+	policy *models.Policy,
+) (*models.Policy, error) {
+	if err := repo.db.Delete(&policy).Error; err != nil {
+		return nil, err
+	}
+	return policy, nil
+}

+ 6 - 0
internal/repository/gorm/repository.go

@@ -42,6 +42,7 @@ type GormRepository struct {
 	buildConfig               repository.BuildConfigRepository
 	allowlist                 repository.AllowlistRepository
 	apiToken                  repository.APITokenRepository
+	policy                    repository.PolicyRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -184,6 +185,10 @@ func (t *GormRepository) APIToken() repository.APITokenRepository {
 	return t.apiToken
 }
 
+func (t *GormRepository) Policy() repository.PolicyRepository {
+	return t.policy
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.CredentialStorage) repository.Repository {
@@ -223,5 +228,6 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		buildConfig:               NewBuildConfigRepository(db),
 		allowlist:                 NewAllowlistRepository(db),
 		apiToken:                  NewAPITokenRepository(db),
+		policy:                    NewPolicyRepository(db),
 	}
 }

+ 14 - 0
internal/repository/policy.go

@@ -0,0 +1,14 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// PolicyRepository represents the set of queries on the Policy model
+type PolicyRepository interface {
+	CreatePolicy(policy *models.Policy) (*models.Policy, error)
+	ListPoliciesByProjectID(projectID uint) ([]*models.Policy, error)
+	ReadPolicy(projectID uint, uid string) (*models.Policy, error)
+	UpdatePolicy(token *models.Policy) (*models.Policy, error)
+	DeletePolicy(policy *models.Policy) (*models.Policy, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -36,4 +36,5 @@ type Repository interface {
 	BuildConfig() BuildConfigRepository
 	Allowlist() AllowlistRepository
 	APIToken() APITokenRepository
+	Policy() PolicyRepository
 }

+ 32 - 0
internal/repository/test/api_token.go

@@ -0,0 +1,32 @@
+package test
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type APITokenRepository struct {
+	canQuery bool
+}
+
+func NewAPITokenRepository(canQuery bool) repository.APITokenRepository {
+	return &APITokenRepository{canQuery}
+}
+
+func (repo *APITokenRepository) CreateAPIToken(a *models.APIToken) (*models.APIToken, error) {
+	panic("unimplemented")
+}
+
+func (repo *APITokenRepository) ListAPITokensByProjectID(projectID uint) ([]*models.APIToken, error) {
+	panic("unimplemented")
+}
+
+func (repo *APITokenRepository) ReadAPIToken(uid string) (*models.APIToken, error) {
+	panic("unimplemented")
+}
+
+func (repo *APITokenRepository) UpdateAPIToken(
+	token *models.APIToken,
+) (*models.APIToken, error) {
+	panic("unimplemented")
+}

+ 40 - 0
internal/repository/test/policy.go

@@ -0,0 +1,40 @@
+package test
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type PolicyRepository struct {
+	canQuery bool
+}
+
+// NewPolicyRepository returns a PolicyRepository which uses
+// gorm.DB for querying the database
+func NewPolicyRepository(canQuery bool) repository.PolicyRepository {
+	return &PolicyRepository{canQuery}
+}
+
+func (repo *PolicyRepository) CreatePolicy(a *models.Policy) (*models.Policy, error) {
+	panic("unimplemented")
+}
+
+func (repo *PolicyRepository) ListPoliciesByProjectID(projectID uint) ([]*models.Policy, error) {
+	panic("unimplemented")
+}
+
+func (repo *PolicyRepository) ReadPolicy(uid string) (*models.Policy, error) {
+	panic("unimplemented")
+}
+
+func (repo *PolicyRepository) UpdatePolicy(
+	policy *models.Policy,
+) (*models.Policy, error) {
+	panic("unimplemented")
+}
+
+func (repo *PolicyRepository) DeletePolicy(
+	policy *models.Policy,
+) (*models.Policy, error) {
+	panic("unimplemented")
+}

+ 12 - 0
internal/repository/test/repository.go

@@ -39,6 +39,8 @@ type TestRepository struct {
 	buildConfig               repository.BuildConfigRepository
 	database                  repository.DatabaseRepository
 	allowlist                 repository.AllowlistRepository
+	apiToken                  repository.APITokenRepository
+	policy                    repository.PolicyRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -177,6 +179,14 @@ func (t *TestRepository) Allowlist() repository.AllowlistRepository {
 	return t.allowlist
 }
 
+func (t *TestRepository) APIToken() repository.APITokenRepository {
+	return t.apiToken
+}
+
+func (t *TestRepository) Policy() repository.PolicyRepository {
+	return t.policy
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
@@ -215,5 +225,7 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		buildConfig:               NewBuildConfigRepository(canQuery),
 		database:                  NewDatabaseRepository(),
 		allowlist:                 NewAllowlistRepository(canQuery),
+		apiToken:                  NewAPITokenRepository(canQuery),
+		policy:                    NewPolicyRepository(canQuery),
 	}
 }