Kaynağa Gözat

add invite scope + get handler

Alexander Belanger 4 yıl önce
ebeveyn
işleme
ee6e19d94f

+ 64 - 0
api/server/authz/invite.go

@@ -0,0 +1,64 @@
+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"
+	"gorm.io/gorm"
+)
+
+type InviteScopedFactory struct {
+	config *shared.Config
+}
+
+func NewInviteScopedFactory(
+	config *shared.Config,
+) *InviteScopedFactory {
+	return &InviteScopedFactory{config}
+}
+
+func (p *InviteScopedFactory) Middleware(next http.Handler) http.Handler {
+	return &RegistryScopedMiddleware{next, p.config}
+}
+
+type InviteScopedMiddleware struct {
+	next   http.Handler
+	config *shared.Config
+}
+
+func (p *InviteScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the project to check scopes
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	// get the registry id from the URL param context
+	reqScopes, _ := r.Context().Value(RequestScopeCtxKey).(map[types.PermissionScope]*policy.RequestAction)
+	inviteID := reqScopes[types.InviteScope].Resource.UInt
+
+	invite, err := p.config.Repo.Invite().ReadInvite(proj.ID, inviteID)
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+				fmt.Errorf("invite with id %d not found in project %d", inviteID, proj.ID),
+			))
+		} else {
+			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		}
+
+		return
+	}
+
+	ctx := NewInviteContext(r.Context(), invite)
+	r = r.WithContext(ctx)
+	p.next.ServeHTTP(w, r)
+}
+
+func NewInviteContext(ctx context.Context, invite *models.Invite) context.Context {
+	return context.WithValue(ctx, types.InviteScope, invite)
+}

+ 32 - 0
api/server/handlers/invite/get.go

