From 6d90195afa529d5dd51acb34271e670a5d1aedd2 Mon Sep 17 00:00:00 2001 From: calvin Date: Wed, 19 Jul 2023 16:34:27 -0600 Subject: [PATCH 01/31] fix: [code-645]: fix commit listing page sipacing --- web/src/components/CommitsView/CommitsView.tsx | 7 ++++++- .../pages/RepositoryCommits/RepositoryCommits.module.scss | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/components/CommitsView/CommitsView.tsx b/web/src/components/CommitsView/CommitsView.tsx index dde8ba5a5..17a3053a1 100644 --- a/web/src/components/CommitsView/CommitsView.tsx +++ b/web/src/components/CommitsView/CommitsView.tsx @@ -187,7 +187,12 @@ export function CommitsView({ + {getString('commitsOn', { date })} }> diff --git a/web/src/pages/RepositoryCommits/RepositoryCommits.module.scss b/web/src/pages/RepositoryCommits/RepositoryCommits.module.scss index ef45d5b40..33682b822 100644 --- a/web/src/pages/RepositoryCommits/RepositoryCommits.module.scss +++ b/web/src/pages/RepositoryCommits/RepositoryCommits.module.scss @@ -8,7 +8,7 @@ } .contentHeader { - padding-bottom: var(--spacing-xlarge) !important; + padding-bottom: var(--spacing-4) !important; > div { align-items: center; } From baa4eb5ac9328ef1635aa887a1b638463ca7a38b Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Thu, 20 Jul 2023 21:20:56 +0000 Subject: [PATCH 02/31] feat: [CODE-626,CODE-627]: space membership API&DB (#194) --- cmd/gitness/wire_gen.go | 18 +- internal/api/controller/space/controller.go | 39 +-- internal/api/controller/space/create.go | 23 +- .../api/controller/space/membership_add.go | 95 +++++++ .../api/controller/space/membership_delete.go | 46 ++++ .../api/controller/space/membership_list.go | 37 +++ .../api/controller/space/membership_update.go | 85 ++++++ internal/api/controller/space/wire.go | 9 +- internal/api/handler/space/membership_add.go | 43 +++ .../api/handler/space/membership_delete.go | 41 +++ internal/api/handler/space/membership_list.go | 35 +++ .../api/handler/space/membership_update.go | 49 ++++ internal/api/openapi/space.go | 56 ++++ internal/auth/authz/membership.go | 99 +++++++ internal/auth/authz/membership_cache.go | 91 +++++++ internal/auth/authz/wire.go | 17 +- internal/router/api.go | 9 + internal/store/database.go | 9 + internal/store/database/membership.go | 256 ++++++++++++++++++ .../0019_create_table_memberships.down.sql | 2 + .../0019_create_table_memberships.up.sql | 21 ++ .../0019_create_table_memberships.down.sql | 2 + .../0019_create_table_memberships.up.sql | 21 ++ internal/store/database/wire.go | 19 +- store/database/util_sqlite.go | 3 +- types/enum/membership_role.go | 80 ++++++ types/enum/permission.go | 4 +- types/membership.go | 30 ++ 28 files changed, 1200 insertions(+), 39 deletions(-) create mode 100644 internal/api/controller/space/membership_add.go create mode 100644 internal/api/controller/space/membership_delete.go create mode 100644 internal/api/controller/space/membership_list.go create mode 100644 internal/api/controller/space/membership_update.go create mode 100644 internal/api/handler/space/membership_add.go create mode 100644 internal/api/handler/space/membership_delete.go create mode 100644 internal/api/handler/space/membership_list.go create mode 100644 internal/api/handler/space/membership_update.go create mode 100644 internal/auth/authz/membership.go create mode 100644 internal/auth/authz/membership_cache.go create mode 100644 internal/store/database/membership.go create mode 100644 internal/store/database/migrate/postgres/0019_create_table_memberships.down.sql create mode 100644 internal/store/database/migrate/postgres/0019_create_table_memberships.up.sql create mode 100644 internal/store/database/migrate/sqlite/0019_create_table_memberships.down.sql create mode 100644 internal/store/database/migrate/sqlite/0019_create_table_memberships.up.sql create mode 100644 types/enum/membership_role.go create mode 100644 types/membership.go diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 4a3cfae61..10aabfc87 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -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) diff --git a/internal/api/controller/space/controller.go b/internal/api/controller/space/controller.go index 96e55201e..25f1b626f 100644 --- a/internal/api/controller/space/controller.go +++ b/internal/api/controller/space/controller.go @@ -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, } } diff --git a/internal/api/controller/space/create.go b/internal/api/controller/space/create.go index c5d27777a..02870e09b 100644 --- a/internal/api/controller/space/create.go +++ b/internal/api/controller/space/create.go @@ -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 { diff --git a/internal/api/controller/space/membership_add.go b/internal/api/controller/space/membership_add.go new file mode 100644 index 000000000..b3a8601c6 --- /dev/null +++ b/internal/api/controller/space/membership_add.go @@ -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 +} diff --git a/internal/api/controller/space/membership_delete.go b/internal/api/controller/space/membership_delete.go new file mode 100644 index 000000000..0bdc8601d --- /dev/null +++ b/internal/api/controller/space/membership_delete.go @@ -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 +} diff --git a/internal/api/controller/space/membership_list.go b/internal/api/controller/space/membership_list.go new file mode 100644 index 000000000..45588753d --- /dev/null +++ b/internal/api/controller/space/membership_list.go @@ -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 +} diff --git a/internal/api/controller/space/membership_update.go b/internal/api/controller/space/membership_update.go new file mode 100644 index 000000000..c07a0c68f --- /dev/null +++ b/internal/api/controller/space/membership_update.go @@ -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 +} diff --git a/internal/api/controller/space/wire.go b/internal/api/controller/space/wire.go index f49648337..d8b9cbbba 100644 --- a/internal/api/controller/space/wire.go +++ b/internal/api/controller/space/wire.go @@ -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) } diff --git a/internal/api/handler/space/membership_add.go b/internal/api/handler/space/membership_add.go new file mode 100644 index 000000000..48368cf71 --- /dev/null +++ b/internal/api/handler/space/membership_add.go @@ -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) + } +} diff --git a/internal/api/handler/space/membership_delete.go b/internal/api/handler/space/membership_delete.go new file mode 100644 index 000000000..eed309be6 --- /dev/null +++ b/internal/api/handler/space/membership_delete.go @@ -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) + } +} diff --git a/internal/api/handler/space/membership_list.go b/internal/api/handler/space/membership_list.go new file mode 100644 index 000000000..db7951868 --- /dev/null +++ b/internal/api/handler/space/membership_list.go @@ -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) + } +} diff --git a/internal/api/handler/space/membership_update.go b/internal/api/handler/space/membership_update.go new file mode 100644 index 000000000..ed00f112f --- /dev/null +++ b/internal/api/handler/space/membership_update.go @@ -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) + } +} diff --git a/internal/api/openapi/space.go b/internal/api/openapi/space.go index 9e5726b4e..0a95c73de 100644 --- a/internal/api/openapi/space.go +++ b/internal/api/openapi/space.go @@ -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) } diff --git a/internal/auth/authz/membership.go b/internal/auth/authz/membership.go new file mode 100644 index 000000000..0bcfc4f19 --- /dev/null +++ b/internal/auth/authz/membership.go @@ -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 +} diff --git a/internal/auth/authz/membership_cache.go b/internal/auth/authz/membership_cache.go new file mode 100644 index 000000000..b920c5b04 --- /dev/null +++ b/internal/auth/authz/membership_cache.go @@ -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 +} diff --git a/internal/auth/authz/wire.go b/internal/auth/authz/wire.go index 1e55f48a4..81cd2fc46 100644 --- a/internal/auth/authz/wire.go +++ b/internal/auth/authz/wire.go @@ -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) } diff --git a/internal/router/api.go b/internal/router/api.go index 7ef294eda..cae16b08a 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -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)) + }) + }) }) }) } diff --git a/internal/store/database.go b/internal/store/database.go index b59200dc5..fe1dedf15 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -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 diff --git a/internal/store/database/membership.go b/internal/store/database/membership.go new file mode 100644 index 000000000..a91c83f74 --- /dev/null +++ b/internal/store/database/membership.go @@ -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 +} diff --git a/internal/store/database/migrate/postgres/0019_create_table_memberships.down.sql b/internal/store/database/migrate/postgres/0019_create_table_memberships.down.sql new file mode 100644 index 000000000..cb23bba6b --- /dev/null +++ b/internal/store/database/migrate/postgres/0019_create_table_memberships.down.sql @@ -0,0 +1,2 @@ +DROP TABLE memberships; + diff --git a/internal/store/database/migrate/postgres/0019_create_table_memberships.up.sql b/internal/store/database/migrate/postgres/0019_create_table_memberships.up.sql new file mode 100644 index 000000000..e0b9ef5b7 --- /dev/null +++ b/internal/store/database/migrate/postgres/0019_create_table_memberships.up.sql @@ -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 +); diff --git a/internal/store/database/migrate/sqlite/0019_create_table_memberships.down.sql b/internal/store/database/migrate/sqlite/0019_create_table_memberships.down.sql new file mode 100644 index 000000000..cb23bba6b --- /dev/null +++ b/internal/store/database/migrate/sqlite/0019_create_table_memberships.down.sql @@ -0,0 +1,2 @@ +DROP TABLE memberships; + diff --git a/internal/store/database/migrate/sqlite/0019_create_table_memberships.up.sql b/internal/store/database/migrate/sqlite/0019_create_table_memberships.up.sql new file mode 100644 index 000000000..e0b9ef5b7 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0019_create_table_memberships.up.sql @@ -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 +); diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go index 4870f3d98..ff6a66870 100644 --- a/internal/store/database/wire.go +++ b/internal/store/database/wire.go @@ -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) } diff --git a/store/database/util_sqlite.go b/store/database/util_sqlite.go index 9eb47aef9..7c044fa57 100644 --- a/store/database/util_sqlite.go +++ b/store/database/util_sqlite.go @@ -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 diff --git a/types/enum/membership_role.go b/types/enum/membership_role.go new file mode 100644 index 000000000..f7888d2c2 --- /dev/null +++ b/types/enum/membership_role.go @@ -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" +) diff --git a/types/enum/permission.go b/types/enum/permission.go index 1a3302156..6824f035a 100644 --- a/types/enum/permission.go +++ b/types/enum/permission.go @@ -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 ( diff --git a/types/membership.go b/types/membership.go new file mode 100644 index 000000000..6520ff378 --- /dev/null +++ b/types/membership.go @@ -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"` +} From 05c0c256c3b2174fcd8cee7bac9d6260ea76bba9 Mon Sep 17 00:00:00 2001 From: calvin Date: Fri, 21 Jul 2023 16:18:50 -0600 Subject: [PATCH 03/31] fix: [code-657]: fix page issue --- web/src/pages/PullRequests/PullRequests.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/pages/PullRequests/PullRequests.tsx b/web/src/pages/PullRequests/PullRequests.tsx index 203181e40..ca5566a87 100644 --- a/web/src/pages/PullRequests/PullRequests.tsx +++ b/web/src/pages/PullRequests/PullRequests.tsx @@ -56,7 +56,9 @@ export default function PullRequests() { const [page, setPage] = usePageIndex(pageInit) useEffect(() => { - updateQueryParams({ page: page.toString() }) + if (page > 1) { + updateQueryParams({ page: page.toString() }) + } }, [setPage]) // eslint-disable-line react-hooks/exhaustive-deps const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata() From 9c8e578d9012d70b8eab05ec6945082ee25f613f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ga=C4=87e=C5=A1a?= Date: Mon, 24 Jul 2023 15:09:01 +0200 Subject: [PATCH 04/31] typo fixes --- internal/api/auth/auth.go | 21 ++++++++----------- internal/api/auth/repo.go | 13 ++++++------ internal/api/auth/service.go | 13 ++++++------ .../{serviceAccount.go => service_account.go} | 16 +++++++------- internal/api/auth/space.go | 13 ++++++------ internal/api/auth/user.go | 13 ++++++------ .../api/controller/space/membership_add.go | 2 +- internal/store/database/membership.go | 4 ++-- types/membership.go | 2 +- 9 files changed, 45 insertions(+), 52 deletions(-) rename internal/api/auth/{serviceAccount.go => service_account.go} (66%) diff --git a/internal/api/auth/auth.go b/internal/api/auth/auth.go index d19e8dce1..499164c04 100644 --- a/internal/api/auth/auth.go +++ b/internal/api/auth/auth.go @@ -25,13 +25,12 @@ var ( ErrParentResourceTypeUnknown = errors.New("Unknown parent resource type") ) -/* - * Check checks if a resource specific permission is granted for the current auth session in the scope. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// Check checks if a resource specific permission is granted for the current auth session in the scope. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func Check(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, - scope *types.Scope, resource *types.Resource, permission enum.Permission) error { + scope *types.Scope, resource *types.Resource, permission enum.Permission, +) error { if session == nil { return ErrNotAuthenticated } @@ -53,12 +52,10 @@ func Check(ctx context.Context, authorizer authz.Authorizer, session *auth.Sessi return nil } -/* - * CheckChild checks if a resource specific permission is granted for the current auth session - * in the scope of a parent. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// CheckChild checks if a resource specific permission is granted for the current auth session +// in the scope of a parent. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func CheckChild(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, spaceStore store.SpaceStore, repoStore store.RepoStore, parentType enum.ParentResourceType, parentID int64, resourceType enum.ResourceType, resourceName string, permission enum.Permission) error { diff --git a/internal/api/auth/repo.go b/internal/api/auth/repo.go index ddf0a8b8d..67b8c454b 100644 --- a/internal/api/auth/repo.go +++ b/internal/api/auth/repo.go @@ -16,14 +16,13 @@ import ( "github.com/pkg/errors" ) -/* - * CheckRepo checks if a repo specific permission is granted for the current auth session - * in the scope of its parent. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// CheckRepo checks if a repo specific permission is granted for the current auth session +// in the scope of its parent. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func CheckRepo(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, - repo *types.Repository, permission enum.Permission, orPublic bool) error { + repo *types.Repository, permission enum.Permission, orPublic bool, +) error { if orPublic && repo.IsPublic { return nil } diff --git a/internal/api/auth/service.go b/internal/api/auth/service.go index 03673bcc3..acc21fc24 100644 --- a/internal/api/auth/service.go +++ b/internal/api/auth/service.go @@ -13,14 +13,13 @@ import ( "github.com/harness/gitness/types/enum" ) -/* - * CheckService checks if a service specific permission is granted for the current auth session. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// CheckService checks if a service specific permission is granted for the current auth session. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func CheckService(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, - svc *types.Service, permission enum.Permission) error { - // a service exists outside of any scope + svc *types.Service, permission enum.Permission, +) error { + // a service exists outside any scope scope := &types.Scope{} resource := &types.Resource{ Type: enum.ResourceTypeService, diff --git a/internal/api/auth/serviceAccount.go b/internal/api/auth/service_account.go similarity index 66% rename from internal/api/auth/serviceAccount.go rename to internal/api/auth/service_account.go index 7a9c65a3b..332e79bc2 100644 --- a/internal/api/auth/serviceAccount.go +++ b/internal/api/auth/service_account.go @@ -13,15 +13,15 @@ import ( "github.com/harness/gitness/types/enum" ) -/* - * CheckServiceAccount checks if a service account specific permission is granted for the current auth session - * in the scope of the parent. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// CheckServiceAccount checks if a service account specific permission is granted for the current auth session +// in the scope of the parent. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func CheckServiceAccount(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, spaceStore store.SpaceStore, repoStore store.RepoStore, parentType enum.ParentResourceType, parentID int64, - saUID string, permission enum.Permission) error { - return CheckChild(ctx, authorizer, session, spaceStore, repoStore, parentType, parentID, + saUID string, permission enum.Permission, +) error { + return CheckChild(ctx, authorizer, session, + spaceStore, repoStore, parentType, parentID, enum.ResourceTypeServiceAccount, saUID, permission) } diff --git a/internal/api/auth/space.go b/internal/api/auth/space.go index 78e46bded..68c28ebe3 100644 --- a/internal/api/auth/space.go +++ b/internal/api/auth/space.go @@ -16,14 +16,13 @@ import ( "github.com/pkg/errors" ) -/* - * CheckSpace checks if a space specific permission is granted for the current auth session - * in the scope of its parent. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// CheckSpace checks if a space specific permission is granted for the current auth session +// in the scope of its parent. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func CheckSpace(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, - space *types.Space, permission enum.Permission, orPublic bool) error { + space *types.Space, permission enum.Permission, orPublic bool, +) error { if orPublic && space.IsPublic { return nil } diff --git a/internal/api/auth/user.go b/internal/api/auth/user.go index c0f766d51..d795eb5e3 100644 --- a/internal/api/auth/user.go +++ b/internal/api/auth/user.go @@ -13,14 +13,13 @@ import ( "github.com/harness/gitness/types/enum" ) -/* - * CheckUser checks if a user specific permission is granted for the current auth session. - * Returns nil if the permission is granted, otherwise returns an error. - * NotAuthenticated, NotAuthorized, or any unerlaying error. - */ +// CheckUser checks if a user specific permission is granted for the current auth session. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. func CheckUser(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, - user *types.User, permission enum.Permission) error { - // a user exists outside of any scope + user *types.User, permission enum.Permission, +) error { + // a user exists outside any scope scope := &types.Scope{} resource := &types.Resource{ Type: enum.ResourceTypeUser, diff --git a/internal/api/controller/space/membership_add.go b/internal/api/controller/space/membership_add.go index b3a8601c6..8538c1013 100644 --- a/internal/api/controller/space/membership_add.go +++ b/internal/api/controller/space/membership_add.go @@ -83,7 +83,7 @@ func (c *Controller) MembershipAdd(ctx context.Context, Role: in.Role, Principal: *user.ToPrincipalInfo(), - AdddedBy: *session.Principal.ToPrincipalInfo(), + AddedBy: *session.Principal.ToPrincipalInfo(), } err = c.membershipStore.Create(ctx, membership) diff --git a/internal/store/database/membership.go b/internal/store/database/membership.go index a91c83f74..26c750b7b 100644 --- a/internal/store/database/membership.go +++ b/internal/store/database/membership.go @@ -213,7 +213,7 @@ func (s *MembershipStore) mapToMembership(ctx context.Context, m *membership) *t log.Ctx(ctx).Error().Err(err).Msg("failed to load membership creator") } if addedBy != nil { - res.AdddedBy = *addedBy + res.AddedBy = *addedBy } principal, err := s.pCache.Get(ctx, res.PrincipalID) @@ -245,7 +245,7 @@ func (s *MembershipStore) mapToMemberships(ctx context.Context, ms []*membership for i, m := range ms { res[i] = mapToMembershipNoPrincipalInfo(m) if addedBy, ok := infoMap[m.CreatedBy]; ok { - res[i].AdddedBy = *addedBy + res[i].AddedBy = *addedBy } if principal, ok := infoMap[m.PrincipalID]; ok { res[i].Principal = *principal diff --git a/types/membership.go b/types/membership.go index 6520ff378..350572ed5 100644 --- a/types/membership.go +++ b/types/membership.go @@ -26,5 +26,5 @@ type Membership struct { Role enum.MembershipRole `json:"role"` Principal PrincipalInfo `json:"principal"` - AdddedBy PrincipalInfo `json:"added_by"` + AddedBy PrincipalInfo `json:"added_by"` } From bc5945f8bad28b01b98475ba7b3ad38ef49e0543 Mon Sep 17 00:00:00 2001 From: Hitesh Aringa Date: Mon, 24 Jul 2023 21:10:45 +0000 Subject: [PATCH 05/31] [CODE-631]: parent ref changes (#214) --- internal/api/controller/repo/create.go | 24 +- internal/api/controller/repo/move.go | 54 ++- internal/api/controller/space/create.go | 46 ++- internal/api/controller/space/move.go | 66 +++- .../NewRepoModalButton/NewRepoModalButton.tsx | 10 +- web/src/services/code/index.tsx | 264 +++++++++++--- web/src/services/code/swagger.yaml | 342 ++++++++++++++---- 7 files changed, 631 insertions(+), 175 deletions(-) diff --git a/internal/api/controller/repo/create.go b/internal/api/controller/repo/create.go index 0175b9c80..953d3d5cf 100644 --- a/internal/api/controller/repo/create.go +++ b/internal/api/controller/repo/create.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "fmt" + "strconv" "strings" "time" @@ -34,7 +35,7 @@ var ( ) type CreateInput struct { - ParentID int64 `json:"parent_id"` + ParentRef string `json:"parent_ref"` UID string `json:"uid"` DefaultBranch string `json:"default_branch"` Description string `json:"description"` @@ -47,7 +48,8 @@ type CreateInput struct { // Create creates a new repository. func (c *Controller) Create(ctx context.Context, session *auth.Session, in *CreateInput) (*types.Repository, error) { - if err := c.checkAuthRepoCreation(ctx, session, in.ParentID); err != nil { + parentSpace, err := c.getSpaceCheckAuthRepoCreation(ctx, session, in.ParentRef) + if err != nil { return nil, err } @@ -64,7 +66,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { // lock parent space path to ensure it doesn't get updated while we setup new repo var spacePath *types.Path - spacePath, err = c.pathStore.FindPrimaryWithLock(ctx, enum.PathTargetTypeSpace, in.ParentID) + spacePath, err = c.pathStore.FindPrimaryWithLock(ctx, enum.PathTargetTypeSpace, parentSpace.ID) if err != nil { return usererror.BadRequest("Parent not found'") } @@ -72,7 +74,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea now := time.Now().UnixMilli() repo = &types.Repository{ Version: 0, - ParentID: in.ParentID, + ParentID: parentSpace.ID, UID: in.UID, GitUID: gitRPCResp.UID, Path: paths.Concatinate(spacePath.Value, in.UID), @@ -123,10 +125,10 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea return repo, nil } -func (c *Controller) checkAuthRepoCreation(ctx context.Context, session *auth.Session, parentID int64) error { - space, err := c.spaceStore.Find(ctx, parentID) +func (c *Controller) getSpaceCheckAuthRepoCreation(ctx context.Context, session *auth.Session, parentRef string) (*types.Space, error) { + space, err := c.spaceStore.FindByRef(ctx, parentRef) if err != nil { - return fmt.Errorf("parent space not found: %w", err) + return nil, fmt.Errorf("parent space not found: %w", err) } // create is a special case - check permission without specific resource @@ -138,14 +140,16 @@ func (c *Controller) checkAuthRepoCreation(ctx context.Context, session *auth.Se err = apiauth.Check(ctx, c.authorizer, session, scope, resource, enum.PermissionRepoEdit) if err != nil { - return fmt.Errorf("auth check failed: %w", err) + return nil, fmt.Errorf("auth check failed: %w", err) } - return nil + return space, nil } func (c *Controller) sanitizeCreateInput(in *CreateInput) error { - if in.ParentID <= 0 { + parentRefAsID, err := strconv.ParseInt(in.ParentRef, 10, 64) + + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.ParentRef)) == 0) { return errRepositoryRequiresParent } diff --git a/internal/api/controller/repo/move.go b/internal/api/controller/repo/move.go index a4a87ab09..c89d239ed 100644 --- a/internal/api/controller/repo/move.go +++ b/internal/api/controller/repo/move.go @@ -7,6 +7,8 @@ package repo import ( "context" "fmt" + "strconv" + "strings" "time" apiauth "github.com/harness/gitness/internal/api/auth" @@ -20,13 +22,34 @@ import ( // MoveInput is used for moving a repo. type MoveInput struct { UID *string `json:"uid"` - ParentID *int64 `json:"parent_id"` + ParentRef *string `json:"parent_ref"` KeepAsAlias bool `json:"keep_as_alias"` } func (i *MoveInput) hasChanges(repo *types.Repository) bool { - return (i.UID != nil && *i.UID != repo.UID) || - (i.ParentID != nil && *i.ParentID != repo.ParentID) + if i.UID != nil && *i.UID != repo.UID { + return true + } + + if i.ParentRef != nil { + parentRefAsID, err := strconv.ParseInt(*i.ParentRef, 10, 64) + // if parsing was successful, user provided actual space id + if err == nil && parentRefAsID != repo.ParentID { + return true + } + + // if parsing was unsucessful, user provided input as path + if err != nil { + // repo is an existing entity, assume that path is not empty and thus no error + repoParentPath, _, _ := paths.DisectLeaf(repo.Path) + parentRefAsPath := *i.ParentRef + if parentRefAsPath != repoParentPath { + return true + } + } + } + + return false } // Move moves a repository to a new space and/or uid. @@ -40,15 +63,22 @@ func (c *Controller) Move(ctx context.Context, session *auth.Session, } permission := enum.PermissionRepoEdit - if in.ParentID != nil && *in.ParentID != repo.ParentID { + var inParentSpaceID *int64 + if in.ParentRef != nil { // ensure user has access to new space (parentId not sanitized!) - if err = c.checkAuthRepoCreation(ctx, session, *in.ParentID); err != nil { + inParentSpace, err := c.getSpaceCheckAuthRepoCreation(ctx, session, *in.ParentRef) + if err != nil { return nil, fmt.Errorf("failed to verify repo creation permissions on new parent space: %w", err) } - // TODO: what would be correct permissions on repo? (technically we are deleting it from the old space) - permission = enum.PermissionRepoDelete + inParentSpaceID = &inParentSpace.ID + + if inParentSpace.ID != repo.ParentID { + // TODO: what would be correct permissions on repo? (technically we are deleting it from the old space) + permission = enum.PermissionRepoDelete + } } + if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, permission, false); err != nil { return nil, err } @@ -66,8 +96,8 @@ func (c *Controller) Move(ctx context.Context, session *auth.Session, if in.UID != nil { r.UID = *in.UID } - if in.ParentID != nil { - r.ParentID = *in.ParentID + if inParentSpaceID != nil { + r.ParentID = *inParentSpaceID } return nil }) @@ -132,9 +162,11 @@ func (c *Controller) sanitizeMoveInput(in *MoveInput) error { } } - if in.ParentID != nil { - if *in.ParentID <= 0 { + if in.ParentRef != nil { + parentRefAsID, err := strconv.ParseInt(*in.ParentRef, 10, 64) + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(*in.ParentRef)) == 0) { return errRepositoryRequiresParent + } } diff --git a/internal/api/controller/space/create.go b/internal/api/controller/space/create.go index 02870e09b..dea0a13f1 100644 --- a/internal/api/controller/space/create.go +++ b/internal/api/controller/space/create.go @@ -7,6 +7,7 @@ package space import ( "context" "fmt" + "strconv" "strings" "time" @@ -27,7 +28,7 @@ var ( ) type CreateInput struct { - ParentID int64 `json:"parent_id"` + ParentRef string `json:"parent_ref"` UID string `json:"uid"` Description string `json:"description"` IsPublic bool `json:"is_public"` @@ -35,7 +36,8 @@ type CreateInput struct { // Create creates a new space. func (c *Controller) Create(ctx context.Context, session *auth.Session, in *CreateInput) (*types.Space, error) { - if err := c.checkAuthSpaceCreation(ctx, session, in.ParentID); err != nil { + parentSpace, err := c.getSpaceCheckAuthSpaceCreation(ctx, session, in.ParentRef) + if err != nil { return nil, err } @@ -44,11 +46,13 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea } var space *types.Space - err := dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { spacePath := in.UID - if in.ParentID > 0 { + parentSpaceID := int64(0) + if parentSpace != nil { + parentSpaceID = parentSpace.ID // lock parent space path to ensure it doesn't get updated while we setup new space - parentPath, err := c.pathStore.FindPrimaryWithLock(ctx, enum.PathTargetTypeSpace, in.ParentID) + parentPath, err := c.pathStore.FindPrimaryWithLock(ctx, enum.PathTargetTypeSpace, parentSpaceID) if err != nil { return usererror.BadRequest("Parent not found") } @@ -64,7 +68,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea now := time.Now().UnixMilli() space = &types.Space{ Version: 0, - ParentID: in.ParentID, + ParentID: parentSpaceID, UID: in.UID, Path: spacePath, Description: in.Description, @@ -73,7 +77,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea Created: now, Updated: now, } - err := c.spaceStore.Create(ctx, space) + err = c.spaceStore.Create(ctx, space) if err != nil { return fmt.Errorf("space creation failed: %w", err) } @@ -120,19 +124,20 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea return space, nil } -func (c *Controller) checkAuthSpaceCreation(ctx context.Context, session *auth.Session, parentID int64) error { - if parentID <= 0 { +func (c *Controller) getSpaceCheckAuthSpaceCreation(ctx context.Context, session *auth.Session, parentRef string) (*types.Space, error) { + parentRefAsID, err := strconv.ParseInt(parentRef, 10, 64) + if (parentRefAsID <= 0 && err == nil) || (len(strings.TrimSpace(parentRef)) == 0) { // TODO: Restrict top level space creation. if session == nil { - return usererror.ErrUnauthorized + return nil, usererror.ErrUnauthorized } - return nil + return nil, nil } - parentSpace, err := c.spaceStore.Find(ctx, parentID) + parentSpace, err := c.spaceStore.FindByRef(ctx, parentRef) if err != nil { - return fmt.Errorf("failed to get parent space: %w", err) + return nil, fmt.Errorf("failed to get parent space: %w", err) } // create is a special case - check permission without specific resource @@ -142,18 +147,25 @@ func (c *Controller) checkAuthSpaceCreation(ctx context.Context, session *auth.S Name: "", } if err = apiauth.Check(ctx, c.authorizer, session, scope, resource, enum.PermissionSpaceCreate); err != nil { - return err + return nil, err } - return nil + return parentSpace, nil } func (c *Controller) sanitizeCreateInput(in *CreateInput) error { - if in.ParentID < 0 { + parentRefAsID, err := strconv.ParseInt(in.ParentRef, 10, 64) + + if err == nil && parentRefAsID < 0 { return errParentIDNegative } - if err := c.uidCheck(in.UID, in.ParentID == 0); err != nil { + isRoot := false + if (err == nil && parentRefAsID == 0) || (len(strings.TrimSpace(in.ParentRef)) == 0) { + isRoot = true + } + + if err := c.uidCheck(in.UID, isRoot); err != nil { return err } diff --git a/internal/api/controller/space/move.go b/internal/api/controller/space/move.go index eb9cc6696..64eb735dd 100644 --- a/internal/api/controller/space/move.go +++ b/internal/api/controller/space/move.go @@ -8,9 +8,11 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "time" + "github.com/gotidy/ptr" apiauth "github.com/harness/gitness/internal/api/auth" "github.com/harness/gitness/internal/api/usererror" "github.com/harness/gitness/internal/auth" @@ -25,13 +27,35 @@ import ( // MoveInput is used for moving a space. type MoveInput struct { UID *string `json:"uid"` - ParentID *int64 `json:"parent_id"` + ParentRef *string `json:"parent_ref"` KeepAsAlias bool `json:"keep_as_alias"` } func (i *MoveInput) hasChanges(space *types.Space) bool { - return (i.UID != nil && *i.UID != space.UID) || - (i.ParentID != nil && *i.ParentID != space.ParentID) + if i.UID != nil && *i.UID != space.UID { + return true + } + + if i.ParentRef != nil { + parentRefAsID, err := strconv.ParseInt(*i.ParentRef, 10, 64) + // if parsing was successful, user provided actual space id + if err == nil && parentRefAsID != space.ParentID { + return true + } + + // if parsing was unsucessful, user provided input as path + if err != nil { + // space is an existing entity, assume that path is not empty and thus no error + spaceParentPath, _, _ := paths.DisectLeaf(space.Path) + parentRefAsPath := *i.ParentRef + if parentRefAsPath != spaceParentPath { + return true + } + } + } + + return false + } // Move moves a space to a new space and/or name. @@ -44,16 +68,27 @@ func (c *Controller) Move(ctx context.Context, session *auth.Session, return nil, err } + var inParentSpaceID *int64 permission := enum.PermissionSpaceEdit - if in.ParentID != nil && *in.ParentID != space.ParentID { + if in.ParentRef != nil { // ensure user has access to new space (parentId not sanitized!) - if err = c.checkAuthSpaceCreation(ctx, session, *in.ParentID); err != nil { + inParentSpace, err := c.getSpaceCheckAuthSpaceCreation(ctx, session, *in.ParentRef) + if err != nil { return nil, fmt.Errorf("failed to verify space creation permissions on new parent space: %w", err) } - // TODO: what would be correct permissions on space? (technically we are deleting it from the old space) - permission = enum.PermissionSpaceDelete + if inParentSpace != nil { + inParentSpaceID = &inParentSpace.ID + } else { + inParentSpaceID = ptr.Int64(0) + } + + if *inParentSpaceID != space.ParentID { + // TODO: what would be correct permissions on space? (technically we are deleting it from the old space) + permission = enum.PermissionSpaceDelete + } } + if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, permission, false); err != nil { return nil, err } @@ -71,9 +106,11 @@ func (c *Controller) Move(ctx context.Context, session *auth.Session, if in.UID != nil { s.UID = *in.UID } - if in.ParentID != nil { - s.ParentID = *in.ParentID + + if inParentSpaceID != nil { + s.ParentID = *inParentSpaceID } + return nil }) if err != nil { @@ -121,11 +158,16 @@ func (c *Controller) Move(ctx context.Context, session *auth.Session, } func (c *Controller) sanitizeMoveInput(in *MoveInput, isRoot bool) error { - if in.ParentID != nil { - if *in.ParentID < 0 { + if in.ParentRef != nil { + parentRefAsID, err := strconv.ParseInt(*in.ParentRef, 10, 64) + + if err == nil && parentRefAsID < 0 { return errParentIDNegative } - isRoot = *in.ParentID == 0 + + if (err == nil && parentRefAsID == 0) || (len(strings.TrimSpace(*in.ParentRef)) == 0) { + isRoot = true + } } if in.UID != nil { diff --git a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx index 71abbed1f..7001efe29 100644 --- a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx +++ b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx @@ -92,9 +92,11 @@ export const NewRepoModalButton: React.FC = ({ const { mutate: createRepo, loading: submitLoading } = useMutate({ verb: 'POST', path: `/api/v1/repos`, - queryParams: { - space_path: space - } + queryParams: standalone + ? undefined + : { + space_path: space + } }) const { data: gitignores, @@ -124,7 +126,7 @@ export const NewRepoModalButton: React.FC = ({ license: get(formData, 'license', 'none'), uid: get(formData, 'name', '').trim(), readme: get(formData, 'addReadme', false), - parent_id: standalone ? Number(space) : 0 // TODO: Backend needs to fix parentID: accept string or number + parent_ref: space } createRepo(payload) .then(response => { diff --git a/web/src/services/code/index.tsx b/web/src/services/code/index.tsx index 6a172e20c..e9ba2d77c 100644 --- a/web/src/services/code/index.tsx +++ b/web/src/services/code/index.tsx @@ -13,6 +13,8 @@ export type EnumCheckStatus = 'error' | 'failure' | 'pending' | 'running' | 'suc export type EnumContentEncodingType = 'base64' | 'utf8' +export type EnumMembershipRole = 'contributor' | 'executor' | 'reader' | 'space_owner' + export type EnumMergeCheckStatus = string export type EnumMergeMethod = 'merge' | 'squash' | 'rebase' @@ -60,18 +62,6 @@ export type EnumWebhookTrigger = | 'tag_deleted' | 'tag_updated' -export interface FormDataOpenapiLoginRequest { - password?: string - username?: string -} - -export interface FormDataOpenapiRegisterRequest { - displayname?: string - email?: string - password?: string - username?: string -} - export interface GitrpcBlamePart { commit?: GitrpcCommit lines?: string[] | null @@ -184,7 +174,7 @@ export interface OpenapiCreateRepositoryRequest { git_ignore?: string is_public?: boolean license?: string - parent_id?: number + parent_ref?: string readme?: boolean uid?: string } @@ -192,7 +182,7 @@ export interface OpenapiCreateRepositoryRequest { export interface OpenapiCreateSpaceRequest { description?: string is_public?: boolean - parent_id?: number + parent_ref?: string uid?: string } @@ -231,6 +221,11 @@ export interface OpenapiGetContentOutput { type?: OpenapiContentType } +export interface OpenapiLoginRequest { + login_identifier?: string + password?: string +} + export interface OpenapiMergePullReq { method?: EnumMergeMethod source_sha?: string @@ -238,16 +233,23 @@ export interface OpenapiMergePullReq { export interface OpenapiMoveRepoRequest { keep_as_alias?: boolean - parent_id?: number | null + parent_ref?: string | null uid?: string | null } export interface OpenapiMoveSpaceRequest { keep_as_alias?: boolean - parent_id?: number | null + parent_ref?: string | null uid?: string | null } +export interface OpenapiRegisterRequest { + display_name?: string + email?: string + password?: string + uid?: string +} + export interface OpenapiReportStatusCheckResultRequest { check_uid?: string link?: string @@ -447,6 +449,14 @@ export interface TypesListCommitResponse { rename_details?: TypesRenameDetails[] | null } +export interface TypesMembership { + added_by?: TypesPrincipalInfo + created?: number + principal?: TypesPrincipalInfo + role?: EnumMembershipRole + updated?: number +} + export interface TypesPath { created?: number created_by?: number @@ -776,10 +786,45 @@ export const useAdminUpdateUser = ({ user_uid, ...props }: UseAdminUpdateUserPro { base: getConfig('code'), pathParams: { user_uid }, ...props } ) -export type OnLoginProps = Omit, 'path' | 'verb'> +export interface UpdateUserAdminPathParams { + user_uid: string +} + +export type UpdateUserAdminProps = Omit< + MutateProps, + 'path' | 'verb' +> & + UpdateUserAdminPathParams + +export const UpdateUserAdmin = ({ user_uid, ...props }: UpdateUserAdminProps) => ( + + verb="PATCH" + path={`/admin/users/${user_uid}/admin`} + base={getConfig('code')} + {...props} + /> +) + +export type UseUpdateUserAdminProps = Omit< + UseMutateProps, + 'path' | 'verb' +> & + UpdateUserAdminPathParams + +export const useUpdateUserAdmin = ({ user_uid, ...props }: UseUpdateUserAdminProps) => + useMutate( + 'PATCH', + (paramsInPath: UpdateUserAdminPathParams) => `/admin/users/${paramsInPath.user_uid}/admin`, + { base: getConfig('code'), pathParams: { user_uid }, ...props } + ) + +export type OnLoginProps = Omit< + MutateProps, + 'path' | 'verb' +> export const OnLogin = (props: OnLoginProps) => ( - + verb="POST" path={`/login`} base={getConfig('code')} @@ -788,12 +833,12 @@ export const OnLogin = (props: OnLoginProps) => ( ) export type UseOnLoginProps = Omit< - UseMutateProps, + UseMutateProps, 'path' | 'verb' > export const useOnLogin = (props: UseOnLoginProps) => - useMutate('POST', `/login`, { + useMutate('POST', `/login`, { base: getConfig('code'), ...props }) @@ -856,10 +901,13 @@ export const useListPrincipals = (props: UseListPrincipalsProps) => ...props }) -export type OnRegisterProps = Omit, 'path' | 'verb'> +export type OnRegisterProps = Omit< + MutateProps, + 'path' | 'verb' +> export const OnRegister = (props: OnRegisterProps) => ( - + verb="POST" path={`/register`} base={getConfig('code')} @@ -868,12 +916,12 @@ export const OnRegister = (props: OnRegisterProps) => ( ) export type UseOnRegisterProps = Omit< - UseMutateProps, + UseMutateProps, 'path' | 'verb' > export const useOnRegister = (props: UseOnRegisterProps) => - useMutate('POST', `/register`, { + useMutate('POST', `/register`, { base: getConfig('code'), ...props }) @@ -3069,6 +3117,142 @@ export const useUpdateSpace = ({ space_ref, ...props }: UseUpdateSpaceProps) => { base: getConfig('code'), pathParams: { space_ref }, ...props } ) +export interface MembershipListPathParams { + space_ref: string +} + +export type MembershipListProps = Omit< + GetProps, + 'path' +> & + MembershipListPathParams + +export const MembershipList = ({ space_ref, ...props }: MembershipListProps) => ( + + path={`/spaces/${space_ref}/members`} + base={getConfig('code')} + {...props} + /> +) + +export type UseMembershipListProps = Omit< + UseGetProps, + 'path' +> & + MembershipListPathParams + +export const useMembershipList = ({ space_ref, ...props }: UseMembershipListProps) => + useGet( + (paramsInPath: MembershipListPathParams) => `/spaces/${paramsInPath.space_ref}/members`, + { base: getConfig('code'), pathParams: { space_ref }, ...props } + ) + +export interface MembershipAddPathParams { + space_ref: string +} + +export interface MembershipAddRequestBody { + role?: EnumMembershipRole + user_uid?: string +} + +export type MembershipAddProps = Omit< + MutateProps, + 'path' | 'verb' +> & + MembershipAddPathParams + +export const MembershipAdd = ({ space_ref, ...props }: MembershipAddProps) => ( + + verb="POST" + path={`/spaces/${space_ref}/members`} + base={getConfig('code')} + {...props} + /> +) + +export type UseMembershipAddProps = Omit< + UseMutateProps, + 'path' | 'verb' +> & + MembershipAddPathParams + +export const useMembershipAdd = ({ space_ref, ...props }: UseMembershipAddProps) => + useMutate( + 'POST', + (paramsInPath: MembershipAddPathParams) => `/spaces/${paramsInPath.space_ref}/members`, + { base: getConfig('code'), pathParams: { space_ref }, ...props } + ) + +export interface MembershipDeletePathParams { + space_ref: string +} + +export type MembershipDeleteProps = Omit< + MutateProps, + 'path' | 'verb' +> & + MembershipDeletePathParams + +export const MembershipDelete = ({ space_ref, ...props }: MembershipDeleteProps) => ( + + verb="DELETE" + path={`/spaces/${space_ref}/members`} + base={getConfig('code')} + {...props} + /> +) + +export type UseMembershipDeleteProps = Omit< + UseMutateProps, + 'path' | 'verb' +> & + MembershipDeletePathParams + +export const useMembershipDelete = ({ space_ref, ...props }: UseMembershipDeleteProps) => + useMutate( + 'DELETE', + (paramsInPath: MembershipDeletePathParams) => `/spaces/${paramsInPath.space_ref}/members`, + { base: getConfig('code'), pathParams: { space_ref }, ...props } + ) + +export interface MembershipUpdatePathParams { + space_ref: string + user_uid: string +} + +export interface MembershipUpdateRequestBody { + role?: EnumMembershipRole +} + +export type MembershipUpdateProps = Omit< + MutateProps, + 'path' | 'verb' +> & + MembershipUpdatePathParams + +export const MembershipUpdate = ({ space_ref, user_uid, ...props }: MembershipUpdateProps) => ( + + verb="PATCH" + path={`/spaces/${space_ref}/members/${user_uid}`} + base={getConfig('code')} + {...props} + /> +) + +export type UseMembershipUpdateProps = Omit< + UseMutateProps, + 'path' | 'verb' +> & + MembershipUpdatePathParams + +export const useMembershipUpdate = ({ space_ref, user_uid, ...props }: UseMembershipUpdateProps) => + useMutate( + 'PATCH', + (paramsInPath: MembershipUpdatePathParams) => `/spaces/${paramsInPath.space_ref}/members/${paramsInPath.user_uid}`, + { base: getConfig('code'), pathParams: { space_ref, user_uid }, ...props } + ) + export interface MoveSpacePathParams { space_ref: string } @@ -3375,38 +3559,6 @@ export const useUpdateUser = (props: UseUpdateUserProps) => ...props }) -export interface UpdateUserAdminPathParams { - user_uid: string -} - -export type UpdateUserAdminProps = Omit< - MutateProps, - 'path' | 'verb' -> & - UpdateUserAdminPathParams - -export const UpdateUserAdmin = ({ user_uid, ...props }: UpdateUserAdminProps) => ( - - verb="PATCH" - path={`/user/${user_uid}/admin`} - base={getConfig('code')} - {...props} - /> -) - -export type UseUpdateUserAdminProps = Omit< - UseMutateProps, - 'path' | 'verb' -> & - UpdateUserAdminPathParams - -export const useUpdateUserAdmin = ({ user_uid, ...props }: UseUpdateUserAdminProps) => - useMutate( - 'PATCH', - (paramsInPath: UpdateUserAdminPathParams) => `/user/${paramsInPath.user_uid}/admin`, - { base: getConfig('code'), pathParams: { user_uid }, ...props } - ) - export type CreateTokenProps = Omit< MutateProps, 'path' | 'verb' diff --git a/web/src/services/code/swagger.yaml b/web/src/services/code/swagger.yaml index 2bf9fd056..aed4b9471 100644 --- a/web/src/services/code/swagger.yaml +++ b/web/src/services/code/swagger.yaml @@ -201,14 +201,49 @@ paths: description: Internal Server Error tags: - admin + /admin/users/{user_uid}/admin: + patch: + operationId: updateUserAdmin + parameters: + - in: path + name: user_uid + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OpenapiUpdateAdminRequest' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TypesUser' + description: OK + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Not Found + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Internal Server Error + tags: + - admin /login: post: operationId: onLogin requestBody: content: - application/x-www-form-urlencoded: + application/json: schema: - $ref: '#/components/schemas/FormDataOpenapiLoginRequest' + $ref: '#/components/schemas/OpenapiLoginRequest' responses: '200': content: @@ -342,9 +377,9 @@ paths: operationId: onRegister requestBody: content: - application/x-www-form-urlencoded: + application/json: schema: - $ref: '#/components/schemas/FormDataOpenapiRegisterRequest' + $ref: '#/components/schemas/OpenapiRegisterRequest' responses: '200': content: @@ -3589,6 +3624,198 @@ paths: description: Internal Server Error tags: - space + /spaces/{space_ref}/members: + get: + operationId: membershipList + parameters: + - in: path + name: space_ref + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/TypesMembership' + type: array + description: OK + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Not Found + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Internal Server Error + tags: + - space + post: + operationId: membershipAdd + parameters: + - in: path + name: space_ref + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + role: + $ref: '#/components/schemas/EnumMembershipRole' + user_uid: + type: string + type: object + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/TypesMembership' + description: Created + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Not Found + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Internal Server Error + tags: + - space + /spaces/{space_ref}/members/{user_uid}: + delete: + operationId: membershipDelete + parameters: + - in: path + name: space_ref + required: true + schema: + type: string + - in: path + name: user_uid + required: true + schema: + type: string + responses: + '204': + description: No Content + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Not Found + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Internal Server Error + tags: + - space + patch: + operationId: membershipUpdate + parameters: + - in: path + name: space_ref + required: true + schema: + type: string + - in: path + name: user_uid + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + role: + $ref: '#/components/schemas/EnumMembershipRole' + type: object + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TypesMembership' + description: OK + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Not Found + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/UsererrorError' + description: Internal Server Error + tags: + - space /spaces/{space_ref}/move: post: operationId: moveSpace @@ -4050,41 +4277,6 @@ paths: description: Internal Server Error tags: - user - /user/{user_uid}/admin: - patch: - operationId: updateUserAdmin - parameters: - - in: path - name: user_uid - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/OpenapiUpdateAdminRequest' - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TypesUser' - description: OK - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/UsererrorError' - description: Not Found - '500': - content: - application/json: - schema: - $ref: '#/components/schemas/UsererrorError' - description: Internal Server Error - tags: - - user /user/token: post: operationId: createToken @@ -4129,6 +4321,13 @@ components: - base64 - utf8 type: string + EnumMembershipRole: + enum: + - contributor + - executor + - reader + - space_owner + type: string EnumMergeCheckStatus: type: string EnumMergeMethod: @@ -4217,24 +4416,6 @@ components: - tag_deleted - tag_updated type: string - FormDataOpenapiLoginRequest: - properties: - password: - type: string - username: - type: string - type: object - FormDataOpenapiRegisterRequest: - properties: - displayname: - type: string - email: - type: string - password: - type: string - username: - type: string - type: object GitrpcBlamePart: properties: commit: @@ -4435,8 +4616,8 @@ components: type: boolean license: type: string - parent_id: - type: integer + parent_ref: + type: string readme: type: boolean uid: @@ -4448,8 +4629,8 @@ components: type: string is_public: type: boolean - parent_id: - type: integer + parent_ref: + type: string uid: type: string type: object @@ -4516,6 +4697,13 @@ components: type: $ref: '#/components/schemas/OpenapiContentType' type: object + OpenapiLoginRequest: + properties: + login_identifier: + type: string + password: + type: string + type: object OpenapiMergePullReq: properties: method: @@ -4527,9 +4715,9 @@ components: properties: keep_as_alias: type: boolean - parent_id: + parent_ref: nullable: true - type: integer + type: string uid: nullable: true type: string @@ -4538,13 +4726,24 @@ components: properties: keep_as_alias: type: boolean - parent_id: + parent_ref: nullable: true - type: integer + type: string uid: nullable: true type: string type: object + OpenapiRegisterRequest: + properties: + display_name: + type: string + email: + type: string + password: + type: string + uid: + type: string + type: object OpenapiReportStatusCheckResultRequest: properties: check_uid: @@ -4872,6 +5071,19 @@ components: nullable: true type: array type: object + TypesMembership: + properties: + added_by: + $ref: '#/components/schemas/TypesPrincipalInfo' + created: + type: integer + principal: + $ref: '#/components/schemas/TypesPrincipalInfo' + role: + $ref: '#/components/schemas/EnumMembershipRole' + updated: + type: integer + type: object TypesPath: properties: created: From 914050c8a1c8fbd8fe23b3b282d26db377a21aa6 Mon Sep 17 00:00:00 2001 From: Hitesh Aringa Date: Mon, 24 Jul 2023 21:21:09 +0000 Subject: [PATCH 06/31] [CODE-631]: use parent ref in space membership (#226) --- internal/api/controller/space/create.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/api/controller/space/create.go b/internal/api/controller/space/create.go index dea0a13f1..ea571ddba 100644 --- a/internal/api/controller/space/create.go +++ b/internal/api/controller/space/create.go @@ -98,7 +98,8 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea } // add space membership to top level space only (as the user doesn't have inhereted permissions alraedy) - if in.ParentID == 0 { + parentRefAsID, err := strconv.ParseInt(in.ParentRef, 10, 64) + if (err == nil && parentRefAsID == 0) || (len(strings.TrimSpace(in.ParentRef)) == 0) { membership := &types.Membership{ SpaceID: space.ID, PrincipalID: session.Principal.ID, From 8e06f49017b6a4a412ce75c7a16e589ff5085073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ctan-nhu=E2=80=9D?= <“tan@harness.io”> Date: Mon, 24 Jul 2023 14:56:27 -0700 Subject: [PATCH 07/31] [CODE-655]: Refactor Nav system to adapt with new design --- web/dist.go | 4 - web/src/App.module.scss | 11 +- web/src/AppContext.tsx | 13 +- web/src/AppUtils.ts | 5 +- web/src/RouteDefinitions.ts | 10 +- web/src/RouteDestinations.tsx | 115 ++++++------- .../BranchTagSelect.module.scss | 6 + .../BranchTagSelect/BranchTagSelect.tsx | 7 - .../CommitModalButton/CommitModalButton.tsx | 7 - .../CreateBranchModal/CreateBranchModal.tsx | 7 - .../CreateTagModal/CreateTagModal.tsx | 7 - .../GitnessLogo/GitnessLogo.module.scss | 20 +++ .../GitnessLogo/GitnessLogo.module.scss.d.ts} | 4 +- .../components/GitnessLogo/GitnessLogo.tsx | 21 +++ .../NewRepoModalButton/NewRepoModalButton.tsx | 8 - .../RepositoryPageHeader.tsx | 8 +- .../ReviewerSelect/ReviewerSelect.tsx | 7 - .../SpaceSelector/SpaceSelector.module.scss | 93 +++++++++++ .../SpaceSelector.module.scss.d.ts | 14 ++ .../SpaceSelector/SpaceSelector.tsx | 96 +++++++++++ web/src/framework/strings/stringTypes.ts | 26 +++ web/src/hooks/useConfirmAction.tsx | 6 - web/src/hooks/useDocumentTitle.tsx | 16 ++ web/src/i18n/strings.en.yaml | 27 +++ web/src/layouts/NavEntry.tsx | 46 ------ web/src/layouts/layout.module.scss | 87 ++-------- web/src/layouts/layout.module.scss.d.ts | 6 +- web/src/layouts/layout.tsx | 82 ++++------ web/src/layouts/menu/AdminMenu.tsx | 27 --- web/src/layouts/menu/DefaultMenu.module.scss | 98 +++++++++++ .../layouts/menu/DefaultMenu.module.scss.d.ts | 15 ++ web/src/layouts/menu/DefaultMenu.tsx | 154 ++++++++++++++++++ web/src/layouts/menu/GlobalSettingsMenu.tsx | 26 --- web/src/layouts/menu/NavMenu.module.scss | 61 ------- web/src/layouts/menu/NavMenuItem.module.scss | 83 ++++++++++ ...scss.d.ts => NavMenuItem.module.scss.d.ts} | 3 +- .../menu/{NavMenu.tsx => NavMenuItem.tsx} | 16 +- web/src/layouts/menu/RepositoryMenu.tsx | 149 ----------------- .../PullRequestsContentHeader.tsx | 20 +-- .../FileContent/FileContent.tsx | 9 +- .../BranchesContentHeader.tsx | 3 +- .../RepositoryCommits/RepositoryCommits.tsx | 10 -- .../RepositoryTagsContentHeader.tsx | 2 +- web/src/pages/SignIn/SignIn.tsx | 14 +- web/src/pages/SignUp/SignUp.tsx | 4 +- .../SpaceAccessControl/SpaceAccessControl.tsx | 6 + web/src/pages/SpaceSettings/SpaceSettings.tsx | 6 + web/src/pages/Spaces/Spaces.module.scss | 13 -- web/src/pages/Spaces/Spaces.tsx | 21 --- web/src/pages/UserProfile/UserProfile.tsx | 3 +- .../WebhooksHeader/WebhooksHeader.tsx | 15 +- web/src/utils/vars.scss | 8 + 52 files changed, 879 insertions(+), 646 deletions(-) create mode 100644 web/src/components/GitnessLogo/GitnessLogo.module.scss rename web/src/{pages/Spaces/Spaces.module.scss.d.ts => components/GitnessLogo/GitnessLogo.module.scss.d.ts} (67%) create mode 100644 web/src/components/GitnessLogo/GitnessLogo.tsx create mode 100644 web/src/components/SpaceSelector/SpaceSelector.module.scss create mode 100644 web/src/components/SpaceSelector/SpaceSelector.module.scss.d.ts create mode 100644 web/src/components/SpaceSelector/SpaceSelector.tsx create mode 100644 web/src/hooks/useDocumentTitle.tsx delete mode 100644 web/src/layouts/NavEntry.tsx delete mode 100644 web/src/layouts/menu/AdminMenu.tsx create mode 100644 web/src/layouts/menu/DefaultMenu.module.scss create mode 100644 web/src/layouts/menu/DefaultMenu.module.scss.d.ts create mode 100644 web/src/layouts/menu/DefaultMenu.tsx delete mode 100644 web/src/layouts/menu/GlobalSettingsMenu.tsx delete mode 100644 web/src/layouts/menu/NavMenu.module.scss create mode 100644 web/src/layouts/menu/NavMenuItem.module.scss rename web/src/layouts/menu/{NavMenu.module.scss.d.ts => NavMenuItem.module.scss.d.ts} (86%) rename web/src/layouts/menu/{NavMenu.tsx => NavMenuItem.tsx} (64%) delete mode 100644 web/src/layouts/menu/RepositoryMenu.tsx create mode 100644 web/src/pages/SpaceAccessControl/SpaceAccessControl.tsx create mode 100644 web/src/pages/SpaceSettings/SpaceSettings.tsx delete mode 100644 web/src/pages/Spaces/Spaces.module.scss delete mode 100644 web/src/pages/Spaces/Spaces.tsx create mode 100644 web/src/utils/vars.scss diff --git a/web/dist.go b/web/dist.go index 0b54c4836..58adb79c5 100644 --- a/web/dist.go +++ b/web/dist.go @@ -1,7 +1,3 @@ -// 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. - //go:build !proxy // +build !proxy diff --git a/web/src/App.module.scss b/web/src/App.module.scss index 08c0d2950..1e9464c87 100644 --- a/web/src/App.module.scss +++ b/web/src/App.module.scss @@ -1,3 +1,5 @@ +@import 'src/utils/vars'; + /* * NOTE: Styles in this file are loaded in both standalone and embedded * versions. Be careful! Don't introduce global states that could be conflict @@ -5,10 +7,7 @@ */ .main { - --nav-item-width: var(--main-nav-width, 88px); - --nav-item-height: 80px; - --nav-menu-width: 184px; - --nav-width: calc(var(--nav-item-width) + var(--nav-menu-width)); + @include vars; &.fullPage { height: 100%; @@ -59,5 +58,9 @@ .Resizer.disabled:hover { border-color: transparent; } + + div[data-testid='page-body'] > div[data-testid='page-error'] { + height: 80vh !important; + } } } diff --git a/web/src/AppContext.tsx b/web/src/AppContext.tsx index 5236d4c03..e07a1250d 100644 --- a/web/src/AppContext.tsx +++ b/web/src/AppContext.tsx @@ -4,6 +4,7 @@ import { useGet } from 'restful-react' import type { AppProps } from 'AppProps' import { routes } from 'RouteDefinitions' import type { TypesUser } from 'services/code' +import { useAPIToken } from 'hooks/useAPIToken' interface AppContextProps extends AppProps { setAppContext: (value: Partial) => void @@ -33,7 +34,11 @@ export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(func value: initialValue, children }) { - const { data: currentUser = defaultCurrentUser } = useGet({ path: '/api/v1/user' }) + const [token, setToken] = useAPIToken() + const { data: currentUser = defaultCurrentUser, error } = useGet({ + path: '/api/v1/user', + lazy: initialValue.standalone && !token + }) const [appStates, setAppStates] = useState(initialValue) useEffect(() => { @@ -42,6 +47,12 @@ export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(func } }, [initialValue, appStates]) + useEffect(() => { + if (initialValue.standalone && error) { + setToken('') + } + }, [initialValue.standalone, error, setToken]) + return ( string toCODEHome: () => string - toCODESpaces: () => string + + toCODESpaceAccessControl: (args: Required>) => string + toCODESpaceSettings: (args: Required>) => string + toCODEGlobalSettings: () => string toCODEUsers: () => string toCODEUserProfile: () => string @@ -66,7 +69,10 @@ export const routes: CODERoutes = { toRegister: (): string => '/register', toCODEHome: () => `/`, - toCODESpaces: () => `/spaces`, + + toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`, + toCODESpaceSettings: ({ space }) => `/settings/${space}`, + toCODEGlobalSettings: () => '/settings', toCODEUsers: () => '/users', toCODEUserProfile: () => '/profile', diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index 041f0642d..fe63f2aac 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -5,12 +5,7 @@ import { SignUp } from 'pages/SignUp/SignUp' import Repository from 'pages/Repository/Repository' import { routes, pathProps } from 'RouteDefinitions' import RepositoriesListing from 'pages/RepositoriesListing/RepositoriesListing' -import Spaces from 'pages/Spaces/Spaces' - -import { LayoutWithSideMenu, LayoutWithSideNav } from 'layouts/layout' -import { GlobalSettingsMenu } from 'layouts/menu/GlobalSettingsMenu' -import { RepositoryMenu } from 'layouts/menu/RepositoryMenu' -import { AdminMenu } from 'layouts/menu/AdminMenu' +import { LayoutWithSideNav, LayoutWithoutSideNav } from 'layouts/layout' import RepositoryFileEdit from 'pages/RepositoryFileEdit/RepositoryFileEdit' import RepositoryCommits from 'pages/RepositoryCommits/RepositoryCommits' import RepositoryBranches from 'pages/RepositoryBranches/RepositoryBranches' @@ -26,24 +21,38 @@ import UsersListing from 'pages/UsersListing/UsersListing' import Home from 'pages/Home/Home' import UserProfile from 'pages/UserProfile/UserProfile' import ChangePassword from 'pages/ChangePassword/ChangePassword' +import SpaceAccessControl from 'pages/SpaceAccessControl/SpaceAccessControl' +import SpaceSettings from 'pages/SpaceSettings/SpaceSettings' +import { useStrings } from 'framework/strings' export const RouteDestinations: React.FC = React.memo(function RouteDestinations() { + const { getString } = useStrings() const repoPath = `${pathProps.space}/${pathProps.repoName}` return ( - + + + - + + + - - - + + + + + + + + + @@ -52,9 +61,9 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations repoPath, diffRefs: pathProps.diffRefs })}> - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - + - }> + - - - - - }> - - + - }> + - + - + + - }> + - + + - }> + - + + - }> + - + diff --git a/web/src/components/BranchTagSelect/BranchTagSelect.module.scss b/web/src/components/BranchTagSelect/BranchTagSelect.module.scss index 23d84233d..4ebf838fa 100644 --- a/web/src/components/BranchTagSelect/BranchTagSelect.module.scss +++ b/web/src/components/BranchTagSelect/BranchTagSelect.module.scss @@ -1,3 +1,9 @@ +html[class=''] { + .button:focus { + --border: 1px solid var(--primary-7) !important; + } +} + .button { --border: 1px solid var(--grey-200) !important; --background-color-active: var(--white) !important; diff --git a/web/src/components/BranchTagSelect/BranchTagSelect.tsx b/web/src/components/BranchTagSelect/BranchTagSelect.tsx index ef0dd1d99..0894d8a3c 100644 --- a/web/src/components/BranchTagSelect/BranchTagSelect.tsx +++ b/web/src/components/BranchTagSelect/BranchTagSelect.tsx @@ -1,10 +1,3 @@ -/* - * Copyright 2021 Harness Inc. All rights reserved. - * Use of this source code is governed by the PolyForm Shield 1.0.0 license - * that can be found in the licenses directory at the root of this repository, also available at - * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. - */ - import React, { useEffect, useMemo, useRef, useState } from 'react' import { Classes, Icon as BPIcon, Menu, MenuItem, PopoverPosition } from '@blueprintjs/core' import { diff --git a/web/src/components/CommitModalButton/CommitModalButton.tsx b/web/src/components/CommitModalButton/CommitModalButton.tsx index fdf39a313..9e0c73ba9 100644 --- a/web/src/components/CommitModalButton/CommitModalButton.tsx +++ b/web/src/components/CommitModalButton/CommitModalButton.tsx @@ -1,10 +1,3 @@ -/* - * Copyright 2021 Harness Inc. All rights reserved. - * Use of this source code is governed by the PolyForm Shield 1.0.0 license - * that can be found in the licenses directory at the root of this repository, also available at - * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. - */ - import React, { useState } from 'react' import { Dialog, Intent } from '@blueprintjs/core' import * as yup from 'yup' diff --git a/web/src/components/CreateBranchModal/CreateBranchModal.tsx b/web/src/components/CreateBranchModal/CreateBranchModal.tsx index 05a4ee719..1a6bf593f 100644 --- a/web/src/components/CreateBranchModal/CreateBranchModal.tsx +++ b/web/src/components/CreateBranchModal/CreateBranchModal.tsx @@ -1,10 +1,3 @@ -/* - * Copyright 2021 Harness Inc. All rights reserved. - * Use of this source code is governed by the PolyForm Shield 1.0.0 license - * that can be found in the licenses directory at the root of this repository, also available at - * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. - */ - import React, { useCallback, useState } from 'react' import { Dialog, Intent } from '@blueprintjs/core' import * as yup from 'yup' diff --git a/web/src/components/CreateTagModal/CreateTagModal.tsx b/web/src/components/CreateTagModal/CreateTagModal.tsx index a37c8a39c..ed700df9a 100644 --- a/web/src/components/CreateTagModal/CreateTagModal.tsx +++ b/web/src/components/CreateTagModal/CreateTagModal.tsx @@ -1,10 +1,3 @@ -/* - * Copyright 2021 Harness Inc. All rights reserved. - * Use of this source code is governed by the PolyForm Shield 1.0.0 license - * that can be found in the licenses directory at the root of this repository, also available at - * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. - */ - import React, { useCallback, useState } from 'react' import { Dialog, Intent } from '@blueprintjs/core' import * as yup from 'yup' diff --git a/web/src/components/GitnessLogo/GitnessLogo.module.scss b/web/src/components/GitnessLogo/GitnessLogo.module.scss new file mode 100644 index 000000000..d99db0b26 --- /dev/null +++ b/web/src/components/GitnessLogo/GitnessLogo.module.scss @@ -0,0 +1,20 @@ +.main { + margin: var(--spacing-large) var(--spacing-large) 0 !important; + padding-bottom: 6px !important; + + > a { + display: block; + } + + .layout { + display: inline-flex !important; + align-items: center !important; + } + + .text { + font-size: 20px; + font-style: normal; + font-weight: 600; + color: var(--black); + } +} diff --git a/web/src/pages/Spaces/Spaces.module.scss.d.ts b/web/src/components/GitnessLogo/GitnessLogo.module.scss.d.ts similarity index 67% rename from web/src/pages/Spaces/Spaces.module.scss.d.ts rename to web/src/components/GitnessLogo/GitnessLogo.module.scss.d.ts index 393b0bb14..a02e77e93 100644 --- a/web/src/pages/Spaces/Spaces.module.scss.d.ts +++ b/web/src/components/GitnessLogo/GitnessLogo.module.scss.d.ts @@ -2,7 +2,7 @@ // this is an auto-generated file declare const styles: { readonly main: string - readonly pageHeading: string - readonly pageContent: string + readonly layout: string + readonly text: string } export default styles diff --git a/web/src/components/GitnessLogo/GitnessLogo.tsx b/web/src/components/GitnessLogo/GitnessLogo.tsx new file mode 100644 index 000000000..9c32b811f --- /dev/null +++ b/web/src/components/GitnessLogo/GitnessLogo.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { Container, Icon, Layout, Text } from '@harness/uicore' +import { Link } from 'react-router-dom' +import { useStrings } from 'framework/strings' +import { routes } from 'RouteDefinitions' +import css from './GitnessLogo.module.scss' + +export const GitnessLogo: React.FC = () => { + const { getString } = useStrings() + + return ( + + + + + {getString('gitness')} + + + + ) +} diff --git a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx index 7001efe29..4ac59e9af 100644 --- a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx +++ b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx @@ -1,10 +1,3 @@ -/* - * Copyright 2021 Harness Inc. All rights reserved. - * Use of this source code is governed by the PolyForm Shield 1.0.0 license - * that can be found in the licenses directory at the root of this repository, also available at - * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. - */ - import React, { useEffect, useMemo, useState } from 'react' import { Icon as BPIcon, Classes, Dialog, Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core' import * as yup from 'yup' @@ -84,7 +77,6 @@ export const NewRepoModalButton: React.FC = ({ ...props }) => { const ModalComponent: React.FC = () => { - const { standalone } = useAppContext() const { getString } = useStrings() const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME) const { showError } = useToaster() diff --git a/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx b/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx index dc8bbe2bc..0e46af66b 100644 --- a/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx +++ b/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx @@ -1,9 +1,10 @@ import React, { Fragment } from 'react' import { Container, Layout, Text, Color, Icon, FontVariation, PageHeader } from '@harness/uicore' -import { Link } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' import { useStrings } from 'framework/strings' import { useAppContext } from 'AppContext' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import type { CODEProps } from 'RouteDefinitions' import type { GitInfoProps } from 'utils/GitUtils' import css from './RepositoryPageHeader.module.scss' @@ -24,6 +25,7 @@ export function RepositoryPageHeader({ dataTooltipId, extraBreadcrumbLinks = [] }: RepositoryPageHeaderProps) { + const { gitRef } = useParams() const { getString } = useStrings() const space = useGetSpaceParam() const { routes } = useAppContext() @@ -40,7 +42,9 @@ export function RepositoryPageHeader({ {getString('repositories')} - {repoMetadata.uid} + + {repoMetadata.uid} + {extraBreadcrumbLinks.map(link => ( diff --git a/web/src/components/ReviewerSelect/ReviewerSelect.tsx b/web/src/components/ReviewerSelect/ReviewerSelect.tsx index 701205b4e..3160c6f8a 100644 --- a/web/src/components/ReviewerSelect/ReviewerSelect.tsx +++ b/web/src/components/ReviewerSelect/ReviewerSelect.tsx @@ -1,10 +1,3 @@ -/* - * Copyright 2021 Harness Inc. All rights reserved. - * Use of this source code is governed by the PolyForm Shield 1.0.0 license - * that can be found in the licenses directory at the root of this repository, also available at - * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. - */ - import React, { useEffect, useRef, useState } from 'react' import { Icon as BPIcon, Menu, MenuItem, PopoverPosition } from '@blueprintjs/core' import { diff --git a/web/src/components/SpaceSelector/SpaceSelector.module.scss b/web/src/components/SpaceSelector/SpaceSelector.module.scss new file mode 100644 index 000000000..a6c6e896e --- /dev/null +++ b/web/src/components/SpaceSelector/SpaceSelector.module.scss @@ -0,0 +1,93 @@ +.spaceSelector { + border-radius: 4px; + padding: 6px 10px !important; + border: 1px solid var(--grey-200); + background: var(--white) !important; + margin-bottom: var(--spacing-medium) !important; + + &.selected, + &:hover { + background: var(--primary-1) !important; + border-radius: 4px; + border: 1px solid var(--primary-3); + box-shadow: 0px 2px 4px 0px rgba(96, 97, 112, 0.16), 0px 0px 1px 0px rgba(40, 41, 61, 0.04); + + .icon { + svg path { + fill: var(--primary-7); + } + } + } + + .label { + flex-grow: 1; + } + + .spaceLabel { + color: var(--gray-500); + font-size: 8px !important; + font-weight: 600 !important; + line-height: 17px !important; + } + + .spaceName { + color: var(--black); + font-size: 13px !important; + line-height: 20px !important; + padding-left: 17px !important; + } + + .icon { + display: flex; + align-items: center; + padding-top: 3px; + } +} + +.popoverPortal { + width: min(calc(100vw - var(--nav-menu-width)), 840px) !important; + height: 100vh; + position: fixed; + left: 5px; + top: -5px; + + > div { + height: 100%; + width: 100%; + } +} + +.popoverTarget { + display: block !important; +} + +.popoverContent { + position: relative; + height: 100%; + width: min(calc(100vw - var(--nav-menu-width)), 840px) !important; + border-radius: 0; + box-shadow: none; + filter: drop-shadow(0px 2px 4px rgba(96, 97, 112, 0.16)) drop-shadow(0px 0px 1px rgba(40, 41, 61, 0.04)); + + &::before { + position: fixed; + content: ''; + top: 0; + left: var(--nav-menu-width); + width: 100vw; + height: 100vh; + background-color: rgba(26, 26, 26, 0.4); + } + + > div { + height: 100%; + width: 100%; + max-width: inherit !important; + border-radius: 0 !important; + + &::before { + box-shadow: none !important; + filter: drop-shadow(0px 2px 4px rgba(96, 97, 112, 0.16)) drop-shadow(0px 0px 1px rgba(40, 41, 61, 0.04)); + } + } +} diff --git a/web/src/components/SpaceSelector/SpaceSelector.module.scss.d.ts b/web/src/components/SpaceSelector/SpaceSelector.module.scss.d.ts new file mode 100644 index 000000000..de414280d --- /dev/null +++ b/web/src/components/SpaceSelector/SpaceSelector.module.scss.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// this is an auto-generated file +declare const styles: { + readonly spaceSelector: string + readonly selected: string + readonly icon: string + readonly label: string + readonly spaceLabel: string + readonly spaceName: string + readonly popoverPortal: string + readonly popoverTarget: string + readonly popoverContent: string +} +export default styles diff --git a/web/src/components/SpaceSelector/SpaceSelector.tsx b/web/src/components/SpaceSelector/SpaceSelector.tsx new file mode 100644 index 000000000..a8f7971ce --- /dev/null +++ b/web/src/components/SpaceSelector/SpaceSelector.tsx @@ -0,0 +1,96 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { Button, Container, Heading, Icon, Layout, Text } from '@harness/uicore' +import cx from 'classnames' +import { Classes, Popover, Position } from '@blueprintjs/core' +import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' +import { useStrings } from 'framework/strings' +import { ButtonRoleProps } from 'utils/Utils' +import { useShowRequestError } from 'hooks/useShowRequestError' +import { TypesSpace, useGetSpace } from 'services/code' +import css from './SpaceSelector.module.scss' + +interface SpaceSelectorProps { + onSelect: (space: TypesSpace, isUserAction: boolean) => void +} + +export const SpaceSelector: React.FC = ({ onSelect }) => { + const { getString } = useStrings() + const [selectedSpace, setSelectedSpace] = useState() + const { space } = useGetRepositoryMetadata() + const [opened, setOpened] = React.useState(false) + const { data, error } = useGetSpace({ space_ref: space, lazy: !space }) + const selectSpace = useCallback( + (_space: TypesSpace, isUserAction: boolean) => { + setSelectedSpace(_space) + onSelect(_space, isUserAction) + }, + [onSelect] + ) + + useEffect(() => { + if (space && !selectedSpace && data) { + selectSpace(data, false) + } + }, [space, selectedSpace, data, onSelect, selectSpace]) + + useShowRequestError(error) + + return ( + + setOpened(!opened)}> + + + + + + {getString('space').toUpperCase()} + + + + + {selectedSpace ? selectedSpace.uid : getString('selectSpace')} + + + + + + + + + + + {getString('spaces')} + + + + + + + ) + }} + + + + + ) } diff --git a/web/src/utils/Utils.ts b/web/src/utils/Utils.ts index a1422f957..7ecf960cd 100644 --- a/web/src/utils/Utils.ts +++ b/web/src/utils/Utils.ts @@ -4,6 +4,11 @@ import { get } from 'lodash-es' import moment from 'moment' import langMap from 'lang-map' +export enum ACCESS_MODES { + VIEW, + EDIT +} + export const LIST_FETCHING_LIMIT = 20 export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a' export const DEFAULT_BRANCH_NAME = 'main' From 5232615e0a3eea10ba5b90a8877df19a6988e9c1 Mon Sep 17 00:00:00 2001 From: "rajarshee.chatterjee@harness.io" Date: Wed, 26 Jul 2023 18:14:31 +0000 Subject: [PATCH 19/31] fix: [CODE-615]: Misc. UX Feedback (#240) --- web/src/framework/strings/stringTypes.ts | 6 +++ web/src/i18n/strings.en.yaml | 6 +++ .../AddNewMember/AddNewMember.tsx | 7 ++-- .../SpaceAccessControl.module.scss | 1 - .../SpaceAccessControl/SpaceAccessControl.tsx | 33 ++++++++++++---- .../pages/UserProfile/NewToken/NewToken.tsx | 38 ++++++++++++------- web/src/pages/UserProfile/UserProfile.tsx | 15 ++++++-- web/src/pages/UsersListing/UsersListing.tsx | 8 +++- 8 files changed, 84 insertions(+), 30 deletions(-) diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 4b6a7f020..43e7ee755 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -86,6 +86,7 @@ export interface StringsMap { confirmation: string content: string contents: string + contributor: string conversation: string copy: string copyBranch: string @@ -161,6 +162,7 @@ export interface StringsMap { enterTagPlaceholder: string enterUser: string error404Text: string + executor: string existingAccount: string expiration: string expirationDate: string @@ -244,6 +246,8 @@ export interface StringsMap { noCommits: string noCommitsMessage: string noCommitsPR: string + noExpiration: string + noExpirationDate: string noOptionalReviewers: string noRequiredReviewers: string noResultMessage: string @@ -261,6 +265,7 @@ export interface StringsMap { optional: string optionalExtendedDescription: string overview: string + owner: string pageLoading: string pageNotFound: string 'pageTitle.accessControl': string @@ -369,6 +374,7 @@ export interface StringsMap { quote: string reactivate: string readMe: string + reader: string refresh: string reject: string rejected: string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index da5e45e9c..5165b6c12 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -507,6 +507,8 @@ confirmNewPassword: Confirm the NEW password accountSetting: Account Setting nDays: '{{number}} Days' expiration: Expiration +noExpiration: No Expiration +noExpirationDate: The token will never expire! token: Token expired: Expired expirationDate: Expiration Date @@ -571,3 +573,7 @@ deleteSpace: Delete Space spaceSetting: intentText: This will permanently delete the space named '{{space}}', and everything contained in it. All repositories in this space will be deleted. setting: Space Setting +contributor: Contributor +reader: Reader +executor: Executor +owner: Owner diff --git a/web/src/pages/SpaceAccessControl/AddNewMember/AddNewMember.tsx b/web/src/pages/SpaceAccessControl/AddNewMember/AddNewMember.tsx index 2bdcc86c8..f20b0ee7d 100644 --- a/web/src/pages/SpaceAccessControl/AddNewMember/AddNewMember.tsx +++ b/web/src/pages/SpaceAccessControl/AddNewMember/AddNewMember.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useState } from 'react' import { Button, ButtonVariation, Dialog, FormikForm, FormInput, SelectOption, useToaster } from '@harness/uicore' import { useModalHook } from '@harness/use-modal' import { Formik } from 'formik' -import { capitalize } from 'lodash-es' import * as Yup from 'yup' @@ -17,7 +16,9 @@ import { } from 'services/code' import { getErrorMessage } from 'utils/Utils' -const roles = ['contributor', 'executor', 'reader', 'space_owner'] as const +import { roleStringKeyMap } from '../SpaceAccessControl' + +const roles = ['reader', 'executor', 'contributor', 'space_owner'] as const const useAddNewMember = ({ onClose }: { onClose: () => void }) => { const [isEditFlow, setIsEditFlow] = useState(false) @@ -37,7 +38,7 @@ const useAddNewMember = ({ onClose }: { onClose: () => void }) => { () => roles.map(role => ({ value: role, - label: capitalize(role) + label: getString(roleStringKeyMap[role]) })), [] ) diff --git a/web/src/pages/SpaceAccessControl/SpaceAccessControl.module.scss b/web/src/pages/SpaceAccessControl/SpaceAccessControl.module.scss index 5601de039..8caa9ba8d 100644 --- a/web/src/pages/SpaceAccessControl/SpaceAccessControl.module.scss +++ b/web/src/pages/SpaceAccessControl/SpaceAccessControl.module.scss @@ -3,7 +3,6 @@ background-color: var(--primary-bg) !important; .roleBadge { - text-transform: capitalize; padding: var(--spacing-xsmall) 6px; border-radius: 4px; border: 1px solid var(--grey-200); diff --git a/web/src/pages/SpaceAccessControl/SpaceAccessControl.tsx b/web/src/pages/SpaceAccessControl/SpaceAccessControl.tsx index d1527c8ce..2f73145b2 100644 --- a/web/src/pages/SpaceAccessControl/SpaceAccessControl.tsx +++ b/web/src/pages/SpaceAccessControl/SpaceAccessControl.tsx @@ -3,10 +3,10 @@ import { Avatar, Button, ButtonVariation, Container, Layout, Page, TableV2, Text import { Color, FontVariation } from '@harness/design-system' import type { CellProps, Column } from 'react-table' -import { useStrings } from 'framework/strings' +import { StringKeys, useStrings } from 'framework/strings' import { useConfirmAct } from 'hooks/useConfirmAction' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' -import { TypesMembership, useMembershipDelete, useMembershipList } from 'services/code' +import { EnumMembershipRole, TypesMembership, useMembershipDelete, useMembershipList } from 'services/code' import { getErrorMessage } from 'utils/Utils' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' @@ -15,6 +15,13 @@ import useAddNewMember from './AddNewMember/AddNewMember' import css from './SpaceAccessControl.module.scss' +export const roleStringKeyMap: Record = { + contributor: 'contributor', + executor: 'executor', + reader: 'reader', + space_owner: 'owner' +} + const SpaceAccessControl = () => { const { getString } = useStrings() const { showError, showSuccess } = useToaster() @@ -55,7 +62,13 @@ const SpaceAccessControl = () => { width: '30%', Cell: ({ row }: CellProps) => ( - + {row.original.principal?.display_name} @@ -65,11 +78,15 @@ const SpaceAccessControl = () => { { Header: getString('role'), width: '40%', - Cell: ({ row }: CellProps) => ( - - {row.original.role} - - ) + Cell: ({ row }: CellProps) => { + const stringKey = row.original.role ? roleStringKeyMap[row.original.role] : undefined + + return ( + + {stringKey ? getString(stringKey) : row.original.role} + + ) + } }, { Header: getString('email'), diff --git a/web/src/pages/UserProfile/NewToken/NewToken.tsx b/web/src/pages/UserProfile/NewToken/NewToken.tsx index c7b9720b6..05be2b0c4 100644 --- a/web/src/pages/UserProfile/NewToken/NewToken.tsx +++ b/web/src/pages/UserProfile/NewToken/NewToken.tsx @@ -10,6 +10,7 @@ import { FormikForm, FormInput, Layout, + SelectOption, Text } from '@harness/uicore' import { useModalHook } from '@harness/use-modal' @@ -18,8 +19,10 @@ import { useMutate } from 'restful-react' import moment from 'moment' import * as Yup from 'yup' import { Else, Match, Render, Truthy } from 'react-jsx-match' +import { omit } from 'lodash-es' import { useStrings } from 'framework/strings' +import type { OpenapiCreateTokenRequest } from 'services/code' import { REGEX_VALID_REPO_NAME } from 'utils/Utils' import { CodeIcon } from 'utils/GitUtils' import { CopyButton } from 'components/CopyButton/CopyButton' @@ -34,44 +37,49 @@ const useNewToken = ({ onClose }: { onClose: () => void }) => { const [generatedToken, setGeneratedToken] = useState() const isTokenGenerated = Boolean(generatedToken) - const lifeTimeOptions = useMemo( + const lifeTimeOptions: SelectOption[] = useMemo( () => [ { label: getString('nDays', { number: 7 }), value: 604800000000000 }, { label: getString('nDays', { number: 30 }), value: 2592000000000000 }, { label: getString('nDays', { number: 60 }), value: 5184000000000000 }, - { label: getString('nDays', { number: 90 }), value: 7776000000000000 } + { label: getString('nDays', { number: 90 }), value: 7776000000000000 }, + { label: getString('noExpiration'), value: Infinity } ], [getString] ) const onModalClose = () => { + setGeneratedToken('') hideModal() onClose() - setGeneratedToken() } const [openModal, hideModal] = useModalHook(() => { return ( - initialValues={{ - uid: '', - lifeTime: 0 + uid: '' }} validationSchema={Yup.object().shape({ uid: Yup.string() .required(getString('validation.nameIsRequired')) .matches(REGEX_VALID_REPO_NAME, getString('validation.nameInvalid')), - lifeTime: Yup.number().required(getString('validation.expirationDateRequired')) + lifetime: Yup.number().required(getString('validation.expirationDateRequired')) })} onSubmit={async values => { - const res = await mutate(values) + let payload = { ...values } + + if (payload.lifetime === Infinity) { + payload = omit(payload, 'lifetime') + } + + const res = await mutate(payload) setGeneratedToken(res?.access_token) }}> {formikProps => { - const expiresAtString = moment(Date.now() + formikProps.values.lifeTime / 1000000).format( - 'dddd, MMMM DD YYYY' - ) + const lifetime = formikProps.values.lifetime || 0 + const expiresAtString = moment(Date.now() + lifetime / 1000000).format('dddd, MMMM DD YYYY') return ( @@ -82,18 +90,20 @@ const useNewToken = ({ onClose }: { onClose: () => void }) => { disabled={isTokenGenerated} /> - {formikProps.values.lifeTime ? ( + {lifetime ? ( - {getString('newToken.expireOn', { date: expiresAtString })} + {lifetime === Infinity + ? getString('noExpirationDate') + : getString('newToken.expireOn', { date: expiresAtString })} ) : null} diff --git a/web/src/pages/UserProfile/UserProfile.tsx b/web/src/pages/UserProfile/UserProfile.tsx index e2cdf062f..849149120 100644 --- a/web/src/pages/UserProfile/UserProfile.tsx +++ b/web/src/pages/UserProfile/UserProfile.tsx @@ -91,7 +91,7 @@ const UserProfile = () => { Header: getString('status'), width: '20%', Cell: ({ row }: CellProps) => { - const isActive = +Date.now() < Number(row.original.expires_at) + const isActive = !row.original.expires_at || +Date.now() < Number(row.original.expires_at) return ( { Cell: ({ row }: CellProps) => { return ( - {moment(row.original.expires_at).format('MMM Do, YYYY h:mm:ss a')} + {row.original.expires_at + ? moment(row.original.expires_at).format('MMM Do, YYYY h:mm:ss a') + : getString('noExpiration')} ) } @@ -163,7 +165,14 @@ const UserProfile = () => { - + diff --git a/web/src/pages/UsersListing/UsersListing.tsx b/web/src/pages/UsersListing/UsersListing.tsx index 28b16d9d6..a6b65661e 100644 --- a/web/src/pages/UsersListing/UsersListing.tsx +++ b/web/src/pages/UsersListing/UsersListing.tsx @@ -77,7 +77,13 @@ const UsersListing = () => { Cell: ({ row }: CellProps) => { return ( - + {row.original.display_name} From 338271fb146f60cebb0c9883ebdbd28b036a0eec Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Wed, 26 Jul 2023 20:53:42 +0000 Subject: [PATCH 20/31] feat: [CODE-674]: user's space membership; membership API pagination (#241) --- cmd/gitness/wire_gen.go | 2 +- internal/api/controller/space/create.go | 10 +- .../api/controller/space/membership_add.go | 31 ++- .../api/controller/space/membership_list.go | 29 +- .../api/controller/space/membership_update.go | 6 +- internal/api/controller/user/controller.go | 17 +- .../api/controller/user/membership_spaces.go | 38 +++ internal/api/controller/user/wire.go | 16 +- internal/api/handler/space/membership_list.go | 7 +- .../api/handler/user/membership_spaces.go | 29 ++ internal/api/openapi/user.go | 8 + internal/api/request/membership.go | 30 ++ internal/router/api.go | 1 + internal/store/database.go | 7 +- internal/store/database/membership.go | 261 ++++++++++++++---- internal/store/database/principal_info.go | 37 ++- types/enum/membership.go | 46 +++ types/membership.go | 23 +- 18 files changed, 497 insertions(+), 101 deletions(-) create mode 100644 internal/api/controller/user/membership_spaces.go create mode 100644 internal/api/handler/user/membership_spaces.go create mode 100644 internal/api/request/membership.go create mode 100644 types/enum/membership.go diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 10aabfc87..dfe3a063a 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -65,7 +65,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro principalUIDTransformation := store.ProvidePrincipalUIDTransformation() principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation) tokenStore := database.ProvideTokenStore(db) - controller := user.NewController(principalUID, authorizer, principalStore, tokenStore) + controller := user.NewController(principalUID, authorizer, principalStore, tokenStore, membershipStore) serviceController := service.NewController(principalUID, authorizer, principalStore) bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller, serviceController) authenticator := authn.ProvideAuthenticator(principalStore, tokenStore) diff --git a/internal/api/controller/space/create.go b/internal/api/controller/space/create.go index 09aa2f765..c5a3081ba 100644 --- a/internal/api/controller/space/create.go +++ b/internal/api/controller/space/create.go @@ -99,13 +99,15 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea 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) + // add space membership to top level space only (as the user doesn't have inherited permissions already) parentRefAsID, err := strconv.ParseInt(in.ParentRef, 10, 64) if (err == nil && parentRefAsID == 0) || (len(strings.TrimSpace(in.ParentRef)) == 0) { membership := &types.Membership{ - SpaceID: space.ID, - PrincipalID: session.Principal.ID, - Role: enum.MembershipRoleSpaceOwner, + MembershipKey: types.MembershipKey{ + SpaceID: space.ID, + PrincipalID: session.Principal.ID, + }, + Role: enum.MembershipRoleSpaceOwner, // membership has been created by the system CreatedBy: bootstrap.NewSystemServiceSession().Principal.ID, diff --git a/internal/api/controller/space/membership_add.go b/internal/api/controller/space/membership_add.go index 8538c1013..0a9f452c8 100644 --- a/internal/api/controller/space/membership_add.go +++ b/internal/api/controller/space/membership_add.go @@ -50,7 +50,7 @@ func (c *Controller) MembershipAdd(ctx context.Context, session *auth.Session, spaceRef string, in *MembershipAddInput, -) (*types.Membership, error) { +) (*types.MembershipUser, error) { space, err := c.spaceStore.FindByRef(ctx, spaceRef) if err != nil { return nil, err @@ -74,22 +74,27 @@ func (c *Controller) MembershipAdd(ctx context.Context, 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(), - AddedBy: *session.Principal.ToPrincipalInfo(), + membership := types.Membership{ + MembershipKey: types.MembershipKey{ + SpaceID: space.ID, + PrincipalID: user.ID, + }, + CreatedBy: session.Principal.ID, + Created: now, + Updated: now, + Role: in.Role, } - err = c.membershipStore.Create(ctx, membership) + err = c.membershipStore.Create(ctx, &membership) if err != nil { return nil, fmt.Errorf("failed to create new membership: %w", err) } - return membership, nil + result := &types.MembershipUser{ + Membership: membership, + Principal: *user.ToPrincipalInfo(), + AddedBy: *session.Principal.ToPrincipalInfo(), + } + + return result, nil } diff --git a/internal/api/controller/space/membership_list.go b/internal/api/controller/space/membership_list.go index 45588753d..07630d878 100644 --- a/internal/api/controller/space/membership_list.go +++ b/internal/api/controller/space/membership_list.go @@ -10,6 +10,7 @@ import ( apiauth "github.com/harness/gitness/internal/api/auth" "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) @@ -18,20 +19,36 @@ import ( func (c *Controller) MembershipList(ctx context.Context, session *auth.Session, spaceRef string, -) ([]*types.Membership, error) { + opts types.MembershipFilter, +) ([]types.MembershipUser, int64, error) { space, err := c.spaceStore.FindByRef(ctx, spaceRef) if err != nil { - return nil, err + return nil, 0, err } if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceView, false); err != nil { - return nil, err + return nil, 0, err } - memberships, err := c.membershipStore.ListForSpace(ctx, space.ID) + var memberships []types.MembershipUser + var membershipsCount int64 + + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { + memberships, err = c.membershipStore.ListUsers(ctx, space.ID, opts) + if err != nil { + return fmt.Errorf("failed to list memberships for space: %w", err) + } + + membershipsCount, err = c.membershipStore.CountUsers(ctx, space.ID, opts) + if err != nil { + return fmt.Errorf("failed to count memberships for space: %w", err) + } + + return nil + }, dbtx.TxDefaultReadOnly) if err != nil { - return nil, fmt.Errorf("failed to list memberships for space: %w", err) + return nil, 0, err } - return memberships, nil + return memberships, membershipsCount, nil } diff --git a/internal/api/controller/space/membership_update.go b/internal/api/controller/space/membership_update.go index c07a0c68f..4c86f8a4f 100644 --- a/internal/api/controller/space/membership_update.go +++ b/internal/api/controller/space/membership_update.go @@ -42,7 +42,7 @@ func (c *Controller) MembershipUpdate(ctx context.Context, spaceRef string, userUID string, in *MembershipUpdateInput, -) (*types.Membership, error) { +) (*types.MembershipUser, error) { space, err := c.spaceStore.FindByRef(ctx, spaceRef) if err != nil { return nil, err @@ -62,7 +62,7 @@ func (c *Controller) MembershipUpdate(ctx context.Context, return nil, fmt.Errorf("failed to find user by uid: %w", err) } - membership, err := c.membershipStore.Find(ctx, types.MembershipKey{ + membership, err := c.membershipStore.FindUser(ctx, types.MembershipKey{ SpaceID: space.ID, PrincipalID: user.ID, }) @@ -76,7 +76,7 @@ func (c *Controller) MembershipUpdate(ctx context.Context, membership.Role = in.Role - err = c.membershipStore.Update(ctx, membership) + err = c.membershipStore.Update(ctx, &membership.Membership) if err != nil { return nil, fmt.Errorf("failed to update membership") } diff --git a/internal/api/controller/user/controller.go b/internal/api/controller/user/controller.go index b90be35db..76060c3c1 100644 --- a/internal/api/controller/user/controller.go +++ b/internal/api/controller/user/controller.go @@ -21,27 +21,36 @@ type Controller struct { authorizer authz.Authorizer principalStore store.PrincipalStore tokenStore store.TokenStore + membershipStore store.MembershipStore } -func NewController(principalUIDCheck check.PrincipalUID, authorizer authz.Authorizer, - principalStore store.PrincipalStore, tokenStore store.TokenStore) *Controller { +func NewController( + principalUIDCheck check.PrincipalUID, + authorizer authz.Authorizer, + principalStore store.PrincipalStore, + tokenStore store.TokenStore, + membershipStore store.MembershipStore, +) *Controller { return &Controller{ principalUIDCheck: principalUIDCheck, authorizer: authorizer, principalStore: principalStore, tokenStore: tokenStore, + membershipStore: membershipStore, } } var hashPassword = bcrypt.GenerateFromPassword func findUserFromUID(ctx context.Context, - principalStore store.PrincipalStore, userUID string) (*types.User, error) { + principalStore store.PrincipalStore, userUID string, +) (*types.User, error) { return principalStore.FindUserByUID(ctx, userUID) } func findUserFromEmail(ctx context.Context, - principalStore store.PrincipalStore, email string) (*types.User, error) { + principalStore store.PrincipalStore, email string, +) (*types.User, error) { return principalStore.FindUserByEmail(ctx, email) } diff --git a/internal/api/controller/user/membership_spaces.go b/internal/api/controller/user/membership_spaces.go new file mode 100644 index 000000000..a954e45d7 --- /dev/null +++ b/internal/api/controller/user/membership_spaces.go @@ -0,0 +1,38 @@ +// 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 user + +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" +) + +// MembershipSpaces lists all spaces in which the user is a member. +func (c *Controller) MembershipSpaces(ctx context.Context, + session *auth.Session, + userUID string, +) ([]types.MembershipSpace, error) { + user, err := findUserFromUID(ctx, c.principalStore, userUID) + if err != nil { + return nil, fmt.Errorf("failed to find user by UID: %w", err) + } + + // Ensure principal has required permissions. + if err = apiauth.CheckUser(ctx, c.authorizer, session, user, enum.PermissionUserView); err != nil { + return nil, err + } + + membershipSpaces, err := c.membershipStore.ListSpaces(ctx, user.ID) + if err != nil { + return nil, fmt.Errorf("failed to list membership spaces for user: %w", err) + } + + return membershipSpaces, nil +} diff --git a/internal/api/controller/user/wire.go b/internal/api/controller/user/wire.go index ae8d0343f..bf8418dbf 100644 --- a/internal/api/controller/user/wire.go +++ b/internal/api/controller/user/wire.go @@ -17,7 +17,17 @@ var WireSet = wire.NewSet( NewController, ) -func ProvideController(principalUIDCheck check.PrincipalUID, authorizer authz.Authorizer, - principalStore store.PrincipalStore, tokenStore store.TokenStore) *Controller { - return NewController(principalUIDCheck, authorizer, principalStore, tokenStore) +func ProvideController( + principalUIDCheck check.PrincipalUID, + authorizer authz.Authorizer, + principalStore store.PrincipalStore, + tokenStore store.TokenStore, + membershipStore store.MembershipStore, +) *Controller { + return NewController( + principalUIDCheck, + authorizer, + principalStore, + tokenStore, + membershipStore) } diff --git a/internal/api/handler/space/membership_list.go b/internal/api/handler/space/membership_list.go index db7951868..715cd48d6 100644 --- a/internal/api/handler/space/membership_list.go +++ b/internal/api/handler/space/membership_list.go @@ -24,12 +24,15 @@ func HandleMembershipList(spaceCtrl *space.Controller) http.HandlerFunc { return } - memberInfos, err := spaceCtrl.MembershipList(ctx, session, spaceRef) + filter := request.ParseMembershipFilter(r) + + memberships, membershipsCount, err := spaceCtrl.MembershipList(ctx, session, spaceRef, filter) if err != nil { render.TranslatedUserError(w, err) return } - render.JSON(w, http.StatusOK, memberInfos) + render.Pagination(r, w, filter.Page, filter.Size, int(membershipsCount)) + render.JSON(w, http.StatusOK, memberships) } } diff --git a/internal/api/handler/user/membership_spaces.go b/internal/api/handler/user/membership_spaces.go new file mode 100644 index 000000000..b869f4f3d --- /dev/null +++ b/internal/api/handler/user/membership_spaces.go @@ -0,0 +1,29 @@ +// 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 user + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/user" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +func HandleMembershipSpaces(userCtrl *user.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + userUID := session.Principal.UID + + membershipSpaces, err := userCtrl.MembershipSpaces(ctx, session, userUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, membershipSpaces) + } +} diff --git a/internal/api/openapi/user.go b/internal/api/openapi/user.go index 0e7c1c32e..b7003efea 100644 --- a/internal/api/openapi/user.go +++ b/internal/api/openapi/user.go @@ -44,4 +44,12 @@ func buildUser(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opToken, new(types.TokenResponse), http.StatusCreated) _ = reflector.SetJSONResponse(&opToken, new(usererror.Error), http.StatusInternalServerError) _ = reflector.Spec.AddOperation(http.MethodPost, "/user/token", opToken) + + opMemberSpaces := openapi3.Operation{} + opMemberSpaces.WithTags("user") + opMemberSpaces.WithMapOfAnything(map[string]interface{}{"operationId": "membershipSpaces"}) + _ = reflector.SetRequest(&opMemberSpaces, struct{}{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opMemberSpaces, new([]types.MembershipSpace), http.StatusOK) + _ = reflector.SetJSONResponse(&opMemberSpaces, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.Spec.AddOperation(http.MethodGet, "/user/memberships", opMemberSpaces) } diff --git a/internal/api/request/membership.go b/internal/api/request/membership.go new file mode 100644 index 000000000..4aed70a67 --- /dev/null +++ b/internal/api/request/membership.go @@ -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 request + +import ( + "net/http" + + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ParseMembershipSort extracts the membership sort parameter from the url. +func ParseMembershipSort(r *http.Request) enum.MembershipSort { + return enum.ParseMembershipSort( + r.URL.Query().Get(QueryParamSort), + ) +} + +// ParseMembershipFilter extracts the membership filter from the url. +func ParseMembershipFilter(r *http.Request) types.MembershipFilter { + return types.MembershipFilter{ + Page: ParsePage(r), + Size: ParseLimit(r), + Query: ParseQuery(r), + Sort: ParseMembershipSort(r), + Order: ParseOrder(r), + } +} diff --git a/internal/router/api.go b/internal/router/api.go index cae16b08a..12b856275 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -354,6 +354,7 @@ func setupUser(r chi.Router, userCtrl *user.Controller) { r.Use(middlewareprincipal.RestrictTo(enum.PrincipalTypeUser)) r.Get("/", handleruser.HandleFind(userCtrl)) r.Patch("/", handleruser.HandleUpdate(userCtrl)) + r.Get("/memberships", handleruser.HandleMembershipSpaces(userCtrl)) // PAT r.Route("/tokens", func(r chi.Router) { diff --git a/internal/store/database.go b/internal/store/database.go index cbf9d7ffb..56926837c 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -226,10 +226,13 @@ type ( // 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 + FindUser(ctx context.Context, key types.MembershipKey) (*types.MembershipUser, error) + Create(ctx context.Context, membership *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) + CountUsers(ctx context.Context, spaceID int64, filter types.MembershipFilter) (int64, error) + ListUsers(ctx context.Context, spaceID int64, filter types.MembershipFilter) ([]types.MembershipUser, error) + ListSpaces(ctx context.Context, userID int64) ([]types.MembershipSpace, error) } // TokenStore defines the token data storage. diff --git a/internal/store/database/membership.go b/internal/store/database/membership.go index 35f7f96af..7c393b87f 100644 --- a/internal/store/database/membership.go +++ b/internal/store/database/membership.go @@ -7,6 +7,7 @@ package database import ( "context" "fmt" + "strings" "time" "github.com/harness/gitness/internal/store" @@ -15,9 +16,8 @@ import ( "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" ) var _ store.MembershipStore = (*MembershipStore)(nil) @@ -47,6 +47,16 @@ type membership struct { Role enum.MembershipRole `db:"membership_role"` } +type membershipPrincipal struct { + membership + principalInfo +} + +type membershipSpace struct { + membership + space +} + const ( membershipColumns = ` membership_space_id @@ -73,7 +83,23 @@ func (s *MembershipStore) Find(ctx context.Context, key types.MembershipKey) (*t return nil, database.ProcessSQLErrorf(err, "Failed to find membership") } - return s.mapToMembership(ctx, dst), nil + result := mapToMembership(dst) + + return &result, nil +} + +func (s *MembershipStore) FindUser(ctx context.Context, key types.MembershipKey) (*types.MembershipUser, error) { + m, err := s.Find(ctx, key) + if err != nil { + return nil, err + } + + result, err := s.addPrincipalInfos(ctx, m) + if err != nil { + return nil, err + } + + return &result, nil } // Create creates a new membership. @@ -154,37 +180,145 @@ func (s *MembershipStore) Delete(ctx context.Context, key types.MembershipKey) e return nil } -// ListForSpace returns a list of memberships for a space. -func (s *MembershipStore) ListForSpace(ctx context.Context, spaceID int64) ([]*types.Membership, error) { +// CountUsers returns a number of users memberships that matches the provided filter. +func (s *MembershipStore) CountUsers(ctx context.Context, + spaceID int64, + filter types.MembershipFilter, +) (int64, error) { stmt := database.Builder. - Select(membershipColumns). + Select("count(*)"). From("memberships"). - Where("membership_space_id = ?", spaceID). - OrderBy("membership_created asc") + InnerJoin("principals ON membership_principal_id = principal_id"). + Where("membership_space_id = ?", spaceID) + + stmt = prepareMembershipListUsersStmt(stmt, filter) sql, args, err := stmt.ToSql() if err != nil { - return nil, errors.Wrap(err, "Failed to convert membership for space list query to sql") + return 0, fmt.Errorf("failed to convert membership count query to sql: %w", err) } - dst := make([]*membership, 0) + db := dbtx.GetAccessor(ctx, s.db) + + var count int64 + err = db.QueryRowContext(ctx, sql, args...).Scan(&count) + if err != nil { + return 0, database.ProcessSQLErrorf(err, "Failed executing membership count query") + } + + return count, nil +} + +// ListUsers returns a list of memberships for a space or a user. +func (s *MembershipStore) ListUsers(ctx context.Context, + spaceID int64, + filter types.MembershipFilter, +) ([]types.MembershipUser, error) { + const columns = membershipColumns + "," + principalInfoCommonColumns + stmt := database.Builder. + Select(columns). + From("memberships"). + InnerJoin("principals ON membership_principal_id = principal_id"). + Where("membership_space_id = ?", spaceID) + + stmt = prepareMembershipListUsersStmt(stmt, filter) + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.Size)) + + order := filter.Order + if order == enum.OrderDefault { + order = enum.OrderAsc + } + + switch filter.Sort { + case enum.MembershipSortName: + stmt = stmt.OrderBy("principal_display_name " + order.String()) + case enum.MembershipSortCreated: + stmt = stmt.OrderBy("membership_created " + order.String()) + case enum.MembershipSortNone: + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert membership users list query to sql: %w", err) + } + + dst := make([]*membershipPrincipal, 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") + return nil, database.ProcessSQLErrorf(err, "Failed executing membership users list query") } - result, err := s.mapToMemberships(ctx, dst) + result, err := s.mapToMembershipUsers(ctx, dst) if err != nil { - return nil, fmt.Errorf("failed to map memberships to external type: %w", err) + return nil, fmt.Errorf("failed to map memberships users to external type: %w", err) } return result, nil } -func mapToMembershipNoPrincipalInfo(m *membership) *types.Membership { - return &types.Membership{ +func prepareMembershipListUsersStmt( + stmt squirrel.SelectBuilder, + opts types.MembershipFilter, +) squirrel.SelectBuilder { + if opts.Query != "" { + searchTerm := "%%" + strings.ToLower(opts.Query) + "%%" + stmt = stmt.Where("LOWER(principal_display_name) LIKE ?", searchTerm) + } + + return stmt +} + +// ListSpaces returns a list of spaces in which the provided user is a member. +func (s *MembershipStore) ListSpaces(ctx context.Context, + userID int64, +) ([]types.MembershipSpace, error) { + const columns = membershipColumns + "," + spaceColumnsForJoin + stmt := database.Builder. + Select(columns). + From("memberships"). + InnerJoin("spaces ON spaces.space_id = membership_space_id"). + InnerJoin(`paths ON spaces.space_id=paths.path_space_id AND paths.path_is_primary=true`). + Where("membership_principal_id = ?", userID). + OrderBy("space_path asc") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert membership spaces list query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + dst := make([]*membershipSpace, 0) + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") + } + + result, err := s.mapToMembershipSpaces(ctx, dst) + if err != nil { + return nil, fmt.Errorf("failed to map memberships spaces to external type: %w", err) + } + + return result, nil +} + +func mapToMembership(m *membership) types.Membership { + return types.Membership{ + MembershipKey: types.MembershipKey{ + 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, @@ -194,44 +328,37 @@ func mapToMembershipNoPrincipalInfo(m *membership) *types.Membership { } } -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) addPrincipalInfos(ctx context.Context, m *types.Membership) (types.MembershipUser, error) { + var result types.MembershipUser + + // pull principal infos from cache + infoMap, err := s.pCache.Map(ctx, []int64{m.CreatedBy, m.PrincipalID}) + if err != nil { + return result, fmt.Errorf("failed to load membership principal infos: %w", err) } + + if user, ok := infoMap[m.PrincipalID]; ok { + result.Principal = *user + } else { + return result, fmt.Errorf("failed to find membership principal info: %w", err) + } + + if addedBy, ok := infoMap[m.CreatedBy]; ok { + result.AddedBy = *addedBy + } + + result.Membership = *m + + return result, nil } -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.AddedBy = *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) { +func (s *MembershipStore) mapToMembershipUsers(ctx context.Context, + ms []*membershipPrincipal, +) ([]types.MembershipUser, error) { // collect all principal IDs - ids := make([]int64, 0, 2*len(ms)) + ids := make([]int64, 0, len(ms)) for _, m := range ms { - ids = append(ids, m.CreatedBy, m.PrincipalID) + ids = append(ids, m.membership.CreatedBy) } // pull principal infos from cache @@ -241,15 +368,41 @@ func (s *MembershipStore) mapToMemberships(ctx context.Context, ms []*membership } // attach the principal infos back to the slice items - res := make([]*types.Membership, len(ms)) + res := make([]types.MembershipUser, len(ms)) for i, m := range ms { - res[i] = mapToMembershipNoPrincipalInfo(m) - if addedBy, ok := infoMap[m.CreatedBy]; ok { + res[i].Membership = mapToMembership(&m.membership) + res[i].Principal = mapToPrincipalInfo(&m.principalInfo) + if addedBy, ok := infoMap[m.membership.CreatedBy]; ok { + res[i].AddedBy = *addedBy + } + } + + return res, nil +} + +func (s *MembershipStore) mapToMembershipSpaces(ctx context.Context, + ms []*membershipSpace, +) ([]types.MembershipSpace, error) { + // collect all principal IDs + ids := make([]int64, 0, len(ms)) + for _, m := range ms { + ids = append(ids, m.membership.CreatedBy) + } + + // 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.MembershipSpace, len(ms)) + for i, m := range ms { + res[i].Membership = mapToMembership(&m.membership) + res[i].Space = *mapToSpace(&m.space) + if addedBy, ok := infoMap[m.membership.CreatedBy]; ok { res[i].AddedBy = *addedBy } - if principal, ok := infoMap[m.PrincipalID]; ok { - res[i].Principal = *principal - } } return res, nil diff --git a/internal/store/database/principal_info.go b/internal/store/database/principal_info.go index a882e4598..4b9e5281e 100644 --- a/internal/store/database/principal_info.go +++ b/internal/store/database/principal_info.go @@ -11,6 +11,7 @@ import ( "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/Masterminds/squirrel" "github.com/jmoiron/sqlx" @@ -32,15 +33,25 @@ type PrincipalInfoView struct { const ( principalInfoCommonColumns = ` - principal_id, - principal_uid, - principal_email, - principal_display_name, - principal_type, - principal_created, - principal_updated` + principal_id + ,principal_uid + ,principal_email + ,principal_display_name + ,principal_type + ,principal_created + ,principal_updated` ) +type principalInfo struct { + ID int64 `db:"principal_id"` + UID string `db:"principal_uid"` + DisplayName string `db:"principal_display_name"` + Email string `db:"principal_email"` + Type enum.PrincipalType `db:"principal_type"` + Created int64 `db:"principal_created"` + Updated int64 `db:"principal_updated"` +} + // Find returns a single principal info object by id from the `principals` database table. func (s *PrincipalInfoView) Find(ctx context.Context, id int64) (*types.PrincipalInfo, error) { const sqlQuery = ` @@ -107,3 +118,15 @@ func (s *PrincipalInfoView) FindMany(ctx context.Context, ids []int64) ([]*types return result, nil } + +func mapToPrincipalInfo(p *principalInfo) types.PrincipalInfo { + return types.PrincipalInfo{ + ID: p.ID, + UID: p.UID, + DisplayName: p.DisplayName, + Email: p.Email, + Type: p.Type, + Created: p.Created, + Updated: p.Updated, + } +} diff --git a/types/enum/membership.go b/types/enum/membership.go new file mode 100644 index 000000000..42a63d73b --- /dev/null +++ b/types/enum/membership.go @@ -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 enum + +import ( + "strings" +) + +// MembershipSort represents membership sort order. +type MembershipSort int + +// Order enumeration. +const ( + MembershipSortNone MembershipSort = iota + MembershipSortName + MembershipSortCreated +) + +// ParseMembershipSort parses the membership sort attribute string +// and returns the equivalent enumeration. +func ParseMembershipSort(s string) MembershipSort { + switch strings.ToLower(s) { + case name: + return MembershipSortName + case created, createdAt: + return MembershipSortCreated + default: + return MembershipSortNone + } +} + +// String returns the string representation of the attribute. +func (a MembershipSort) String() string { + switch a { + case MembershipSortName: + return name + case MembershipSortCreated: + return created + case MembershipSortNone: + return "" + default: + return undefined + } +} diff --git a/types/membership.go b/types/membership.go index 350572ed5..e8a908c23 100644 --- a/types/membership.go +++ b/types/membership.go @@ -16,15 +16,34 @@ type MembershipKey struct { // Membership represents a user's membership of a space. type Membership struct { - SpaceID int64 `json:"-"` - PrincipalID int64 `json:"-"` + MembershipKey `json:"-"` CreatedBy int64 `json:"-"` Created int64 `json:"created"` Updated int64 `json:"updated"` Role enum.MembershipRole `json:"role"` +} +// MembershipUser adds user info to the Membership data. +type MembershipUser struct { + Membership Principal PrincipalInfo `json:"principal"` AddedBy PrincipalInfo `json:"added_by"` } + +// MembershipSpace adds space info to the Membership data. +type MembershipSpace struct { + Membership + Space Space `json:"space"` + AddedBy PrincipalInfo `json:"added_by"` +} + +// MembershipFilter holds membership query parameters. +type MembershipFilter struct { + Page int `json:"page"` + Size int `json:"size"` + Query string `json:"query"` + Sort enum.MembershipSort `json:"sort"` + Order enum.Order `json:"order"` +} From 8708122eeabc4bd979740f2386032276a1f3e9c3 Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Wed, 26 Jul 2023 21:23:40 +0000 Subject: [PATCH 21/31] [Mock] Refresh Client Mock (#243) --- mocks/mock_client.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mocks/mock_client.go b/mocks/mock_client.go index 3216e04cb..b16a6490e 100644 --- a/mocks/mock_client.go +++ b/mocks/mock_client.go @@ -37,33 +37,33 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { } // Login mocks base method. -func (m *MockClient) Login(arg0 context.Context, arg1, arg2 string) (*types.TokenResponse, error) { +func (m *MockClient) Login(arg0 context.Context, arg1 *user.LoginInput) (*types.TokenResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Login", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Login", arg0, arg1) ret0, _ := ret[0].(*types.TokenResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // Login indicates an expected call of Login. -func (mr *MockClientMockRecorder) Login(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockClient)(nil).Login), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockClient)(nil).Login), arg0, arg1) } // Register mocks base method. -func (m *MockClient) Register(arg0 context.Context, arg1, arg2, arg3, arg4 string) (*types.TokenResponse, error) { +func (m *MockClient) Register(arg0 context.Context, arg1 *user.RegisterInput) (*types.TokenResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Register", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "Register", arg0, arg1) ret0, _ := ret[0].(*types.TokenResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // Register indicates an expected call of Register. -func (mr *MockClientMockRecorder) Register(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) Register(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1) } // Self mocks base method. From 30fc262c13d30650db55f3174f6ceecf26bb3d58 Mon Sep 17 00:00:00 2001 From: Hitesh Aringa Date: Wed, 26 Jul 2023 21:30:34 +0000 Subject: [PATCH 22/31] feat: [code-661]: add refetch on space popovver and user mem (#244) --- .../NewSpaceModalButton.tsx | 5 +- .../SpaceSelector/SpaceSelector.tsx | 98 ++++--------------- web/src/pages/Home/Home.tsx | 20 ++-- .../RepositoriesListing.tsx | 1 - 4 files changed, 27 insertions(+), 97 deletions(-) diff --git a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx index 3edd7d7fc..a2f6ede04 100644 --- a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx +++ b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx @@ -53,7 +53,7 @@ export interface NewSpaceModalButtonProps extends Omit { modalTitle: string submitButtonTitle?: string cancelButtonTitle?: string - // onSubmit: (data: TypesRepository) => void + onRefetch: () => void } export interface OpenapiCreateSpaceRequestExtended extends OpenapiCreateSpaceRequest { parent_id?: number @@ -64,7 +64,7 @@ export const NewSpaceModalButton: React.FC = ({ modalTitle, submitButtonTitle, cancelButtonTitle, - // onSubmit, + onRefetch, ...props }) => { const ModalComponent: React.FC = () => { @@ -90,6 +90,7 @@ export const NewSpaceModalButton: React.FC = ({ createSpace(payload) .then(() => { hideModal() + onRefetch() }) .catch(_error => { showError(getErrorMessage(_error), 0, getString('failedToCreateSpace')) diff --git a/web/src/components/SpaceSelector/SpaceSelector.tsx b/web/src/components/SpaceSelector/SpaceSelector.tsx index e7d835ff8..7d75cea97 100644 --- a/web/src/components/SpaceSelector/SpaceSelector.tsx +++ b/web/src/components/SpaceSelector/SpaceSelector.tsx @@ -14,13 +14,14 @@ import { } from '@harness/uicore' import cx from 'classnames' import Keywords from 'react-keywords' +import { useGet } from 'restful-react' import { Classes, Popover, Position } from '@blueprintjs/core' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useStrings } from 'framework/strings' -import { ButtonRoleProps, LIST_FETCHING_LIMIT } from 'utils/Utils' +import { ButtonRoleProps } from 'utils/Utils' import { useShowRequestError } from 'hooks/useShowRequestError' -import { TypesRepository, TypesSpace, useGetSpace } from 'services/code' +import { TypesSpace, useGetSpace } from 'services/code' import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' import { NewSpaceModalButton } from 'components/NewSpaceModalButton/NewSpaceModalButton' // import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination' @@ -43,74 +44,11 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { // const [page, setPage] = usePageIndex(1) const { data, error } = useGetSpace({ space_ref: encodeURIComponent(space), lazy: !space }) - // const { - // data: t, - // // loading, - // // refetch, - // response - // } = useGet({ - // path: `/api/v1/spaces/testspace`, - // queryParams: { page, limit: LIST_FETCHING_LIMIT, query: searchTerm } - // }) - const spaces = [ - { - id: 79, - parent_id: 11, - uid: 'root', - path: 'root', - description: 'This is a root description', - is_public: false, - created_by: 6, - created: 1687816598981, - updated: 1687816598981, - default_branch: 'main', - fork_id: 0, - num_forks: 0, - num_pulls: 0, - num_closed_pulls: 0, - num_open_pulls: 0, - num_merged_pulls: 0, - git_url: 'test.com/1' - }, - { - id: 8, - parent_id: 11, - uid: 'rootChild1', - path: 'root/rootChild1', - description: 'This is a rootchild1 description', - is_public: false, - created_by: 6, - created: 1678875305900, - updated: 1678875305900, - default_branch: 'main', - fork_id: 0, - num_forks: 0, - num_pulls: 0, - num_closed_pulls: 0, - num_open_pulls: 0, - num_merged_pulls: 0, - git_url: 'test.com/2' - }, - { - id: 80, - parent_id: 11, - uid: 'home', - path: 'home', - description: 'This is a home description', - is_public: false, - created_by: 6, - created: 1687816598981, - updated: 1687816598981, - default_branch: 'main', - fork_id: 0, - num_forks: 0, - num_pulls: 0, - num_closed_pulls: 0, - num_open_pulls: 0, - num_merged_pulls: 0, - git_url: 'test.com/3' - } - ] + + const { data: spaces, refetch } = useGet({ + path: '/api/v1/user/memberships' + }) + const selectSpace = useCallback( (_space: TypesSpace, isUserAction: boolean) => { setSelectedSpace(_space) @@ -133,15 +71,16 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { text={getString('newSpace')} variation={ButtonVariation.PRIMARY} icon="plus" + onRefetch={refetch} /> ) - const columns: Column[] = useMemo( + const columns: Column<{ space: TypesSpace }>[] = useMemo( () => [ { Header: getString('spaces'), width: 'calc(100% - 180px)', - Cell: ({ row }: CellProps) => { + Cell: ({ row }: CellProps<{ space: TypesSpace }>) => { const record = row.original return ( @@ -154,11 +93,11 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { /> - {record.uid} + {record.space.uid} - {record.description && ( + {record.space.description && ( - {record.description} + {record.space.description} )} @@ -227,17 +166,16 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { {!!spaces?.length && ( - + hideHeaders className={css.table} columns={columns} data={spaces || []} onRowClick={data => { - console.log(data) setOpened(false) - selectSpace({ uid: data?.uid, path: data?.path }, true) + selectSpace({ uid: data?.space?.uid, path: data?.space?.path }, true) }} - getRowClassName={row => cx(css.row, !row.original.description && css.noDesc)} + getRowClassName={row => cx(css.row, !row.original.space.description && css.noDesc)} /> )} {spaces?.length === 0 && ( @@ -249,7 +187,7 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { text={getString('createSpace')} variation={ButtonVariation.PRIMARY} icon="plus" - // onSubmit={() => {}} + onRefetch={refetch} /> } message={ {getString('emptySpaceText')}} diff --git a/web/src/pages/Home/Home.tsx b/web/src/pages/Home/Home.tsx index b1b1cbd54..b05397325 100644 --- a/web/src/pages/Home/Home.tsx +++ b/web/src/pages/Home/Home.tsx @@ -1,15 +1,14 @@ import React from 'react' import { ButtonVariation, Container, FontVariation, Layout, PageBody, Text } from '@harness/uicore' -// import { useGet } from 'restful-react' +import { useGet } from 'restful-react' // import type { TypesSpace } from 'services/code' import { useStrings } from 'framework/strings' -import { voidFn } from 'utils/Utils' // import { usePageIndex } from 'hooks/usePageIndex' -import css from './Home.module.scss' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { useAppContext } from 'AppContext' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { NewSpaceModalButton } from 'components/NewSpaceModalButton/NewSpaceModalButton' +import css from './Home.module.scss' export default function Home() { const { getString } = useStrings() @@ -19,16 +18,9 @@ export default function Home() { const { space } = useGetRepositoryMetadata() const spaces = [] - // const { - // // data: noSpaceYet, - // // loading, - // refetch - // // response, - // // error - // } = useGet({ - // path: `/api/v1/spaces/testspace`, - // queryParams: { page, limit: LIST_FETCHING_LIMIT } - // }) + const { refetch } = useGet({ + path: '/api/v1/user/memberships' + }) const NewSpaceButton = ( {}} + onRefetch={refetch} /> ) return ( diff --git a/web/src/pages/RepositoriesListing/RepositoriesListing.tsx b/web/src/pages/RepositoriesListing/RepositoriesListing.tsx index 29eaea210..220ce50ea 100644 --- a/web/src/pages/RepositoriesListing/RepositoriesListing.tsx +++ b/web/src/pages/RepositoriesListing/RepositoriesListing.tsx @@ -41,7 +41,6 @@ export default function RepositoriesListing() { const [searchTerm, setSearchTerm] = useState() const { routes } = useAppContext() const { updateQueryParams } = useUpdateQueryParams() - const pageBrowser = useQueryParams() const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1 const [page, setPage] = usePageIndex(pageInit) From 9436855194713fd83963848c459b050cb36baf55 Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Wed, 26 Jul 2023 23:37:22 +0000 Subject: [PATCH 23/31] Proposal - Remove PR diff api and use repo API (#242) --- internal/api/controller/pullreq/pr_diff.go | 47 ---------------------- internal/api/handler/pullreq/pr_diff.go | 45 --------------------- internal/api/openapi/pullreq.go | 11 ----- internal/router/api.go | 1 - web/src/components/Changes/Changes.tsx | 4 +- 5 files changed, 2 insertions(+), 106 deletions(-) delete mode 100644 internal/api/controller/pullreq/pr_diff.go delete mode 100644 internal/api/handler/pullreq/pr_diff.go diff --git a/internal/api/controller/pullreq/pr_diff.go b/internal/api/controller/pullreq/pr_diff.go deleted file mode 100644 index bab0d403b..000000000 --- a/internal/api/controller/pullreq/pr_diff.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 pullreq - -import ( - "context" - "fmt" - "io" - - "github.com/harness/gitness/gitrpc" - "github.com/harness/gitness/internal/auth" - "github.com/harness/gitness/types/enum" -) - -// RawDiff writes raw git diff to writer w. -func (c *Controller) RawDiff( - ctx context.Context, - session *auth.Session, - repoRef string, - pullreqNum int64, - setSHAs func(sourceSHA, mergeBaseSHA string), - w io.Writer, -) error { - repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) - if err != nil { - return fmt.Errorf("failed to acquire access to target repo: %w", err) - } - - pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, pullreqNum) - if err != nil { - return fmt.Errorf("failed to get pull request by number: %w", err) - } - - headRef := pr.SourceSHA - baseRef := pr.MergeBaseSHA - - setSHAs(headRef, baseRef) - - return c.gitRPCClient.RawDiff(ctx, &gitrpc.DiffParams{ - ReadParams: gitrpc.CreateRPCReadParams(repo), - BaseRef: baseRef, - HeadRef: headRef, - MergeBase: true, - }, w) -} diff --git a/internal/api/handler/pullreq/pr_diff.go b/internal/api/handler/pullreq/pr_diff.go deleted file mode 100644 index 86e80e0e1..000000000 --- a/internal/api/handler/pullreq/pr_diff.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 pullreq - -import ( - "net/http" - - "github.com/harness/gitness/internal/api/controller/pullreq" - "github.com/harness/gitness/internal/api/render" - "github.com/harness/gitness/internal/api/request" -) - -// HandleRawDiff returns raw git diff for PR. -func HandleRawDiff(pullreqCtrl *pullreq.Controller) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - session, _ := request.AuthSessionFrom(ctx) - - repoRef, err := request.GetRepoRefFromPath(r) - if err != nil { - render.TranslatedUserError(w, err) - return - } - - pullreqNumber, err := request.GetPullReqNumberFromPath(r) - if err != nil { - render.TranslatedUserError(w, err) - return - } - - setSHAs := func(sourceSHA, mergeBaseSHA string) { - w.Header().Set("X-Source-Sha", sourceSHA) - w.Header().Set("X-Merge-Base-Sha", mergeBaseSHA) - } - - if err = pullreqCtrl.RawDiff(ctx, session, repoRef, pullreqNumber, setSHAs, w); err != nil { - render.TranslatedUserError(w, err) - return - } - - w.WriteHeader(http.StatusOK) - } -} diff --git a/internal/api/openapi/pullreq.go b/internal/api/openapi/pullreq.go index bd7c42207..fdff3d37f 100644 --- a/internal/api/openapi/pullreq.go +++ b/internal/api/openapi/pullreq.go @@ -459,17 +459,6 @@ func pullReqOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opListCommits, new(usererror.Error), http.StatusNotFound) _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/pullreq/{pullreq_number}/commits", opListCommits) - opRawDiff := openapi3.Operation{} - opRawDiff.WithTags("pullreq") - opRawDiff.WithMapOfAnything(map[string]interface{}{"operationId": "rawPullReqDiff"}) - _ = reflector.SetRequest(&opRawDiff, new(pullReqRequest), http.MethodGet) - _ = reflector.SetStringResponse(&opRawDiff, http.StatusOK, "text/plain") - _ = reflector.SetJSONResponse(&opRawDiff, new(usererror.Error), http.StatusInternalServerError) - _ = reflector.SetJSONResponse(&opRawDiff, new(usererror.Error), http.StatusUnauthorized) - _ = reflector.SetJSONResponse(&opRawDiff, new(usererror.Error), http.StatusForbidden) - _ = reflector.SetJSONResponse(&opRawDiff, new(usererror.Error), http.StatusNotFound) - _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/pullreq/{pullreq_number}/diff", opRawDiff) - opMetaData := openapi3.Operation{} opMetaData.WithTags("pullreq") opMetaData.WithMapOfAnything(map[string]interface{}{"operationId": "pullReqMetaData"}) diff --git a/internal/router/api.go b/internal/router/api.go index 12b856275..7a349868d 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -310,7 +310,6 @@ func SetupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) { r.Post("/", handlerpullreq.HandleReviewSubmit(pullreqCtrl)) }) r.Post("/merge", handlerpullreq.HandleMerge(pullreqCtrl)) - r.Get("/diff", handlerpullreq.HandleRawDiff(pullreqCtrl)) r.Get("/commits", handlerpullreq.HandleCommits(pullreqCtrl)) r.Get("/metadata", handlerpullreq.HandleMetadata(pullreqCtrl)) }) diff --git a/web/src/components/Changes/Changes.tsx b/web/src/components/Changes/Changes.tsx index 0e7c28879..f5bf3a9b8 100644 --- a/web/src/components/Changes/Changes.tsx +++ b/web/src/components/Changes/Changes.tsx @@ -76,8 +76,8 @@ export const Changes: React.FC = ({ refetch, response } = useGet({ - path: `/api/v1/repos/${repoMetadata?.path}/+/${ - pullRequestMetadata ? `pullreq/${pullRequestMetadata.number}/diff` : `compare/${targetBranch}...${sourceBranch}` + path: `/api/v1/repos/${repoMetadata?.path}/+/compare/${ + pullRequestMetadata ? `${pullRequestMetadata.merge_base_sha}...${pullRequestMetadata.source_sha}` : `${targetBranch}...${sourceBranch}` }`, lazy: !targetBranch || !sourceBranch }) From 4460ef86c7270966fac7999fa28417f1943260fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ctan-nhu=E2=80=9D?= <“tan@harness.io”> Date: Wed, 26 Jul 2023 17:13:00 -0700 Subject: [PATCH 24/31] Navigate to signin when hitting 403 + enable sourcemap by a env var --- web/config/webpack.prod.js | 2 +- .../SpaceSelector/SpaceSelector.tsx | 25 +++++++++++++------ web/src/layouts/layout.tsx | 18 +++++++------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/web/config/webpack.prod.js b/web/config/webpack.prod.js index 982448683..162fc1da8 100644 --- a/web/config/webpack.prod.js +++ b/web/config/webpack.prod.js @@ -11,7 +11,7 @@ const prodConfig = { context: CONTEXT, entry: path.resolve(CONTEXT, '/src/index.tsx'), mode: 'production', - devtool: 'source-map', + devtool: process.env.ENABLE_SOURCE_MAP ? 'source-map' : false, output: { filename: '[name].[contenthash:6].js', chunkFilename: '[name].[id].[contenthash:6].js' diff --git a/web/src/components/SpaceSelector/SpaceSelector.tsx b/web/src/components/SpaceSelector/SpaceSelector.tsx index 7d75cea97..440bb2959 100644 --- a/web/src/components/SpaceSelector/SpaceSelector.tsx +++ b/web/src/components/SpaceSelector/SpaceSelector.tsx @@ -15,20 +15,20 @@ import { import cx from 'classnames' import Keywords from 'react-keywords' import { useGet } from 'restful-react' - +import type { CellProps, Column } from 'react-table' +import { useHistory } from 'react-router-dom' import { Classes, Popover, Position } from '@blueprintjs/core' +import { routes } from 'RouteDefinitions' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useStrings } from 'framework/strings' -import { ButtonRoleProps } from 'utils/Utils' +import { ButtonRoleProps, voidFn } from 'utils/Utils' import { useShowRequestError } from 'hooks/useShowRequestError' import { TypesSpace, useGetSpace } from 'services/code' import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' import { NewSpaceModalButton } from 'components/NewSpaceModalButton/NewSpaceModalButton' // import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination' -// import { useGet } from 'restful-react' // import { usePageIndex } from 'hooks/usePageIndex' -import type { CellProps, Column } from 'react-table' import css from './SpaceSelector.module.scss' interface SpaceSelectorProps { @@ -37,6 +37,7 @@ interface SpaceSelectorProps { export const SpaceSelector: React.FC = ({ onSelect }) => { const { getString } = useStrings() + const history = useHistory() const [selectedSpace, setSelectedSpace] = useState() const { space } = useGetRepositoryMetadata() const [opened, setOpened] = React.useState(false) @@ -45,7 +46,11 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { const { data, error } = useGetSpace({ space_ref: encodeURIComponent(space), lazy: !space }) - const { data: spaces, refetch } = useGet({ + const { + data: spaces, + refetch, + response + } = useGet({ path: '/api/v1/user/memberships' }) @@ -63,6 +68,12 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { } }, [space, selectedSpace, data, onSelect, selectSpace]) + useEffect(() => { + if (response?.status === 403) { + history.push(routes.toSignIn()) + } + }, [response, history]) + useShowRequestError(error) const NewSpaceButton = ( = ({ onSelect }) => { text={getString('newSpace')} variation={ButtonVariation.PRIMARY} icon="plus" - onRefetch={refetch} + onRefetch={voidFn(refetch)} /> ) @@ -187,7 +198,7 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { text={getString('createSpace')} variation={ButtonVariation.PRIMARY} icon="plus" - onRefetch={refetch} + onRefetch={voidFn(refetch)} /> } message={ {getString('emptySpaceText')}} diff --git a/web/src/layouts/layout.tsx b/web/src/layouts/layout.tsx index 7e8a45072..7864920fd 100644 --- a/web/src/layouts/layout.tsx +++ b/web/src/layouts/layout.tsx @@ -38,14 +38,16 @@ export const LayoutWithSideNav: React.FC = ({ title, chi - - - - - + + + + + + + {children} From ad1234177673506f306fa991bff7910008d837ff Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Thu, 27 Jul 2023 00:22:52 +0000 Subject: [PATCH 25/31] fix: [code-682]: fix ui issues in standalone (#246) --- web/src/components/SpaceSelector/SpaceSelector.tsx | 5 +++++ web/src/framework/strings/stringTypes.ts | 2 ++ web/src/i18n/strings.en.yaml | 6 ++++-- .../pages/SpaceAccessControl/AddNewMember/AddNewMember.tsx | 6 +++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/web/src/components/SpaceSelector/SpaceSelector.tsx b/web/src/components/SpaceSelector/SpaceSelector.tsx index 440bb2959..325fb1fb7 100644 --- a/web/src/components/SpaceSelector/SpaceSelector.tsx +++ b/web/src/components/SpaceSelector/SpaceSelector.tsx @@ -172,6 +172,11 @@ export const SpaceSelector: React.FC = ({ onSelect }) => { ) - }, [isEditFlow, membershipDetails]) + }, [isEditFlow, membershipDetails, userOptions]) return { openModal: (isEditing?: boolean, memberInfo?: TypesPrincipalInfo) => { From a0f8caac10b53ee10450291b4d4785e389bcefb9 Mon Sep 17 00:00:00 2001 From: Enver Bisevac Date: Thu, 27 Jul 2023 20:09:29 +0200 Subject: [PATCH 28/31] PRs mergeable status should change if target branch is updated --- internal/services/pullreq/handlers_branch.go | 38 +++++++++++++++----- internal/store/database.go | 3 ++ internal/store/database/pullreq.go | 24 +++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/internal/services/pullreq/handlers_branch.go b/internal/services/pullreq/handlers_branch.go index 3dceee943..89b97f404 100644 --- a/internal/services/pullreq/handlers_branch.go +++ b/internal/services/pullreq/handlers_branch.go @@ -24,6 +24,21 @@ import ( func (s *Service) triggerPREventOnBranchUpdate(ctx context.Context, event *events.Event[*gitevents.BranchUpdatedPayload], ) error { + // we should always update PR mergeable status check when target branch is updated. + // - main + // |- develop + // |- feature1 + // |- feature2 + // when feature2 merge changes into develop branch then feature1 branch is not consistent anymore + // and need to run mergeable check even nothing was changed on feature1, same applies to main if someone + // push new commit to main then develop should merge status should be unchecked. + if branch, err := getBranchFromRef(event.Payload.Ref); err == nil { + err = s.pullreqStore.UpdateMergeCheckStatus(ctx, event.Payload.RepoID, branch, enum.MergeCheckStatusUnchecked) + if err != nil { + return err + } + } + // TODO: This function is currently executed directly on branch update event. // TODO: But it should be executed after the PR's head ref has been updated. // TODO: This is to make sure the commit exists on the target repository for forked repositories. @@ -148,17 +163,11 @@ func (s *Service) forEveryOpenPR(ctx context.Context, repoID int64, ref string, fn func(pr *types.PullReq) error, ) { - const refPrefix = "refs/heads/" const largeLimit = 1000000 - if !strings.HasPrefix(ref, refPrefix) { - log.Ctx(ctx).Error().Msg("failed to get branch name from branch ref") - return - } - - branch := ref[len(refPrefix):] + branch, err := getBranchFromRef(ref) if len(branch) == 0 { - log.Ctx(ctx).Error().Msg("got an empty branch name from branch ref") + log.Ctx(ctx).Err(err).Send() return } @@ -182,3 +191,16 @@ func (s *Service) forEveryOpenPR(ctx context.Context, } } } + +func getBranchFromRef(ref string) (string, error) { + const refPrefix = "refs/heads/" + if !strings.HasPrefix(ref, refPrefix) { + return "", fmt.Errorf("failed to get branch name from branch ref %s", ref) + } + + branch := ref[len(refPrefix):] + if len(branch) == 0 { + return "", fmt.Errorf("got an empty branch name from branch ref %s", ref) + } + return branch, nil +} diff --git a/internal/store/database.go b/internal/store/database.go index 56926837c..b71673ece 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -285,6 +285,9 @@ type ( // It will set new values to the ActivitySeq, Version and Updated fields. UpdateActivitySeq(ctx context.Context, pr *types.PullReq) (*types.PullReq, error) + // Update all PR where target branch points to new SHA + UpdateMergeCheckStatus(ctx context.Context, targetRepo int64, targetBranch string, status enum.MergeCheckStatus) error + // Delete the pull request. Delete(ctx context.Context, id int64) error diff --git a/internal/store/database/pullreq.go b/internal/store/database/pullreq.go index 8ad4bbd44..866575daa 100644 --- a/internal/store/database/pullreq.go +++ b/internal/store/database/pullreq.go @@ -335,6 +335,30 @@ func (s *PullReqStore) UpdateActivitySeq(ctx context.Context, pr *types.PullReq) }) } +// UpdateMergeCheckStatus updates the pull request's mergeability status for all pr which target branch points to targetBranch. +func (s *PullReqStore) UpdateMergeCheckStatus(ctx context.Context, targetRepo int64, targetBranch string, status enum.MergeCheckStatus) error { + const query = ` + UPDATE pullreqs + SET + pullreq_updated = $1 + ,pullreq_merge_check_status = $2 + WHERE pullreq_target_repo_id = $3 AND + pullreq_target_branch = $4 AND + pullreq_state not in ($5, $6)` + + db := dbtx.GetAccessor(ctx, s.db) + + updatedAt := time.Now() + + _, err := db.ExecContext(ctx, query, updatedAt, status, targetRepo, targetBranch, + enum.PullReqStateClosed, enum.PullReqStateClosed) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to update mergeable status check %s in pull requests", status) + } + + return nil +} + // Delete the pull request. func (s *PullReqStore) Delete(ctx context.Context, id int64) error { const pullReqDelete = `DELETE FROM pullreqs WHERE pullreq_id = $1` From f5084697b1fd2a3b83ca1c516bf64bccb27b47bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ga=C4=87e=C5=A1a?= Date: Fri, 28 Jul 2023 12:57:16 +0200 Subject: [PATCH 29/31] Minor fixes and refactoring --- internal/api/controller/space/delete.go | 2 +- internal/api/controller/space/list_spaces.go | 34 ++++++++---- internal/api/handler/space/delete.go | 7 +-- internal/router/api.go | 6 +-- internal/store/database.go | 2 +- internal/store/database/membership.go | 2 +- internal/store/database/space.go | 54 ++++++++++---------- mocks/mock_store.go | 4 +- types/enum/user_test.go | 2 +- 9 files changed, 61 insertions(+), 52 deletions(-) diff --git a/internal/api/controller/space/delete.go b/internal/api/controller/space/delete.go index 10547d127..5a4e96dbd 100644 --- a/internal/api/controller/space/delete.go +++ b/internal/api/controller/space/delete.go @@ -36,7 +36,7 @@ func (c *Controller) Delete(ctx context.Context, session *auth.Session, spaceRef func (c *Controller) DeleteNoAuth(ctx context.Context, session *auth.Session, spaceID int64) error { filter := &types.SpaceFilter{ Page: 1, - Size: int(math.MaxInt), + Size: math.MaxInt, Query: "", Order: enum.OrderAsc, Sort: enum.SpaceAttrNone, diff --git a/internal/api/controller/space/list_spaces.go b/internal/api/controller/space/list_spaces.go index 2e2d7db83..e2f2d5ea5 100644 --- a/internal/api/controller/space/list_spaces.go +++ b/internal/api/controller/space/list_spaces.go @@ -10,13 +10,17 @@ import ( apiauth "github.com/harness/gitness/internal/api/auth" "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) // ListSpaces lists the child spaces of a space. -func (c *Controller) ListSpaces(ctx context.Context, session *auth.Session, - spaceRef string, filter *types.SpaceFilter) ([]*types.Space, int64, error) { +func (c *Controller) ListSpaces(ctx context.Context, + session *auth.Session, + spaceRef string, + filter *types.SpaceFilter, +) ([]types.Space, int64, error) { space, err := c.spaceStore.FindByRef(ctx, spaceRef) if err != nil { return nil, 0, err @@ -28,20 +32,30 @@ func (c *Controller) ListSpaces(ctx context.Context, session *auth.Session, return c.ListSpacesNoAuth(ctx, space.ID, filter) } -// List spaces WITHOUT checking PermissionSpaceView. +// ListSpacesNoAuth lists spaces WITHOUT checking PermissionSpaceView. func (c *Controller) ListSpacesNoAuth( ctx context.Context, spaceID int64, filter *types.SpaceFilter, -) ([]*types.Space, int64, error) { - count, err := c.spaceStore.Count(ctx, spaceID, filter) - if err != nil { - return nil, 0, fmt.Errorf("failed to count child spaces: %w", err) - } +) ([]types.Space, int64, error) { + var spaces []types.Space + var count int64 - spaces, err := c.spaceStore.List(ctx, spaceID, filter) + err := dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) (err error) { + count, err = c.spaceStore.Count(ctx, spaceID, filter) + if err != nil { + return fmt.Errorf("failed to count child spaces: %w", err) + } + + spaces, err = c.spaceStore.List(ctx, spaceID, filter) + if err != nil { + return fmt.Errorf("failed to list child spaces: %w", err) + } + + return nil + }, dbtx.TxDefaultReadOnly) if err != nil { - return nil, 0, fmt.Errorf("failed to list child spaces: %w", err) + return nil, 0, err } /* diff --git a/internal/api/handler/space/delete.go b/internal/api/handler/space/delete.go index c93b2ead4..acad17621 100644 --- a/internal/api/handler/space/delete.go +++ b/internal/api/handler/space/delete.go @@ -7,16 +7,13 @@ package space import ( "net/http" - "github.com/harness/gitness/internal/api/controller/repo" "github.com/harness/gitness/internal/api/controller/space" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/request" ) -/* - * Deletes a space. - */ -func HandleDelete(spaceCtrl *space.Controller, repoCtrl *repo.Controller) http.HandlerFunc { +// HandleDelete handles the delete space HTTP API. +func HandleDelete(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) diff --git a/internal/router/api.go b/internal/router/api.go index 7a349868d..cfd5d9f6f 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -122,7 +122,7 @@ func setupRoutesV1(r chi.Router, principalCtrl principal.Controller, checkCtrl *check.Controller, ) { - setupSpaces(r, spaceCtrl, repoCtrl) + setupSpaces(r, spaceCtrl) setupRepos(r, repoCtrl, pullreqCtrl, webhookCtrl, checkCtrl) setupUser(r, userCtrl) setupServiceAccounts(r, saCtrl) @@ -134,7 +134,7 @@ func setupRoutesV1(r chi.Router, setupResources(r) } -func setupSpaces(r chi.Router, spaceCtrl *space.Controller, repoCtrl *repo.Controller) { +func setupSpaces(r chi.Router, spaceCtrl *space.Controller) { r.Route("/spaces", func(r chi.Router) { // Create takes path and parentId via body, not uri r.Post("/", handlerspace.HandleCreate(spaceCtrl)) @@ -143,7 +143,7 @@ func setupSpaces(r chi.Router, spaceCtrl *space.Controller, repoCtrl *repo.Contr // space operations r.Get("/", handlerspace.HandleFind(spaceCtrl)) r.Patch("/", handlerspace.HandleUpdate(spaceCtrl)) - r.Delete("/", handlerspace.HandleDelete(spaceCtrl, repoCtrl)) + r.Delete("/", handlerspace.HandleDelete(spaceCtrl)) r.Post("/move", handlerspace.HandleMove(spaceCtrl)) r.Get("/spaces", handlerspace.HandleListSpaces(spaceCtrl)) diff --git a/internal/store/database.go b/internal/store/database.go index 56926837c..43027ea0c 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -187,7 +187,7 @@ type ( Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error) // List returns a list of child spaces in a space. - List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]*types.Space, error) + List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]types.Space, error) } // RepoStore defines the repository data storage. diff --git a/internal/store/database/membership.go b/internal/store/database/membership.go index 7c393b87f..4aaf54ebc 100644 --- a/internal/store/database/membership.go +++ b/internal/store/database/membership.go @@ -399,7 +399,7 @@ func (s *MembershipStore) mapToMembershipSpaces(ctx context.Context, res := make([]types.MembershipSpace, len(ms)) for i, m := range ms { res[i].Membership = mapToMembership(&m.membership) - res[i].Space = *mapToSpace(&m.space) + res[i].Space = mapToSpace(&m.space) if addedBy, ok := infoMap[m.membership.CreatedBy]; ok { res[i].AddedBy = *addedBy } diff --git a/internal/store/database/space.go b/internal/store/database/space.go index 904ca0f2e..36395aa41 100644 --- a/internal/store/database/space.go +++ b/internal/store/database/space.go @@ -86,7 +86,9 @@ func (s *SpaceStore) Find(ctx context.Context, id int64) (*types.Space, error) { return nil, database.ProcessSQLErrorf(err, "Failed to find space") } - return mapToSpace(dst), nil + result := mapToSpace(dst) + + return &result, nil } // FindByRef finds the space using the spaceRef as either the id or the space path. @@ -113,6 +115,10 @@ func (s *SpaceStore) FindByRef(ctx context.Context, spaceRef string) (*types.Spa // Create a new space. func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error { + if space == nil { + return errors.New("space is nil") + } + const sqlQuery = ` INSERT INTO spaces ( space_version @@ -134,14 +140,9 @@ func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error { ,:space_updated ) RETURNING space_id` - dbSpace, err := mapToInternalSpace(space) - if err != nil { - return fmt.Errorf("failed to map space: %w", err) - } - db := dbtx.GetAccessor(ctx, s.db) - query, args, err := db.BindNamed(sqlQuery, dbSpace) + query, args, err := db.BindNamed(sqlQuery, mapToInternalSpace(space)) if err != nil { return database.ProcessSQLErrorf(err, "Failed to bind space object") } @@ -153,8 +154,12 @@ func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error { return nil } -// Updates the space details. +// Update updates the space details. func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error { + if space == nil { + return errors.New("space is nil") + } + const sqlQuery = ` UPDATE spaces SET @@ -166,10 +171,7 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error { ,space_is_public = :space_is_public WHERE space_id = :space_id AND space_version = :space_version - 1` - dbSpace, err := mapToInternalSpace(space) - if err != nil { - return fmt.Errorf("failed to map space: %w", err) - } + dbSpace := mapToInternalSpace(space) // update Version (used for optimistic locking) and Updated time dbSpace.Version++ @@ -205,7 +207,8 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error { // UpdateOptLock updates the space using the optimistic locking mechanism. func (s *SpaceStore) UpdateOptLock(ctx context.Context, space *types.Space, - mutateFn func(space *types.Space) error) (*types.Space, error) { + mutateFn func(space *types.Space) error, +) (*types.Space, error) { for { dup := *space @@ -229,7 +232,7 @@ func (s *SpaceStore) UpdateOptLock(ctx context.Context, } } -// Deletes the space. +// Delete deletes a space. func (s *SpaceStore) Delete(ctx context.Context, id int64) error { const sqlQuery = ` DELETE FROM spaces @@ -271,7 +274,7 @@ func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilte } // List returns a list of spaces under the parent space. -func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]*types.Space, error) { +func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]types.Space, error) { stmt := database.Builder. Select(spaceColumnsForJoin). From("spaces"). @@ -310,7 +313,7 @@ func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter db := dbtx.GetAccessor(ctx, s.db) - dst := []*space{} + var dst []*space if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") } @@ -318,8 +321,8 @@ func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter return mapToSpaces(dst), nil } -func mapToSpace(s *space) *types.Space { - res := &types.Space{ +func mapToSpace(s *space) types.Space { + res := types.Space{ ID: s.ID, Version: s.Version, UID: s.UID, @@ -339,21 +342,16 @@ func mapToSpace(s *space) *types.Space { return res } -func mapToSpaces(spaces []*space) []*types.Space { - res := make([]*types.Space, len(spaces)) +func mapToSpaces(spaces []*space) []types.Space { + res := make([]types.Space, len(spaces)) for i := range spaces { res[i] = mapToSpace(spaces[i]) } return res } -func mapToInternalSpace(s *types.Space) (*space, error) { - // space comes from outside. - if s == nil { - return nil, fmt.Errorf("space is nil") - } - - res := &space{ +func mapToInternalSpace(s *types.Space) space { + res := space{ ID: s.ID, Version: s.Version, UID: s.UID, @@ -371,5 +369,5 @@ func mapToInternalSpace(s *types.Space) (*space, error) { res.ParentID = null.IntFrom(s.ParentID) } - return res, nil + return res } diff --git a/mocks/mock_store.go b/mocks/mock_store.go index 34c2db348..0af0bbc34 100644 --- a/mocks/mock_store.go +++ b/mocks/mock_store.go @@ -529,10 +529,10 @@ func (mr *MockSpaceStoreMockRecorder) FindByRef(arg0, arg1 interface{}) *gomock. } // List mocks base method. -func (m *MockSpaceStore) List(arg0 context.Context, arg1 int64, arg2 *types.SpaceFilter) ([]*types.Space, error) { +func (m *MockSpaceStore) List(arg0 context.Context, arg1 int64, arg2 *types.SpaceFilter) ([]types.Space, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) - ret0, _ := ret[0].([]*types.Space) + ret0, _ := ret[0].([]types.Space) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/types/enum/user_test.go b/types/enum/user_test.go index cc0845499..010ccd5c3 100644 --- a/types/enum/user_test.go +++ b/types/enum/user_test.go @@ -11,7 +11,7 @@ func TestParseUserAttr(t *testing.T) { text string want UserAttr }{ - {"id", UserAttrUID}, + {"uid", UserAttrUID}, {"name", UserAttrName}, {"email", UserAttrEmail}, {"created", UserAttrCreated}, From afca55f9d8e282fc2286d270976eadc9696067b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ctan-nhu=E2=80=9D?= <“tan@harness.io”> Date: Fri, 28 Jul 2023 12:07:30 -0700 Subject: [PATCH 30/31] [CODE-687]: Export NewRepoModalButton for CI integration --- web/config/moduleFederation.config.js | 3 ++- web/src/components/NewRepoModalButton/NewRepoModalButton.tsx | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js index 384ba5d64..8b4f0ccb3 100644 --- a/web/config/moduleFederation.config.js +++ b/web/config/moduleFederation.config.js @@ -37,7 +37,8 @@ module.exports = { './Settings': './src/pages/RepositorySettings/RepositorySettings.tsx', './Webhooks': './src/pages/Webhooks/Webhooks.tsx', './WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx', - './WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx' + './WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx', + './NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx' }, shared: { formik: packageJSON.dependencies['formik'], diff --git a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx index 4ac59e9af..b9ae369a9 100644 --- a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx +++ b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx @@ -353,3 +353,5 @@ const BranchName: React.FC = ({ currentBranchName, onSelect }) ) } + +export default NewRepoModalButton From 592a9aeb63cab1e3f1879f323dd9c6007c594f09 Mon Sep 17 00:00:00 2001 From: Hitesh Aringa Date: Fri, 28 Jul 2023 23:15:01 +0000 Subject: [PATCH 31/31] [PR Check] Fix PR Status Check Bug (#253) --- cmd/gitness/wire_gen.go | 3 ++- internal/api/controller/check/controller.go | 4 +++- internal/api/controller/check/wire.go | 13 ++++++++++--- internal/store/database/pullreq.go | 10 ++++++++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index dfe3a063a..ee8870a0f 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -136,7 +136,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro githookController := githook.ProvideController(db, authorizer, principalStore, repoStore, eventsReporter) serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore) principalController := principal.ProvideController(principalStore) - checkController := check2.ProvideController(db, authorizer, repoStore, gitrpcInterface) + checkStore := database.ProvideCheckStore(db, principalInfoCache) + checkController := check2.ProvideController(db, authorizer, repoStore, checkStore, gitrpcInterface) apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, spaceController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController) gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface) webHandler := router.ProvideWebHandler(config) diff --git a/internal/api/controller/check/controller.go b/internal/api/controller/check/controller.go index 3c866f170..3c3055156 100644 --- a/internal/api/controller/check/controller.go +++ b/internal/api/controller/check/controller.go @@ -23,8 +23,8 @@ import ( type Controller struct { db *sqlx.DB authorizer authz.Authorizer - checkStore store.CheckStore repoStore store.RepoStore + checkStore store.CheckStore gitRPCClient gitrpc.Interface } @@ -32,12 +32,14 @@ func NewController( db *sqlx.DB, authorizer authz.Authorizer, repoStore store.RepoStore, + checkStore store.CheckStore, gitRPCClient gitrpc.Interface, ) *Controller { return &Controller{ db: db, authorizer: authorizer, repoStore: repoStore, + checkStore: checkStore, gitRPCClient: gitRPCClient, } } diff --git a/internal/api/controller/check/wire.go b/internal/api/controller/check/wire.go index dacda81a2..6a15eef0c 100644 --- a/internal/api/controller/check/wire.go +++ b/internal/api/controller/check/wire.go @@ -18,11 +18,18 @@ var WireSet = wire.NewSet( ProvideController, ) -func ProvideController(db *sqlx.DB, authorizer authz.Authorizer, +func ProvideController( + db *sqlx.DB, + authorizer authz.Authorizer, repoStore store.RepoStore, + checkStore store.CheckStore, rpcClient gitrpc.Interface, ) *Controller { - return NewController(db, authorizer, + return NewController( + db, + authorizer, repoStore, - rpcClient) + checkStore, + rpcClient, + ) } diff --git a/internal/store/database/pullreq.go b/internal/store/database/pullreq.go index 866575daa..dd95d9050 100644 --- a/internal/store/database/pullreq.go +++ b/internal/store/database/pullreq.go @@ -335,8 +335,14 @@ func (s *PullReqStore) UpdateActivitySeq(ctx context.Context, pr *types.PullReq) }) } -// UpdateMergeCheckStatus updates the pull request's mergeability status for all pr which target branch points to targetBranch. -func (s *PullReqStore) UpdateMergeCheckStatus(ctx context.Context, targetRepo int64, targetBranch string, status enum.MergeCheckStatus) error { +// UpdateMergeCheckStatus updates the pull request's mergeability status +// for all pr which target branch points to targetBranch. +func (s *PullReqStore) UpdateMergeCheckStatus( + ctx context.Context, + targetRepo int64, + targetBranch string, + status enum.MergeCheckStatus, +) error { const query = ` UPDATE pullreqs SET