mirror of https://github.com/harness/drone.git
feat: [CODE-626,CODE-627]: space membership API&DB (#194)
parent
7fb7e47560
commit
baa4eb5ac9
|
@ -48,12 +48,20 @@ import (
|
|||
|
||||
func initSystem(ctx context.Context, config *types.Config) (*server.System, error) {
|
||||
principalUID := check.ProvidePrincipalUIDCheck()
|
||||
authorizer := authz.ProvideAuthorizer()
|
||||
databaseConfig := server.ProvideDatabaseConfig(config)
|
||||
db, err := database.ProvideDatabase(ctx, databaseConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pathTransformation := store.ProvidePathTransformation()
|
||||
pathStore := database.ProvidePathStore(db, pathTransformation)
|
||||
pathCache := cache.ProvidePathCache(pathStore, pathTransformation)
|
||||
spaceStore := database.ProvideSpaceStore(db, pathCache)
|
||||
principalInfoView := database.ProvidePrincipalInfoView(db)
|
||||
principalInfoCache := cache.ProvidePrincipalInfoCache(principalInfoView)
|
||||
membershipStore := database.ProvideMembershipStore(db, principalInfoCache)
|
||||
permissionCache := authz.ProvidePermissionCache(spaceStore, membershipStore)
|
||||
authorizer := authz.ProvideAuthorizer(permissionCache)
|
||||
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
|
||||
principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation)
|
||||
tokenStore := database.ProvideTokenStore(db)
|
||||
|
@ -66,11 +74,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
return nil, err
|
||||
}
|
||||
pathUID := check.ProvidePathUIDCheck()
|
||||
pathTransformation := store.ProvidePathTransformation()
|
||||
pathStore := database.ProvidePathStore(db, pathTransformation)
|
||||
pathCache := cache.ProvidePathCache(pathStore, pathTransformation)
|
||||
repoStore := database.ProvideRepoStore(db, pathCache)
|
||||
spaceStore := database.ProvideSpaceStore(db, pathCache)
|
||||
gitrpcConfig, err := server.ProvideGitRPCClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -80,9 +84,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
return nil, err
|
||||
}
|
||||
repoController := repo.ProvideController(config, db, provider, pathUID, authorizer, pathStore, repoStore, spaceStore, principalStore, gitrpcInterface)
|
||||
spaceController := space.ProvideController(db, provider, pathUID, authorizer, pathStore, spaceStore, repoStore, principalStore, repoController)
|
||||
principalInfoView := database.ProvidePrincipalInfoView(db)
|
||||
principalInfoCache := cache.ProvidePrincipalInfoCache(principalInfoView)
|
||||
spaceController := space.ProvideController(db, provider, pathUID, authorizer, pathStore, spaceStore, repoStore, principalStore, repoController, membershipStore)
|
||||
pullReqStore := database.ProvidePullReqStore(db, principalInfoCache)
|
||||
pullReqActivityStore := database.ProvidePullReqActivityStore(db, principalInfoCache)
|
||||
codeCommentView := database.ProvideCodeCommentView(db)
|
||||
|
|
|
@ -15,31 +15,34 @@ import (
|
|||
)
|
||||
|
||||
type Controller struct {
|
||||
db *sqlx.DB
|
||||
urlProvider *url.Provider
|
||||
uidCheck check.PathUID
|
||||
authorizer authz.Authorizer
|
||||
pathStore store.PathStore
|
||||
spaceStore store.SpaceStore
|
||||
repoStore store.RepoStore
|
||||
principalStore store.PrincipalStore
|
||||
repoCtrl *repo.Controller
|
||||
db *sqlx.DB
|
||||
urlProvider *url.Provider
|
||||
uidCheck check.PathUID
|
||||
authorizer authz.Authorizer
|
||||
pathStore store.PathStore
|
||||
spaceStore store.SpaceStore
|
||||
repoStore store.RepoStore
|
||||
principalStore store.PrincipalStore
|
||||
repoCtrl *repo.Controller
|
||||
membershipStore store.MembershipStore
|
||||
}
|
||||
|
||||
func NewController(db *sqlx.DB, urlProvider *url.Provider,
|
||||
uidCheck check.PathUID, authorizer authz.Authorizer,
|
||||
pathStore store.PathStore, spaceStore store.SpaceStore,
|
||||
repoStore store.RepoStore, principalStore store.PrincipalStore, repoCtrl *repo.Controller,
|
||||
membershipStore store.MembershipStore,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
db: db,
|
||||
urlProvider: urlProvider,
|
||||
uidCheck: uidCheck,
|
||||
authorizer: authorizer,
|
||||
pathStore: pathStore,
|
||||
spaceStore: spaceStore,
|
||||
repoStore: repoStore,
|
||||
principalStore: principalStore,
|
||||
repoCtrl: repoCtrl,
|
||||
db: db,
|
||||
urlProvider: urlProvider,
|
||||
uidCheck: uidCheck,
|
||||
authorizer: authorizer,
|
||||
pathStore: pathStore,
|
||||
spaceStore: spaceStore,
|
||||
repoStore: repoStore,
|
||||
principalStore: principalStore,
|
||||
repoCtrl: repoCtrl,
|
||||
membershipStore: membershipStore,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
apiauth "github.com/harness/gitness/internal/api/auth"
|
||||
"github.com/harness/gitness/internal/api/usererror"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/internal/bootstrap"
|
||||
"github.com/harness/gitness/internal/paths"
|
||||
"github.com/harness/gitness/store/database/dbtx"
|
||||
"github.com/harness/gitness/types"
|
||||
|
@ -84,14 +85,32 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea
|
|||
TargetType: enum.PathTargetTypeSpace,
|
||||
TargetID: space.ID,
|
||||
CreatedBy: space.CreatedBy,
|
||||
Created: space.Created,
|
||||
Updated: space.Updated,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
err = c.pathStore.Create(ctx, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create path: %w", err)
|
||||
}
|
||||
|
||||
// add space membership to top level space only (as the user doesn't have inhereted permissions alraedy)
|
||||
if in.ParentID == 0 {
|
||||
membership := &types.Membership{
|
||||
SpaceID: space.ID,
|
||||
PrincipalID: session.Principal.ID,
|
||||
Role: enum.MembershipRoleSpaceOwner,
|
||||
|
||||
// membership has been created by the system
|
||||
CreatedBy: bootstrap.NewSystemServiceSession().Principal.ID,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
err = c.membershipStore.Create(ctx, membership)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make user owner of the space: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apiauth "github.com/harness/gitness/internal/api/auth"
|
||||
"github.com/harness/gitness/internal/api/usererror"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type MembershipAddInput struct {
|
||||
UserUID string `json:"user_uid"`
|
||||
Role enum.MembershipRole `json:"role"`
|
||||
}
|
||||
|
||||
func (in *MembershipAddInput) Validate() error {
|
||||
if in.UserUID == "" {
|
||||
return usererror.BadRequest("UserUID must be provided")
|
||||
}
|
||||
|
||||
if in.Role == "" {
|
||||
return usererror.BadRequest("Role must be provided")
|
||||
}
|
||||
|
||||
role, ok := in.Role.Sanitize()
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("Provided role '%s' is not suppored. Valid values are: %v",
|
||||
in.Role, enum.MembershipRoles)
|
||||
return usererror.BadRequest(msg)
|
||||
}
|
||||
|
||||
in.Role = role
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MembershipAdd adds a new membership to a space.
|
||||
func (c *Controller) MembershipAdd(ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
in *MembershipAddInput,
|
||||
) (*types.Membership, error) {
|
||||
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceEdit, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = in.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := c.principalStore.FindUserByUID(ctx, in.UserUID)
|
||||
if errors.Is(err, store.ErrResourceNotFound) {
|
||||
return nil, usererror.BadRequestf("User '%s' not found", in.UserUID)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to find the user: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
membership := &types.Membership{
|
||||
SpaceID: space.ID,
|
||||
PrincipalID: user.ID,
|
||||
CreatedBy: session.Principal.ID,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
Role: in.Role,
|
||||
|
||||
Principal: *user.ToPrincipalInfo(),
|
||||
AdddedBy: *session.Principal.ToPrincipalInfo(),
|
||||
}
|
||||
|
||||
err = c.membershipStore.Create(ctx, membership)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new membership: %w", err)
|
||||
}
|
||||
|
||||
return membership, nil
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apiauth "github.com/harness/gitness/internal/api/auth"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
// MembershipDelete removes an existing membership from a space.
|
||||
func (c *Controller) MembershipDelete(ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
userUID string,
|
||||
) error {
|
||||
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceEdit, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := c.principalStore.FindUserByUID(ctx, userUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find user by uid: %w", err)
|
||||
}
|
||||
|
||||
err = c.membershipStore.Delete(ctx, types.MembershipKey{
|
||||
SpaceID: space.ID,
|
||||
PrincipalID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete user membership: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apiauth "github.com/harness/gitness/internal/api/auth"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
// MembershipList lists all space memberships.
|
||||
func (c *Controller) MembershipList(ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
) ([]*types.Membership, error) {
|
||||
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceView, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memberships, err := c.membershipStore.ListForSpace(ctx, space.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list memberships for space: %w", err)
|
||||
}
|
||||
|
||||
return memberships, nil
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apiauth "github.com/harness/gitness/internal/api/auth"
|
||||
"github.com/harness/gitness/internal/api/usererror"
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
type MembershipUpdateInput struct {
|
||||
Role enum.MembershipRole `json:"role"`
|
||||
}
|
||||
|
||||
func (in *MembershipUpdateInput) Validate() error {
|
||||
if in.Role == "" {
|
||||
return usererror.BadRequest("Role must be provided")
|
||||
}
|
||||
|
||||
role, ok := in.Role.Sanitize()
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("Provided role '%s' is not suppored. Valid values are: %v",
|
||||
in.Role, enum.MembershipRoles)
|
||||
return usererror.BadRequest(msg)
|
||||
}
|
||||
|
||||
in.Role = role
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MembershipUpdate changes the role of an existing membership.
|
||||
func (c *Controller) MembershipUpdate(ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
userUID string,
|
||||
in *MembershipUpdateInput,
|
||||
) (*types.Membership, error) {
|
||||
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceEdit, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = in.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := c.principalStore.FindUserByUID(ctx, userUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find user by uid: %w", err)
|
||||
}
|
||||
|
||||
membership, err := c.membershipStore.Find(ctx, types.MembershipKey{
|
||||
SpaceID: space.ID,
|
||||
PrincipalID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find membership for update: %w", err)
|
||||
}
|
||||
|
||||
if membership.Role == in.Role {
|
||||
return membership, nil
|
||||
}
|
||||
|
||||
membership.Role = in.Role
|
||||
|
||||
err = c.membershipStore.Update(ctx, membership)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update membership")
|
||||
}
|
||||
|
||||
return membership, nil
|
||||
}
|
|
@ -22,6 +22,11 @@ var WireSet = wire.NewSet(
|
|||
|
||||
func ProvideController(db *sqlx.DB, urlProvider *url.Provider, uidCheck check.PathUID, authorizer authz.Authorizer,
|
||||
pathStore store.PathStore, spaceStore store.SpaceStore, repoStore store.RepoStore,
|
||||
principalStore store.PrincipalStore, repoCtrl *repo.Controller) *Controller {
|
||||
return NewController(db, urlProvider, uidCheck, authorizer, pathStore, spaceStore, repoStore, principalStore, repoCtrl)
|
||||
principalStore store.PrincipalStore, repoCtrl *repo.Controller,
|
||||
membershipStore store.MembershipStore,
|
||||
) *Controller {
|
||||
return NewController(db, urlProvider, uidCheck, authorizer,
|
||||
pathStore, spaceStore, repoStore,
|
||||
principalStore, repoCtrl,
|
||||
membershipStore)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/space"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
)
|
||||
|
||||
// HandleMembershipAdd handles API that adds a new membership to a space.
|
||||
func HandleMembershipAdd(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
in := new(space.MembershipAddInput)
|
||||
err = json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
memberInfo, err := spaceCtrl.MembershipAdd(ctx, session, spaceRef, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, http.StatusCreated, memberInfo)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/space"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
)
|
||||
|
||||
// HandleMembershipDelete handles API that deletes an existing space membership.
|
||||
func HandleMembershipDelete(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
userUID, err := request.GetUserUIDFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = spaceCtrl.MembershipDelete(ctx, session, spaceRef, userUID)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.DeleteSuccessful(w)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/space"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
)
|
||||
|
||||
// HandleMembershipList handles API that lists all memberships of a space.
|
||||
func HandleMembershipList(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
memberInfos, err := spaceCtrl.MembershipList(ctx, session, spaceRef)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, http.StatusOK, memberInfos)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/internal/api/controller/space"
|
||||
"github.com/harness/gitness/internal/api/render"
|
||||
"github.com/harness/gitness/internal/api/request"
|
||||
)
|
||||
|
||||
// HandleMembershipUpdate handles API that changes the role of an existing space membership.
|
||||
func HandleMembershipUpdate(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
userUID, err := request.GetUserUIDFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
in := new(space.MembershipUpdateInput)
|
||||
err = json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
memberInfo, err := spaceCtrl.MembershipUpdate(ctx, session, spaceRef, userUID, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, http.StatusOK, memberInfo)
|
||||
}
|
||||
}
|
|
@ -244,4 +244,60 @@ func spaceOperations(reflector *openapi3.Reflector) {
|
|||
_ = reflector.SetJSONResponse(&onDeletePath, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&onDeletePath, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodDelete, "/spaces/{space_ref}/paths/{path_id}", onDeletePath)
|
||||
|
||||
opMembershipAdd := openapi3.Operation{}
|
||||
opMembershipAdd.WithTags("space")
|
||||
opMembershipAdd.WithMapOfAnything(map[string]interface{}{"operationId": "membershipAdd"})
|
||||
_ = reflector.SetRequest(&opMembershipAdd, struct {
|
||||
spaceRequest
|
||||
space.MembershipAddInput
|
||||
}{}, http.MethodPost)
|
||||
_ = reflector.SetJSONResponse(&opMembershipAdd, &types.Membership{}, http.StatusCreated)
|
||||
_ = reflector.SetJSONResponse(&opMembershipAdd, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opMembershipAdd, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opMembershipAdd, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opMembershipAdd, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/members", opMembershipAdd)
|
||||
|
||||
opMembershipDelete := openapi3.Operation{}
|
||||
opMembershipDelete.WithTags("space")
|
||||
opMembershipDelete.WithMapOfAnything(map[string]interface{}{"operationId": "membershipDelete"})
|
||||
_ = reflector.SetRequest(&opMembershipDelete, struct {
|
||||
spaceRequest
|
||||
UserUID string `path:"user_uid"`
|
||||
}{}, http.MethodDelete)
|
||||
_ = reflector.SetJSONResponse(&opMembershipDelete, nil, http.StatusNoContent)
|
||||
_ = reflector.SetJSONResponse(&opMembershipDelete, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opMembershipDelete, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opMembershipDelete, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opMembershipDelete, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodDelete, "/spaces/{space_ref}/members/{user_uid}", opMembershipDelete)
|
||||
|
||||
opMembershipUpdate := openapi3.Operation{}
|
||||
opMembershipUpdate.WithTags("space")
|
||||
opMembershipUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "membershipUpdate"})
|
||||
_ = reflector.SetRequest(&opMembershipUpdate, &struct {
|
||||
spaceRequest
|
||||
UserUID string `path:"user_uid"`
|
||||
space.MembershipUpdateInput
|
||||
}{}, http.MethodPatch)
|
||||
_ = reflector.SetJSONResponse(&opMembershipUpdate, &types.Membership{}, http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opMembershipUpdate, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opMembershipUpdate, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opMembershipUpdate, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opMembershipUpdate, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodPatch, "/spaces/{space_ref}/members/{user_uid}", opMembershipUpdate)
|
||||
|
||||
opMembershipList := openapi3.Operation{}
|
||||
opMembershipList.WithTags("space")
|
||||
opMembershipList.WithMapOfAnything(map[string]interface{}{"operationId": "membershipList"})
|
||||
_ = reflector.SetRequest(&opMembershipList, &struct {
|
||||
spaceRequest
|
||||
}{}, http.MethodGet)
|
||||
_ = reflector.SetJSONResponse(&opMembershipList, []types.Membership{}, http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opMembershipList, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opMembershipList, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opMembershipList, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opMembershipList, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/members", opMembershipList)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/harness/gitness/internal/auth"
|
||||
"github.com/harness/gitness/internal/paths"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var _ Authorizer = (*MembershipAuthorizer)(nil)
|
||||
|
||||
type MembershipAuthorizer struct {
|
||||
permissionCache PermissionCache
|
||||
}
|
||||
|
||||
func NewMembershipAuthorizer(
|
||||
permissionCache PermissionCache,
|
||||
) *MembershipAuthorizer {
|
||||
return &MembershipAuthorizer{
|
||||
permissionCache: permissionCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *MembershipAuthorizer) Check(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
scope *types.Scope,
|
||||
resource *types.Resource,
|
||||
permission enum.Permission,
|
||||
) (bool, error) {
|
||||
log.Ctx(ctx).Debug().Msgf(
|
||||
"[MembershipAuthorizer] %s with id '%d' requests %s for %s '%s' in scope %#v with metadata %#v",
|
||||
session.Principal.Type,
|
||||
session.Principal.ID,
|
||||
permission,
|
||||
resource.Type,
|
||||
resource.Name,
|
||||
scope,
|
||||
session.Metadata,
|
||||
)
|
||||
|
||||
if session.Principal.Admin {
|
||||
return true, nil // system admin can call any API
|
||||
}
|
||||
|
||||
var spaceRef string
|
||||
|
||||
switch resource.Type {
|
||||
case enum.ResourceTypeSpace:
|
||||
spaceRef = paths.Concatinate(scope.SpacePath, resource.Name)
|
||||
|
||||
case enum.ResourceTypeRepo:
|
||||
spaceRef = scope.SpacePath
|
||||
|
||||
case enum.ResourceTypeServiceAccount:
|
||||
spaceRef = scope.SpacePath
|
||||
|
||||
case enum.ResourceTypeUser:
|
||||
// a user is allowed to view / edit themselves
|
||||
if resource.Name == session.Principal.UID &&
|
||||
(permission == enum.PermissionUserView || permission == enum.PermissionUserEdit) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// everything else is reserved for admins only (like operations on users other than yourself, or setting admin)
|
||||
return false, nil
|
||||
|
||||
// Service operations aren't exposed to users
|
||||
case enum.ResourceTypeService:
|
||||
return false, nil
|
||||
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return a.permissionCache.Get(ctx, PermissionCacheKey{
|
||||
PrincipalID: session.Principal.ID,
|
||||
SpaceRef: spaceRef,
|
||||
Permission: permission,
|
||||
})
|
||||
}
|
||||
func (a *MembershipAuthorizer) CheckAll(ctx context.Context, session *auth.Session,
|
||||
permissionChecks ...types.PermissionCheck) (bool, error) {
|
||||
for _, p := range permissionChecks {
|
||||
if _, err := a.Check(ctx, session, &p.Scope, &p.Resource, p.Permission); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/cache"
|
||||
"github.com/harness/gitness/internal/paths"
|
||||
"github.com/harness/gitness/internal/store"
|
||||
gitness_store "github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type PermissionCacheKey struct {
|
||||
PrincipalID int64
|
||||
SpaceRef string
|
||||
Permission enum.Permission
|
||||
}
|
||||
type PermissionCache cache.Cache[PermissionCacheKey, bool]
|
||||
|
||||
func NewPermissionCache(
|
||||
spaceStore store.SpaceStore,
|
||||
membershipStore store.MembershipStore,
|
||||
cacheDuration time.Duration,
|
||||
) PermissionCache {
|
||||
return cache.New[PermissionCacheKey, bool](permissionCacheGetter{
|
||||
spaceStore: spaceStore,
|
||||
membershipStore: membershipStore,
|
||||
}, cacheDuration)
|
||||
}
|
||||
|
||||
type permissionCacheGetter struct {
|
||||
spaceStore store.SpaceStore
|
||||
membershipStore store.MembershipStore
|
||||
}
|
||||
|
||||
func (g permissionCacheGetter) Find(ctx context.Context, key PermissionCacheKey) (bool, error) {
|
||||
spaceRef := key.SpaceRef
|
||||
principalID := key.PrincipalID
|
||||
|
||||
// Find the starting space.
|
||||
space, err := g.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to find space '%s': %w", spaceRef, err)
|
||||
}
|
||||
|
||||
// limit the depth to be safe (e.g. root/space1/space2 => maxDepth of 3)
|
||||
maxDepth := len(paths.Segments(spaceRef))
|
||||
|
||||
for depth := 0; depth < maxDepth; depth++ {
|
||||
// Find the membership in the current space.
|
||||
membership, err := g.membershipStore.Find(ctx, types.MembershipKey{
|
||||
SpaceID: space.ID,
|
||||
PrincipalID: principalID,
|
||||
})
|
||||
if err != nil && !errors.Is(err, gitness_store.ErrResourceNotFound) {
|
||||
return false, fmt.Errorf("failed to find membership: %w", err)
|
||||
}
|
||||
|
||||
// If the membership is defined in the current space, check if the user has the required permission.
|
||||
if membership != nil {
|
||||
_, hasRole := slices.BinarySearch(membership.Role.Permissions(), key.Permission)
|
||||
if hasRole {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If membership with the requested permission has not been found in the current space,
|
||||
// move to the parent space, if any.
|
||||
|
||||
if space.ParentID == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
space, err = g.spaceStore.Find(ctx, space.ParentID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to find parent space with id %d: %w", space.ParentID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -5,14 +5,27 @@
|
|||
package authz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/store"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// WireSet provides a wire set for this package.
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideAuthorizer,
|
||||
ProvidePermissionCache,
|
||||
)
|
||||
|
||||
func ProvideAuthorizer() Authorizer {
|
||||
return NewUnsafeAuthorizer()
|
||||
func ProvideAuthorizer(pCache PermissionCache) Authorizer {
|
||||
return NewMembershipAuthorizer(pCache)
|
||||
}
|
||||
|
||||
func ProvidePermissionCache(
|
||||
spaceStore store.SpaceStore,
|
||||
membershipStore store.MembershipStore,
|
||||
) PermissionCache {
|
||||
const permissionCacheTimeout = time.Second * 15
|
||||
return NewPermissionCache(spaceStore, membershipStore, permissionCacheTimeout)
|
||||
}
|
||||
|
|
|
@ -160,6 +160,15 @@ func setupSpaces(r chi.Router, spaceCtrl *space.Controller, repoCtrl *repo.Contr
|
|||
r.Delete("/", handlerspace.HandleDeletePath(spaceCtrl))
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/members", func(r chi.Router) {
|
||||
r.Get("/", handlerspace.HandleMembershipList(spaceCtrl))
|
||||
r.Post("/", handlerspace.HandleMembershipAdd(spaceCtrl))
|
||||
r.Route(fmt.Sprintf("/{%s}", request.PathParamUserUID), func(r chi.Router) {
|
||||
r.Delete("/", handlerspace.HandleMembershipDelete(spaceCtrl))
|
||||
r.Patch("/", handlerspace.HandleMembershipUpdate(spaceCtrl))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -223,6 +223,15 @@ type (
|
|||
Find(ctx context.Context, id int64) (*types.RepositoryGitInfo, error)
|
||||
}
|
||||
|
||||
// MembershipStore defines the membership data storage.
|
||||
MembershipStore interface {
|
||||
Find(ctx context.Context, key types.MembershipKey) (*types.Membership, error)
|
||||
Create(ctx context.Context, v *types.Membership) error
|
||||
Update(ctx context.Context, membership *types.Membership) error
|
||||
Delete(ctx context.Context, key types.MembershipKey) error
|
||||
ListForSpace(ctx context.Context, spaceID int64) ([]*types.Membership, error)
|
||||
}
|
||||
|
||||
// TokenStore defines the token data storage.
|
||||
TokenStore interface {
|
||||
// Find finds the token by id
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/internal/store"
|
||||
"github.com/harness/gitness/store/database"
|
||||
"github.com/harness/gitness/store/database/dbtx"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ store.MembershipStore = (*MembershipStore)(nil)
|
||||
|
||||
// NewMembershipStore returns a new MembershipStore.
|
||||
func NewMembershipStore(db *sqlx.DB, pCache store.PrincipalInfoCache) *MembershipStore {
|
||||
return &MembershipStore{
|
||||
db: db,
|
||||
pCache: pCache,
|
||||
}
|
||||
}
|
||||
|
||||
// MembershipStore implements store.MembershipStore backed by a relational database.
|
||||
type MembershipStore struct {
|
||||
db *sqlx.DB
|
||||
pCache store.PrincipalInfoCache
|
||||
}
|
||||
|
||||
type membership struct {
|
||||
SpaceID int64 `db:"membership_space_id"`
|
||||
PrincipalID int64 `db:"membership_principal_id"`
|
||||
|
||||
CreatedBy int64 `db:"membership_created_by"`
|
||||
Created int64 `db:"membership_created"`
|
||||
Updated int64 `db:"membership_updated"`
|
||||
|
||||
Role enum.MembershipRole `db:"membership_role"`
|
||||
}
|
||||
|
||||
const (
|
||||
membershipColumns = `
|
||||
membership_space_id
|
||||
,membership_principal_id
|
||||
,membership_created_by
|
||||
,membership_created
|
||||
,membership_updated
|
||||
,membership_role`
|
||||
|
||||
membershipSelectBase = `
|
||||
SELECT` + membershipColumns + `
|
||||
FROM memberships`
|
||||
)
|
||||
|
||||
// Find finds the membership by space id and principal id.
|
||||
func (s *MembershipStore) Find(ctx context.Context, key types.MembershipKey) (*types.Membership, error) {
|
||||
const sqlQuery = membershipSelectBase + `
|
||||
WHERE membership_space_id = $1 AND membership_principal_id = $2`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
dst := &membership{}
|
||||
if err := db.GetContext(ctx, dst, sqlQuery, key.SpaceID, key.PrincipalID); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(err, "Failed to find membership")
|
||||
}
|
||||
|
||||
return s.mapToMembership(ctx, dst), nil
|
||||
}
|
||||
|
||||
// Create creates a new membership.
|
||||
func (s *MembershipStore) Create(ctx context.Context, membership *types.Membership) error {
|
||||
const sqlQuery = `
|
||||
INSERT INTO memberships (
|
||||
membership_space_id
|
||||
,membership_principal_id
|
||||
,membership_created_by
|
||||
,membership_created
|
||||
,membership_updated
|
||||
,membership_role
|
||||
) values (
|
||||
:membership_space_id
|
||||
,:membership_principal_id
|
||||
,:membership_created_by
|
||||
,:membership_created
|
||||
,:membership_updated
|
||||
,:membership_role
|
||||
)`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
query, arg, err := db.BindNamed(sqlQuery, mapToInternalMembership(membership))
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(err, "Failed to bind membership object")
|
||||
}
|
||||
|
||||
if _, err = db.ExecContext(ctx, query, arg...); err != nil {
|
||||
return database.ProcessSQLErrorf(err, "Failed to insert membership")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates the role of a member of a space.
|
||||
func (s *MembershipStore) Update(ctx context.Context, membership *types.Membership) error {
|
||||
const sqlQuery = `
|
||||
UPDATE memberships
|
||||
SET
|
||||
membership_updated = :membership_updated
|
||||
,membership_role = :membership_role
|
||||
WHERE membership_space_id = :membership_space_id AND
|
||||
membership_principal_id = :membership_principal_id`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
dbMembership := mapToInternalMembership(membership)
|
||||
dbMembership.Updated = time.Now().UnixMilli()
|
||||
|
||||
query, arg, err := db.BindNamed(sqlQuery, dbMembership)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(err, "Failed to bind membership object")
|
||||
}
|
||||
|
||||
_, err = db.ExecContext(ctx, query, arg...)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(err, "Failed to update membership role")
|
||||
}
|
||||
|
||||
membership.Updated = dbMembership.Updated
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes the membership.
|
||||
func (s *MembershipStore) Delete(ctx context.Context, key types.MembershipKey) error {
|
||||
const sqlQuery = `
|
||||
DELETE from memberships
|
||||
WHERE membership_space_id = $1 AND
|
||||
membership_principal_id = $2`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
if _, err := db.ExecContext(ctx, sqlQuery, key.SpaceID, key.PrincipalID); err != nil {
|
||||
return database.ProcessSQLErrorf(err, "delete membership query failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListForSpace returns a list of memberships for a space.
|
||||
func (s *MembershipStore) ListForSpace(ctx context.Context, spaceID int64) ([]*types.Membership, error) {
|
||||
stmt := database.Builder.
|
||||
Select(membershipColumns).
|
||||
From("memberships").
|
||||
Where("membership_space_id = ?", spaceID).
|
||||
OrderBy("membership_created asc")
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to convert membership for space list query to sql")
|
||||
}
|
||||
|
||||
dst := make([]*membership, 0)
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(err, "Failed executing membership list query")
|
||||
}
|
||||
|
||||
result, err := s.mapToMemberships(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map memberships to external type: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mapToMembershipNoPrincipalInfo(m *membership) *types.Membership {
|
||||
return &types.Membership{
|
||||
SpaceID: m.SpaceID,
|
||||
PrincipalID: m.PrincipalID,
|
||||
CreatedBy: m.CreatedBy,
|
||||
Created: m.Created,
|
||||
Updated: m.Updated,
|
||||
Role: m.Role,
|
||||
}
|
||||
}
|
||||
|
||||
func mapToInternalMembership(m *types.Membership) *membership {
|
||||
return &membership{
|
||||
SpaceID: m.SpaceID,
|
||||
PrincipalID: m.PrincipalID,
|
||||
CreatedBy: m.CreatedBy,
|
||||
Created: m.Created,
|
||||
Updated: m.Updated,
|
||||
Role: m.Role,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MembershipStore) mapToMembership(ctx context.Context, m *membership) *types.Membership {
|
||||
res := mapToMembershipNoPrincipalInfo(m)
|
||||
|
||||
addedBy, err := s.pCache.Get(ctx, res.CreatedBy)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("failed to load membership creator")
|
||||
}
|
||||
if addedBy != nil {
|
||||
res.AdddedBy = *addedBy
|
||||
}
|
||||
|
||||
principal, err := s.pCache.Get(ctx, res.PrincipalID)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("failed to load membership principal")
|
||||
}
|
||||
if principal != nil {
|
||||
res.Principal = *principal
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *MembershipStore) mapToMemberships(ctx context.Context, ms []*membership) ([]*types.Membership, error) {
|
||||
// collect all principal IDs
|
||||
ids := make([]int64, 0, 2*len(ms))
|
||||
for _, m := range ms {
|
||||
ids = append(ids, m.CreatedBy, m.PrincipalID)
|
||||
}
|
||||
|
||||
// pull principal infos from cache
|
||||
infoMap, err := s.pCache.Map(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load membership principal infos: %w", err)
|
||||
}
|
||||
|
||||
// attach the principal infos back to the slice items
|
||||
res := make([]*types.Membership, len(ms))
|
||||
for i, m := range ms {
|
||||
res[i] = mapToMembershipNoPrincipalInfo(m)
|
||||
if addedBy, ok := infoMap[m.CreatedBy]; ok {
|
||||
res[i].AdddedBy = *addedBy
|
||||
}
|
||||
if principal, ok := infoMap[m.PrincipalID]; ok {
|
||||
res[i].Principal = *principal
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE memberships;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
CREATE TABLE memberships (
|
||||
membership_space_id INTEGER NOT NULL
|
||||
,membership_principal_id INTEGER NOT NULL
|
||||
,membership_created_by INTEGER NOT NULL
|
||||
,membership_created BIGINT NOT NULL
|
||||
,membership_updated BIGINT NOT NULL
|
||||
,membership_role TEXT NOT NULL
|
||||
,CONSTRAINT pk_memberships PRIMARY KEY (membership_space_id, membership_principal_id)
|
||||
,CONSTRAINT fk_membership_space_id FOREIGN KEY (membership_space_id)
|
||||
REFERENCES spaces (space_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
,CONSTRAINT fk_membership_principal_id FOREIGN KEY (membership_principal_id)
|
||||
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
,CONSTRAINT fk_membership_created_by FOREIGN KEY (membership_created_by)
|
||||
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION
|
||||
);
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE memberships;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
CREATE TABLE memberships (
|
||||
membership_space_id INTEGER NOT NULL
|
||||
,membership_principal_id INTEGER NOT NULL
|
||||
,membership_created_by INTEGER NOT NULL
|
||||
,membership_created BIGINT NOT NULL
|
||||
,membership_updated BIGINT NOT NULL
|
||||
,membership_role TEXT NOT NULL
|
||||
,CONSTRAINT pk_memberships PRIMARY KEY (membership_space_id, membership_principal_id)
|
||||
,CONSTRAINT fk_membership_space_id FOREIGN KEY (membership_space_id)
|
||||
REFERENCES spaces (space_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
,CONSTRAINT fk_membership_principal_id FOREIGN KEY (membership_principal_id)
|
||||
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
,CONSTRAINT fk_membership_created_by FOREIGN KEY (membership_created_by)
|
||||
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION
|
||||
);
|
|
@ -24,6 +24,7 @@ var WireSet = wire.NewSet(
|
|||
ProvideSpaceStore,
|
||||
ProvideRepoStore,
|
||||
ProvideRepoGitInfoView,
|
||||
ProvideMembershipStore,
|
||||
ProvideTokenStore,
|
||||
ProvidePullReqStore,
|
||||
ProvidePullReqActivityStore,
|
||||
|
@ -36,7 +37,7 @@ var WireSet = wire.NewSet(
|
|||
ProvideReqCheckStore,
|
||||
)
|
||||
|
||||
// helper function to setup the database by performing automated
|
||||
// migrator is helper function to set up the database by performing automated
|
||||
// database migration steps.
|
||||
func migrator(ctx context.Context, db *sqlx.DB) error {
|
||||
return migrate.Migrate(ctx, db)
|
||||
|
@ -82,6 +83,13 @@ func ProvideRepoGitInfoView(db *sqlx.DB) store.RepoGitInfoView {
|
|||
return NewRepoGitInfoView(db)
|
||||
}
|
||||
|
||||
func ProvideMembershipStore(
|
||||
db *sqlx.DB,
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
) store.MembershipStore {
|
||||
return NewMembershipStore(db, principalInfoCache)
|
||||
}
|
||||
|
||||
// ProvideTokenStore provides a token store.
|
||||
func ProvideTokenStore(db *sqlx.DB) store.TokenStore {
|
||||
return NewTokenStore(db)
|
||||
|
@ -89,13 +97,15 @@ func ProvideTokenStore(db *sqlx.DB) store.TokenStore {
|
|||
|
||||
// ProvidePullReqStore provides a pull request store.
|
||||
func ProvidePullReqStore(db *sqlx.DB,
|
||||
principalInfoCache store.PrincipalInfoCache) store.PullReqStore {
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
) store.PullReqStore {
|
||||
return NewPullReqStore(db, principalInfoCache)
|
||||
}
|
||||
|
||||
// ProvidePullReqActivityStore provides a pull request activity store.
|
||||
func ProvidePullReqActivityStore(db *sqlx.DB,
|
||||
principalInfoCache store.PrincipalInfoCache) store.PullReqActivityStore {
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
) store.PullReqActivityStore {
|
||||
return NewPullReqActivityStore(db, principalInfoCache)
|
||||
}
|
||||
|
||||
|
@ -111,7 +121,8 @@ func ProvidePullReqReviewStore(db *sqlx.DB) store.PullReqReviewStore {
|
|||
|
||||
// ProvidePullReqReviewerStore provides a pull request reviewer store.
|
||||
func ProvidePullReqReviewerStore(db *sqlx.DB,
|
||||
principalInfoCache store.PrincipalInfoCache) store.PullReqReviewerStore {
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
) store.PullReqReviewerStore {
|
||||
return NewPullReqReviewerStore(db, principalInfoCache)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ import (
|
|||
func isSQLUniqueConstraintError(original error) bool {
|
||||
var sqliteErr sqlite3.Error
|
||||
if errors.As(original, &sqliteErr) {
|
||||
return errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique)
|
||||
return errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) ||
|
||||
errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintPrimaryKey)
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package enum
|
||||
|
||||
import "golang.org/x/exp/slices"
|
||||
|
||||
// MembershipRole represents the different level of space memberships (permission set).
|
||||
type MembershipRole string
|
||||
|
||||
func (MembershipRole) Enum() []interface{} { return toInterfaceSlice(MembershipRoles) }
|
||||
func (m MembershipRole) Sanitize() (MembershipRole, bool) { return Sanitize(m, GetAllMembershipRoles) }
|
||||
func GetAllMembershipRoles() ([]MembershipRole, MembershipRole) { return MembershipRoles, "" }
|
||||
|
||||
var MembershipRoles = sortEnum([]MembershipRole{
|
||||
MembershipRoleReader,
|
||||
MembershipRoleExecutor,
|
||||
MembershipRoleContributor,
|
||||
MembershipRoleSpaceOwner,
|
||||
})
|
||||
|
||||
var membershipRoleReaderPermissions = slices.Clip(slices.Insert([]Permission{}, 0,
|
||||
PermissionRepoView,
|
||||
PermissionSpaceView,
|
||||
PermissionServiceAccountView,
|
||||
))
|
||||
|
||||
var membershipRoleExecutorPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0,
|
||||
PermissionCommitCheckReport,
|
||||
))
|
||||
|
||||
var membershipRoleContributorPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0,
|
||||
PermissionRepoPush,
|
||||
))
|
||||
|
||||
var membershipRoleSpaceOwnerPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0,
|
||||
PermissionRepoEdit,
|
||||
PermissionRepoDelete,
|
||||
PermissionRepoPush,
|
||||
PermissionCommitCheckReport,
|
||||
|
||||
PermissionSpaceEdit,
|
||||
PermissionSpaceCreate,
|
||||
PermissionSpaceDelete,
|
||||
|
||||
PermissionServiceAccountCreate,
|
||||
PermissionServiceAccountEdit,
|
||||
PermissionServiceAccountDelete,
|
||||
))
|
||||
|
||||
func init() {
|
||||
slices.Sort(membershipRoleReaderPermissions)
|
||||
slices.Sort(membershipRoleExecutorPermissions)
|
||||
slices.Sort(membershipRoleContributorPermissions)
|
||||
slices.Sort(membershipRoleSpaceOwnerPermissions)
|
||||
}
|
||||
|
||||
// Permissions returns the list of permissions for the role.
|
||||
func (m MembershipRole) Permissions() []Permission {
|
||||
switch m {
|
||||
case MembershipRoleReader:
|
||||
return membershipRoleReaderPermissions
|
||||
case MembershipRoleExecutor:
|
||||
return membershipRoleExecutorPermissions
|
||||
case MembershipRoleContributor:
|
||||
return membershipRoleContributorPermissions
|
||||
case MembershipRoleSpaceOwner:
|
||||
return membershipRoleSpaceOwnerPermissions
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
MembershipRoleReader MembershipRole = "reader"
|
||||
MembershipRoleExecutor MembershipRole = "executor"
|
||||
MembershipRoleContributor MembershipRole = "contributor"
|
||||
MembershipRoleSpaceOwner MembershipRole = "space_owner"
|
||||
)
|
|
@ -47,7 +47,7 @@ const (
|
|||
PermissionUserView Permission = "user_view"
|
||||
PermissionUserEdit Permission = "user_edit"
|
||||
PermissionUserDelete Permission = "user_delete"
|
||||
PermissionUserEditAdmin Permission = "user_editadmin"
|
||||
PermissionUserEditAdmin Permission = "user_editAdmin"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -68,7 +68,7 @@ const (
|
|||
PermissionServiceView Permission = "service_view"
|
||||
PermissionServiceEdit Permission = "service_edit"
|
||||
PermissionServiceDelete Permission = "service_delete"
|
||||
PermissionServiceEditAdmin Permission = "service_editadmin"
|
||||
PermissionServiceEditAdmin Permission = "service_editAdmin"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2022 Harness Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Polyform Free Trial License
|
||||
// that can be found in the LICENSE.md file for this repository.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
// MembershipKey can be used as a key for finding a user's space membership info.
|
||||
type MembershipKey struct {
|
||||
SpaceID int64
|
||||
PrincipalID int64
|
||||
}
|
||||
|
||||
// Membership represents a user's membership of a space.
|
||||
type Membership struct {
|
||||
SpaceID int64 `json:"-"`
|
||||
PrincipalID int64 `json:"-"`
|
||||
|
||||
CreatedBy int64 `json:"-"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
|
||||
Role enum.MembershipRole `json:"role"`
|
||||
|
||||
Principal PrincipalInfo `json:"principal"`
|
||||
AdddedBy PrincipalInfo `json:"added_by"`
|
||||
}
|
Loading…
Reference in New Issue