From 1115a5083b098cb1d3da5a8e05d37f415c624f32 Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Thu, 8 Sep 2022 21:39:15 -0700 Subject: [PATCH] Add `Paths` support and error improvements (#11) This change is adding the concept of Paths. A repository and space always have a Primary Path which always is represents the ancestry to the root space. All access history / resource visibility / child listings / UI traversal / etc. is done via that path. Additionally, repos and spaces can have Alias Paths, which as the name states are aliases. via the primary path. They sole impact is that a space or repo can be reached via different paths from the UI / rest apis / git apis. This fulfills two major purposes: - Customers can rename or move projects and spaces without breaking any existing references from CI pipeliens / code bases / local repos / ... - Customer can create shorter aliases for important repos when in harness embeded mode! (acc/org/proj/repo can be shortened to acc/repo, or acc/repo' Apart from the path changes, this PR adds: Improved User facing errors Improved internal error handling and wrapping update / rename operation for repo and space path list / delete / create operation for repo and space --- go.mod | 3 +- go.sum | 5 +- internal/api/guard/error.go | 49 --- internal/api/guard/guard.go | 39 +- internal/api/guard/guard.repo.go | 36 +- internal/api/guard/guard.space.go | 31 +- internal/api/handler/account/login.go | 12 +- internal/api/handler/account/register.go | 35 +- internal/api/handler/common/path.go | 10 + internal/api/handler/repo/create.go | 65 ++-- internal/api/handler/repo/createPath.go | 77 ++++ internal/api/handler/repo/delete.go | 23 +- internal/api/handler/repo/deletePath.go | 55 +++ internal/api/handler/repo/listPaths.go | 46 +++ internal/api/handler/repo/move.go | 126 ++++++ internal/api/handler/repo/update.go | 60 ++- internal/api/handler/space/create.go | 88 +++-- internal/api/handler/space/createPath.go | 77 ++++ internal/api/handler/space/delete.go | 18 +- internal/api/handler/space/deletePath.go | 55 +++ internal/api/handler/space/list.go | 28 +- internal/api/handler/space/listPaths.go | 47 +++ internal/api/handler/space/listRepos.go | 28 +- internal/api/handler/space/move.go | 123 ++++++ internal/api/handler/space/update.go | 63 ++- internal/api/handler/user/find.go | 8 +- internal/api/handler/user/token.go | 13 +- internal/api/handler/user/update.go | 29 +- internal/api/handler/users/create.go | 35 +- internal/api/handler/users/delete.go | 25 +- internal/api/handler/users/find.go | 19 +- internal/api/handler/users/list.go | 15 +- internal/api/handler/users/update.go | 54 +-- internal/api/middleware/authn/authn.go | 4 +- internal/api/middleware/encode/encode.go | 26 +- internal/api/middleware/repo/repo.go | 27 +- internal/api/middleware/space/space.go | 27 +- internal/api/render/render.go | 12 + internal/api/request/path.go | 27 ++ internal/api/request/repo.go | 6 +- internal/api/request/space.go | 6 +- internal/api/request/util.go | 29 +- internal/auth/authn/authenticator.go | 11 + internal/auth/authn/harness/harness.go | 3 + internal/auth/authn/token.go | 4 + internal/auth/authz/authz.go | 18 + internal/auth/authz/harness/authorizer.go | 12 +- internal/auth/authz/unsafe.go | 7 +- internal/paths/paths.go | 48 +++ internal/router/api.go | 44 ++- internal/router/git.go | 6 +- internal/router/web.go | 6 +- .../postgres/0001_create_table_paths.up.sql | 11 + .../0001_create_table_repositories.up.sql | 4 +- .../postgres/0001_create_table_spaces.up.sql | 2 - ...ate_index_paths_targetType_targetId.up.sql | 2 + .../sqlite/0001_create_table_paths.up.sql | 11 + .../0001_create_table_repositories.up.sql | 4 +- .../sqlite/0001_create_table_spaces.up.sql | 2 - ...ate_index_paths_targetType_targetId.up.sql | 2 + internal/store/database/path.go | 362 ++++++++++++++++++ internal/store/database/repo.go | 267 ++++++++++--- internal/store/database/repo_sync.go | 46 ++- internal/store/database/space.go | 289 +++++++++++--- internal/store/database/space_sync.go | 46 ++- internal/store/database/store.go | 7 +- internal/store/database/testdata/repos.json | 2 - internal/store/database/testdata/spaces.json | 2 - internal/store/database/user.go | 63 ++- internal/store/database/user_sync.go | 2 +- internal/store/database/user_test.go | 2 +- internal/store/database/util.go | 23 ++ internal/store/store.go | 50 ++- internal/token/token.go | 17 +- mocks/mock_store.go | 148 ++++++- types/authz.go | 6 +- types/check/common.go | 56 +++ types/check/path.go | 61 +++ types/check/repo.go | 45 +-- types/check/space.go | 54 +-- types/check/user.go | 13 +- types/enum/authz.go | 8 +- types/enum/path.go | 56 +++ types/enum/repo.go | 6 +- types/enum/space.go | 8 +- types/errs/dynamic.go | 65 ++++ types/errs/static.go | 25 ++ types/path.go | 41 ++ types/repo.go | 3 +- types/space.go | 31 +- types/types.go | 39 -- types/user.go | 51 +++ 92 files changed, 2940 insertions(+), 712 deletions(-) delete mode 100644 internal/api/guard/error.go create mode 100644 internal/api/handler/common/path.go create mode 100644 internal/api/handler/repo/createPath.go create mode 100644 internal/api/handler/repo/deletePath.go create mode 100644 internal/api/handler/repo/listPaths.go create mode 100644 internal/api/handler/repo/move.go create mode 100644 internal/api/handler/space/createPath.go create mode 100644 internal/api/handler/space/deletePath.go create mode 100644 internal/api/handler/space/listPaths.go create mode 100644 internal/api/handler/space/move.go create mode 100644 internal/api/request/path.go create mode 100644 internal/paths/paths.go create mode 100644 internal/store/database/migrate/postgres/0001_create_table_paths.up.sql create mode 100644 internal/store/database/migrate/postgres/0002_create_index_paths_targetType_targetId.up.sql create mode 100644 internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql create mode 100644 internal/store/database/migrate/sqlite/0002_create_index_paths_targetType_targetId.up.sql create mode 100644 internal/store/database/path.go create mode 100644 types/check/common.go create mode 100644 types/check/path.go create mode 100644 types/enum/path.go create mode 100644 types/errs/dynamic.go create mode 100644 types/errs/static.go create mode 100644 types/path.go create mode 100644 types/user.go diff --git a/go.mod b/go.mod index f0daf5258..c86eaf9ca 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/golang/mock v1.5.0 github.com/google/go-cmp v0.5.5 github.com/google/wire v0.5.0 - github.com/gosimple/slug v1.11.2 github.com/gotidy/ptr v1.3.0 github.com/jmoiron/sqlx v1.3.1 github.com/joho/godotenv v1.3.0 @@ -23,6 +22,7 @@ require ( github.com/maragudk/migrate v0.4.1 github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-sqlite3 v1.14.10-0.20211026011849-85436841b33e + github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.26.0 github.com/swaggest/openapi-go v0.2.13 github.com/swaggest/swgui v1.4.2 @@ -36,7 +36,6 @@ require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect github.com/google/subcommands v1.0.1 // indirect - github.com/gosimple/unidecode v1.0.1 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 052c1e995..35cb62e9c 100644 --- a/go.sum +++ b/go.sum @@ -140,10 +140,6 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gosimple/slug v1.11.2 h1:MxFR0TmQ/qz0KvIrBbf4phu+G0RBgpwxOn6jPKFKFOw= -github.com/gosimple/slug v1.11.2/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= -github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= -github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/gotidy/ptr v1.3.0 h1:5wdrH1G8X4txy6fbWWRznr7k974wMWtePWP3p6s1API= github.com/gotidy/ptr v1.3.0/go.mod h1:vpltyHhOZE+NGXUiwpVl3wV9AGEBlxhdnaimPDxRLxg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -338,6 +334,7 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/api/guard/error.go b/internal/api/guard/error.go deleted file mode 100644 index 5cf156aa5..000000000 --- a/internal/api/guard/error.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2021 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 guard - -import ( - "fmt" - - "github.com/harness/gitness/types" - "github.com/harness/gitness/types/enum" -) - -type notAuthenticatedError struct { - resource *types.Resource - permission enum.Permission -} - -func (e *notAuthenticatedError) Error() string { - return fmt.Sprintf("Operation %s on resource %v requires authentication.", e.permission, e.resource) -} - -func (e *notAuthenticatedError) Is(target error) bool { - _, ok := target.(*notAuthenticatedError) - return ok -} - -type notAuthorizedError struct { - user *types.User - scope *types.Scope - resource *types.Resource - permission enum.Permission -} - -func (e *notAuthorizedError) Error() string { - // ASSUMPTION: user is never nil at this point (type is not exported) - return fmt.Sprintf( - "User '%s' (%s) is not authorized to execute %s on resource %v in scope %v.", - e.user.Name, - e.user.Email, - e.permission, - e.resource, - e.scope) -} - -func (e *notAuthorizedError) Is(target error) bool { - _, ok := target.(*notAuthorizedError) - return ok -} diff --git a/internal/api/guard/guard.go b/internal/api/guard/guard.go index 3143db64a..5a40a5c84 100644 --- a/internal/api/guard/guard.go +++ b/internal/api/guard/guard.go @@ -5,7 +5,6 @@ package guard import ( - "errors" "fmt" "net/http" @@ -14,6 +13,13 @@ import ( "github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/pkg/errors" + "github.com/rs/zerolog/hlog" +) + +const ( + actionRequiresAuthentication = "Action requires authentication." ) type Guard struct { @@ -32,12 +38,12 @@ func (g *Guard) EnforceAdmin(next http.Handler) http.Handler { ctx := r.Context() user, ok := request.UserFrom(ctx) if !ok { - render.Unauthorized(w, errors.New("Requires authentication")) + render.Unauthorizedf(w, actionRequiresAuthentication) return } if !user.Admin { - render.Forbidden(w, errors.New("Requires admin privileges.")) + render.Forbiddenf(w, "Action requires admin privileges.") return } @@ -53,7 +59,7 @@ func (g *Guard) EnforceAuthenticated(next http.Handler) http.Handler { ctx := r.Context() _, ok := request.UserFrom(ctx) if !ok { - render.Unauthorized(w, errors.New("Requires authentication")) + render.Unauthorizedf(w, actionRequiresAuthentication) return } @@ -66,14 +72,23 @@ func (g *Guard) EnforceAuthenticated(next http.Handler) http.Handler { * returns true if it's the case, otherwise renders the appropriate error and returns false. */ func (g *Guard) Enforce(w http.ResponseWriter, r *http.Request, scope *types.Scope, resource *types.Resource, permission enum.Permission) bool { + err := g.Check(r, scope, resource, permission) - if errors.Is(err, ¬AuthenticatedError{}) { - render.Unauthorized(w, err) - } else if errors.Is(err, ¬AuthorizedError{}) { - render.Forbidden(w, err) + // render error if needed + if errors.Is(err, errs.NotAuthenticated) { + render.Unauthorizedf(w, actionRequiresAuthentication) + } else if errors.Is(err, errs.NotAuthorized) { + render.Forbiddenf(w, "User not authorized to perform %s on resource %v in scope %v", + permission, + resource, + scope) } else if err != nil { - render.InternalError(w, err) + // log err for debugging + log := hlog.FromRequest(r) + log.Err(err).Msg("Encountered unexpected error while enforcing permission.") + + render.InternalError(w, errs.Internal) } return err == nil @@ -87,7 +102,7 @@ func (g *Guard) Enforce(w http.ResponseWriter, r *http.Request, scope *types.Sco func (g *Guard) Check(r *http.Request, scope *types.Scope, resource *types.Resource, permission enum.Permission) error { u, present := request.UserFrom(r.Context()) if !present { - return ¬AuthenticatedError{resource, permission} + return errs.NotAuthenticated } // TODO: don't hardcode principal type USER @@ -98,11 +113,11 @@ func (g *Guard) Check(r *http.Request, scope *types.Scope, resource *types.Resou resource, permission) if err != nil { - return err + return errors.Wrap(err, "Authorization check failed") } if !authorized { - return ¬AuthorizedError{u, scope, resource, permission} + return errs.NotAuthorized } return nil diff --git a/internal/api/guard/guard.repo.go b/internal/api/guard/guard.repo.go index 88a804b25..bfe922869 100644 --- a/internal/api/guard/guard.repo.go +++ b/internal/api/guard/guard.repo.go @@ -5,14 +5,17 @@ package guard import ( - "errors" - "fmt" "net/http" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/pkg/errors" + "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" ) /* @@ -40,12 +43,17 @@ func (g *Guard) Repo(permission enum.Permission, orPublic bool, guarded http.Han ctx := r.Context() rep, ok := request.RepoFrom(ctx) if !ok { - render.InternalError(w, errors.New("Expected repository to be available.")) + // log error for debugging + log := hlog.FromRequest(r) + log.Error().Msg("Method expects the repository to be availabe in the request context, but it wasnt.") + + render.InternalError(w, errs.Internal) + return } // Enforce permission (renders error) - if !(orPublic && rep.IsPublic) && !g.EnforceRepo(w, r, permission, rep.Fqn) { + if !(orPublic && rep.IsPublic) && !g.EnforceRepo(w, r, permission, rep.Path) { return } @@ -58,14 +66,18 @@ func (g *Guard) Repo(permission enum.Permission, orPublic bool, guarded http.Han * Enforces that the executing principal has requested permission on the repository. * Returns true if that is the case, otherwise renders the appropriate error and returns false. */ -func (g *Guard) EnforceRepo(w http.ResponseWriter, r *http.Request, permission enum.Permission, fqn string) bool { - parentSpace, name, err := types.DisectFqn(fqn) +func (g *Guard) EnforceRepo(w http.ResponseWriter, r *http.Request, permission enum.Permission, path string) bool { + spacePath, name, err := paths.Disect(path) if err != nil { - render.InternalError(w, errors.New(fmt.Sprintf("Failed to disect fqn '%s' into scope: %s", fqn, err))) + // log error for debugging + hlog.FromRequest(r) + log.Err(err).Msgf("Failed to disect path '%s'.", path) + + render.InternalError(w, errs.Internal) return false } - scope := &types.Scope{SpaceFqn: parentSpace} + scope := &types.Scope{SpacePath: spacePath} resource := &types.Resource{ Type: enum.ResourceTypeRepo, Name: name, @@ -79,13 +91,13 @@ func (g *Guard) EnforceRepo(w http.ResponseWriter, r *http.Request, permission e * Returns nil if the user is confirmed to be permitted to execute the action, otherwise returns errors * NotAuthenticated, NotAuthorized, or any unerlaying error. */ -func (g *Guard) CheckRepo(r *http.Request, permission enum.Permission, fqn string) error { - parentSpace, name, err := types.DisectFqn(fqn) +func (g *Guard) CheckRepo(r *http.Request, permission enum.Permission, path string) error { + parentSpace, name, err := paths.Disect(path) if err != nil { - return errors.New(fmt.Sprintf("Failed to disect fqn '%s' into scope: %s", fqn, err)) + return errors.Wrapf(err, "Failed to disect path '%s'", path) } - scope := &types.Scope{SpaceFqn: parentSpace} + scope := &types.Scope{SpacePath: parentSpace} resource := &types.Resource{ Type: enum.ResourceTypeRepo, Name: name, diff --git a/internal/api/guard/guard.space.go b/internal/api/guard/guard.space.go index 3e573e794..8c85b2aa3 100644 --- a/internal/api/guard/guard.space.go +++ b/internal/api/guard/guard.space.go @@ -5,14 +5,17 @@ package guard import ( - "errors" - "fmt" "net/http" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/pkg/errors" + "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" ) /* @@ -40,12 +43,12 @@ func (g *Guard) Space(permission enum.Permission, orPublic bool, guarded http.Ha ctx := r.Context() s, ok := request.SpaceFrom(ctx) if !ok { - render.InternalError(w, errors.New("Expected space to be available.")) + render.InternalError(w, errors.New("Expected space to be available")) return } // Enforce permission (renders error) - if !(orPublic && s.IsPublic) && !g.EnforceSpace(w, r, permission, s.Fqn) { + if !(orPublic && s.IsPublic) && !g.EnforceSpace(w, r, permission, s.Path) { return } @@ -58,14 +61,18 @@ func (g *Guard) Space(permission enum.Permission, orPublic bool, guarded http.Ha * Enforces that the executing principal has requested permission on the space. * Returns true if that is the case, otherwise renders the appropriate error and returns false. */ -func (g *Guard) EnforceSpace(w http.ResponseWriter, r *http.Request, permission enum.Permission, fqn string) bool { - parentSpace, name, err := types.DisectFqn(fqn) +func (g *Guard) EnforceSpace(w http.ResponseWriter, r *http.Request, permission enum.Permission, path string) bool { + parentSpace, name, err := paths.Disect(path) if err != nil { - render.InternalError(w, errors.New(fmt.Sprintf("Failed to disect fqn '%s' into scope: %s", fqn, err))) + // log error for debugging + hlog.FromRequest(r) + log.Err(err).Msgf("Failed to disect path '%s'.", path) + + render.InternalError(w, errs.Internal) return false } - scope := &types.Scope{SpaceFqn: parentSpace} + scope := &types.Scope{SpacePath: parentSpace} resource := &types.Resource{ Type: enum.ResourceTypeSpace, Name: name, @@ -79,13 +86,13 @@ func (g *Guard) EnforceSpace(w http.ResponseWriter, r *http.Request, permission * Returns nil if the user is confirmed to be permitted to execute the action, otherwise returns errors * NotAuthenticated, NotAuthorized, or any unerlaying error. */ -func (g *Guard) CheckSpace(r *http.Request, permission enum.Permission, fqn string) error { - parentSpace, name, err := types.DisectFqn(fqn) +func (g *Guard) CheckSpace(r *http.Request, permission enum.Permission, path string) error { + parentSpace, name, err := paths.Disect(path) if err != nil { - return errors.New(fmt.Sprintf("Failed to disect fqn '%s' into scope: %s", fqn, err)) + return errors.Wrapf(err, "Failed to disect path '%s'", path) } - scope := &types.Scope{SpaceFqn: parentSpace} + scope := &types.Scope{SpacePath: parentSpace} resource := &types.Resource{ Type: enum.ResourceTypeSpace, Name: name, diff --git a/internal/api/handler/account/login.go b/internal/api/handler/account/login.go index 28ae0c238..df02337dc 100644 --- a/internal/api/handler/account/login.go +++ b/internal/api/handler/account/login.go @@ -28,10 +28,12 @@ func HandleLogin(users store.UserStore, system store.SystemStore) http.HandlerFu password := r.FormValue("password") user, err := users.FindEmail(ctx, username) if err != nil { - render.NotFoundf(w, "Invalid email or password") log.Debug().Err(err). Str("user", username). Msg("cannot find user") + + // always give not found error as extra security measurement. + render.NotFoundf(w, "Invalid email or password") return } @@ -40,20 +42,22 @@ func HandleLogin(users store.UserStore, system store.SystemStore) http.HandlerFu []byte(password), ) if err != nil { - render.NotFoundf(w, "Invalid email or password") log.Debug().Err(err). Str("user", username). Msg("invalid password") + + render.NotFoundf(w, "Invalid email or password") return } expires := time.Now().Add(system.Config(ctx).Token.Expire) token_, err := token.GenerateExp(user, expires.Unix(), user.Salt) if err != nil { - render.InternalErrorf(w, "Failed to create session") - log.Debug().Err(err). + log.Err(err). Str("user", username). Msg("failed to generate token") + + render.InternalErrorf(w, "Failed to create session") return } diff --git a/internal/api/handler/account/register.go b/internal/api/handler/account/register.go index 6fce42840..1207a7eaf 100644 --- a/internal/api/handler/account/register.go +++ b/internal/api/handler/account/register.go @@ -13,6 +13,7 @@ import ( "github.com/harness/gitness/internal/token" "github.com/harness/gitness/types" "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/errs" "github.com/dchest/uniuri" "github.com/rs/zerolog/hlog" @@ -31,13 +32,15 @@ func HandleRegister(users store.UserStore, system store.SystemStore) http.Handle hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - render.InternalError(w, err) - log.Debug().Err(err). + log.Err(err). Str("email", username). - Msg("cannot hash password") + Msg("Failed to hash password") + + render.InternalError(w, errs.Internal) return } + // TODO: allow to provide email and name separately ... user := &types.User{ Name: username, Email: username, @@ -48,18 +51,20 @@ func HandleRegister(users store.UserStore, system store.SystemStore) http.Handle } if ok, err := check.User(user); !ok { - render.BadRequest(w, err) log.Debug().Err(err). Str("email", username). Msg("invalid user input") + + render.BadRequest(w, err) return } if err := users.Create(ctx, user); err != nil { - render.InternalError(w, err) - log.Error().Err(err). + log.Err(err). Str("email", username). - Msg("cannot create user") + Msg("Failed to create user") + + render.InternalError(w, errs.Internal) return } @@ -69,19 +74,25 @@ func HandleRegister(users store.UserStore, system store.SystemStore) http.Handle if user.ID == 1 { user.Admin = true if err := users.Update(ctx, user); err != nil { - log.Error().Err(err). + log.Err(err). Str("email", username). - Msg("cannot enable admin user") + Int64("user_id", user.ID). + Msg("Failed to enable admin user") + + render.InternalError(w, errs.Internal) + return } } expires := time.Now().Add(system.Config(ctx).Token.Expire) token_, err := token.GenerateExp(user, expires.Unix(), user.Salt) if err != nil { - render.InternalErrorf(w, "Failed to create session") - log.Error().Err(err). + log.Err(err). Str("email", username). - Msg("failed to generate token") + Int64("user_id", user.ID). + Msg("Failed to generate token") + + render.InternalError(w, errs.Internal) return } diff --git a/internal/api/handler/common/path.go b/internal/api/handler/common/path.go new file mode 100644 index 000000000..3ef670e42 --- /dev/null +++ b/internal/api/handler/common/path.go @@ -0,0 +1,10 @@ +// Copyright 2021 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 common + +// Used for path creation apis +type CreatePathRequest struct { + Path string +} diff --git a/internal/api/handler/repo/create.go b/internal/api/handler/repo/create.go index 61937758c..362e3fb73 100644 --- a/internal/api/handler/repo/create.go +++ b/internal/api/handler/repo/create.go @@ -14,10 +14,12 @@ import ( "github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" ) @@ -41,39 +43,38 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore, repos store.RepoS in := new(repoCreateInput) err := json.NewDecoder(r.Body).Decode(in) if err != nil { - render.BadRequest(w, err) log.Debug().Err(err). Msg("Decoding json body failed.") + + render.BadRequestf(w, "Invalid Request Body: %s.", err) return } // ensure we reference a space if in.SpaceId <= 0 { - render.BadRequest(w, errors.New("A repository can only be created within a space.")) - log.Debug(). - Msg("No space was provided.") + render.BadRequestf(w, "A repository can only be created within a space.") return } parentSpace, err := spaces.Find(ctx, in.SpaceId) - if err != nil { - render.BadRequest(w, err) - log.Debug(). - Err(err). - Msgf("Parent space with id '%s' doesn't exist.", in.SpaceId) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Provided space wasn't found.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get space with id '%s'.", in.SpaceId) + render.InternalError(w, errs.Internal) return } - // parentFqn is assumed to be valid, in.Name gets validated in check.Repo function - parentFqn := parentSpace.Fqn - fqn := parentFqn + "/" + in.Name + // parentPath is assumed to be valid, in.Name gets validated in check.Repo function + parentPath := parentSpace.Path /* * AUTHORIZATION * Create is a special case - check permission without specific resource */ - scope := &types.Scope{SpaceFqn: parentFqn} + scope := &types.Scope{SpacePath: parentPath} resource := &types.Resource{ Type: enum.ResourceTypeRepo, Name: "", @@ -85,11 +86,10 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore, repos store.RepoS // get current user (safe to be there, or enforce would fail) usr, _ := request.UserFrom(ctx) - // create repo + // create new repo object repo := &types.Repository{ Name: strings.ToLower(in.Name), SpaceId: in.SpaceId, - Fqn: strings.ToLower(fqn), DisplayName: in.DisplayName, Description: in.Description, IsPublic: in.IsPublic, @@ -99,20 +99,35 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore, repos store.RepoS ForkId: in.ForkId, } - if ok, err := check.Repo(repo); !ok { + // validate repo + if err := check.Repo(repo); err != nil { render.BadRequest(w, err) - log.Debug().Err(err). - Msg("Repository validation failed.") return } - err = repos.Create(ctx, repo) - if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Msg("Repository creation failed") - } else { - render.JSON(w, repo, 200) + // validate path (Due to racing conditions we can't be 100% sure on the path here, but that's okay) + path := paths.Concatinate(parentPath, repo.Name) + if err = check.PathParams(path, false); err != nil { + render.BadRequest(w, err) + return } + + // create in store + err = repos.Create(ctx, repo) + if errors.Is(err, errs.Duplicate) { + log.Warn().Err(err). + Msg("Repository creation failed as a duplicate was detected.") + + render.BadRequestf(w, "Path '%s' already exists.", path) + return + } else if err != nil { + log.Error().Err(err). + Msg("Repository creation failed.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, repo, 200) } } diff --git a/internal/api/handler/repo/createPath.go b/internal/api/handler/repo/createPath.go new file mode 100644 index 000000000..040de401e --- /dev/null +++ b/internal/api/handler/repo/createPath.go @@ -0,0 +1,77 @@ +// Copyright 2021 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 repo + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + "time" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/handler/common" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +/* + * Writes json-encoded path information to the http response body. + */ +func HandleCreatePath(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc { + return guard.Repo( + enum.PermissionRepoEdit, + false, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + repo, _ := request.RepoFrom(ctx) + usr, _ := request.UserFrom(ctx) + + in := new(common.CreatePathRequest) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid request body: %s.", err) + return + } + + params := &types.PathParams{ + Path: strings.ToLower(in.Path), + CreatedBy: usr.ID, + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // validate path + if err = check.PathParams(params.Path, false); err != nil { + render.BadRequest(w, err) + return + } + + // TODO: ensure user is authorized to create a path pointing to in.Path + path, err := repos.CreatePath(ctx, repo.ID, params) + if errors.Is(err, errs.Duplicate) { + log.Warn().Err(err). + Msg("Failed to create path for repo as a duplicate was detected.") + + render.BadRequestf(w, "Path '%s' already exists.", params.Path) + return + } else if err != nil { + log.Error().Err(err). + Msg("Failed to create path for repo.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, path, 200) + }) +} diff --git a/internal/api/handler/repo/delete.go b/internal/api/handler/repo/delete.go index f85d3f647..def8e1bce 100644 --- a/internal/api/handler/repo/delete.go +++ b/internal/api/handler/repo/delete.go @@ -5,6 +5,7 @@ package repo import ( + "errors" "net/http" "github.com/harness/gitness/internal/api/guard" @@ -12,7 +13,8 @@ import ( "github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types/enum" - "github.com/rs/zerolog/log" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" ) /* @@ -23,20 +25,19 @@ func HandleDelete(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc { enum.PermissionRepoDelete, false, func(w http.ResponseWriter, r *http.Request) { - // TODO: return 200 if repo confirmed doesn't exist - ctx := r.Context() - rep, _ := request.RepoFrom(ctx) + log := hlog.FromRequest(r) + repo, _ := request.RepoFrom(ctx) - err := repos.Delete(r.Context(), rep.ID) - if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Int64("repo_id", rep.ID). - Str("repo_fqn", rep.Fqn). - Msg("Failed to delete repository.") + err := repos.Delete(r.Context(), repo.ID) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Repository doesn't exist.") return + } else if err != nil { + log.Err(err).Msgf("Failed to delete the Repository.") + render.InternalError(w, errs.Internal) + return } w.WriteHeader(http.StatusNoContent) diff --git a/internal/api/handler/repo/deletePath.go b/internal/api/handler/repo/deletePath.go new file mode 100644 index 000000000..9c27edaeb --- /dev/null +++ b/internal/api/handler/repo/deletePath.go @@ -0,0 +1,55 @@ +// Copyright 2021 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 repo + +import ( + "errors" + "net/http" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +/* + * Deletes a given path. + */ +func HandleDeletePath(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc { + return guard.Repo( + enum.PermissionRepoEdit, + false, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + repo, _ := request.RepoFrom(ctx) + + pathId, err := request.GetPathId(r) + if err != nil { + render.BadRequest(w, err) + return + } + + err = repos.DeletePath(ctx, repo.ID, pathId) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Path doesn't exist.") + return + } else if errors.Is(err, errs.PrimaryPathCantBeDeleted) { + render.BadRequestf(w, "Deleting a primary path is not allowed.") + return + } else if err != nil { + log.Err(err).Int64("path_id", pathId). + Msgf("Failed to delete repo path.") + + render.InternalError(w, errs.Internal) + return + } + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/internal/api/handler/repo/listPaths.go b/internal/api/handler/repo/listPaths.go new file mode 100644 index 000000000..397c43ac7 --- /dev/null +++ b/internal/api/handler/repo/listPaths.go @@ -0,0 +1,46 @@ +// Copyright 2021 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 repo + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +/* + * Writes json-encoded path information to the http response body. + */ +func HandleListPaths(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc { + return guard.Repo( + enum.PermissionRepoView, + true, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + repo, _ := request.RepoFrom(ctx) + + params := request.ParsePathFilter(r) + if params.Order == enum.OrderDefault { + params.Order = enum.OrderAsc + } + + paths, err := repos.ListAllPaths(ctx, repo.ID, params) + if err != nil { + log.Err(err).Msgf("Failed to get list of repo paths.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, paths, 200) + }) +} diff --git a/internal/api/handler/repo/move.go b/internal/api/handler/repo/move.go new file mode 100644 index 000000000..eb492526c --- /dev/null +++ b/internal/api/handler/repo/move.go @@ -0,0 +1,126 @@ +// Copyright 2021 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 repo + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +type repoMoveRequest struct { + Name *string `json:"name"` + SpaceId *int64 `json:"spaceId"` + KeepAsAlias bool `json:"keepAsAlias"` +} + +/* + * Moves an existing repo. + */ +func HandleMove(guard *guard.Guard, repos store.RepoStore, spaces store.SpaceStore) http.HandlerFunc { + return guard.Repo( + enum.PermissionRepoEdit, + false, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + usr, _ := request.UserFrom(ctx) + repo, _ := request.RepoFrom(ctx) + + in := new(repoMoveRequest) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid request body: %s.", err) + return + } + + // backfill data + if in.Name == nil { + in.Name = &repo.Name + } + if in.SpaceId == nil { + in.SpaceId = &repo.SpaceId + } + + // convert name to lower case for easy of api use + *in.Name = strings.ToLower(*in.Name) + + // ensure we don't end up in any missconfiguration, and block no-ops + if err = check.Name(*in.Name); err != nil { + render.BadRequest(w, err) + return + } else if *in.SpaceId == repo.SpaceId && *in.Name == repo.Name { + render.BadRequest(w, errs.NoChangeInRequestedMove) + return + } else if *in.SpaceId <= 0 { + render.BadRequest(w, check.RepositoryRequiresSpaceIdError) + return + } + + // Ensure we have access to the target space (if its a space move) + if *in.SpaceId != repo.SpaceId { + newSpace, err := spaces.Find(ctx, *in.SpaceId) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Parent space not found.") + return + } else if err != nil { + log.Err(err). + Msgf("Failed to get target space with id %d for the move.", *in.SpaceId) + + render.InternalError(w, errs.Internal) + return + } + + // Ensure we can create repos within the space (using space as scope, similar to create) + scope := &types.Scope{SpacePath: newSpace.Path} + resource := &types.Resource{ + Type: enum.ResourceTypeRepo, + Name: "", + } + if !guard.Enforce(w, r, scope, resource, enum.PermissionRepoCreate) { + return + } + + /* + * Validate path (Due to racing conditions we can't be 100% sure on the path here, but that's okay) + * Only needed if we actually change the parent (for move to top level we already validate the name) + */ + path := paths.Concatinate(newSpace.Path, *in.Name) + if err = check.PathParams(path, false); err != nil { + render.BadRequest(w, err) + return + } + } + + res, err := repos.Move(ctx, usr.ID, repo.ID, *in.SpaceId, *in.Name, in.KeepAsAlias) + if errors.Is(err, errs.Duplicate) { + log.Warn().Err(err). + Msg("Failed to move the repo as a duplicate was detected.") + + render.BadRequestf(w, "Unable to move the repository as the destination path is already taken.") + return + } else if err != nil { + log.Error().Err(err). + Msg("Failed to move the repository.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, res, http.StatusOK) + }) +} diff --git a/internal/api/handler/repo/update.go b/internal/api/handler/repo/update.go index bea4fe77c..ad1aede3c 100644 --- a/internal/api/handler/repo/update.go +++ b/internal/api/handler/repo/update.go @@ -5,25 +5,73 @@ package repo import ( - "errors" + "encoding/json" "net/http" + "time" "github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/log" ) +type repoUpdateRequest struct { + DisplayName *string `json:"displayName"` + Description *string `json:"description"` + IsPublic *bool `json:"isPublic"` +} + /* * Updates an existing repository. */ -func HandleUpdate(guard *guard.Guard) http.HandlerFunc { +func HandleUpdate(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc { return guard.Repo( enum.PermissionRepoEdit, false, func(w http.ResponseWriter, r *http.Request) { - /* - * TO-DO: Add support for updating an existing repository. - */ - render.BadRequest(w, errors.New("Updating an existing repo is not supported.")) + ctx := r.Context() + repo, _ := request.RepoFrom(ctx) + + in := new(repoUpdateRequest) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid request body: %s.", err) + return + } + + // update values only if provided + if in.DisplayName != nil { + repo.DisplayName = *in.DisplayName + } + if in.Description != nil { + repo.Description = *in.Description + } + if in.IsPublic != nil { + repo.IsPublic = *in.IsPublic + } + + // always update time + repo.Updated = time.Now().UnixMilli() + + // ensure provided values are valid + if err := check.Repo(repo); err != nil { + render.BadRequest(w, err) + return + } + + err = repos.Update(ctx, repo) + if err != nil { + log.Error().Err(err). + Msg("Repository update failed.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, repo, http.StatusOK) }) } diff --git a/internal/api/handler/space/create.go b/internal/api/handler/space/create.go index 1f758d1d4..4d615fb70 100644 --- a/internal/api/handler/space/create.go +++ b/internal/api/handler/space/create.go @@ -14,14 +14,16 @@ import ( "github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" ) -type spaceCreateInput struct { +type spaceCreateRequest struct { Name string `json:"name"` ParentId int64 `json:"parentId"` DisplayName string `json:"displayName"` @@ -37,37 +39,19 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc ctx := r.Context() log := hlog.FromRequest(r) - in := new(spaceCreateInput) + in := new(spaceCreateRequest) err := json.NewDecoder(r.Body).Decode(in) if err != nil { - render.BadRequest(w, err) - log.Debug().Err(err). - Msg("Decoding json body failed.") + render.BadRequestf(w, "Invalid request body: %s.", err) return } - // Get fqn and parentFqn - parentFqn := "" - fqn := in.Name - if in.ParentId > 0 { - parentSpace, err := spaces.Find(ctx, in.ParentId) - if err != nil { - render.BadRequest(w, err) - log.Debug(). - Err(err). - Msgf("Parent space '%s' doesn't exist.", parentFqn) - - return - } - - // parentFqn is assumed to be valid, in.Name gets validated in check.Space function - parentFqn = parentSpace.Fqn - fqn = parentFqn + "/" + in.Name - } - // get current user (will be enforced to not be nil via explicit check or guard.Enforce) usr, _ := request.UserFrom(ctx) + // Collect parent path along the way - needed for duplicate error message + parentPath := "" + /* * AUTHORIZATION * Can only be done once we know the parent space @@ -75,12 +59,23 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc if in.ParentId <= 0 { // TODO: Restrict top level space creation. if usr == nil { - render.Unauthorized(w, errors.New("Authentication required.")) + render.Unauthorized(w, errs.NotAuthenticated) return } } else { - // Create is a special case - check permission without specific resource - scope := &types.Scope{SpaceFqn: parentFqn} + // Create is a special case - we need the parent path + parent, err := spaces.Find(ctx, in.ParentId) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Provided parent space wasn't found.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get space with id '%s'.", in.ParentId) + + render.InternalError(w, errs.Internal) + return + } + + scope := &types.Scope{SpacePath: parent.Path} resource := &types.Resource{ Type: enum.ResourceTypeSpace, Name: "", @@ -88,12 +83,14 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc if !guard.Enforce(w, r, scope, resource, enum.PermissionSpaceCreate) { return } + + parentPath = parent.Path } + // create new space object space := &types.Space{ Name: strings.ToLower(in.Name), ParentId: in.ParentId, - Fqn: strings.ToLower(fqn), DisplayName: in.DisplayName, Description: in.Description, IsPublic: in.IsPublic, @@ -102,20 +99,35 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc Updated: time.Now().UnixMilli(), } - if ok, err := check.Space(space); !ok { + // validate space + if err := check.Space(space); err != nil { render.BadRequest(w, err) - log.Debug().Err(err). - Msg("Space validation failed.") return } - err = spaces.Create(ctx, space) - if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Msg("Space creation failed") - } else { - render.JSON(w, space, 200) + // validate path (Due to racing conditions we can't be 100% sure on the path here, but that's okay) + path := paths.Concatinate(parentPath, space.Name) + if err = check.PathParams(path, true); err != nil { + render.BadRequest(w, err) + return } + + // create in store + err = spaces.Create(ctx, space) + if errors.Is(err, errs.Duplicate) { + log.Warn().Err(err). + Msg("Space creation failed as a duplicate was detected.") + + render.BadRequestf(w, "Path '%s' already exists.", path) + return + } else if err != nil { + log.Error().Err(err). + Msg("Space creation failed.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, space, 200) } } diff --git a/internal/api/handler/space/createPath.go b/internal/api/handler/space/createPath.go new file mode 100644 index 000000000..3539b25a7 --- /dev/null +++ b/internal/api/handler/space/createPath.go @@ -0,0 +1,77 @@ +// Copyright 2021 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" + "errors" + "net/http" + "strings" + "time" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/handler/common" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +/* + * Writes json-encoded path information to the http response body. + */ +func HandleCreatePath(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { + return guard.Space( + enum.PermissionSpaceEdit, + false, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + space, _ := request.SpaceFrom(ctx) + usr, _ := request.UserFrom(ctx) + + in := new(common.CreatePathRequest) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid request body: %s.", err) + return + } + + params := &types.PathParams{ + Path: strings.ToLower(in.Path), + CreatedBy: usr.ID, + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // validate path + if err = check.PathParams(params.Path, true); err != nil { + render.BadRequest(w, err) + return + } + + // TODO: ensure user is authorized to create a path pointing to in.Path + path, err := spaces.CreatePath(ctx, space.ID, params) + if errors.Is(err, errs.Duplicate) { + log.Warn().Err(err). + Msg("Failed to create path for space as a duplicate was detected.") + + render.BadRequestf(w, "Path '%s' already exists.", params.Path) + return + } else if err != nil { + log.Error().Err(err). + Msg("Failed to create path for space.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, path, 200) + }) +} diff --git a/internal/api/handler/space/delete.go b/internal/api/handler/space/delete.go index a5eb089fa..2eac2b499 100644 --- a/internal/api/handler/space/delete.go +++ b/internal/api/handler/space/delete.go @@ -5,6 +5,7 @@ package space import ( + "errors" "net/http" "github.com/harness/gitness/internal/api/guard" @@ -12,7 +13,8 @@ import ( "github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types/enum" - "github.com/rs/zerolog/log" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" ) /* @@ -23,19 +25,19 @@ func HandleDelete(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc enum.PermissionSpaceDelete, false, func(w http.ResponseWriter, r *http.Request) { - // TODO: return 200 if space confirmed doesn't exist - ctx := r.Context() + log := hlog.FromRequest(r) s, _ := request.SpaceFrom(ctx) err := spaces.Delete(r.Context(), s.ID) - if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Str("space_fqn", s.Fqn). - Msg("Failed to delete space.") + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Space not found.") return + } else if err != nil { + log.Err(err).Msgf("Failed to delete the space.") + render.InternalError(w, errs.Internal) + return } w.WriteHeader(http.StatusNoContent) diff --git a/internal/api/handler/space/deletePath.go b/internal/api/handler/space/deletePath.go new file mode 100644 index 000000000..325fb5e97 --- /dev/null +++ b/internal/api/handler/space/deletePath.go @@ -0,0 +1,55 @@ +// Copyright 2021 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 ( + "errors" + "net/http" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +/* + * Deletes a given path. + */ +func HandleDeletePath(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { + return guard.Space( + enum.PermissionSpaceEdit, + false, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + space, _ := request.SpaceFrom(ctx) + + pathId, err := request.GetPathId(r) + if err != nil { + render.BadRequest(w, err) + return + } + + err = spaces.DeletePath(ctx, space.ID, pathId) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Path doesn't exist.") + return + } else if errors.Is(err, errs.PrimaryPathCantBeDeleted) { + render.BadRequestf(w, "Deleting a primary path is not allowed.") + return + } else if err != nil { + log.Err(err).Int64("path_id", pathId). + Msgf("Failed to delete space path.") + + render.InternalError(w, errs.Internal) + return + } + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/internal/api/handler/space/list.go b/internal/api/handler/space/list.go index 2fec52ead..746053c3f 100644 --- a/internal/api/handler/space/list.go +++ b/internal/api/handler/space/list.go @@ -13,7 +13,8 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" - "github.com/rs/zerolog/log" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" ) /* @@ -25,28 +26,27 @@ func HandleList(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { true, func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - s, _ := request.SpaceFrom(ctx) + log := hlog.FromRequest(r) + space, _ := request.SpaceFrom(ctx) params := request.ParseSpaceFilter(r) if params.Order == enum.OrderDefault { params.Order = enum.OrderAsc } - count, err := spaces.Count(ctx, s.ID) + count, err := spaces.Count(ctx, space.ID) if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Str("space_fqn", s.Fqn). - Msg("Failed to retrieve count of child spaces.") + log.Err(err).Msgf("Failed to count child spaces.") + + render.InternalError(w, errs.Internal) return } - allSpaces, err := spaces.List(ctx, s.ID, params) + allSpaces, err := spaces.List(ctx, space.ID, params) if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Str("space_fqn", s.Fqn). - Msg("Failed to retrieve list of child spaces.") + log.Err(err).Msgf("Failed to list child spaces.") + + render.InternalError(w, errs.Internal) return } @@ -58,10 +58,10 @@ func HandleList(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { result := make([]*types.Space, 0, len(allSpaces)) for _, cs := range allSpaces { if !cs.IsPublic { - err := guard.CheckSpace(r, enum.PermissionSpaceView, cs.Fqn) + err := guard.CheckSpace(r, enum.PermissionSpaceView, cs.Path) if err != nil { log.Debug().Err(err). - Msgf("Skip space '%s' in output.", cs.Fqn) + Msgf("Skip space '%s' in output.", cs.Path) continue } } diff --git a/internal/api/handler/space/listPaths.go b/internal/api/handler/space/listPaths.go new file mode 100644 index 000000000..63c89f009 --- /dev/null +++ b/internal/api/handler/space/listPaths.go @@ -0,0 +1,47 @@ +// Copyright 2021 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/guard" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +/* + * Writes json-encoded path information to the http response body. + */ +func HandleListPaths(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { + return guard.Space( + enum.PermissionSpaceView, + true, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + space, _ := request.SpaceFrom(ctx) + + params := request.ParsePathFilter(r) + if params.Order == enum.OrderDefault { + params.Order = enum.OrderAsc + } + + paths, err := spaces.ListAllPaths(ctx, space.ID, params) + if err != nil { + log.Err(err).Msgf("Failed to get list of space paths.") + + render.InternalError(w, errs.Internal) + return + } + + // TODO: do we need pagination? we should block that many paths in the first place. + render.JSON(w, paths, 200) + }) +} diff --git a/internal/api/handler/space/listRepos.go b/internal/api/handler/space/listRepos.go index 94611cc01..bd31312ce 100644 --- a/internal/api/handler/space/listRepos.go +++ b/internal/api/handler/space/listRepos.go @@ -13,7 +13,8 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" - "github.com/rs/zerolog/log" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" ) /* @@ -25,28 +26,27 @@ func HandleListRepos(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc true, func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - s, _ := request.SpaceFrom(ctx) + log := hlog.FromRequest(r) + space, _ := request.SpaceFrom(ctx) params := request.ParseRepoFilter(r) if params.Order == enum.OrderDefault { params.Order = enum.OrderAsc } - count, err := repos.Count(ctx, s.ID) + count, err := repos.Count(ctx, space.ID) if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Str("space_fqn", s.Fqn). - Msg("Failed to retrieve count of repos.") + log.Err(err).Msgf("Failed to count child repos.") + + render.InternalError(w, errs.Internal) return } - allRepos, err := repos.List(ctx, s.ID, params) + allRepos, err := repos.List(ctx, space.ID, params) if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Str("space_fqn", s.Fqn). - Msg("Failed to retrieve list of repos.") + log.Err(err).Msgf("Failed to list child repos.") + + render.InternalError(w, errs.Internal) return } @@ -58,10 +58,10 @@ func HandleListRepos(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc result := make([]*types.Repository, 0, len(allRepos)) for _, rep := range allRepos { if !rep.IsPublic { - err := guard.CheckRepo(r, enum.PermissionRepoView, rep.Fqn) + err := guard.CheckRepo(r, enum.PermissionRepoView, rep.Path) if err != nil { log.Debug().Err(err). - Msgf("Skip repo '%s' in output.", rep.Fqn) + Msgf("Skip repo '%s' in output.", rep.Path) continue } diff --git a/internal/api/handler/space/move.go b/internal/api/handler/space/move.go new file mode 100644 index 000000000..631720015 --- /dev/null +++ b/internal/api/handler/space/move.go @@ -0,0 +1,123 @@ +// Copyright 2021 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" + "errors" + "net/http" + "strings" + + "github.com/harness/gitness/internal/api/guard" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/hlog" +) + +type spaceMoveRequest struct { + Name *string `json:"name"` + ParentId *int64 `json:"parentId"` + KeepAsAlias bool `json:"keepAsAlias"` +} + +/* + * Moves an existing space. + */ +func HandleMove(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { + return guard.Space( + enum.PermissionSpaceEdit, + false, + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + usr, _ := request.UserFrom(ctx) + space, _ := request.SpaceFrom(ctx) + + in := new(spaceMoveRequest) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid request body: %s.", err) + return + } + + // backfill data + if in.Name == nil { + in.Name = &space.Name + } + if in.ParentId == nil { + in.ParentId = &space.ParentId + } + + // convert name to lower case for easy of api use + *in.Name = strings.ToLower(*in.Name) + + // ensure we don't end up in any missconfiguration, and block no-ops + if err = check.Name(*in.Name); err != nil { + render.BadRequest(w, err) + return + } else if *in.ParentId == space.ParentId && *in.Name == space.Name { + render.BadRequest(w, errs.NoChangeInRequestedMove) + return + } + + // TODO: restrict top level move + // Ensure we can create spaces within the target space (using parent space as scope, similar to create) + if *in.ParentId > 0 && *in.ParentId != space.ParentId { + newParent, err := spaces.Find(ctx, *in.ParentId) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Parent space not found.") + return + } else if err != nil { + log.Err(err). + Msgf("Failed to get target space with id %d for the move.", *in.ParentId) + + render.InternalError(w, errs.Internal) + return + } + + scope := &types.Scope{SpacePath: newParent.Path} + resource := &types.Resource{ + Type: enum.ResourceTypeSpace, + Name: "", + } + if !guard.Enforce(w, r, scope, resource, enum.PermissionSpaceCreate) { + return + } + + /* + * Validate path (Due to racing conditions we can't be 100% sure on the path here, but that's okay) + * Only needed if we actually change the parent (and can skip top level, as we already validate the name) + */ + path := paths.Concatinate(newParent.Path, *in.Name) + if err = check.PathParams(path, true); err != nil { + render.BadRequest(w, err) + return + } + } + + res, err := spaces.Move(ctx, usr.ID, space.ID, *in.ParentId, *in.Name, in.KeepAsAlias) + if errors.Is(err, errs.Duplicate) { + log.Warn().Err(err). + Msg("Failed to move the space as a duplicate was detected.") + + render.BadRequestf(w, "Unable to move the space as the destination path is already taken.") + return + } else if err != nil { + log.Error().Err(err). + Msg("Failed to move the space.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, res, http.StatusOK) + }) +} diff --git a/internal/api/handler/space/update.go b/internal/api/handler/space/update.go index 7fd362c44..1bb19ca96 100644 --- a/internal/api/handler/space/update.go +++ b/internal/api/handler/space/update.go @@ -5,28 +5,73 @@ package space import ( - "errors" + "encoding/json" "net/http" + "time" "github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog/log" ) +type spaceUpdateRequest struct { + DisplayName *string `json:"displayName"` + Description *string `json:"description"` + IsPublic *bool `json:"isPublic"` +} + /* * Updates an existing space. */ -func HandleUpdate(guard *guard.Guard) http.HandlerFunc { +func HandleUpdate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc { return guard.Space( enum.PermissionSpaceEdit, false, func(w http.ResponseWriter, r *http.Request) { - /* - * TO-DO: Add support for updating an existing space. - * Requires Solving: - * - Update all FQNs of child spaces (or change design) - * - Update all acl permissions? (or change design) - */ - render.BadRequest(w, errors.New("Updating an existing space is not supported.")) + ctx := r.Context() + space, _ := request.SpaceFrom(ctx) + + in := new(spaceUpdateRequest) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid request body: %s.", err) + return + } + + // update values only if provided + if in.DisplayName != nil { + space.DisplayName = *in.DisplayName + } + if in.Description != nil { + space.Description = *in.Description + } + if in.IsPublic != nil { + space.IsPublic = *in.IsPublic + } + + // always update time + space.Updated = time.Now().UnixMilli() + + // ensure provided values are valid + if err := check.Space(space); err != nil { + render.BadRequest(w, err) + return + } + + err = spaces.Update(ctx, space) + if err != nil { + log.Error().Err(err). + Msg("Space update failed.") + + render.InternalError(w, errs.Internal) + return + } + + render.JSON(w, space, http.StatusOK) }) } diff --git a/internal/api/handler/user/find.go b/internal/api/handler/user/find.go index ba3cee761..e8a0101db 100644 --- a/internal/api/handler/user/find.go +++ b/internal/api/handler/user/find.go @@ -17,8 +17,8 @@ import ( func HandleFind() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - viewer, _ := request.UserFrom(ctx) - render.JSON(w, viewer, 200) + user, _ := request.UserFrom(ctx) + render.JSON(w, user, 200) } } @@ -28,7 +28,7 @@ func HandleFind() http.HandlerFunc { func HandleCurrent() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - viewer, _ := request.UserFrom(ctx) - platform.RenderResource(w, viewer, 200) + user, _ := request.UserFrom(ctx) + platform.RenderResource(w, user, 200) } } diff --git a/internal/api/handler/user/token.go b/internal/api/handler/user/token.go index 4406e6b80..8b16dc768 100644 --- a/internal/api/handler/user/token.go +++ b/internal/api/handler/user/token.go @@ -12,6 +12,7 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/token" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" ) @@ -19,15 +20,15 @@ import ( // writes a json-encoded token to the http.Response body. func HandleToken(users store.UserStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - viewer, _ := request.UserFrom(r.Context()) + log := hlog.FromRequest(r) + user, _ := request.UserFrom(r.Context()) - token, err := token.Generate(viewer, viewer.Salt) + token, err := token.Generate(user, user.Salt) if err != nil { - render.InternalErrorf(w, "Failed to generate token") - hlog.FromRequest(r). - Error().Err(err). - Str("user", viewer.Email). + log.Err(err). Msg("failed to generate token") + + render.InternalError(w, errs.Internal) return } diff --git a/internal/api/handler/user/update.go b/internal/api/handler/user/update.go index f8a6708de..738481d73 100644 --- a/internal/api/handler/user/update.go +++ b/internal/api/handler/user/update.go @@ -28,45 +28,42 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := hlog.FromRequest(r) - viewer, _ := request.UserFrom(ctx) + user, _ := request.UserFrom(ctx) in := new(types.UserInput) err := json.NewDecoder(r.Body).Decode(in) if err != nil { - render.BadRequest(w, err) - log.Error().Err(err). - Str("email", viewer.Email). - Msg("cannot unmarshal request") + render.BadRequestf(w, "Invalid request body: %s.", err) return } if in.Password != nil { hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) if err != nil { + log.Err(err).Msg("Failed to hash password.") + render.InternalError(w, err) - log.Debug().Err(err). - Msg("cannot hash password") return } - viewer.Password = string(hash) + user.Password = string(hash) } if in.Name != nil { - viewer.Name = ptr.ToString(in.Name) + user.Name = ptr.ToString(in.Name) } if in.Company != nil { - viewer.Company = ptr.ToString(in.Company) + user.Company = ptr.ToString(in.Company) } - err = users.Update(ctx, viewer) + err = users.Update(ctx, user) if err != nil { + log.Err(err).Msg("Failed to update the user.") + render.InternalError(w, err) - log.Error().Err(err). - Str("email", viewer.Email). - Msg("cannot update user") - } else { - render.JSON(w, viewer, 200) + return } + + render.JSON(w, user, 200) } } diff --git a/internal/api/handler/users/create.go b/internal/api/handler/users/create.go index 9d640f734..fa26ca111 100644 --- a/internal/api/handler/users/create.go +++ b/internal/api/handler/users/create.go @@ -13,6 +13,7 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" "golang.org/x/crypto/bcrypt" @@ -35,17 +36,17 @@ func HandleCreate(users store.UserStore) http.HandlerFunc { in := new(userCreateInput) err := json.NewDecoder(r.Body).Decode(in) if err != nil { - render.BadRequest(w, err) - log.Debug().Err(err). - Msg("cannot unmarshal json request") + render.BadRequestf(w, "Invalid request body: %s.", err) return } hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) if err != nil { - render.InternalError(w, err) - log.Debug().Err(err). - Msg("cannot hash password") + log.Err(err). + Str("email", in.Username). + Msg("Failed to hash password") + + render.InternalError(w, errs.Internal) return } @@ -59,22 +60,24 @@ func HandleCreate(users store.UserStore) http.HandlerFunc { } if ok, err := check.User(user); !ok { - render.BadRequest(w, err) log.Debug().Err(err). - Str("user_email", user.Email). - Msg("cannot validate user") + Str("email", user.Email). + Msg("invalid user input") + + render.BadRequest(w, err) return } err = users.Create(ctx, user) if err != nil { - render.InternalError(w, err) - log.Error().Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). - Msg("cannot create user") - } else { - render.JSON(w, user, 200) + log.Err(err). + Str("email", user.Email). + Msg("failed to create user") + + render.InternalError(w, errs.Internal) + return } + + render.JSON(w, user, 200) } } diff --git a/internal/api/handler/users/delete.go b/internal/api/handler/users/delete.go index 89abd5a5b..b41cd3b85 100644 --- a/internal/api/handler/users/delete.go +++ b/internal/api/handler/users/delete.go @@ -5,10 +5,12 @@ package users import ( + "errors" "net/http" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" "github.com/go-chi/chi" @@ -23,23 +25,28 @@ func HandleDelete(users store.UserStore) http.HandlerFunc { key := chi.URLParam(r, "user") user, err := users.FindKey(ctx, key) - if err != nil { - render.NotFound(w, err) - log.Debug().Err(err). - Str("user_key", key). - Msg("cannot find user") + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "User not found.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get user using key '%s'.", key) + + render.InternalError(w, errs.Internal) return } + err = users.Delete(ctx, user) if err != nil { - render.InternalError(w, err) log.Error().Err(err). Int64("user_id", user.ID). Str("user_email", user.Email). - Msg("cannot delete user") + Msg("failed to delete user") + + render.InternalError(w, err) + return - } else { - w.WriteHeader(http.StatusNoContent) } + + w.WriteHeader(http.StatusNoContent) } } diff --git a/internal/api/handler/users/find.go b/internal/api/handler/users/find.go index 4036f42ec..c10092b0c 100644 --- a/internal/api/handler/users/find.go +++ b/internal/api/handler/users/find.go @@ -5,10 +5,12 @@ package users import ( + "errors" "net/http" "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" "github.com/go-chi/chi" @@ -23,13 +25,16 @@ func HandleFind(users store.UserStore) http.HandlerFunc { key := chi.URLParam(r, "user") user, err := users.FindKey(ctx, key) - if err != nil { - render.NotFound(w, err) - log.Debug().Err(err). - Str("user_key", key). - Msg("cannot find user") - } else { - render.JSON(w, user, 200) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "User doesn't exist.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get user using key '%s'.", key) + + render.InternalError(w, errs.Internal) + return } + + render.JSON(w, user, 200) } } diff --git a/internal/api/handler/users/list.go b/internal/api/handler/users/list.go index ccc6d7dcd..b50e020ab 100644 --- a/internal/api/handler/users/list.go +++ b/internal/api/handler/users/list.go @@ -18,10 +18,8 @@ import ( // list of all registered system users to the response body. func HandleList(users store.UserStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - log = hlog.FromRequest(r) - ) + ctx := r.Context() + log := hlog.FromRequest(r) params := request.ParseUserFilter(r) if params.Order == enum.OrderDefault { @@ -30,15 +28,16 @@ func HandleList(users store.UserStore) http.HandlerFunc { count, err := users.Count(ctx) if err != nil { - log.Error().Err(err). - Msg("cannot retrieve user count") + log.Err(err). + Msg("Failed to retrieve user count") } list, err := users.List(ctx, params) if err != nil { + log.Err(err). + Msg("Failed to retrieve user list") + render.InternalError(w, err) - log.Error().Err(err). - Msg("cannot retrieve user list") return } diff --git a/internal/api/handler/users/update.go b/internal/api/handler/users/update.go index b7011d605..9c2696d65 100644 --- a/internal/api/handler/users/update.go +++ b/internal/api/handler/users/update.go @@ -6,6 +6,7 @@ package users import ( "encoding/json" + "errors" "net/http" "time" @@ -14,6 +15,7 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/errs" "github.com/rs/zerolog/hlog" "github.com/go-chi/chi" @@ -33,32 +35,31 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc { key := chi.URLParam(r, "user") user, err := users.FindKey(ctx, key) - if err != nil { - render.NotFound(w, err) - log.Debug().Err(err). - Str("user_key", key). - Msg("cannot find user") + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "User not found.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get user using key '%s'.", key) + + render.InternalError(w, errs.Internal) return } in := new(types.UserInput) if err := json.NewDecoder(r.Body).Decode(in); err != nil { - render.BadRequest(w, err) - log.Debug().Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). - Msg("cannot unmarshal request") + render.BadRequestf(w, "Invalid request body: %s.", err) return } if in.Password != nil { hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) if err != nil { - render.InternalError(w, err) - log.Debug().Err(err). + log.Err(err). Int64("user_id", user.ID). Str("user_email", user.Email). - Msg("cannot hash password") + Msg("Failed to hash password") + + render.InternalError(w, errs.Internal) return } user.Password = string(hash) @@ -76,23 +77,28 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc { user.Admin = ptr.ToBool(in.Admin) } + // TODO: why are we overwriting the password twice? if in.Password != nil { hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) if err != nil { - render.InternalError(w, err) - log.Debug().Err(err). - Msg("cannot hash password") + log.Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("Failed to hash password") + + render.InternalError(w, errs.Internal) return } user.Password = string(hash) } if ok, err := check.User(user); !ok { - render.BadRequest(w, err) log.Debug().Err(err). Int64("user_id", user.ID). Str("user_email", user.Email). - Msg("cannot update user") + Msg("invalid user input") + + render.BadRequest(w, err) return } @@ -100,13 +106,15 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc { err = users.Update(ctx, user) if err != nil { - render.InternalError(w, err) - log.Error().Err(err). + log.Err(err). Int64("user_id", user.ID). Str("user_email", user.Email). - Msg("cannot update user") - } else { - render.JSON(w, user, 200) + Msg("Failed to update the usser") + + render.InternalError(w, errs.Internal) + return } + + render.JSON(w, user, 200) } } diff --git a/internal/api/middleware/authn/authn.go b/internal/api/middleware/authn/authn.go index a451a48d8..4e7096c75 100644 --- a/internal/api/middleware/authn/authn.go +++ b/internal/api/middleware/authn/authn.go @@ -34,10 +34,10 @@ func Attempt(authenticator authn.Authenticator) func(http.Handler) http.Handler return } - // otherwise update the logging context and inject user in context + // Update the logging context and inject user in context ctx := r.Context() log.Ctx(ctx).UpdateContext(func(c zerolog.Context) zerolog.Context { - return c.Str("session_email", user.Email).Bool("session_admin", user.Admin) + return c.Int64("user_id", user.ID).Bool("user_admin", user.Admin) }) next.ServeHTTP(w, r.WithContext( diff --git a/internal/api/middleware/encode/encode.go b/internal/api/middleware/encode/encode.go index 48774a548..3263c6be7 100644 --- a/internal/api/middleware/encode/encode.go +++ b/internal/api/middleware/encode/encode.go @@ -5,31 +5,33 @@ import ( "net/http" "net/url" "strings" + + "github.com/harness/gitness/types" ) /* - * Wraps an http.HandlerFunc in a layer that encodes FQNs coming as part of the GIT api + * Wraps an http.HandlerFunc in a layer that encodes Paths coming as part of the GIT api * (e.g. "space1/repo.git") before executing the provided http.HandlerFunc. * The first prefix that matches the URL.Path will be used during encoding. */ -func GitFqnBefore(h http.HandlerFunc) http.HandlerFunc { +func GitPathBefore(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - r, _ = encodeFQNWithMarker(r, "", ".git", false) + r, _ = pathTerminatedWithMarker(r, "", ".git", false) h.ServeHTTP(w, r) } } /* - * Wraps an http.HandlerFunc in a layer that encodes a terminated FQN (e.g. "/space1/space2/+") + * Wraps an http.HandlerFunc in a layer that encodes a terminated path (e.g. "/space1/space2/+") * before executing the provided http.HandlerFunc. * The first prefix that matches the URL.Path will be used during encoding. */ -func TerminatedFqnBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc { +func TerminatedPathBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { for _, p := range prefixes { // IMPORTANT: define changed separately to avoid overshadowing r changed := false - if r, changed = encodeFQNWithMarker(r, p, "/+", false); changed { + if r, changed = pathTerminatedWithMarker(r, p, "/+", false); changed { break } } @@ -39,7 +41,7 @@ func TerminatedFqnBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc } /* - * This function encodes an FQN followed by a custom marker and returns a request with an updated URL.Path. + * This function encodes a path followed by a custom marker and returns a request with an updated URL.Path. * A non-empty prefix can be provided to encode encode only after the prefix. * It allows our Rest API to handle paths of the form "/spaces/space1/space2/+/authToken" * @@ -48,14 +50,14 @@ func TerminatedFqnBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc * Prefix: "" Path: "/space1/space2.git" => "/space1%2Fspace2" * Prefix: "/spaces" Path: "/spaces/space1/space2/+/authToken" => "/spaces/space1%2Fspace2/authToken" */ -func encodeFQNWithMarker(r *http.Request, prefix string, marker string, keepMarker bool) (*http.Request, bool) { +func pathTerminatedWithMarker(r *http.Request, prefix string, marker string, keepMarker bool) (*http.Request, bool) { // In case path doesn't start with prefix - nothing to encode if len(r.URL.Path) < len(prefix) || r.URL.Path[0:len(prefix)] != prefix { return r, false } originalSubPath := r.URL.Path[len(prefix):] - fqn, suffix, found := strings.Cut(originalSubPath, marker) + path, suffix, found := strings.Cut(originalSubPath, marker) // If we don't find a marker - nothing to encode if !found { @@ -63,11 +65,11 @@ func encodeFQNWithMarker(r *http.Request, prefix string, marker string, keepMark } // if marker was found - convert to escaped version (skip first character in case path starts with '/') - escapedFqn := fqn[0:1] + strings.Replace(fqn[1:], "/", "%2F", -1) + escapedPath := path[0:1] + strings.Replace(path[1:], types.PathSeparator, "%2F", -1) if keepMarker { - escapedFqn += marker + escapedPath += marker } - updatedSubPath := escapedFqn + suffix + updatedSubPath := escapedPath + suffix // TODO: Proper Logging fmt.Printf( diff --git a/internal/api/middleware/repo/repo.go b/internal/api/middleware/repo/repo.go index 56887d159..d34e84112 100644 --- a/internal/api/middleware/repo/repo.go +++ b/internal/api/middleware/repo/repo.go @@ -5,6 +5,7 @@ package repo import ( + "errors" "net/http" "strconv" @@ -12,6 +13,9 @@ import ( "github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) /* @@ -29,24 +33,33 @@ func Required(repos store.RepoStore) func(http.Handler) http.Handler { } ctx := r.Context() - var rep *types.Repository + var repo *types.Repository // check if ref is repoId - ASSUMPTION: digit only is no valid repo name id, err := strconv.ParseInt(ref, 10, 64) if err == nil { - rep, err = repos.Find(ctx, id) + repo, err = repos.Find(ctx, id) } else { - rep, err = repos.FindFqn(ctx, ref) + repo, err = repos.FindByPath(ctx, ref) } - if err != nil { - // TODO: what about errors that aren't notfound? - render.NotFoundf(w, "Resolving repository reference '%s' failed: %s", ref, err) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Repository doesn't exist.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get repo using ref '%s'.", ref) + + render.InternalError(w, errs.Internal) return } + // Update the logging context and inject repo in context + log.Ctx(ctx).UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Int64("repo_id", repo.ID).Str("repo_path", repo.Path) + }) + next.ServeHTTP(w, r.WithContext( - request.WithRepo(ctx, rep), + request.WithRepo(ctx, repo), )) }) } diff --git a/internal/api/middleware/space/space.go b/internal/api/middleware/space/space.go index ceedff78d..10d909523 100644 --- a/internal/api/middleware/space/space.go +++ b/internal/api/middleware/space/space.go @@ -5,6 +5,7 @@ package space import ( + "errors" "net/http" "strconv" @@ -12,6 +13,9 @@ import ( "github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/errs" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) /* @@ -29,24 +33,33 @@ func Required(spaces store.SpaceStore) func(http.Handler) http.Handler { } ctx := r.Context() - var s *types.Space + var space *types.Space // check if ref is spaceId - ASSUMPTION: digit only is no valid space name id, err := strconv.ParseInt(ref, 10, 64) if err == nil { - s, err = spaces.Find(ctx, id) + space, err = spaces.Find(ctx, id) } else { - s, err = spaces.FindFqn(ctx, ref) + space, err = spaces.FindByPath(ctx, ref) } - if err != nil { - // TODO: what about errors that aren't notfound? - render.NotFoundf(w, "Resolving space reference '%s' failed: %s", ref, err) + if errors.Is(err, errs.ResourceNotFound) { + render.NotFoundf(w, "Space not found.") + return + } else if err != nil { + log.Err(err).Msgf("Failed to get space using ref '%s'.", ref) + + render.InternalError(w, errs.Internal) return } + // Update the logging context and inject repo in context + log.Ctx(ctx).UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Int64("space_id", space.ID).Str("space_path", space.Path) + }) + next.ServeHTTP(w, r.WithContext( - request.WithSpace(ctx, s), + request.WithSpace(ctx, space), )) }) } diff --git a/internal/api/render/render.go b/internal/api/render/render.go index 240b4408b..45c064c11 100644 --- a/internal/api/render/render.go +++ b/internal/api/render/render.go @@ -56,12 +56,24 @@ func Unauthorized(w http.ResponseWriter, err error) { ErrorCode(w, err, 401) } +// Unauthorizedf writes the json-encoded error message to the response +// with a 401 unauthorized status code. +func Unauthorizedf(w http.ResponseWriter, format string, a ...interface{}) { + ErrorCode(w, fmt.Errorf(format, a...), 401) +} + // Forbidden writes the json-encoded error message to the response // with a 403 forbidden status code. func Forbidden(w http.ResponseWriter, err error) { ErrorCode(w, err, 403) } +// Forbiddenf writes the json-encoded error message to the response +// with a 403 forbidden status code. +func Forbiddenf(w http.ResponseWriter, format string, a ...interface{}) { + ErrorCode(w, fmt.Errorf(format, a...), 403) +} + // BadRequest writes the json-encoded error message to the response // with a 400 bad request status code. func BadRequest(w http.ResponseWriter, err error) { diff --git a/internal/api/request/path.go b/internal/api/request/path.go new file mode 100644 index 000000000..004448d2d --- /dev/null +++ b/internal/api/request/path.go @@ -0,0 +1,27 @@ +package request + +import ( + "errors" + "net/http" + "strconv" + + "github.com/go-chi/chi" +) + +const ( + PathIdParamName = "pathId" +) + +func GetPathId(r *http.Request) (int64, error) { + rawId := chi.URLParam(r, PathIdParamName) + if rawId == "" { + return 0, errors.New("Path id parameter not found in request.") + } + + id, err := strconv.ParseInt(rawId, 10, 64) + if err != nil { + return 0, err + } + + return id, nil +} diff --git a/internal/api/request/repo.go b/internal/api/request/repo.go index 63d9ebbd4..139823e5f 100644 --- a/internal/api/request/repo.go +++ b/internal/api/request/repo.go @@ -1,12 +1,12 @@ package request import ( - "errors" "net/http" "net/url" "strings" "github.com/go-chi/chi" + "github.com/harness/gitness/types/errs" ) const ( @@ -16,10 +16,10 @@ const ( func GetRepoRef(r *http.Request) (string, error) { rawRef := chi.URLParam(r, RepoRefParamName) if rawRef == "" { - return "", errors.New("Repository ref parameter not found in request.") + return "", errs.RepoReferenceNotFoundInRequest } - // fqns are unescaped and lower + // paths are unescaped and lower ref, err := url.PathUnescape(rawRef) return strings.ToLower(ref), err } diff --git a/internal/api/request/space.go b/internal/api/request/space.go index 610a12bcb..495979f7c 100644 --- a/internal/api/request/space.go +++ b/internal/api/request/space.go @@ -1,12 +1,12 @@ package request import ( - "errors" "net/http" "net/url" "strings" "github.com/go-chi/chi" + "github.com/harness/gitness/types/errs" ) const ( @@ -16,10 +16,10 @@ const ( func GetSpaceRef(r *http.Request) (string, error) { rawRef := chi.URLParam(r, SpaceRefParamName) if rawRef == "" { - return "", errors.New("Space ref parameter not found in request.") + return "", errs.SpaceReferenceNotFoundInRequest } - // fqns are unescaped and lower + // paths are unescaped and lower ref, err := url.PathUnescape(rawRef) return strings.ToLower(ref), err } diff --git a/internal/api/request/util.go b/internal/api/request/util.go index ca902b4d0..8173ec80a 100644 --- a/internal/api/request/util.go +++ b/internal/api/request/util.go @@ -67,6 +67,13 @@ func ParseSortRepo(r *http.Request) enum.RepoAttr { ) } +// ParseSortPath extracts the path sort parameter from the url. +func ParseSortPath(r *http.Request) enum.PathAttr { + return enum.ParsePathAttr( + r.FormValue("sort"), + ) +} + // ParseParams extracts the query parameter from the url. func ParseParams(r *http.Request) types.Params { return types.Params{ @@ -78,8 +85,8 @@ func ParseParams(r *http.Request) types.Params { } // ParseUserFilter extracts the user query parameter from the url. -func ParseUserFilter(r *http.Request) types.UserFilter { - return types.UserFilter{ +func ParseUserFilter(r *http.Request) *types.UserFilter { + return &types.UserFilter{ Order: ParseOrder(r), Page: ParsePage(r), Sort: ParseSortUser(r), @@ -88,8 +95,8 @@ func ParseUserFilter(r *http.Request) types.UserFilter { } // ParseSpaceFilter extracts the space query parameter from the url. -func ParseSpaceFilter(r *http.Request) types.SpaceFilter { - return types.SpaceFilter{ +func ParseSpaceFilter(r *http.Request) *types.SpaceFilter { + return &types.SpaceFilter{ Order: ParseOrder(r), Page: ParsePage(r), Sort: ParseSortSpace(r), @@ -98,11 +105,21 @@ func ParseSpaceFilter(r *http.Request) types.SpaceFilter { } // ParseRepoFilter extracts the repository query parameter from the url. -func ParseRepoFilter(r *http.Request) types.RepoFilter { - return types.RepoFilter{ +func ParseRepoFilter(r *http.Request) *types.RepoFilter { + return &types.RepoFilter{ Order: ParseOrder(r), Page: ParsePage(r), Sort: ParseSortRepo(r), Size: ParseSize(r), } } + +// ParsePathFilter extracts the path query parameter from the url. +func ParsePathFilter(r *http.Request) *types.PathFilter { + return &types.PathFilter{ + Order: ParseOrder(r), + Page: ParsePage(r), + Sort: ParseSortPath(r), + Size: ParseSize(r), + } +} diff --git a/internal/auth/authn/authenticator.go b/internal/auth/authn/authenticator.go index 26170f723..1658ccedf 100644 --- a/internal/auth/authn/authenticator.go +++ b/internal/auth/authn/authenticator.go @@ -10,6 +10,17 @@ import ( "github.com/harness/gitness/types" ) +/* + * An abstraction of an entity thats responsible for authenticating users + * that are making calls via HTTP. + */ type Authenticator interface { + /* + * Tries to authenticate a user if credentials are available. + * Returns: + * (user, nil) - request contained auth data and user was verified + * (nil, err) - request contained auth data but verification failed + * (nil, nil) - request didn't contain any auth data + */ Authenticate(r *http.Request) (*types.User, error) } diff --git a/internal/auth/authn/harness/harness.go b/internal/auth/authn/harness/harness.go index 8206d2898..d14a6fe05 100644 --- a/internal/auth/authn/harness/harness.go +++ b/internal/auth/authn/harness/harness.go @@ -13,6 +13,9 @@ import ( var _ authn.Authenticator = (*Authenticator)(nil) +/* + * An authenticator that validates access token provided by harness SAAS. + */ type Authenticator struct { // some config to validate jwt } diff --git a/internal/auth/authn/token.go b/internal/auth/authn/token.go index 6b4abbc41..52b28b20c 100644 --- a/internal/auth/authn/token.go +++ b/internal/auth/authn/token.go @@ -21,6 +21,10 @@ import ( var _ Authenticator = (*TokenAuthenticator)(nil) +/* + * Authenticates a user by checking for an access token in the + * "Authorization" header or the "access_token" form value. + */ type TokenAuthenticator struct { users store.UserStore } diff --git a/internal/auth/authz/authz.go b/internal/auth/authz/authz.go index bcf8cfb42..b324e3cfb 100644 --- a/internal/auth/authz/authz.go +++ b/internal/auth/authz/authz.go @@ -9,7 +9,25 @@ import ( "github.com/harness/gitness/types/enum" ) +/* + * An abstraction of an entity responsible for authorizing access to resources. + */ type Authorizer interface { + /* + * Checks whether the provided principal has the permission to execute the action on the resource within the scope. + * Returns + * (true, nil) - the principal has permission to perform the action + * (false, nil) - the principal does not have permission to perform the action + * (false, err) - an error occured while performing the permission check and the action should be denied + */ Check(principalType enum.PrincipalType, principalId string, scope *types.Scope, resource *types.Resource, permission enum.Permission) (bool, error) + + /* + * Checks whether the provided principal the required permission to execute ALL the requested actions on the resource within the scope. + * Returns + * (true, nil) - the principal has permission to perform all the requested actions + * (false, nil) - the principal does not have permission to perform all the actions (at least one is not allowed) + * (false, err) - an error occured while performing the permission check and all actions should be denied + */ CheckAll(principalType enum.PrincipalType, principalId string, permissionChecks ...*types.PermissionCheck) (bool, error) } diff --git a/internal/auth/authz/harness/authorizer.go b/internal/auth/authz/harness/authorizer.go index 87f9ad6a7..27f52f3dc 100644 --- a/internal/auth/authz/harness/authorizer.go +++ b/internal/auth/authz/harness/authorizer.go @@ -16,6 +16,7 @@ import ( "github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" ) var _ authz.Authorizer = (*Authorizer)(nil) @@ -47,7 +48,7 @@ func (a *Authorizer) Check(principalType enum.PrincipalType, principalId string, func (a *Authorizer) CheckAll(principalType enum.PrincipalType, principalId string, permissionChecks ...*types.PermissionCheck) (bool, error) { if len(permissionChecks) == 0 { - return false, fmt.Errorf("No permission checks provided.") + return false, errs.NoPermissionCheckProvided } requestDto, err := createAclRequest(principalType, principalId, permissionChecks) @@ -147,8 +148,7 @@ func checkAclResponse(permissionChecks []*types.PermissionCheck, responseDto acl } if !permissionPermitted { - return false, fmt.Errorf( - "Permission '%s' is not permitted according to ACL (correlationId: '%s').", + return false, fmt.Errorf("Permission '%s' is not permitted according to ACL (correlationId: '%s')", check.Permission, responseDto.CorrelationID) } @@ -164,7 +164,7 @@ func mapScope(scope types.Scope) (*aclResourceScope, error) { * Harness embeded structure is mapped to the following scm space: * {Account}/{Organization}/{Project} * - * We can use that assumption to translate back from scope.spaceFqn to harness scope. + * We can use that assumption to translate back from scope.spacePath to harness scope. * However, this only works as long as resources exist within spaces only. * For controlling access to any child resources of a repository, harness doesn't have a matching * structure out of the box (e.g. branches, ...) @@ -175,9 +175,9 @@ func mapScope(scope types.Scope) (*aclResourceScope, error) { * TODO: Handle scope.Repository in harness embedded mode */ - harnessIdentifiers := strings.Split(scope.SpaceFqn, "/") + harnessIdentifiers := strings.Split(scope.SpacePath, "/") if len(harnessIdentifiers) > 3 { - return nil, fmt.Errorf("Unable to convert '%s' to harness resource scope (expected {Account}/{Organization}/{Project} or a sub scope).", scope.SpaceFqn) + return nil, fmt.Errorf("Unable to convert '%s' to harness resource scope (expected {Account}/{Organization}/{Project} or a sub scope).", scope.SpacePath) } aclScope := &aclResourceScope{} diff --git a/internal/auth/authz/unsafe.go b/internal/auth/authz/unsafe.go index ff451ad05..f8f3cbece 100644 --- a/internal/auth/authz/unsafe.go +++ b/internal/auth/authz/unsafe.go @@ -13,12 +13,15 @@ import ( var _ Authorizer = (*UnsafeAuthorizer)(nil) +/* + * An unsafe authorizer that gives permits any action and simply logs the permission request. + */ +type UnsafeAuthorizer struct{} + func NewUnsafeAuthorizer() Authorizer { return &UnsafeAuthorizer{} } -type UnsafeAuthorizer struct{} - func (a *UnsafeAuthorizer) Check(principalType enum.PrincipalType, principalId string, scope *types.Scope, resource *types.Resource, permission enum.Permission) (bool, error) { fmt.Printf( "[Authz] %s '%s' requests %s for %s '%s' in scope %v\n", diff --git a/internal/paths/paths.go b/internal/paths/paths.go new file mode 100644 index 000000000..b715637fa --- /dev/null +++ b/internal/paths/paths.go @@ -0,0 +1,48 @@ +// Copyright 2021 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 paths + +import ( + "strings" + + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/errs" +) + +/* + * Splits a path into its parent path and the leaf name. + * e.g. /space1/space2/space3 -> (/space1/space2, space3, nil) + */ +func Disect(path string) (string, string, error) { + if path == "" { + return "", "", errs.PathEmpty + } + + i := strings.LastIndex(path, types.PathSeparator) + if i == -1 { + return "", path, nil + } + + return path[:i], path[i+1:], nil +} + +/* + * Concatinates two paths together (takes care of leading / trailing '/') + * e.g. (space1/, /space2/) -> space1/space2 + * + * NOTE: "//" is not a valid path, so all '/' will be trimmed. + */ +func Concatinate(path1 string, path2 string) string { + path1 = strings.Trim(path1, types.PathSeparator) + path2 = strings.Trim(path2, types.PathSeparator) + + if path1 == "" { + return path2 + } else if path2 == "" { + return path1 + } + + return path1 + types.PathSeparator + path2 +} diff --git a/internal/router/api.go b/internal/router/api.go index 4e3d0eb53..218fbfa0d 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -28,7 +28,7 @@ import ( /* * Mounts the Rest API Router under mountPath (path has to end with ). - * The handler is wrapped within a layer that handles encoding terminated FQNs. + * The handler is wrapped within a layer that handles encoding terminated Paths. */ func newApiHandler( mountPath string, @@ -77,13 +77,13 @@ func newApiHandler( }) }) - // Generate list of all path prefixes that expect terminated FQNs - terminatedFQNPrefixes := []string{ + // Generate list of all path prefixes that expect terminated Paths + terminatedPathPrefixes := []string{ mountPath + "/v1/spaces", mountPath + "/v1/repos", } - return encode.TerminatedFqnBefore(terminatedFQNPrefixes, r.ServeHTTP), nil + return encode.TerminatedPathBefore(terminatedPathPrefixes, r.ServeHTTP), nil } func setupRoutesV1( @@ -97,27 +97,38 @@ func setupRoutesV1( // SPACES r.Route("/spaces", func(r chi.Router) { - // Create takes fqn and parentId via body, not uri + // Create takes path and parentId via body, not uri r.Post("/", handler_space.HandleCreate(guard, spaceStore)) r.Route(fmt.Sprintf("/{%s}", request.SpaceRefParamName), func(r chi.Router) { // resolves the space and stores in the context r.Use(space.Required(spaceStore)) - // space level operations + // space operations r.Get("/", handler_space.HandleFind(guard, spaceStore)) - r.Put("/", handler_space.HandleUpdate(guard)) + r.Put("/", handler_space.HandleUpdate(guard, spaceStore)) r.Delete("/", handler_space.HandleDelete(guard, spaceStore)) - // space sub operations + r.Post("/move", handler_space.HandleMove(guard, spaceStore)) r.Get("/spaces", handler_space.HandleList(guard, spaceStore)) r.Get("/repos", handler_space.HandleListRepos(guard, repoStore)) + + // Child collections + r.Route("/paths", func(r chi.Router) { + r.Get("/", handler_space.HandleListPaths(guard, spaceStore)) + r.Post("/", handler_space.HandleCreatePath(guard, spaceStore)) + + // per path operations + r.Route(fmt.Sprintf("/{%s}", request.PathIdParamName), func(r chi.Router) { + r.Delete("/", handler_space.HandleDeletePath(guard, spaceStore)) + }) + }) }) }) // REPOS r.Route("/repos", func(r chi.Router) { - // Create takes fqn and parentId via body, not uri + // Create takes path and parentId via body, not uri r.Post("/", handler_repo.HandleCreate(guard, spaceStore, repoStore)) r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) { @@ -126,8 +137,21 @@ func setupRoutesV1( // repo level operations r.Get("/", handler_repo.HandleFind(guard, repoStore)) - r.Put("/", handler_repo.HandleUpdate(guard)) + r.Put("/", handler_repo.HandleUpdate(guard, repoStore)) r.Delete("/", handler_repo.HandleDelete(guard, repoStore)) + + r.Post("/move", handler_repo.HandleMove(guard, repoStore, spaceStore)) + + // Child collections + r.Route("/paths", func(r chi.Router) { + r.Get("/", handler_repo.HandleListPaths(guard, repoStore)) + r.Post("/", handler_repo.HandleCreatePath(guard, repoStore)) + + // per path operations + r.Route(fmt.Sprintf("/{%s}", request.PathIdParamName), func(r chi.Router) { + r.Delete("/", handler_repo.HandleDeletePath(guard, repoStore)) + }) + }) }) }) diff --git a/internal/router/git.go b/internal/router/git.go index 167c2e5d0..7fab716ff 100644 --- a/internal/router/git.go +++ b/internal/router/git.go @@ -22,7 +22,7 @@ import ( /* * Mounts the GIT Router under mountPath. - * The handler is wrapped within a layer that handles encoding FQNS. + * The handler is wrapped within a layer that handles encoding Paths. */ func newGitHandler( mountPath string, @@ -84,7 +84,7 @@ func newGitHandler( }) }) - return encode.GitFqnBefore(r.ServeHTTP), nil + return encode.GitPathBefore(r.ServeHTTP), nil } func stubGitHandler(w http.ResponseWriter, r *http.Request) { @@ -97,7 +97,7 @@ func stubGitHandler(w http.ResponseWriter, r *http.Request) { " Method: '%s'\n"+ " Path: '%s'\n"+ " Query: '%s'", - rep.DisplayName, rep.Fqn, + rep.DisplayName, rep.Path, r.Method, r.URL.Path, r.URL.RawQuery, diff --git a/internal/router/web.go b/internal/router/web.go index f4163848c..e9d74ba67 100644 --- a/internal/router/web.go +++ b/internal/router/web.go @@ -14,7 +14,7 @@ import ( /* * Mounts the WEB Router under mountPath. - * The handler is wrapped within a layer that handles encoding FQNS. + * The handler is wrapped within a layer that handles encoding Paths. */ func newWebHandler( mountPath string, @@ -61,6 +61,6 @@ func newWebHandler( ) }) - // web doesn't have any prefixes for terminated fqns - return encode.TerminatedFqnBefore([]string{""}, r.ServeHTTP), nil + // web doesn't have any prefixes for terminated paths + return encode.TerminatedPathBefore([]string{""}, r.ServeHTTP), nil } diff --git a/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql b/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql new file mode 100644 index 000000000..0bb6c66b6 --- /dev/null +++ b/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS paths ( + path_id SERIAL PRIMARY KEY +,path_value TEXT +,path_isAlias BOOLEAN +,path_targetType TEXT CHECK (path_targetType in ('space', 'repo')) +,path_targetId INTEGER +,path_createdBy INTEGER +,path_created INTEGER +,path_updated INTEGER +,UNIQUE(path_value) +); \ No newline at end of file diff --git a/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql b/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql index abd9c2dd1..fc31af3c4 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS repositories ( repo_id SERIAL PRIMARY KEY ,repo_name TEXT ,repo_spaceId INTEGER -,repo_fqn TEXT +,repo_path TEXT ,repo_displayName TEXT ,repo_description TEXT ,repo_isPublic BOOLEAN @@ -14,5 +14,5 @@ CREATE TABLE IF NOT EXISTS repositories ( ,repo_numPulls INTEGER ,repo_numClosedPulls INTEGER ,repo_numOpenPulls INTEGER -,UNIQUE(repo_fqn) +,UNIQUE(repo_path) ); diff --git a/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql b/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql index c2d91a2d7..baeb93db6 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql @@ -1,7 +1,6 @@ CREATE TABLE IF NOT EXISTS spaces ( space_id SERIAL PRIMARY KEY ,space_name TEXT -,space_fqn TEXT ,space_parentId INTEGER ,space_displayName TEXT ,space_description TEXT @@ -9,5 +8,4 @@ CREATE TABLE IF NOT EXISTS spaces ( ,space_createdBy INTEGER ,space_created INTEGER ,space_updated INTEGER -,UNIQUE(space_fqn) ); \ No newline at end of file diff --git a/internal/store/database/migrate/postgres/0002_create_index_paths_targetType_targetId.up.sql b/internal/store/database/migrate/postgres/0002_create_index_paths_targetType_targetId.up.sql new file mode 100644 index 000000000..40306696d --- /dev/null +++ b/internal/store/database/migrate/postgres/0002_create_index_paths_targetType_targetId.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS paths_targetType_targetId +ON paths(path_targetType, path_targetId); diff --git a/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql new file mode 100644 index 000000000..2490e91bc --- /dev/null +++ b/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS paths ( + path_id INTEGER PRIMARY KEY AUTOINCREMENT +,path_value TEXT COLLATE NOCASE +,path_isAlias BOOLEAN +,path_targetType TEXT COLLATE NOCASE CHECK (path_targetType in ('space', 'repo')) +,path_targetId INTEGER +,path_createdBy INTEGER +,path_created INTEGER +,path_updated INTEGER +,UNIQUE(path_value COLLATE NOCASE) +); \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql index 4ce68f7e0..51e980832 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS repositories ( repo_id INTEGER PRIMARY KEY AUTOINCREMENT ,repo_name TEXT COLLATE NOCASE ,repo_spaceId INTEGER -,repo_fqn TEXT COLLATE NOCASE +,repo_path TEXT COLLATE NOCASE ,repo_displayName TEXT ,repo_description TEXT ,repo_isPublic BOOLEAN @@ -14,5 +14,5 @@ CREATE TABLE IF NOT EXISTS repositories ( ,repo_numPulls INTEGER ,repo_numClosedPulls INTEGER ,repo_numOpenPulls INTEGER -,UNIQUE(repo_fqn COLLATE NOCASE) +,UNIQUE(repo_path COLLATE NOCASE) ); diff --git a/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql index 15ac32405..35d388683 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql @@ -1,7 +1,6 @@ CREATE TABLE IF NOT EXISTS spaces ( space_id INTEGER PRIMARY KEY AUTOINCREMENT ,space_name TEXT COLLATE NOCASE -,space_fqn TEXT COLLATE NOCASE ,space_parentId INTEGER ,space_displayName TEXT ,space_description TEXT @@ -9,5 +8,4 @@ CREATE TABLE IF NOT EXISTS spaces ( ,space_createdBy INTEGER ,space_created INTEGER ,space_updated INTEGER -,UNIQUE(space_fqn COLLATE NOCASE) ); \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0002_create_index_paths_targetType_targetId.up.sql b/internal/store/database/migrate/sqlite/0002_create_index_paths_targetType_targetId.up.sql new file mode 100644 index 000000000..40306696d --- /dev/null +++ b/internal/store/database/migrate/sqlite/0002_create_index_paths_targetType_targetId.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS paths_targetType_targetId +ON paths(path_targetType, path_targetId); diff --git a/internal/store/database/path.go b/internal/store/database/path.go new file mode 100644 index 000000000..f1c90a51f --- /dev/null +++ b/internal/store/database/path.go @@ -0,0 +1,362 @@ +// Copyright 2021 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" + + "github.com/harness/gitness/internal/paths" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +// Creates a new path +func CreatePath(ctx context.Context, db *sqlx.DB, path *types.Path) error { + + // In case it's not an alias, ensure there are no duplicates + if !path.IsAlias { + if cnt, err := CountPaths(ctx, db, path.TargetType, path.TargetId); err != nil { + return err + } else if cnt > 0 { + return errs.PrimaryPathAlreadyExists + } + } + + query, arg, err := db.BindNamed(pathInsert, path) + if err != nil { + return wrapSqlErrorf(err, "Failed to bind path object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&path.ID); err != nil { + return wrapSqlErrorf(err, "Insert query failed") + } + + return nil +} + +// Creates a new path as part of a transaction +func CreatePathTx(ctx context.Context, db *sqlx.DB, tx *sqlx.Tx, path *types.Path) error { + + // In case it's not an alias, ensure there are no duplicates + if !path.IsAlias { + if cnt, err := CountPathsTx(ctx, tx, path.TargetType, path.TargetId); err != nil { + return err + } else if cnt > 0 { + return errs.PrimaryPathAlreadyExists + } + } + + query, arg, err := db.BindNamed(pathInsert, path) + if err != nil { + return wrapSqlErrorf(err, "Failed to bind path object") + } + + if err = tx.QueryRowContext(ctx, query, arg...).Scan(&path.ID); err != nil { + return wrapSqlErrorf(err, "Insert query failed") + } + + return nil +} + +func CountPrimaryChildPathsTx(ctx context.Context, tx *sqlx.Tx, prefix string) (int64, error) { + var count int64 + err := tx.QueryRowContext(ctx, pathCountPrimaryForPrefix, paths.Concatinate(prefix, "%")).Scan(&count) + if err != nil { + return 0, wrapSqlErrorf(err, "Count query failed") + } + return count, nil +} + +func ListPrimaryChildPathsTx(ctx context.Context, tx *sqlx.Tx, prefix string) ([]*types.Path, error) { + childs := []*types.Path{} + + if err := tx.SelectContext(ctx, &childs, pathSelectPrimaryForPrefix, paths.Concatinate(prefix, "%")); err != nil { + return nil, wrapSqlErrorf(err, "Select query failed") + } + + return childs, nil +} + +// Replaces the path for a target as part of a transaction - keeps the existing as alias if requested. +func ReplacePathTx(ctx context.Context, db *sqlx.DB, tx *sqlx.Tx, path *types.Path, keepAsAlias bool) error { + + if path.IsAlias { + return errs.PrimaryPathRequired + } + + // existing is always non-alias (as query filters for IsAlias=0) + existing := new(types.Path) + err := tx.GetContext(ctx, existing, pathSelectPrimaryForTarget, string(path.TargetType), fmt.Sprint(path.TargetId)) + if err != nil { + return wrapSqlErrorf(err, "Failed to get the existing primary path") + } + + // Only look for childs if the type can have childs + if path.TargetType == enum.PathTargetTypeSpace { + + // get all primary paths that start with the current path before updating (or we can run into recursion) + childs, err := ListPrimaryChildPathsTx(ctx, tx, existing.Value) + if err != nil { + return errors.Wrapf(err, "Failed to get primary child paths for '%s'", existing.Value) + } + + for _, child := range childs { + // create path with updated path (child already is primary) + updatedChild := new(types.Path) + *updatedChild = *child + updatedChild.ID = 0 // will be regenerated + updatedChild.Created = path.Created + updatedChild.Updated = path.Updated + updatedChild.CreatedBy = path.CreatedBy + updatedChild.Value = path.Value + updatedChild.Value[len(existing.Value):] + + query, arg, err := db.BindNamed(pathInsert, updatedChild) + if err != nil { + return wrapSqlErrorf(err, "Failed to bind path object") + } + + _, err = tx.ExecContext(ctx, query, arg...) + if err != nil { + return wrapSqlErrorf(err, "Failed to create new primary child path '%s'", updatedChild.Value) + } + + // make current child an alias or delete it + if keepAsAlias { + _, err = tx.ExecContext(ctx, pathMakeAlias, child.ID) + } else { + _, err = tx.ExecContext(ctx, pathDeleteId, child.ID) + } + if err != nil { + return wrapSqlErrorf(err, "Failed to mark existing child path '%s' as alias", updatedChild.Value) + } + } + } + + // insert the new Path + query, arg, err := db.BindNamed(pathInsert, path) + if err != nil { + return wrapSqlErrorf(err, "Failed to bind path object") + } + + _, err = tx.ExecContext(ctx, query, arg...) + if err != nil { + return wrapSqlErrorf(err, "Failed to create new primary path '%s'", path.Value) + } + + // make existing an alias + if keepAsAlias { + _, err = tx.ExecContext(ctx, pathMakeAlias, existing.ID) + } else { + _, err = tx.ExecContext(ctx, pathDeleteId, existing.ID) + } + if err != nil { + return wrapSqlErrorf(err, "Failed to mark existing path '%s' as alias", existing.Value) + } + + return nil +} + +// Finds the primary path for a target. +func FindPathTx(ctx context.Context, tx *sqlx.Tx, targetType enum.PathTargetType, targetId int64) (*types.Path, error) { + dst := new(types.Path) + err := tx.GetContext(ctx, dst, pathSelectPrimaryForTarget, string(targetType), fmt.Sprint(targetId)) + if err != nil { + return nil, wrapSqlErrorf(err, "Select query failed") + } + + return dst, nil +} + +// Deletes a specific path alias (primary can't be deleted, only with delete all). +func DeletePath(ctx context.Context, db *sqlx.DB, id int64) error { + tx, err := db.BeginTxx(ctx, nil) + if err != nil { + return wrapSqlErrorf(err, "Failed to start a new transaction") + } + defer tx.Rollback() + + // ensure path is an alias + dst := new(types.Path) + err = tx.GetContext(ctx, dst, pathSelectId, id) + if err != nil { + return wrapSqlErrorf(err, "Failed to find path with id %d", id) + } else if dst.IsAlias == false { + return errs.PrimaryPathCantBeDeleted + } + + // delete the path + if _, err = tx.ExecContext(ctx, pathDeleteId, id); err != nil { + return wrapSqlErrorf(err, "Delete query failed", id) + } + + if err = tx.Commit(); err != nil { + return wrapSqlErrorf(err, "Failed to commit transaction") + } + + return nil +} + +// Deletes all paths for a target as part of a transaction. +func DeleteAllPaths(ctx context.Context, tx *sqlx.Tx, targetType enum.PathTargetType, targetId int64) error { + // delete all entries for the target + if _, err := tx.ExecContext(ctx, pathDeleteTarget, string(targetType), fmt.Sprint(targetId)); err != nil { + return wrapSqlErrorf(err, "Query for deleting all pahts failed") + } + return nil +} + +// Lists all paths for a target. +func ListPaths(ctx context.Context, db *sqlx.DB, targetType enum.PathTargetType, targetId int64, opts *types.PathFilter) ([]*types.Path, error) { + dst := []*types.Path{} + + // if the user does not provide any customer filter + // or sorting we use the default select statement. + if opts.Sort == enum.PathAttrNone { + err := db.SelectContext(ctx, &dst, pathSelect, string(targetType), fmt.Sprint(targetId), limit(opts.Size), offset(opts.Page, opts.Size)) + if err != nil { + return nil, wrapSqlErrorf(err, "Default select query failed") + } + + return dst, nil + } + + // else we construct the sql statement. + stmt := builder.Select("*").From("paths").Where("path_targetType = $1 AND path_targetId = $2", string(targetType), fmt.Sprint(targetId)) + stmt = stmt.Limit(uint64(limit(opts.Size))) + stmt = stmt.Offset(uint64(offset(opts.Page, opts.Size))) + + switch opts.Sort { + case enum.PathAttrCreated: + // NOTE: string concatination is safe because the + // order attribute is an enum and is not user-defined, + // and is therefore not subject to injection attacks. + stmt = stmt.OrderBy("path_created " + opts.Order.String()) + case enum.PathAttrUpdated: + stmt = stmt.OrderBy("path_updated " + opts.Order.String()) + case enum.PathAttrId: + stmt = stmt.OrderBy("path_id " + opts.Order.String()) + case enum.PathAttrPath: + stmt = stmt.OrderBy("path_value" + opts.Order.String()) + } + + sql, _, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + if err = db.SelectContext(ctx, &dst, sql); err != nil { + return nil, wrapSqlErrorf(err, "Customer select query failed") + } + + return dst, nil +} + +// Coutn paths for a target. +func CountPaths(ctx context.Context, db *sqlx.DB, targetType enum.PathTargetType, targetId int64) (int64, error) { + var count int64 + err := db.QueryRowContext(ctx, pathCount, string(targetType), fmt.Sprint(targetId)).Scan(&count) + if err != nil { + return 0, wrapSqlErrorf(err, "Query failed") + } + return count, nil +} + +// Count paths for a target as part of a transaction. +func CountPathsTx(ctx context.Context, tx *sqlx.Tx, targetType enum.PathTargetType, targetId int64) (int64, error) { + var count int64 + err := tx.QueryRowContext(ctx, pathCount, string(targetType), fmt.Sprint(targetId)).Scan(&count) + if err != nil { + return 0, wrapSqlErrorf(err, "Query failed") + } + return count, nil +} + +const pathBase = ` +SELECT +path_id +,path_value +,path_isAlias +,path_targetType +,path_targetId +,path_createdBy +,path_created +,path_updated +FROM paths +` +const pathSelect = pathBase + ` +WHERE path_targetType = $1 AND path_targetId = $2 +ORDER BY path_isAlias DESC, path_value ASC +LIMIT $3 OFFSET $4 +` + +// there's only one entry with a given target & targetId for isAlias -- false +const pathSelectPrimaryForTarget = pathBase + ` +WHERE path_targetType = $1 AND path_targetId = $2 AND path_isAlias = 0 +` + +const pathSelectPrimaryForPrefix = pathBase + ` +WHERE path_value LIKE $1 AND path_isAlias = 0 +` + +const pathCount = ` +SELECT count(*) +FROM paths +WHERE path_targetType = $1 AND path_targetId = $2 +` + +const pathCountPrimaryForPrefix = ` +SELECT count(*) +FROM paths +WHERE path_value LIKE $1 AND path_isAlias = 0 +` + +const pathInsert = ` +INSERT INTO paths ( + path_value + ,path_isAlias + ,path_targetType + ,path_targetId + ,path_createdBy + ,path_created + ,path_updated +) values ( + :path_value + ,:path_isAlias + ,:path_targetType + ,:path_targetId + ,:path_createdBy + ,:path_created + ,:path_updated +) RETURNING path_id +` + +const pathSelectId = pathBase + ` +WHERE path_id = $1 +` + +const pathSelectPath = pathBase + ` +WHERE path_value = $1 +` + +const pathDeleteId = ` +DELETE FROM paths +WHERE path_id = $1 +` + +const pathDeleteTarget = ` +DELETE FROM paths +WHERE path_targetType = $1 AND path_targetId = $2 +` + +const pathMakeAlias = ` +UPDATE paths +SET +path_isAlias = 1 +WHERE path_id = :path_id +` diff --git a/internal/store/database/repo.go b/internal/store/database/repo.go index 1a46738fa..c9bcb3b03 100644 --- a/internal/store/database/repo.go +++ b/internal/store/database/repo.go @@ -7,10 +7,14 @@ package database import ( "context" "fmt" + "time" + "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/pkg/errors" "github.com/jmoiron/sqlx" ) @@ -30,66 +34,204 @@ type RepoStore struct { // Finds the repo by id. func (s *RepoStore) Find(ctx context.Context, id int64) (*types.Repository, error) { dst := new(types.Repository) - err := s.db.Get(dst, repoSelectID, id) - return dst, err + if err := s.db.GetContext(ctx, dst, repoSelectById, id); err != nil { + return nil, wrapSqlErrorf(err, "Select query failed") + } + return dst, nil } -// Finds the repo by the full qualified repo name. -func (s *RepoStore) FindFqn(ctx context.Context, fqn string) (*types.Repository, error) { +// Finds the repo by path. +func (s *RepoStore) FindByPath(ctx context.Context, path string) (*types.Repository, error) { dst := new(types.Repository) - err := s.db.Get(dst, repoSelectFqn, fqn) - return dst, err + if err := s.db.GetContext(ctx, dst, repoSelectByPath, path); err != nil { + return nil, wrapSqlErrorf(err, "Select query failed") + } + return dst, nil } // Creates a new repo func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error { - // TODO: Ensure parent exists!! - // TODO: Ensure forkId exists! + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return wrapSqlErrorf(err, "Failed to start a new transaction") + } + defer tx.Rollback() + + // insert repo first so we get id query, arg, err := s.db.BindNamed(repoInsert, repo) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to bind repo object") } - return s.db.QueryRow(query, arg...).Scan(&repo.ID) + + if err = tx.QueryRow(query, arg...).Scan(&repo.ID); err != nil { + return wrapSqlErrorf(err, "Insert query failed") + } + + // Get parent path (repo always has a parent) + parentPath, err := FindPathTx(ctx, tx, enum.PathTargetTypeSpace, repo.SpaceId) + if err != nil { + return errors.Wrap(err, "Failed to find path of parent space") + } + + // all existing paths are valid, repo name is assumed to be valid. + path := paths.Concatinate(parentPath.Value, repo.Name) + + // create path only once we know the id of the repo + p := &types.Path{ + TargetType: enum.PathTargetTypeRepo, + TargetId: repo.ID, + IsAlias: false, + Value: path, + CreatedBy: repo.CreatedBy, + Created: repo.Created, + Updated: repo.Updated, + } + + if err = CreatePathTx(ctx, s.db, tx, p); err != nil { + return errors.Wrap(err, "Failed to create primary path of repo") + } + + // commit + if err = tx.Commit(); err != nil { + return wrapSqlErrorf(err, "Failed to commit transaction") + } + + // update path in repo object + repo.Path = p.Value + + return nil +} + +// Moves an existing space. +func (s *RepoStore) Move(ctx context.Context, userId int64, repoId int64, newSpaceId int64, newName string, keepAsAlias bool) (*types.Repository, error) { + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return nil, wrapSqlErrorf(err, "Failed to start a new transaction") + } + defer tx.Rollback() + + // get current path of repo + currentPath, err := FindPathTx(ctx, tx, enum.PathTargetTypeRepo, repoId) + if err != nil { + return nil, errors.Wrap(err, "Failed to find the primary path of the repo") + } + + // get path of new parent space + spacePath, err := FindPathTx(ctx, tx, enum.PathTargetTypeSpace, newSpaceId) + if err != nil { + return nil, errors.Wrap(err, "Failed to find the primary path of the new space") + } + newPath := paths.Concatinate(spacePath.Value, newName) + + if newPath == currentPath.Value { + return nil, errs.NoChangeInRequestedMove + } + + p := &types.Path{ + TargetType: enum.PathTargetTypeRepo, + TargetId: repoId, + IsAlias: false, + Value: newPath, + CreatedBy: userId, + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // replace the primary path (also updates all child primary paths) + if err = ReplacePathTx(ctx, s.db, tx, p, keepAsAlias); err != nil { + return nil, errors.Wrap(err, "Failed to update the primary path of the repo") + } + + // Rename the repo itself + if _, err := tx.ExecContext(ctx, repoUpdateNameAndSpaceId, newName, newSpaceId, repoId); err != nil { + return nil, wrapSqlErrorf(err, "Query for renaming and updating the space id failed") + } + + // TODO: return repo as part of rename db operation? + dst := new(types.Repository) + if err = tx.GetContext(ctx, dst, repoSelectById, repoId); err != nil { + return nil, wrapSqlErrorf(err, "Select query to get the repo's latest state failed") + } + + // commit + if err = tx.Commit(); err != nil { + return nil, wrapSqlErrorf(err, "Failed to commit transaction") + } + + return dst, nil } // Updates the repo details. func (s *RepoStore) Update(ctx context.Context, repo *types.Repository) error { query, arg, err := s.db.BindNamed(repoUpdate, repo) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to bind repo object") } - _, err = s.db.Exec(query, arg...) - return err + + if _, err = s.db.ExecContext(ctx, query, arg...); err != nil { + wrapSqlErrorf(err, "Update query failed") + } + + return nil } // Deletes the repo. func (s *RepoStore) Delete(ctx context.Context, id int64) error { - tx, err := s.db.BeginTx(ctx, nil) + tx, err := s.db.BeginTxx(ctx, nil) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to start a new transaction") } defer tx.Rollback() - // delete the repo - if _, err := tx.Exec(repoDelete, id); err != nil { - return err + // delete all paths + err = DeleteAllPaths(ctx, tx, enum.PathTargetTypeRepo, id) + if err != nil { + return errors.Wrap(err, "Failed to delete all paths of the repo") } - return tx.Commit() + + // delete the repo + if _, err := tx.ExecContext(ctx, repoDelete, id); err != nil { + return wrapSqlErrorf(err, "The delete query failed") + } + + if err = tx.Commit(); err != nil { + return wrapSqlErrorf(err, "Failed to commit transaction") + } + + return nil +} + +// Count of repos in a space. +func (s *RepoStore) Count(ctx context.Context, spaceId int64) (int64, error) { + var count int64 + err := s.db.QueryRow(repoCount, spaceId).Scan(&count) + if err != nil { + return 0, wrapSqlErrorf(err, "Failed executing count query") + } + return count, nil } // List returns a list of repos in a space. -func (s *RepoStore) List(ctx context.Context, spaceId int64, opts types.RepoFilter) ([]*types.Repository, error) { +// TODO: speed up list - for some reason is 200ms for 1 repo as well as 1000 +func (s *RepoStore) List(ctx context.Context, spaceId int64, opts *types.RepoFilter) ([]*types.Repository, error) { dst := []*types.Repository{} // if the user does not provide any customer filter // or sorting we use the default select statement. if opts.Sort == enum.RepoAttrNone { - err := s.db.Select(&dst, repoSelect, spaceId, limit(opts.Size), offset(opts.Page, opts.Size)) - return dst, err + err := s.db.SelectContext(ctx, &dst, repoSelect, spaceId, limit(opts.Size), offset(opts.Page, opts.Size)) + if err != nil { + return nil, wrapSqlErrorf(err, "Failed executing default list query") + } + return dst, nil } // else we construct the sql statement. - stmt := builder.Select("*").From("repositories").Where("repo_spaceId = " + fmt.Sprint(spaceId)) + stmt := builder. + Select("repositories.*,path_value AS repo_path"). + From("repositories"). + InnerJoin("paths ON repositories.repo_id=paths.path_targetId AND paths.path_targetType='repo' AND paths.path_isAlias=0"). + Where("repo_spaceId = " + fmt.Sprint(spaceId)) stmt = stmt.Limit(uint64(limit(opts.Size))) stmt = stmt.Offset(uint64(offset(opts.Page, opts.Size))) @@ -105,32 +247,57 @@ func (s *RepoStore) List(ctx context.Context, spaceId int64, opts types.RepoFilt stmt = stmt.OrderBy("repo_id " + opts.Order.String()) case enum.RepoAttrName: stmt = stmt.OrderBy("repo_name " + opts.Order.String()) - case enum.RepoAttrFqn: - stmt = stmt.OrderBy("repo_fqn " + opts.Order.String()) + case enum.RepoAttrDisplayName: + stmt = stmt.OrderBy("repo_displayName " + opts.Order.String()) + case enum.RepoAttrPath: + stmt = stmt.OrderBy("repo_path " + opts.Order.String()) } sql, _, err := stmt.ToSql() if err != nil { - return dst, err + return nil, errors.Wrap(err, "Failed to convert query to sql") } - err = s.db.Select(&dst, sql) - return dst, err + if err = s.db.SelectContext(ctx, &dst, sql); err != nil { + return nil, wrapSqlErrorf(err, "Failed executing custom list query") + } + + return dst, nil } -// Count of repos in a space. -func (s *RepoStore) Count(ctx context.Context, spaceId int64) (int64, error) { - var count int64 - err := s.db.QueryRow(repoCount, spaceId).Scan(&count) - return count, err +// List returns a list of all paths of a repo. +func (s *RepoStore) ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) { + return ListPaths(ctx, s.db, enum.PathTargetTypeRepo, id, opts) } -const repoBase = ` +// Create an alias for a repo +func (s *RepoStore) CreatePath(ctx context.Context, repoId int64, params *types.PathParams) (*types.Path, error) { + p := &types.Path{ + TargetType: enum.PathTargetTypeRepo, + TargetId: repoId, + IsAlias: true, + + // get remaining infor from params + Value: params.Path, + CreatedBy: params.CreatedBy, + Created: params.Created, + Updated: params.Updated, + } + + return p, CreatePath(ctx, s.db, p) +} + +// Delete an alias of a repo +func (s *RepoStore) DeletePath(ctx context.Context, repoId int64, pathId int64) error { + return DeletePath(ctx, s.db, pathId) +} + +const repoSelectBase = ` SELECT repo_id ,repo_name ,repo_spaceId -,repo_fqn +,paths.path_value AS repo_path ,repo_displayName ,repo_description ,repo_isPublic @@ -142,11 +309,17 @@ repo_id ,repo_numPulls ,repo_numClosedPulls ,repo_numOpenPulls -FROM repositories ` -const repoSelect = repoBase + ` + +const repoSelectBaseWithJoin = repoSelectBase + ` +FROM repositories +INNER JOIN paths +ON repositories.repo_id=paths.path_targetId AND paths.path_targetType='repo' AND paths.path_isAlias=0 +` + +const repoSelect = repoSelectBaseWithJoin + ` WHERE repo_spaceId = $1 -ORDER BY repo_fqn ASC +ORDER BY repo_name ASC LIMIT $2 OFFSET $3 ` @@ -156,12 +329,14 @@ FROM repositories WHERE repo_spaceId = $1 ` -const repoSelectID = repoBase + ` +const repoSelectById = repoSelectBaseWithJoin + ` WHERE repo_id = $1 ` -const repoSelectFqn = repoBase + ` -WHERE repo_fqn = $1 +const repoSelectByPath = repoSelectBase + ` +FROM paths paths1 +INNER JOIN repositories ON repositories.repo_id=paths1.path_targetId AND paths1.path_targetType='repo' AND paths1.path_value = $1 +INNER JOIN paths ON repositories.repo_id=paths.path_targetId AND paths.path_targetType='repo' AND paths.path_isAlias=0 ` const repoDelete = ` @@ -173,7 +348,6 @@ const repoInsert = ` INSERT INTO repositories ( repo_name ,repo_spaceId - ,repo_fqn ,repo_displayName ,repo_description ,repo_isPublic @@ -188,7 +362,6 @@ INSERT INTO repositories ( ) values ( :repo_name ,:repo_spaceId - ,:repo_fqn ,:repo_displayName ,:repo_description ,:repo_isPublic @@ -216,3 +389,11 @@ SET ,repo_numOpenPulls = :repo_numOpenPulls WHERE repo_id = :repo_id ` + +const repoUpdateNameAndSpaceId = ` +UPDATE repositories +SET +repo_name = $1 +,repo_spaceId = $2 +WHERE repo_id = $3 +` diff --git a/internal/store/database/repo_sync.go b/internal/store/database/repo_sync.go index 2a4f88ef9..289e26e32 100644 --- a/internal/store/database/repo_sync.go +++ b/internal/store/database/repo_sync.go @@ -33,11 +33,11 @@ func (s *RepoStoreSync) Find(ctx context.Context, id int64) (*types.Repository, return s.base.Find(ctx, id) } -// Finds the repo by the full qualified repo name. -func (s *RepoStoreSync) FindFqn(ctx context.Context, fqn string) (*types.Repository, error) { +// Finds the repo by path. +func (s *RepoStoreSync) FindByPath(ctx context.Context, path string) (*types.Repository, error) { mutex.RLock() defer mutex.RUnlock() - return s.base.FindFqn(ctx, fqn) + return s.base.FindByPath(ctx, path) } // Creates a new repo @@ -47,6 +47,13 @@ func (s *RepoStoreSync) Create(ctx context.Context, repo *types.Repository) erro return s.base.Create(ctx, repo) } +// Moves an existing repo. +func (s *RepoStoreSync) Move(ctx context.Context, userId int64, repoId int64, newSpaceId int64, newName string, keepAsAlias bool) (*types.Repository, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Move(ctx, userId, repoId, newSpaceId, newName, keepAsAlias) +} + // Updates the repo details. func (s *RepoStoreSync) Update(ctx context.Context, repo *types.Repository) error { mutex.RLock() @@ -61,16 +68,35 @@ func (s *RepoStoreSync) Delete(ctx context.Context, id int64) error { return s.base.Delete(ctx, id) } -// List returns a list of repos in a space. -func (s *RepoStoreSync) List(ctx context.Context, spaceId int64, opts types.RepoFilter) ([]*types.Repository, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.List(ctx, spaceId, opts) -} - // Count of repos in a space. func (s *RepoStoreSync) Count(ctx context.Context, spaceId int64) (int64, error) { mutex.RLock() defer mutex.RUnlock() return s.base.Count(ctx, spaceId) } + +// List returns a list of repos in a space. +func (s *RepoStoreSync) List(ctx context.Context, spaceId int64, opts *types.RepoFilter) ([]*types.Repository, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.List(ctx, spaceId, opts) +} + +// List returns a list of all paths of a repo. +func (s *RepoStoreSync) ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) { + return s.base.ListAllPaths(ctx, id, opts) +} + +// Create an alias for a repo +func (s *RepoStoreSync) CreatePath(ctx context.Context, repoId int64, params *types.PathParams) (*types.Path, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.CreatePath(ctx, repoId, params) +} + +// Delete an alias of a repo +func (s *RepoStoreSync) DeletePath(ctx context.Context, repoId int64, pathId int64) error { + mutex.RLock() + defer mutex.RUnlock() + return s.base.DeletePath(ctx, repoId, pathId) +} diff --git a/internal/store/database/space.go b/internal/store/database/space.go index 004749c6d..e4f43bffa 100644 --- a/internal/store/database/space.go +++ b/internal/store/database/space.go @@ -6,12 +6,16 @@ package database import ( "context" - "errors" "fmt" + "strings" + "time" + "github.com/harness/gitness/internal/paths" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/harness/gitness/types/errs" + "github.com/pkg/errors" "github.com/jmoiron/sqlx" ) @@ -31,74 +35,233 @@ type SpaceStore struct { // Finds the space by id. func (s *SpaceStore) Find(ctx context.Context, id int64) (*types.Space, error) { dst := new(types.Space) - err := s.db.Get(dst, spaceSelectID, id) - return dst, err + if err := s.db.GetContext(ctx, dst, spaceSelectById, id); err != nil { + return nil, wrapSqlErrorf(err, "Select query failed") + } + return dst, nil } -// Finds the space by the full qualified space name. -func (s *SpaceStore) FindFqn(ctx context.Context, fqn string) (*types.Space, error) { +// Finds the space by path. +func (s *SpaceStore) FindByPath(ctx context.Context, path string) (*types.Space, error) { dst := new(types.Space) - err := s.db.Get(dst, spaceSelectFqn, fqn) - return dst, err + if err := s.db.GetContext(ctx, dst, spaceSelectByPath, path); err != nil { + return nil, wrapSqlErrorf(err, "Select query failed") + } + return dst, nil } // Creates a new space func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error { - // TODO: Ensure parent exists!! + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return wrapSqlErrorf(err, "Failed to start a new transaction") + } + defer tx.Rollback() + + // insert space first so we get id query, arg, err := s.db.BindNamed(spaceInsert, space) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to bind space object") } - return s.db.QueryRow(query, arg...).Scan(&space.ID) + + if err = tx.QueryRow(query, arg...).Scan(&space.ID); err != nil { + return wrapSqlErrorf(err, "Insert query failed") + } + + // Get path (get parent if needed) + path := space.Name + if space.ParentId > 0 { + parentPath, err := FindPathTx(ctx, tx, enum.PathTargetTypeSpace, space.ParentId) + if err != nil { + return errors.Wrap(err, "Failed to find path of parent space") + } + + // all existing paths are valid, space name is assumed to be valid. + path = paths.Concatinate(parentPath.Value, space.Name) + } + + // create path only once we know the id of the space + p := &types.Path{ + TargetType: enum.PathTargetTypeSpace, + TargetId: space.ID, + IsAlias: false, + Value: path, + CreatedBy: space.CreatedBy, + Created: space.Created, + Updated: space.Updated, + } + err = CreatePathTx(ctx, s.db, tx, p) + if err != nil { + return errors.Wrap(err, "Failed to create primary path of space") + } + + // commit + if err = tx.Commit(); err != nil { + return wrapSqlErrorf(err, "Failed to commit transaction") + } + + // update path in space object + space.Path = p.Value + + return nil +} + +// Moves an existing space. +func (s *SpaceStore) Move(ctx context.Context, userId int64, spaceId int64, newParentId int64, newName string, keepAsAlias bool) (*types.Space, error) { + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return nil, wrapSqlErrorf(err, "Failed to start a new transaction") + } + defer tx.Rollback() + + // always get currentpath (either it didn't change or we need to for validation) + currentPath, err := FindPathTx(ctx, tx, enum.PathTargetTypeSpace, spaceId) + if err != nil { + return nil, errors.Wrap(err, "Failed to find the primary path of the space") + } + + // get path of new parent if needed + newPath := newName + if newParentId > 0 { + // get path of new parent space + spacePath, err := FindPathTx(ctx, tx, enum.PathTargetTypeSpace, newParentId) + if err != nil { + return nil, errors.Wrap(err, "Failed to find the primary path of the new parent space") + } + + newPath = paths.Concatinate(spacePath.Value, newName) + } + + /* + * IMPORTANT + * To avoid cycles in the primary graph, we have to ensure that the old path isn't a prefix of the new path. + */ + if newPath == currentPath.Value { + return nil, errs.NoChangeInRequestedMove + } else if strings.HasPrefix(newPath, currentPath.Value) { + return nil, errs.IllegalMoveCyclicHierarchy + } + + p := &types.Path{ + TargetType: enum.PathTargetTypeSpace, + TargetId: spaceId, + IsAlias: false, + Value: newPath, + CreatedBy: userId, + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // replace the primary path (also updates all child primary paths) + if err = ReplacePathTx(ctx, s.db, tx, p, keepAsAlias); err != nil { + return nil, errors.Wrap(err, "Failed to update the primary path of the space") + } + + // Update the space itself + if _, err := tx.ExecContext(ctx, spaceUpdateNameAndParentId, newName, newParentId, spaceId); err != nil { + return nil, wrapSqlErrorf(err, "Query for renaming and updating the parent id failed") + } + + // TODO: return space as part of rename operation + dst := new(types.Space) + if err = tx.GetContext(ctx, dst, spaceSelectById, spaceId); err != nil { + return nil, wrapSqlErrorf(err, "Select query to get the space's latest state failed") + } + + // commit + if err = tx.Commit(); err != nil { + return nil, wrapSqlErrorf(err, "Failed to commit transaction") + } + + return dst, nil } // Updates the space details. func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error { query, arg, err := s.db.BindNamed(spaceUpdate, space) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to bind space object") } - _, err = s.db.Exec(query, arg...) - return err + + if _, err = s.db.ExecContext(ctx, query, arg...); err != nil { + wrapSqlErrorf(err, "Update query failed") + } + + return nil } // Deletes the space. func (s *SpaceStore) Delete(ctx context.Context, id int64) error { - tx, err := s.db.BeginTx(ctx, nil) + tx, err := s.db.BeginTxx(ctx, nil) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to start a new transaction") } defer tx.Rollback() - // ensure there are no child spaces - var count int64 + // get primary path + path, err := FindPathTx(ctx, tx, enum.PathTargetTypeSpace, id) + if err != nil { + return errors.Wrap(err, "Failed to find the primary path of the space") + } + + // Get child count and ensure there are none + count, err := CountPrimaryChildPathsTx(ctx, tx, path.Value) if err := tx.QueryRow(spaceCount, id).Scan(&count); err != nil { - return err + return errors.Wrap(err, "Failed to count the child paths of the space") } else if count > 0 { // TODO: still returns 500 - return errors.New(fmt.Sprintf("Space still contains %d child space(s).", count)) + return errs.SpaceWithChildsCantBeDeleted + } + + // delete all paths + err = DeleteAllPaths(ctx, tx, enum.PathTargetTypeSpace, id) + if err != nil { + return errors.Wrap(err, "Failed to delete all paths of the space") } // delete the space if _, err := tx.Exec(spaceDelete, id); err != nil { - return err + return wrapSqlErrorf(err, "The delete query failed") } - return tx.Commit() + + if err = tx.Commit(); err != nil { + return wrapSqlErrorf(err, "Failed to commit transaction") + } + + return nil +} + +// Count the child spaces of a space. +func (s *SpaceStore) Count(ctx context.Context, id int64) (int64, error) { + var count int64 + err := s.db.QueryRowContext(ctx, spaceCount, id).Scan(&count) + if err != nil { + return 0, wrapSqlErrorf(err, "Failed executing count query") + } + return count, nil } // 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) { +// TODO: speed up list - for some reason is 200ms for 1 space as well as 1000 +func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]*types.Space, error) { dst := []*types.Space{} // if the user does not provide any customer filter // or sorting we use the default select statement. if opts.Sort == enum.SpaceAttrNone { - err := s.db.Select(&dst, spaceSelect, id, limit(opts.Size), offset(opts.Page, opts.Size)) - return dst, err + err := s.db.SelectContext(ctx, &dst, spaceSelect, id, limit(opts.Size), offset(opts.Page, opts.Size)) + if err != nil { + return nil, wrapSqlErrorf(err, "Failed executing default list query") + } + return dst, nil } // else we construct the sql statement. - stmt := builder.Select("*").From("spaces").Where("space_parentId = " + fmt.Sprint(id)) + stmt := builder. + Select("spaces.*,path_value AS space_path"). + From("spaces"). + InnerJoin("paths ON spaces.space_id=paths.path_targetId AND paths.path_targetType='space' AND paths.path_isAlias=0"). + Where("space_parentId = " + fmt.Sprint(id)) stmt = stmt.Limit(uint64(limit(opts.Size))) stmt = stmt.Offset(uint64(offset(opts.Page, opts.Size))) @@ -114,31 +277,54 @@ func (s *SpaceStore) List(ctx context.Context, id int64, opts types.SpaceFilter) stmt = stmt.OrderBy("space_id " + opts.Order.String()) case enum.SpaceAttrName: stmt = stmt.OrderBy("space_name " + opts.Order.String()) - case enum.SpaceAttrFqn: - stmt = stmt.OrderBy("space_fqn " + opts.Order.String()) + case enum.SpaceAttrPath: + stmt = stmt.OrderBy("space_path " + opts.Order.String()) } sql, _, err := stmt.ToSql() if err != nil { - return dst, err + return nil, errors.Wrap(err, "Failed to convert query to sql") } - err = s.db.Select(&dst, sql) - return dst, err + if err = s.db.SelectContext(ctx, &dst, sql); err != nil { + return nil, wrapSqlErrorf(err, "Failed executing custom list query") + } + + return dst, nil } -// Count the child spaces of a space. -func (s *SpaceStore) Count(ctx context.Context, id int64) (int64, error) { - var count int64 - err := s.db.QueryRow(spaceCount, id).Scan(&count) - return count, err +// List returns a list of all paths of a space. +func (s *SpaceStore) ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) { + return ListPaths(ctx, s.db, enum.PathTargetTypeSpace, id, opts) } -const spaceBase = ` +// Create an alias for a space. +func (s *SpaceStore) CreatePath(ctx context.Context, spaceId int64, params *types.PathParams) (*types.Path, error) { + p := &types.Path{ + TargetType: enum.PathTargetTypeSpace, + TargetId: spaceId, + IsAlias: true, + + // get remaining infor from params + Value: params.Path, + CreatedBy: params.CreatedBy, + Created: params.Created, + Updated: params.Updated, + } + + return p, CreatePath(ctx, s.db, p) +} + +// Delete an alias of a space. +func (s *SpaceStore) DeletePath(ctx context.Context, spaceId int64, pathId int64) error { + return DeletePath(ctx, s.db, pathId) +} + +const spaceSelectBase = ` SELECT space_id ,space_name -,space_fqn +,paths.path_value AS space_path ,space_parentId ,space_displayName ,space_description @@ -146,12 +332,17 @@ SELECT ,space_createdBy ,space_created ,space_updated -FROM spaces ` -const spaceSelect = spaceBase + ` +const spaceSelectBaseWithJoin = spaceSelectBase + ` +FROM spaces +INNER JOIN paths +ON spaces.space_id=paths.path_targetId AND paths.path_targetType='space' AND paths.path_isAlias=0 +` + +const spaceSelect = spaceSelectBaseWithJoin + ` WHERE space_parentId = $1 -ORDER BY space_fqn ASC +ORDER BY space_name ASC LIMIT $2 OFFSET $3 ` @@ -161,12 +352,14 @@ FROM spaces WHERE space_parentId = $1 ` -const spaceSelectID = spaceBase + ` +const spaceSelectById = spaceSelectBaseWithJoin + ` WHERE space_id = $1 ` -const spaceSelectFqn = spaceBase + ` -WHERE space_fqn = $1 +const spaceSelectByPath = spaceSelectBase + ` +FROM paths paths1 +INNER JOIN spaces ON spaces.space_id=paths1.path_targetId AND paths1.path_targetType='space' AND paths1.path_value = $1 +INNER JOIN paths ON spaces.space_id=paths.path_targetId AND paths.path_targetType='space' AND paths.path_isAlias=0 ` const spaceDelete = ` @@ -177,7 +370,6 @@ WHERE space_id = $1 const spaceInsert = ` INSERT INTO spaces ( space_name - ,space_fqn ,space_parentId ,space_displayName ,space_description @@ -187,7 +379,6 @@ INSERT INTO spaces ( ,space_updated ) values ( :space_name - ,:space_fqn ,:space_parentId ,:space_displayName ,:space_description @@ -207,3 +398,11 @@ space_displayName = :space_displayName ,space_updated = :space_updated WHERE space_id = :space_id ` + +const spaceUpdateNameAndParentId = ` +UPDATE spaces +SET +space_name = $1 +,space_parentId = $2 +WHERE space_id = $3 +` diff --git a/internal/store/database/space_sync.go b/internal/store/database/space_sync.go index 412749442..6d68b95eb 100644 --- a/internal/store/database/space_sync.go +++ b/internal/store/database/space_sync.go @@ -33,11 +33,11 @@ func (s *SpaceStoreSync) Find(ctx context.Context, id int64) (*types.Space, erro return s.base.Find(ctx, id) } -// Finds the space by the full qualified space name. -func (s *SpaceStoreSync) FindFqn(ctx context.Context, fqn string) (*types.Space, error) { +// Finds the space by path. +func (s *SpaceStoreSync) FindByPath(ctx context.Context, path string) (*types.Space, error) { mutex.RLock() defer mutex.RUnlock() - return s.base.FindFqn(ctx, fqn) + return s.base.FindByPath(ctx, path) } // Creates a new space @@ -47,6 +47,13 @@ func (s *SpaceStoreSync) Create(ctx context.Context, space *types.Space) error { return s.base.Create(ctx, space) } +// Moves an existing space. +func (s *SpaceStoreSync) Move(ctx context.Context, userId int64, spaceId int64, newParentId int64, newName string, keepAsAlias bool) (*types.Space, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Move(ctx, userId, spaceId, newParentId, newName, keepAsAlias) +} + // Updates the space details. func (s *SpaceStoreSync) Update(ctx context.Context, space *types.Space) error { mutex.RLock() @@ -61,16 +68,35 @@ func (s *SpaceStoreSync) Delete(ctx context.Context, id int64) error { return s.base.Delete(ctx, id) } -// List returns a list of spaces under the parent space. -func (s *SpaceStoreSync) List(ctx context.Context, id int64, opts types.SpaceFilter) ([]*types.Space, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.List(ctx, id, opts) -} - // Count the child spaces of a space. func (s *SpaceStoreSync) Count(ctx context.Context, id int64) (int64, error) { mutex.RLock() defer mutex.RUnlock() return s.base.Count(ctx, id) } + +// List returns a list of spaces under the parent space. +func (s *SpaceStoreSync) List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]*types.Space, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.List(ctx, id, opts) +} + +// List returns a list of all paths of a space. +func (s *SpaceStoreSync) ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) { + return s.base.ListAllPaths(ctx, id, opts) +} + +// Create a path for a space. +func (s *SpaceStoreSync) CreatePath(ctx context.Context, spaceId int64, params *types.PathParams) (*types.Path, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.CreatePath(ctx, spaceId, params) +} + +// Delete a path of a space. +func (s *SpaceStoreSync) DeletePath(ctx context.Context, spaceId int64, pathId int64) error { + mutex.RLock() + defer mutex.RUnlock() + return s.base.DeletePath(ctx, spaceId, pathId) +} diff --git a/internal/store/database/store.go b/internal/store/database/store.go index 78f22fc57..e9a97cdf5 100644 --- a/internal/store/database/store.go +++ b/internal/store/database/store.go @@ -11,6 +11,7 @@ import ( "time" "github.com/harness/gitness/internal/store/database/migrate" + "github.com/pkg/errors" "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" @@ -24,14 +25,14 @@ var builder = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) func Connect(driver, datasource string) (*sqlx.DB, error) { db, err := sql.Open(driver, datasource) if err != nil { - return nil, err + return nil, errors.Wrap(err, "Failed to open the db") } dbx := sqlx.NewDb(db, driver) if err := pingDatabase(dbx); err != nil { - return nil, err + return nil, errors.Wrap(err, "Failed to ping the db") } if err := setupDatabase(dbx); err != nil { - return nil, err + return nil, errors.Wrap(err, "Failed to setup the db") } return dbx, nil } diff --git a/internal/store/database/testdata/repos.json b/internal/store/database/testdata/repos.json index adca542f7..713436bb5 100644 --- a/internal/store/database/testdata/repos.json +++ b/internal/store/database/testdata/repos.json @@ -3,7 +3,6 @@ "id": 1, "name": "repo1", "spaceId": 1, - "fqn": "space1/repo1", "displayName": "Repository 1", "description": "Some repository.", "isPublic": true, @@ -20,7 +19,6 @@ "id": 2, "name": "repo2", "spaceId": 2, - "fqn": "space1/space2/repo2", "displayName": "Repository 2", "description": "Some other repository.", "isPublic": true, diff --git a/internal/store/database/testdata/spaces.json b/internal/store/database/testdata/spaces.json index 42e90ac67..3c1d74e71 100644 --- a/internal/store/database/testdata/spaces.json +++ b/internal/store/database/testdata/spaces.json @@ -2,7 +2,6 @@ { "id": 1, "name": "space1", - "fqn": "space1", "parentId": 0, "displayName": "Space 1", "description": "Some space.", @@ -14,7 +13,6 @@ { "id": 2, "name": "space2", - "fqn": "space1/space2", "parentId": 1, "displayName": "Space 2", "description": "Some subspace.", diff --git a/internal/store/database/user.go b/internal/store/database/user.go index 6bb8604a0..9f6e53ef8 100644 --- a/internal/store/database/user.go +++ b/internal/store/database/user.go @@ -11,6 +11,7 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + "github.com/pkg/errors" "github.com/jmoiron/sqlx" ) @@ -31,15 +32,19 @@ type UserStore struct { // Find finds the user by id. func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) { dst := new(types.User) - err := s.db.Get(dst, userSelectID, id) - return dst, err + if err := s.db.GetContext(ctx, dst, userSelectID, id); err != nil { + return nil, wrapSqlErrorf(err, "Select by id query failed") + } + return dst, nil } // FindEmail finds the user by email. func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) { dst := new(types.User) - err := s.db.Get(dst, userSelectEmail, email) - return dst, err + if err := s.db.GetContext(ctx, dst, userSelectEmail, email); err != nil { + return nil, wrapSqlErrorf(err, "Select by email query failed") + } + return dst, nil } // FindKey finds the user unique key (email or id). @@ -53,14 +58,17 @@ func (s *UserStore) FindKey(ctx context.Context, key string) (*types.User, error } // List returns a list of users. -func (s *UserStore) List(ctx context.Context, opts types.UserFilter) ([]*types.User, error) { +func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) { dst := []*types.User{} // if the user does not provide any customer filter // or sorting we use the default select statement. if opts.Sort == enum.UserAttrNone { - err := s.db.Select(&dst, userSelect, limit(opts.Size), offset(opts.Page, opts.Size)) - return dst, err + err := s.db.SelectContext(ctx, &dst, userSelect, limit(opts.Size), offset(opts.Page, opts.Size)) + if err != nil { + return nil, wrapSqlErrorf(err, "Failed executing default list query") + } + return dst, nil } // else we construct the sql statement. @@ -84,29 +92,41 @@ func (s *UserStore) List(ctx context.Context, opts types.UserFilter) ([]*types.U sql, _, err := stmt.ToSql() if err != nil { - return dst, err + return nil, errors.Wrap(err, "Failed to convert query to sql") } - err = s.db.Select(&dst, sql) - return dst, err + if err = s.db.SelectContext(ctx, &dst, sql); err != nil { + return nil, wrapSqlErrorf(err, "Failed executing custom list query") + } + + return dst, nil } // Create saves the user details. func (s *UserStore) Create(ctx context.Context, user *types.User) error { query, arg, err := s.db.BindNamed(userInsert, user) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to bind user object") } - return s.db.QueryRow(query, arg...).Scan(&user.ID) + + if err = s.db.QueryRowContext(ctx, query, arg...).Scan(&user.ID); err != nil { + return wrapSqlErrorf(err, "Insert query failed") + } + + return nil } // Update updates the user details. func (s *UserStore) Update(ctx context.Context, user *types.User) error { query, arg, err := s.db.BindNamed(userUpdate, user) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to bind user object") } - _, err = s.db.Exec(query, arg...) + + if _, err = s.db.ExecContext(ctx, query, arg...); err != nil { + return wrapSqlErrorf(err, "Update query failed") + } + return err } @@ -114,21 +134,24 @@ func (s *UserStore) Update(ctx context.Context, user *types.User) error { func (s *UserStore) Delete(ctx context.Context, user *types.User) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { - return err + return wrapSqlErrorf(err, "Failed to start a new transaction") } defer tx.Rollback() // delete the user - if _, err := tx.Exec(userDelete, user.ID); err != nil { - return err + if _, err := tx.ExecContext(ctx, userDelete, user.ID); err != nil { + return wrapSqlErrorf(err, "The delete query failed") } return tx.Commit() } // Count returns a count of users. -func (s *UserStore) Count(context.Context) (int64, error) { +func (s *UserStore) Count(ctx context.Context) (int64, error) { var count int64 - err := s.db.QueryRow(userCount).Scan(&count) - return count, err + err := s.db.QueryRowContext(ctx, userCount).Scan(&count) + if err != nil { + return 0, wrapSqlErrorf(err, "Failed executing count query") + } + return count, nil } const userCount = ` diff --git a/internal/store/database/user_sync.go b/internal/store/database/user_sync.go index b654b3825..d8379b831 100644 --- a/internal/store/database/user_sync.go +++ b/internal/store/database/user_sync.go @@ -46,7 +46,7 @@ func (s *UserStoreSync) FindKey(ctx context.Context, key string) (*types.User, e } // List returns a list of users. -func (s *UserStoreSync) List(ctx context.Context, opts types.UserFilter) ([]*types.User, error) { +func (s *UserStoreSync) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) { mutex.RLock() defer mutex.RUnlock() return s.base.List(ctx, opts) diff --git a/internal/store/database/user_test.go b/internal/store/database/user_test.go index fa3e653d6..7905eb218 100644 --- a/internal/store/database/user_test.go +++ b/internal/store/database/user_test.go @@ -194,7 +194,7 @@ func testUserList(store store.UserStore) func(t *testing.T) { t.Error(err) return } - got, err := store.List(noContext, types.UserFilter{Page: 0, Size: 100}) + got, err := store.List(noContext, &types.UserFilter{Page: 0, Size: 100}) if err != nil { t.Error(err) return diff --git a/internal/store/database/util.go b/internal/store/database/util.go index 195bf8cd8..1f6b261ac 100644 --- a/internal/store/database/util.go +++ b/internal/store/database/util.go @@ -4,6 +4,14 @@ package database +import ( + "database/sql" + + "github.com/harness/gitness/types/errs" + "github.com/mattn/go-sqlite3" + "github.com/pkg/errors" +) + // default query range limit. const defaultLimit = 100 @@ -26,3 +34,18 @@ func offset(page, size int) int { page = page - 1 return page * size } + +func wrapSqlErrorf(original error, format string, args ...interface{}) error { + if original == sql.ErrNoRows { + original = errs.WrapInResourceNotFound(original) + } else if isSqlUniqueConstraintError(original) { + original = errs.WrapInDuplicate(original) + } + + return errors.Wrapf(original, format, args...) +} + +func isSqlUniqueConstraintError(original error) bool { + o3, ok := original.(sqlite3.Error) + return ok && errors.Is(o3.ExtendedCode, sqlite3.ErrConstraintUnique) +} diff --git a/internal/store/store.go b/internal/store/store.go index 6f16cb9e8..e9fd0dcdf 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -23,9 +23,6 @@ type ( // FindKey finds the user by unique key (email or id). FindKey(ctx context.Context, key string) (*types.User, error) - // List returns a list of users. - List(ctx context.Context, params types.UserFilter) ([]*types.User, error) - // Create saves the user details. Create(ctx context.Context, user *types.User) error @@ -35,6 +32,9 @@ type ( // Delete deletes the user. Delete(ctx context.Context, user *types.User) error + // List returns a list of users. + List(ctx context.Context, params *types.UserFilter) ([]*types.User, error) + // Count returns a count of users. Count(ctx context.Context) (int64, error) } @@ -44,23 +44,35 @@ type ( // Finds the space by id. Find(ctx context.Context, id int64) (*types.Space, error) - // Finds the space by the full qualified space name. - FindFqn(ctx context.Context, fqn string) (*types.Space, error) + // Finds the space by its path. + FindByPath(ctx context.Context, path string) (*types.Space, error) // Creates a new space Create(ctx context.Context, space *types.Space) error + // Moves an existing space. + Move(ctx context.Context, userId int64, spaceId int64, newParentId int64, newName string, keepAsAlias bool) (*types.Space, error) + // Updates the space details. Update(ctx context.Context, space *types.Space) error // Deletes the space. Delete(ctx context.Context, id int64) error - // List returns a list of child spaces in a space. - List(ctx context.Context, id int64, opts types.SpaceFilter) ([]*types.Space, error) - // Count the child spaces of a space. Count(ctx context.Context, id int64) (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 returns a list of all paths of a space. + ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) + + // Create an alias for a space + CreatePath(ctx context.Context, spaceId int64, params *types.PathParams) (*types.Path, error) + + // Delete an alias of a space + DeletePath(ctx context.Context, spaceId int64, pathId int64) error } // RepoStore defines the repository data storage. @@ -68,23 +80,35 @@ type ( // Finds the repo by id. Find(ctx context.Context, id int64) (*types.Repository, error) - // Finds the repo by the full qualified space name. - FindFqn(ctx context.Context, fqn string) (*types.Repository, error) + // Finds the repo by path. + FindByPath(ctx context.Context, path string) (*types.Repository, error) // Creates a new repo Create(ctx context.Context, repo *types.Repository) error + // Moves an existing repo. + Move(ctx context.Context, userId int64, repoId int64, newSpaceId int64, newName string, keepAsAlias bool) (*types.Repository, error) + // Updates the repo details. Update(ctx context.Context, repo *types.Repository) error // Deletes the repo. Delete(ctx context.Context, id int64) error - // List returns a list of repos in a space. - List(ctx context.Context, spaceId int64, opts types.RepoFilter) ([]*types.Repository, error) - // Count of repos in a space. Count(ctx context.Context, spaceId int64) (int64, error) + + // List returns a list of repos in a space. + List(ctx context.Context, spaceId int64, opts *types.RepoFilter) ([]*types.Repository, error) + + // List returns a list of all alias paths of a repo. + ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) + + // Create an alias for a repo + CreatePath(ctx context.Context, repoId int64, params *types.PathParams) (*types.Path, error) + + // Delete an alias of a repo + DeletePath(ctx context.Context, repoId int64, pathId int64) error } // SystemStore defines internal system metadata storage. diff --git a/internal/token/token.go b/internal/token/token.go index ecb402a1e..550fe2b3c 100644 --- a/internal/token/token.go +++ b/internal/token/token.go @@ -9,6 +9,7 @@ import ( "time" "github.com/harness/gitness/types" + "github.com/pkg/errors" "github.com/dgrijalva/jwt-go" ) @@ -29,7 +30,13 @@ func Generate(user *types.User, secret string) (string, error) { IssuedAt: time.Now().Unix(), }, }) - return token.SignedString([]byte(secret)) + + res, err := token.SignedString([]byte(secret)) + if err != nil { + return "", errors.Wrap(err, "Failed to sign token") + } + + return res, nil } // GenerateExp generates a token with an expiration date. @@ -42,5 +49,11 @@ func GenerateExp(user *types.User, exp int64, secret string) (string, error) { IssuedAt: time.Now().Unix(), }, }) - return token.SignedString([]byte(secret)) + + res, err := token.SignedString([]byte(secret)) + if err != nil { + return "", errors.Wrap(err, "Failed to sign token") + } + + return res, nil } diff --git a/mocks/mock_store.go b/mocks/mock_store.go index 07e966202..7f679498e 100644 --- a/mocks/mock_store.go +++ b/mocks/mock_store.go @@ -161,7 +161,7 @@ func (mr *MockUserStoreMockRecorder) FindKey(arg0, arg1 interface{}) *gomock.Cal } // List mocks base method. -func (m *MockUserStore) List(arg0 context.Context, arg1 types.UserFilter) ([]*types.User, error) { +func (m *MockUserStore) List(arg0 context.Context, arg1 *types.UserFilter) ([]*types.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", arg0, arg1) ret0, _ := ret[0].([]*types.User) @@ -241,6 +241,21 @@ func (mr *MockSpaceStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSpaceStore)(nil).Create), arg0, arg1) } +// CreatePath mocks base method. +func (m *MockSpaceStore) CreatePath(arg0 context.Context, arg1 int64, arg2 *types.PathParams) (*types.Path, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePath", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.Path) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePath indicates an expected call of CreatePath. +func (mr *MockSpaceStoreMockRecorder) CreatePath(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePath", reflect.TypeOf((*MockSpaceStore)(nil).CreatePath), arg0, arg1, arg2) +} + // Delete mocks base method. func (m *MockSpaceStore) Delete(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() @@ -255,6 +270,20 @@ func (mr *MockSpaceStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSpaceStore)(nil).Delete), arg0, arg1) } +// DeletePath mocks base method. +func (m *MockSpaceStore) DeletePath(arg0 context.Context, arg1, arg2 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePath", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePath indicates an expected call of DeletePath. +func (mr *MockSpaceStoreMockRecorder) DeletePath(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePath", reflect.TypeOf((*MockSpaceStore)(nil).DeletePath), arg0, arg1, arg2) +} + // Find mocks base method. func (m *MockSpaceStore) Find(arg0 context.Context, arg1 int64) (*types.Space, error) { m.ctrl.T.Helper() @@ -270,23 +299,23 @@ func (mr *MockSpaceStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockSpaceStore)(nil).Find), arg0, arg1) } -// FindFqn mocks base method. -func (m *MockSpaceStore) FindFqn(arg0 context.Context, arg1 string) (*types.Space, error) { +// FindByPath mocks base method. +func (m *MockSpaceStore) FindByPath(arg0 context.Context, arg1 string) (*types.Space, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindFqn", arg0, arg1) + ret := m.ctrl.Call(m, "FindByPath", arg0, arg1) ret0, _ := ret[0].(*types.Space) ret1, _ := ret[1].(error) return ret0, ret1 } -// FindFqn indicates an expected call of FindFqn. -func (mr *MockSpaceStoreMockRecorder) FindFqn(arg0, arg1 interface{}) *gomock.Call { +// FindByPath indicates an expected call of FindByPath. +func (mr *MockSpaceStoreMockRecorder) FindByPath(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindFqn", reflect.TypeOf((*MockSpaceStore)(nil).FindFqn), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByPath", reflect.TypeOf((*MockSpaceStore)(nil).FindByPath), arg0, arg1) } // 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) @@ -300,6 +329,36 @@ func (mr *MockSpaceStoreMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockSpaceStore)(nil).List), arg0, arg1, arg2) } +// ListAllPaths mocks base method. +func (m *MockSpaceStore) ListAllPaths(arg0 context.Context, arg1 int64, arg2 *types.PathFilter) ([]*types.Path, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAllPaths", arg0, arg1, arg2) + ret0, _ := ret[0].([]*types.Path) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAllPaths indicates an expected call of ListAllPaths. +func (mr *MockSpaceStoreMockRecorder) ListAllPaths(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAllPaths", reflect.TypeOf((*MockSpaceStore)(nil).ListAllPaths), arg0, arg1, arg2) +} + +// Move mocks base method. +func (m *MockSpaceStore) Move(arg0 context.Context, arg1, arg2, arg3 int64, arg4 string, arg5 bool) (*types.Space, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Move", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(*types.Space) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Move indicates an expected call of Move. +func (mr *MockSpaceStoreMockRecorder) Move(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Move", reflect.TypeOf((*MockSpaceStore)(nil).Move), arg0, arg1, arg2, arg3, arg4, arg5) +} + // Update mocks base method. func (m *MockSpaceStore) Update(arg0 context.Context, arg1 *types.Space) error { m.ctrl.T.Helper() @@ -366,6 +425,21 @@ func (mr *MockRepoStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepoStore)(nil).Create), arg0, arg1) } +// CreatePath mocks base method. +func (m *MockRepoStore) CreatePath(arg0 context.Context, arg1 int64, arg2 *types.PathParams) (*types.Path, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePath", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.Path) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePath indicates an expected call of CreatePath. +func (mr *MockRepoStoreMockRecorder) CreatePath(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePath", reflect.TypeOf((*MockRepoStore)(nil).CreatePath), arg0, arg1, arg2) +} + // Delete mocks base method. func (m *MockRepoStore) Delete(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() @@ -380,6 +454,20 @@ func (mr *MockRepoStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepoStore)(nil).Delete), arg0, arg1) } +// DeletePath mocks base method. +func (m *MockRepoStore) DeletePath(arg0 context.Context, arg1, arg2 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePath", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePath indicates an expected call of DeletePath. +func (mr *MockRepoStoreMockRecorder) DeletePath(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePath", reflect.TypeOf((*MockRepoStore)(nil).DeletePath), arg0, arg1, arg2) +} + // Find mocks base method. func (m *MockRepoStore) Find(arg0 context.Context, arg1 int64) (*types.Repository, error) { m.ctrl.T.Helper() @@ -395,23 +483,23 @@ func (mr *MockRepoStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepoStore)(nil).Find), arg0, arg1) } -// FindFqn mocks base method. -func (m *MockRepoStore) FindFqn(arg0 context.Context, arg1 string) (*types.Repository, error) { +// FindByPath mocks base method. +func (m *MockRepoStore) FindByPath(arg0 context.Context, arg1 string) (*types.Repository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindFqn", arg0, arg1) + ret := m.ctrl.Call(m, "FindByPath", arg0, arg1) ret0, _ := ret[0].(*types.Repository) ret1, _ := ret[1].(error) return ret0, ret1 } -// FindFqn indicates an expected call of FindFqn. -func (mr *MockRepoStoreMockRecorder) FindFqn(arg0, arg1 interface{}) *gomock.Call { +// FindByPath indicates an expected call of FindByPath. +func (mr *MockRepoStoreMockRecorder) FindByPath(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindFqn", reflect.TypeOf((*MockRepoStore)(nil).FindFqn), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByPath", reflect.TypeOf((*MockRepoStore)(nil).FindByPath), arg0, arg1) } // List mocks base method. -func (m *MockRepoStore) List(arg0 context.Context, arg1 int64, arg2 types.RepoFilter) ([]*types.Repository, error) { +func (m *MockRepoStore) List(arg0 context.Context, arg1 int64, arg2 *types.RepoFilter) ([]*types.Repository, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) ret0, _ := ret[0].([]*types.Repository) @@ -425,6 +513,36 @@ func (mr *MockRepoStoreMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRepoStore)(nil).List), arg0, arg1, arg2) } +// ListAllPaths mocks base method. +func (m *MockRepoStore) ListAllPaths(arg0 context.Context, arg1 int64, arg2 *types.PathFilter) ([]*types.Path, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAllPaths", arg0, arg1, arg2) + ret0, _ := ret[0].([]*types.Path) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAllPaths indicates an expected call of ListAllPaths. +func (mr *MockRepoStoreMockRecorder) ListAllPaths(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAllPaths", reflect.TypeOf((*MockRepoStore)(nil).ListAllPaths), arg0, arg1, arg2) +} + +// Move mocks base method. +func (m *MockRepoStore) Move(arg0 context.Context, arg1, arg2, arg3 int64, arg4 string, arg5 bool) (*types.Repository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Move", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(*types.Repository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Move indicates an expected call of Move. +func (mr *MockRepoStoreMockRecorder) Move(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Move", reflect.TypeOf((*MockRepoStore)(nil).Move), arg0, arg1, arg2, arg3, arg4, arg5) +} + // Update mocks base method. func (m *MockRepoStore) Update(arg0 context.Context, arg1 *types.Repository) error { m.ctrl.T.Helper() diff --git a/types/authz.go b/types/authz.go index 602901f2c..522f3c342 100644 --- a/types/authz.go +++ b/types/authz.go @@ -28,10 +28,10 @@ type Resource struct { * Represents the scope of a permission check. * Notes: * - In case the permission check is for resource REPO, keep repo empty (repo is resource, not scope) - * - In case the permission check is for resource SPACE, spaceFqn is an ancestor of the space (space is resource, not scope) + * - In case the permission check is for resource SPACE, SpacePath is an ancestor of the space (space is resource, not scope) * - Repo isn't use as of now (will be useful once we add access control for repo child resources, e.g. branches) */ type Scope struct { - SpaceFqn string - Repo string + SpacePath string + Repo string } diff --git a/types/check/common.go b/types/check/common.go new file mode 100644 index 000000000..cf4139053 --- /dev/null +++ b/types/check/common.go @@ -0,0 +1,56 @@ +// Copyright 2021 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 check + +import ( + "fmt" + "regexp" +) + +const ( + minNameLength = 1 + maxNameLength = 64 + nameRegex = "^[a-z][a-z0-9\\-\\_]*$" + + minDisplayNameLength = 1 + maxDisplayNameLength = 256 + displayNameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$" +) + +var ( + ErrNameLength = fmt.Errorf("Name has to be between %d and %d in length.", minNameLength, maxNameLength) + ErrNameRegex = fmt.Errorf("Name has start with a letter and only contain the following [a-z0-9-_].") + + ErrDisplayNameLength = fmt.Errorf("Display name has to be between %d and %d in length.", minDisplayNameLength, maxDisplayNameLength) + ErrDisplayNameRegex = fmt.Errorf("Display name has start with a letter and only contain the following [a-zA-Z0-9-_ ].") +) + +// Name checks the provided name and returns an error in it isn't valid. +func Name(name string) error { + l := len(name) + if l < minNameLength || l > maxNameLength { + return ErrNameLength + } + + if ok, _ := regexp.Match(nameRegex, []byte(name)); !ok { + return ErrNameRegex + } + + return nil +} + +// DisplayName checks the provided name and returns an error in it isn't valid. +func DisplayName(name string) error { + l := len(name) + if l < minDisplayNameLength || l > maxDisplayNameLength { + return ErrDisplayNameLength + } + + if ok, _ := regexp.Match(displayNameRegex, []byte(name)); !ok { + return ErrDisplayNameRegex + } + + return nil +} diff --git a/types/check/path.go b/types/check/path.go new file mode 100644 index 000000000..4f6db8c84 --- /dev/null +++ b/types/check/path.go @@ -0,0 +1,61 @@ +// Copyright 2021 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 check + +import ( + "fmt" + "strings" + + "github.com/harness/gitness/types" + "github.com/pkg/errors" +) + +const ( + minPathSegments = 1 + maxPathSegmentsForSpace = 9 + maxPathSegments = 10 +) + +var ( + ErrPathEmpty = fmt.Errorf("Path can't be empty.") + ErrPathInvalidSize = fmt.Errorf("A path has to be between %d and %d segments long (%d for spaces).", minPathSegments, maxPathSegments, maxPathSegmentsForSpace) + ErrEmptyPathSegment = fmt.Errorf("Empty segments are not allowed.") + ErrPathCantBeginOrEndWithSeparator = fmt.Errorf("Path can't start or end with the separator ('%s').", types.PathSeparator) +) + +/* + * PathParams checks the provided path params and returns an error in it isn't valid. + * + * NOTE: A repository path can be one deeper than a space path (as otherwise the space would be useless) + */ +func PathParams(path string, isSpace bool) error { + + if path == "" { + return ErrPathEmpty + } + + // ensure path doesn't begin or end with / + if path[:1] == types.PathSeparator || path[len(path)-1:] == types.PathSeparator { + return ErrPathCantBeginOrEndWithSeparator + } + + // ensure path is not too deep + segments := strings.Split(path, types.PathSeparator) + l := len(segments) + if l < minPathSegments || (isSpace == false && l > maxPathSegments) || (isSpace && l > maxPathSegmentsForSpace) { + return ErrPathInvalidSize + } + + // ensure all segments of the path are valid + for _, s := range segments { + if s == "" { + return ErrEmptyPathSegment + } else if err := Name(s); err != nil { + return errors.Wrapf(err, "Invalid segment '%s'", s) + } + } + + return nil +} diff --git a/types/check/repo.go b/types/check/repo.go index 2d8a388ae..dafc2e994 100644 --- a/types/check/repo.go +++ b/types/check/repo.go @@ -5,50 +5,31 @@ package check import ( - "errors" "fmt" - "regexp" "github.com/harness/gitness/types" ) -const ( - minRepoNameLength = 1 - maxRepoNameLength = 64 - repoNameRegex = "^[a-z][a-z0-9\\-\\_]*$" - - minRepoDisplayNameLength = 1 - maxRepoDisplayNameLength = 256 - repoDisplayNameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$" -) - var ( - ErrRepoNameLength = errors.New(fmt.Sprintf("Repository name has to be between %d and %d in length.", minRepoNameLength, maxRepoNameLength)) - ErrRepoNameRegex = errors.New("Repository name has start with a letter and only contain the following [a-z0-9-_].") - - ErrRepoDisplayNameLength = errors.New(fmt.Sprintf("Repository display name has to be between %d and %d in length.", minRepoDisplayNameLength, maxRepoDisplayNameLength)) - ErrRepoDisplayNameRegex = errors.New("Repository display name has start with a letter and only contain the following [a-zA-Z0-9-_ ].") + RepositoryRequiresSpaceIdError = fmt.Errorf("SpaceId required - Repositories don't exist outside of a space.") ) -// Repo returns true if the Repo if valid. -func Repo(repo *types.Repository) (bool, error) { - l := len(repo.Name) - if l < minRepoNameLength || l > maxRepoNameLength { - return false, ErrRepoNameLength +// Repo checks the provided repository and returns an error in it isn't valid. +func Repo(repo *types.Repository) error { + // validate name + if err := Name(repo.Name); err != nil { + return err } - if ok, _ := regexp.Match(repoNameRegex, []byte(repo.Name)); !ok { - return false, ErrRepoNameRegex + // validate display name + if err := DisplayName(repo.DisplayName); err != nil { + return err } - l = len(repo.DisplayName) - if l < minRepoDisplayNameLength || l > maxRepoDisplayNameLength { - return false, ErrRepoDisplayNameLength + // validate repo within a space + if repo.SpaceId <= 0 { + return RepositoryRequiresSpaceIdError } - if ok, _ := regexp.Match(repoDisplayNameRegex, []byte(repo.DisplayName)); !ok { - return false, ErrRepoDisplayNameRegex - } - - return true, nil + return nil } diff --git a/types/check/space.go b/types/check/space.go index e54812370..76a287a87 100644 --- a/types/check/space.go +++ b/types/check/space.go @@ -5,69 +5,43 @@ package check import ( - "errors" "fmt" - "regexp" "strings" "github.com/harness/gitness/types" ) -const ( - minSpaceNameLength = 1 - maxSpaceNameLength = 64 - spaceNameRegex = "^[a-z][a-z0-9\\-\\_]*$" - - minSpaceDisplayNameLength = 1 - maxSpaceDisplayNameLength = 256 - spaceDisplayNameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$" -) - var ( - ErrSpaceNameLength = errors.New(fmt.Sprintf("Space name has to be between %d and %d in length.", minSpaceNameLength, maxSpaceNameLength)) - ErrSpaceNameRegex = errors.New("Space name has start with a letter and only contain the following [a-z0-9-_].") + illegalRootSpaceNames = []string{"api"} - ErrSpaceDisplayNameLength = errors.New(fmt.Sprintf("Space display name has to be between %d and %d in length.", minSpaceDisplayNameLength, maxSpaceDisplayNameLength)) - ErrSpaceDisplayNameRegex = errors.New("Space display name has start with a letter and only contain the following [a-zA-Z0-9-_ ].") - - illegalRootSpaceNames = []string{"api"} - ErrRootSpaceNameNotAllowed = errors.New(fmt.Sprintf("The following names are not allowed for a root space: %v", illegalRootSpaceNames)) - - ErrInvalidParentSpaceId = errors.New("Parent space ID has to be either zero for a root space or greater than zero for a child space.") + ErrRootSpaceNameNotAllowed = fmt.Errorf("The following names are not allowed for a root space: %v", illegalRootSpaceNames) + ErrInvalidParentSpaceId = fmt.Errorf("Parent space ID has to be either zero for a root space or greater than zero for a child space.") ) -// User returns true if the User if valid. -func Space(space *types.Space) (bool, error) { - l := len(space.Name) - if l < minSpaceNameLength || l > maxSpaceNameLength { - return false, ErrSpaceNameLength +// Repo checks the provided space and returns an error in it isn't valid. +func Space(space *types.Space) error { + // validate name + if err := Name(space.Name); err != nil { + return err } - if ok, _ := regexp.Match(spaceNameRegex, []byte(space.Name)); !ok { - return false, ErrSpaceNameRegex - } - - l = len(space.DisplayName) - if l < minSpaceDisplayNameLength || l > maxSpaceDisplayNameLength { - return false, ErrSpaceDisplayNameLength - } - - if ok, _ := regexp.Match(spaceDisplayNameRegex, []byte(space.DisplayName)); !ok { - return false, ErrSpaceDisplayNameRegex + // validate display name + if err := DisplayName(space.DisplayName); err != nil { + return err } if space.ParentId < 0 { - return false, ErrInvalidParentSpaceId + return ErrInvalidParentSpaceId } // root space specific validations if space.ParentId == 0 { for _, p := range illegalRootSpaceNames { if strings.HasPrefix(space.Name, p) { - return false, ErrRootSpaceNameNotAllowed + return ErrRootSpaceNameNotAllowed } } } - return true, nil + return nil } diff --git a/types/check/user.go b/types/check/user.go index 2d43d6478..5ffe689cd 100644 --- a/types/check/user.go +++ b/types/check/user.go @@ -5,20 +5,27 @@ package check import ( - "errors" + "fmt" "github.com/harness/gitness/types" ) +const ( + minEmailLength = 1 + maxEmailLength = 250 +) + var ( // ErrEmailLen is returned when the email address // exceeds the maximum number of characters. - ErrEmailLen = errors.New("Email address cannot exceed 250 characters") + ErrEmailLen = fmt.Errorf("Email address has to be within %d and %d characters", minEmailLength, maxEmailLength) ) // User returns true if the User if valid. func User(user *types.User) (bool, error) { - if len(user.Email) > 250 { + // validate email + l := len(user.Email) + if l < minEmailLength || l > maxEmailLength { return false, ErrEmailLen } return true, nil diff --git a/types/enum/authz.go b/types/enum/authz.go index 60644a1aa..c77cd5439 100644 --- a/types/enum/authz.go +++ b/types/enum/authz.go @@ -4,6 +4,7 @@ package enum +// Represents the different types of resources that can be guarded with permissions. type ResourceType string const ( @@ -12,6 +13,7 @@ const ( // ResourceType_Branch ResourceType = "BRANCH" ) +// Represents the available permissions type Permission string const ( @@ -34,9 +36,13 @@ const ( // PermissionBranchDelete Permission = "branch_delete" ) +// Represents the type of the entity requesting permission type PrincipalType string const ( - PrincipalTypeUser PrincipalType = "USER" + // Represents actions executed by a loged-in user + PrincipalTypeUser PrincipalType = "USER" + + // Represents actions executed by an entity with an api key PrincipalTypeApiKey PrincipalType = "API_KEY" ) diff --git a/types/enum/path.go b/types/enum/path.go new file mode 100644 index 000000000..06fa8f52b --- /dev/null +++ b/types/enum/path.go @@ -0,0 +1,56 @@ +// Copyright 2021 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" + +// Defines the type of the target of a path +type PathTargetType string + +const ( + PathTargetTypeRepo PathTargetType = "repo" + PathTargetTypeSpace PathTargetType = "space" +) + +// TODO: Should we replace Path.IsAlias with a Path.Type property? Unless needed, bool would be more efficient +// // Defines the type of a path +// type PathType string + +// const ( +// // Path is only an alias - it doesn't dictate where the target is actually residing. +// PathTypeAlias PathTargetType = "alias" + +// // Path is representing the residency of a resource (e.g. chain of parent spaces) +// PathTypePrimary PathTargetType = "primary" +// ) + +// Defines path attributes that can be used for sorting and filtering. +type PathAttr int + +// Order enumeration. +const ( + PathAttrNone PathAttr = iota + PathAttrId + PathAttrPath + PathAttrCreated + PathAttrUpdated +) + +// ParsePathAttr parses the path attribute string +// and returns the equivalent enumeration. +func ParsePathAttr(s string) PathAttr { + switch strings.ToLower(s) { + case "id": + return PathAttrId + case "path": + return PathAttrPath + case "created", "created_at": + return PathAttrCreated + case "updated", "updated_at": + return PathAttrUpdated + default: + return PathAttrNone + } +} diff --git a/types/enum/repo.go b/types/enum/repo.go index 18a0f1df3..a6b71b3cf 100644 --- a/types/enum/repo.go +++ b/types/enum/repo.go @@ -14,7 +14,7 @@ const ( RepoAttrNone RepoAttr = iota RepoAttrId RepoAttrName - RepoAttrFqn + RepoAttrPath RepoAttrDisplayName RepoAttrCreated RepoAttrUpdated @@ -28,8 +28,8 @@ func ParseRepoAtrr(s string) RepoAttr { return RepoAttrId case "name": return RepoAttrName - case "fqn": - return RepoAttrFqn + case "path": + return RepoAttrPath case "displayName": return RepoAttrDisplayName case "created", "created_at": diff --git a/types/enum/space.go b/types/enum/space.go index 7741c9f63..561b6d1fe 100644 --- a/types/enum/space.go +++ b/types/enum/space.go @@ -14,7 +14,7 @@ const ( SpaceAttrNone SpaceAttr = iota SpaceAttrId SpaceAttrName - SpaceAttrFqn + SpaceAttrPath SpaceAttrDisplayName SpaceAttrCreated SpaceAttrUpdated @@ -28,9 +28,9 @@ func ParseSpaceAttr(s string) SpaceAttr { return SpaceAttrId case "name": return SpaceAttrName - case "fqn": - return SpaceAttrFqn - case "displayName": + case "path": + return SpaceAttrPath + case "displayname", "display_name": return SpaceAttrDisplayName case "created", "created_at": return SpaceAttrCreated diff --git a/types/errs/dynamic.go b/types/errs/dynamic.go new file mode 100644 index 000000000..a4f261901 --- /dev/null +++ b/types/errs/dynamic.go @@ -0,0 +1,65 @@ +// Copyright 2021 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 errs + +import ( + "fmt" +) + +// Static errors +var ( + // Indicates that a requested resource wasn't found. + ResourceNotFound error = &dynamicError{0, "Resource not found", nil} + Duplicate error = &dynamicError{1, "Resource is a duplicate", nil} +) + +// Wrappers +func WrapInResourceNotFound(inner error) error { + return cloneWithNewInner(ResourceNotFound.(*dynamicError), inner) +} +func WrapInDuplicate(inner error) error { + return cloneWithNewInner(Duplicate.(*dynamicError), inner) +} + +// Error type (on purpose not using explicit definitions and iota, to make overhead as small as possible) +type dynamicErrorType int + +/* + * This is an abstraction of an error that can be both a standalone error or a wrapping error. + * The idea is to allow errors.Is(err, errs.MyError) for wrapping errors while keeping code to a minimum + */ +type dynamicError struct { + errorType dynamicErrorType + msg string + inner error +} + +func (e *dynamicError) Error() string { + if e.inner == nil { + return e.msg + } else { + return fmt.Sprintf("%s: %s", e.msg, e.inner) + } +} +func (e *dynamicError) Unwrap() error { + return e.inner +} + +func (e *dynamicError) Is(target error) bool { + te, ok := target.(*dynamicError) + return ok && te.errorType == e.errorType +} + +func cloneWithNewMsg(d *dynamicError, msg string) *dynamicError { + return &dynamicError{d.errorType, msg, nil} +} + +func cloneWithNewInner(d *dynamicError, inner error) *dynamicError { + return &dynamicError{d.errorType, d.msg, inner} +} + +func cloneWithNewMsgAndInner(d *dynamicError, msg string, inner error) *dynamicError { + return &dynamicError{d.errorType, msg, inner} +} diff --git a/types/errs/static.go b/types/errs/static.go new file mode 100644 index 000000000..f371a6576 --- /dev/null +++ b/types/errs/static.go @@ -0,0 +1,25 @@ +// Copyright 2021 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 errs + +import "errors" + +var ( + Internal = errors.New("Internal error occured - Please contact operator for more information.") + NotAuthenticated = errors.New("Not authenticated.") + NotAuthorized = errors.New("Not authorized.") + RepositoryRequired = errors.New("The operation requires a repository.") + PathEmpty = errors.New("Path is empty.") + PrimaryPathAlreadyExists = errors.New("Primary path already exists for resource.") + AliasPathRequired = errors.New("Path has to be an alias.") + PrimaryPathRequired = errors.New("Path has to be primary.") + PrimaryPathCantBeDeleted = errors.New("Primary path can't be deleted.") + NoChangeInRequestedMove = errors.New(("The requested move doesn't change anything.")) + IllegalMoveCyclicHierarchy = errors.New(("The requested move is not permitted as it would cause a cyclic depdency.")) + SpaceWithChildsCantBeDeleted = errors.New("The space can't be deleted as it still contains spaces or repos.") + RepoReferenceNotFoundInRequest = errors.New("No repository reference found in request.") + SpaceReferenceNotFoundInRequest = errors.New("No space reference found in request.") + NoPermissionCheckProvided = errors.New("No permission checks provided") +) diff --git a/types/path.go b/types/path.go new file mode 100644 index 000000000..4671989f8 --- /dev/null +++ b/types/path.go @@ -0,0 +1,41 @@ +// Copyright 2021 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" +) + +const ( + PathSeparator = "/" +) + +// Represents a path to a resource (e.g. space) that can be used to address the resource. +type Path struct { + ID int64 `db:"path_id" json:"id"` + Value string `db:"path_value" json:"value"` + IsAlias bool `db:"path_isAlias" json:"isAlias"` + TargetType enum.PathTargetType `db:"path_targetType" json:"targetType"` + TargetId int64 `db:"path_targetId" json:"targetId"` + CreatedBy int64 `db:"path_createdBy" json:"createdBy"` + Created int64 `db:"path_created" json:"created"` + Updated int64 `db:"path_updated" json:"updated"` +} + +// Used for creating paths (alias or rename) +type PathParams struct { + Path string + CreatedBy int64 + Created int64 + Updated int64 +} + +// Stores path query parameters. +type PathFilter struct { + Page int `json:"page"` + Size int `json:"size"` + Sort enum.PathAttr `json:"sort"` + Order enum.Order `json:"direction"` +} diff --git a/types/repo.go b/types/repo.go index 48e8b0b1a..93e7c9fd2 100644 --- a/types/repo.go +++ b/types/repo.go @@ -8,12 +8,13 @@ import ( "github.com/harness/gitness/types/enum" ) +// Represents a code repository type Repository struct { // Core properties ID int64 `db:"repo_id" json:"id"` Name string `db:"repo_name" json:"name"` SpaceId int64 `db:"repo_spaceId" json:"spaceId"` - Fqn string `db:"repo_fqn" json:"fqn"` + Path string `db:"repo_path" json:"path"` DisplayName string `db:"repo_displayName" json:"displayName"` Description string `db:"repo_description" json:"description"` IsPublic bool `db:"repo_isPublic" json:"isPublic"` diff --git a/types/space.go b/types/space.go index ebd1893eb..84badd3b6 100644 --- a/types/space.go +++ b/types/space.go @@ -5,37 +5,16 @@ package types import ( - "errors" - "strings" - "github.com/harness/gitness/types/enum" ) -/* - * Splits an FQN into the parent and the leave. - * e.g. /space1/space2/space3 -> (/space1/space2, space3, nil) - * TODO: move to better locaion - */ -func DisectFqn(fqn string) (string, string, error) { - if fqn == "" { - return "", "", errors.New("Can't disect empty fqn.") - } - - i := strings.LastIndex(fqn, "/") - if i == -1 { - return "", fqn, nil - } - - return fqn[:i], fqn[i+1:], nil -} - /* * Represents a space. * There isn't a one-solves-all hierarchical data structure for DBs, - * so for now we are using a mix of materialized paths and adjacency list, - * meaning any space stores its full qualified space name as well as the id of its parent. - * PRO: Quick lookup of childs, quick lookup based on fqdn (apis) - * CON: Changing a space name requires changing all its ancestors' FQNs. + * so for now we are using a mix of materialized paths and adjacency list. + * Every space stores its parent, and a space's path is stored in a separate table. + * PRO: Quick lookup of childs, quick lookup based on fqdn (apis) + * CON: Changing a space name requires changing all its ancestors' Paths. * * Interesting reads: * https://stackoverflow.com/questions/4048151/what-are-the-options-for-storing-hierarchical-data-in-a-relational-database @@ -44,7 +23,7 @@ func DisectFqn(fqn string) (string, string, error) { type Space struct { ID int64 `db:"space_id" json:"id"` Name string `db:"space_name" json:"name"` - Fqn string `db:"space_fqn" json:"fqn"` + Path string `db:"space_path" json:"path"` ParentId int64 `db:"space_parentId" json:"parentId"` DisplayName string `db:"space_displayName" json:"displayName"` Description string `db:"space_description" json:"description"` diff --git a/types/types.go b/types/types.go index 49b70df63..0886f6a56 100644 --- a/types/types.go +++ b/types/types.go @@ -20,49 +20,10 @@ type ( Order enum.Order `json:"direction"` } - // User stores user account details. - User struct { - ID int64 `db:"user_id" json:"id"` - Email string `db:"user_email" json:"email"` - Password string `db:"user_password" json:"-"` - Salt string `db:"user_salt" json:"-"` - Name string `db:"user_name" json:"name"` - Company string `db:"user_company" json:"company"` - Admin bool `db:"user_admin" json:"admin"` - Blocked bool `db:"user_blocked" json:"-"` - Created int64 `db:"user_created" json:"created"` - Updated int64 `db:"user_updated" json:"updated"` - Authed int64 `db:"user_authed" json:"authed"` - } - - // UserInput store user account details used to - // create or update a user. - UserInput struct { - Username *string `json:"email"` - Password *string `json:"password"` - Name *string `json:"name"` - Company *string `json:"company"` - Admin *bool `json:"admin"` - } - - // UserFilter stores user query parameters. - UserFilter struct { - Page int `json:"page"` - Size int `json:"size"` - Sort enum.UserAttr `json:"sort"` - Order enum.Order `json:"direction"` - } - // Token stores token details. Token struct { Value string `json:"access_token"` Address string `json:"uri,omitempty"` Expires time.Time `json:"expires_at,omitempty"` } - - // UserToken stores user account and token details. - UserToken struct { - User *User `json:"user"` - Token *Token `json:"token"` - } ) diff --git a/types/user.go b/types/user.go new file mode 100644 index 000000000..794ac383e --- /dev/null +++ b/types/user.go @@ -0,0 +1,51 @@ +// Copyright 2021 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 defines common data structures. +package types + +import ( + "github.com/harness/gitness/types/enum" +) + +type ( + // User stores user account details. + User struct { + ID int64 `db:"user_id" json:"id"` + Email string `db:"user_email" json:"email"` + Password string `db:"user_password" json:"-"` + Salt string `db:"user_salt" json:"-"` + Name string `db:"user_name" json:"name"` + Company string `db:"user_company" json:"company"` + Admin bool `db:"user_admin" json:"admin"` + Blocked bool `db:"user_blocked" json:"-"` + Created int64 `db:"user_created" json:"created"` + Updated int64 `db:"user_updated" json:"updated"` + Authed int64 `db:"user_authed" json:"authed"` + } + + // UserInput store user account details used to + // create or update a user. + UserInput struct { + Username *string `json:"email"` + Password *string `json:"password"` + Name *string `json:"name"` + Company *string `json:"company"` + Admin *bool `json:"admin"` + } + + // UserFilter stores user query parameters. + UserFilter struct { + Page int `json:"page"` + Size int `json:"size"` + Sort enum.UserAttr `json:"sort"` + Order enum.Order `json:"direction"` + } + + // UserToken stores user account and token details. + UserToken struct { + User *User `json:"user"` + Token *Token `json:"token"` + } +)