@@ -0,0 +1,32 @@
+package invite
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type InviteGetHandler struct {
+	handlers.PorterHandlerWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewInviteGetHandler(
+	config *shared.Config,
+	writer shared.ResultWriter,
+) *InviteGetHandler {
+	return &InviteGetHandler{
+		PorterHandlerWriter:   handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *InviteGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	invite, _ := r.Context().Value(types.InviteScope).(*models.Invite)
+
+	c.WriteResult(w, invite.ToInviteType())
+}

+ 83 - 0
api/server/router/invite.go

@@ -0,0 +1,83 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/invite"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewInviteScopedRegisterer(children ...*Registerer) *Registerer {
+	return &Registerer{
+		GetRoutes: GetInviteScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetInviteScopedRoutes(
+	r chi.Router,
+	config *shared.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*Registerer,
+) []*Route {
+	routes, projPath := getInviteRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getInviteRoutes(
+	r chi.Router,
+	config *shared.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*Route, *types.Path) {
+	relPath := "/invites/{invite_id}"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	routes := make([]*Route, 0)
+
+	// GET /api/projects/{project_id}/invites/{invite_id} -> registry.NewInviteGetHandler
+	getEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.InviteScope,
+			},
+		},
+	)
+
+	getHandler := invite.NewInviteGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getEndpoint,
+		Handler:  getHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

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

@@ -25,7 +25,13 @@ func NewAPIRouter(config *shared.Config) *chi.Mux {
 	clusterRegisterer := NewClusterScopedRegisterer(namespaceRegisterer)
 	registryRegisterer := NewRegistryScopedRegisterer()
 	helmRepoRegisterer := NewHelmRepoScopedRegisterer()
-	projRegisterer := NewProjectScopedRegisterer(clusterRegisterer, registryRegisterer, helmRepoRegisterer)
+	inviteRegisterer := NewInviteScopedRegisterer()
+	projRegisterer := NewProjectScopedRegisterer(
+		clusterRegisterer,
+		registryRegisterer,
+		helmRepoRegisterer,
+		inviteRegisterer,
+	)
 	userRegisterer := NewUserScopedRegisterer(projRegisterer)
 
 	r.Route("/api", func(r chi.Router) {

+ 10 - 0
api/types/invite.go

@@ -0,0 +1,10 @@
+package types
+
+type Invite struct {
+	ID       uint   `json:"id"`
+	Token    string `json:"token"`
+	Expired  bool   `json:"expired"`
+	Email    string `json:"email"`
+	Accepted bool   `json:"accepted"`
+	Kind     string `json:"kind"`
+}

+ 1 - 0
api/types/policy.go

@@ -7,6 +7,7 @@ const (
 	ProjectScope   PermissionScope = "project"
 	ClusterScope   PermissionScope = "cluster"
 	RegistryScope  PermissionScope = "registry"
+	InviteScope    PermissionScope = "invite"
 	HelmRepoScope  PermissionScope = "helm_repo"
 	NamespaceScope PermissionScope = "namespace"
 	SettingsScope  PermissionScope = "settings"

+ 1 - 0
api/types/request.go

@@ -37,6 +37,7 @@ const (
 	URLParamClusterID      URLParam = "cluster_id"
 	URLParamRegistryID     URLParam = "registry_id"
 	URLParamHelmRepoID     URLParam = "helm_repo_id"
+	URLParamInviteID       URLParam = "invite_id"
 	URLParamNamespace      URLParam = "namespace"
 	URLParamReleaseName    URLParam = "name"
 	URLParamReleaseVersion URLParam = "version"

+ 13 - 0
internal/models/invite.go

@@ -3,6 +3,7 @@ package models
 import (
 	"time"
 
+	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 )
 
@@ -43,6 +44,18 @@ func (i *Invite) Externalize() *InviteExternal {
 	}
 }
 
+// ToInviteType generates an external Invite to be shared over REST
+func (i *Invite) ToInviteType() *types.Invite {
+	return &types.Invite{
+		ID:       i.Model.ID,
+		Token:    i.Token,
+		Email:    i.Email,
+		Expired:  i.IsExpired(),
+		Accepted: i.IsAccepted(),
+		Kind:     i.Kind,
+	}
+}
+
 func (i *Invite) IsExpired() bool {
 	timeLeft := i.Expiry.Sub(time.Now())
 	return timeLeft < 0

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

@@ -39,10 +39,10 @@ func (repo *InviteRepository) CreateInvite(invite *models.Invite) (*models.Invit
 }
 
 // ReadInvite gets an invite specified by a unique id
-func (repo *InviteRepository) ReadInvite(id uint) (*models.Invite, error) {
+func (repo *InviteRepository) ReadInvite(projectID, inviteID uint) (*models.Invite, error) {
 	invite := &models.Invite{}
 
-	if err := repo.db.Where("id = ?", id).First(&invite).Error; err != nil {
+	if err := repo.db.Where("project_id = ? AND id = ?", projectID, inviteID).First(&invite).Error; err != nil {
 		return nil, err
 	}
 

+ 1 - 1
internal/repository/gorm/invite_test.go

@@ -33,7 +33,7 @@ func TestCreateInvite(t *testing.T) {
 		t.Fatalf("%v\n", err)
 	}
 
-	invite, err = tester.repo.Invite().ReadInvite(invite.Model.ID)
+	invite, err = tester.repo.Invite().ReadInvite(1, invite.Model.ID)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)

+ 1 - 1
internal/repository/invite.go

@@ -7,7 +7,7 @@ import (
 // InviteRepository represents the set of queries on the Invite model
 type InviteRepository interface {
 	CreateInvite(invite *models.Invite) (*models.Invite, error)
-	ReadInvite(id uint) (*models.Invite, error)
+	ReadInvite(projectID, inviteID uint) (*models.Invite, error)
 	ReadInviteByToken(token string) (*models.Invite, error)
 	ListInvitesByProjectID(projectID uint) ([]*models.Invite, error)
 	UpdateInvite(invite *models.Invite) (*models.Invite, error)

+ 1 - 1
internal/repository/test/invite.go

@@ -33,7 +33,7 @@ func (repo *InviteRepository) CreateInvite(invite *models.Invite) (*models.Invit
 }
 
 // ReadInvite gets an invite specified by a unique id
-func (repo *InviteRepository) ReadInvite(id uint) (*models.Invite, error) {
+func (repo *InviteRepository) ReadInvite(projectID, id uint) (*models.Invite, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot read from database")
 	}