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
This commit is contained in:
Johannes Batzill 2022-09-08 21:39:15 -07:00 committed by GitHub
parent 1db4fc9c8d
commit 1115a5083b
92 changed files with 2940 additions and 712 deletions

3
go.mod
View File

@ -14,7 +14,6 @@ require (
github.com/golang/mock v1.5.0 github.com/golang/mock v1.5.0
github.com/google/go-cmp v0.5.5 github.com/google/go-cmp v0.5.5
github.com/google/wire v0.5.0 github.com/google/wire v0.5.0
github.com/gosimple/slug v1.11.2
github.com/gotidy/ptr v1.3.0 github.com/gotidy/ptr v1.3.0
github.com/jmoiron/sqlx v1.3.1 github.com/jmoiron/sqlx v1.3.1
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
@ -23,6 +22,7 @@ require (
github.com/maragudk/migrate v0.4.1 github.com/maragudk/migrate v0.4.1
github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-sqlite3 v1.14.10-0.20211026011849-85436841b33e 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/rs/zerolog v1.26.0
github.com/swaggest/openapi-go v0.2.13 github.com/swaggest/openapi-go v0.2.13
github.com/swaggest/swgui v1.4.2 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/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/google/subcommands v1.0.1 // 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/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect

5
go.sum
View File

@ -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.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/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/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 h1:5wdrH1G8X4txy6fbWWRznr7k974wMWtePWP3p6s1API=
github.com/gotidy/ptr v1.3.0/go.mod h1:vpltyHhOZE+NGXUiwpVl3wV9AGEBlxhdnaimPDxRLxg= 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= 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/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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/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/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@ -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
}

View File

@ -5,7 +5,6 @@
package guard package guard
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
@ -14,6 +13,13 @@ import (
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "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 { type Guard struct {
@ -32,12 +38,12 @@ func (g *Guard) EnforceAdmin(next http.Handler) http.Handler {
ctx := r.Context() ctx := r.Context()
user, ok := request.UserFrom(ctx) user, ok := request.UserFrom(ctx)
if !ok { if !ok {
render.Unauthorized(w, errors.New("Requires authentication")) render.Unauthorizedf(w, actionRequiresAuthentication)
return return
} }
if !user.Admin { if !user.Admin {
render.Forbidden(w, errors.New("Requires admin privileges.")) render.Forbiddenf(w, "Action requires admin privileges.")
return return
} }
@ -53,7 +59,7 @@ func (g *Guard) EnforceAuthenticated(next http.Handler) http.Handler {
ctx := r.Context() ctx := r.Context()
_, ok := request.UserFrom(ctx) _, ok := request.UserFrom(ctx)
if !ok { if !ok {
render.Unauthorized(w, errors.New("Requires authentication")) render.Unauthorizedf(w, actionRequiresAuthentication)
return 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. * 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 { 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) err := g.Check(r, scope, resource, permission)
if errors.Is(err, &notAuthenticatedError{}) { // render error if needed
render.Unauthorized(w, err) if errors.Is(err, errs.NotAuthenticated) {
} else if errors.Is(err, &notAuthorizedError{}) { render.Unauthorizedf(w, actionRequiresAuthentication)
render.Forbidden(w, err) } 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 { } 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 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 { func (g *Guard) Check(r *http.Request, scope *types.Scope, resource *types.Resource, permission enum.Permission) error {
u, present := request.UserFrom(r.Context()) u, present := request.UserFrom(r.Context())
if !present { if !present {
return &notAuthenticatedError{resource, permission} return errs.NotAuthenticated
} }
// TODO: don't hardcode principal type USER // 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, resource,
permission) permission)
if err != nil { if err != nil {
return err return errors.Wrap(err, "Authorization check failed")
} }
if !authorized { if !authorized {
return &notAuthorizedError{u, scope, resource, permission} return errs.NotAuthorized
} }
return nil return nil

View File

@ -5,14 +5,17 @@
package guard package guard
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "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() ctx := r.Context()
rep, ok := request.RepoFrom(ctx) rep, ok := request.RepoFrom(ctx)
if !ok { 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 return
} }
// Enforce permission (renders error) // 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 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. * 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. * 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 { func (g *Guard) EnforceRepo(w http.ResponseWriter, r *http.Request, permission enum.Permission, path string) bool {
parentSpace, name, err := types.DisectFqn(fqn) spacePath, name, err := paths.Disect(path)
if err != nil { 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 return false
} }
scope := &types.Scope{SpaceFqn: parentSpace} scope := &types.Scope{SpacePath: spacePath}
resource := &types.Resource{ resource := &types.Resource{
Type: enum.ResourceTypeRepo, Type: enum.ResourceTypeRepo,
Name: name, 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 * Returns nil if the user is confirmed to be permitted to execute the action, otherwise returns errors
* NotAuthenticated, NotAuthorized, or any unerlaying error. * NotAuthenticated, NotAuthorized, or any unerlaying error.
*/ */
func (g *Guard) CheckRepo(r *http.Request, permission enum.Permission, fqn string) error { func (g *Guard) CheckRepo(r *http.Request, permission enum.Permission, path string) error {
parentSpace, name, err := types.DisectFqn(fqn) parentSpace, name, err := paths.Disect(path)
if err != nil { 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{ resource := &types.Resource{
Type: enum.ResourceTypeRepo, Type: enum.ResourceTypeRepo,
Name: name, Name: name,

View File

@ -5,14 +5,17 @@
package guard package guard
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "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() ctx := r.Context()
s, ok := request.SpaceFrom(ctx) s, ok := request.SpaceFrom(ctx)
if !ok { if !ok {
render.InternalError(w, errors.New("Expected space to be available.")) render.InternalError(w, errors.New("Expected space to be available"))
return return
} }
// Enforce permission (renders error) // 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 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. * 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. * 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 { func (g *Guard) EnforceSpace(w http.ResponseWriter, r *http.Request, permission enum.Permission, path string) bool {
parentSpace, name, err := types.DisectFqn(fqn) parentSpace, name, err := paths.Disect(path)
if err != nil { 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 return false
} }
scope := &types.Scope{SpaceFqn: parentSpace} scope := &types.Scope{SpacePath: parentSpace}
resource := &types.Resource{ resource := &types.Resource{
Type: enum.ResourceTypeSpace, Type: enum.ResourceTypeSpace,
Name: name, 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 * Returns nil if the user is confirmed to be permitted to execute the action, otherwise returns errors
* NotAuthenticated, NotAuthorized, or any unerlaying error. * NotAuthenticated, NotAuthorized, or any unerlaying error.
*/ */
func (g *Guard) CheckSpace(r *http.Request, permission enum.Permission, fqn string) error { func (g *Guard) CheckSpace(r *http.Request, permission enum.Permission, path string) error {
parentSpace, name, err := types.DisectFqn(fqn) parentSpace, name, err := paths.Disect(path)
if err != nil { 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{ resource := &types.Resource{
Type: enum.ResourceTypeSpace, Type: enum.ResourceTypeSpace,
Name: name, Name: name,

View File

@ -28,10 +28,12 @@ func HandleLogin(users store.UserStore, system store.SystemStore) http.HandlerFu
password := r.FormValue("password") password := r.FormValue("password")
user, err := users.FindEmail(ctx, username) user, err := users.FindEmail(ctx, username)
if err != nil { if err != nil {
render.NotFoundf(w, "Invalid email or password")
log.Debug().Err(err). log.Debug().Err(err).
Str("user", username). Str("user", username).
Msg("cannot find user") Msg("cannot find user")
// always give not found error as extra security measurement.
render.NotFoundf(w, "Invalid email or password")
return return
} }
@ -40,20 +42,22 @@ func HandleLogin(users store.UserStore, system store.SystemStore) http.HandlerFu
[]byte(password), []byte(password),
) )
if err != nil { if err != nil {
render.NotFoundf(w, "Invalid email or password")
log.Debug().Err(err). log.Debug().Err(err).
Str("user", username). Str("user", username).
Msg("invalid password") Msg("invalid password")
render.NotFoundf(w, "Invalid email or password")
return return
} }
expires := time.Now().Add(system.Config(ctx).Token.Expire) expires := time.Now().Add(system.Config(ctx).Token.Expire)
token_, err := token.GenerateExp(user, expires.Unix(), user.Salt) token_, err := token.GenerateExp(user, expires.Unix(), user.Salt)
if err != nil { if err != nil {
render.InternalErrorf(w, "Failed to create session") log.Err(err).
log.Debug().Err(err).
Str("user", username). Str("user", username).
Msg("failed to generate token") Msg("failed to generate token")
render.InternalErrorf(w, "Failed to create session")
return return
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/harness/gitness/internal/token" "github.com/harness/gitness/internal/token"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/errs"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"github.com/rs/zerolog/hlog" "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) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).
log.Debug().Err(err).
Str("email", username). Str("email", username).
Msg("cannot hash password") Msg("Failed to hash password")
render.InternalError(w, errs.Internal)
return return
} }
// TODO: allow to provide email and name separately ...
user := &types.User{ user := &types.User{
Name: username, Name: username,
Email: username, Email: username,
@ -48,18 +51,20 @@ func HandleRegister(users store.UserStore, system store.SystemStore) http.Handle
} }
if ok, err := check.User(user); !ok { if ok, err := check.User(user); !ok {
render.BadRequest(w, err)
log.Debug().Err(err). log.Debug().Err(err).
Str("email", username). Str("email", username).
Msg("invalid user input") Msg("invalid user input")
render.BadRequest(w, err)
return return
} }
if err := users.Create(ctx, user); err != nil { if err := users.Create(ctx, user); err != nil {
render.InternalError(w, err) log.Err(err).
log.Error().Err(err).
Str("email", username). Str("email", username).
Msg("cannot create user") Msg("Failed to create user")
render.InternalError(w, errs.Internal)
return return
} }
@ -69,19 +74,25 @@ func HandleRegister(users store.UserStore, system store.SystemStore) http.Handle
if user.ID == 1 { if user.ID == 1 {
user.Admin = true user.Admin = true
if err := users.Update(ctx, user); err != nil { if err := users.Update(ctx, user); err != nil {
log.Error().Err(err). log.Err(err).
Str("email", username). 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) expires := time.Now().Add(system.Config(ctx).Token.Expire)
token_, err := token.GenerateExp(user, expires.Unix(), user.Salt) token_, err := token.GenerateExp(user, expires.Unix(), user.Salt)
if err != nil { if err != nil {
render.InternalErrorf(w, "Failed to create session") log.Err(err).
log.Error().Err(err).
Str("email", username). Str("email", username).
Msg("failed to generate token") Int64("user_id", user.ID).
Msg("Failed to generate token")
render.InternalError(w, errs.Internal)
return return
} }

View File

@ -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
}

View File

@ -14,10 +14,12 @@ import (
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
) )
@ -41,39 +43,38 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore, repos store.RepoS
in := new(repoCreateInput) in := new(repoCreateInput)
err := json.NewDecoder(r.Body).Decode(in) err := json.NewDecoder(r.Body).Decode(in)
if err != nil { if err != nil {
render.BadRequest(w, err)
log.Debug().Err(err). log.Debug().Err(err).
Msg("Decoding json body failed.") Msg("Decoding json body failed.")
render.BadRequestf(w, "Invalid Request Body: %s.", err)
return return
} }
// ensure we reference a space // ensure we reference a space
if in.SpaceId <= 0 { if in.SpaceId <= 0 {
render.BadRequest(w, errors.New("A repository can only be created within a space.")) render.BadRequestf(w, "A repository can only be created within a space.")
log.Debug().
Msg("No space was provided.")
return return
} }
parentSpace, err := spaces.Find(ctx, in.SpaceId) parentSpace, err := spaces.Find(ctx, in.SpaceId)
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
render.BadRequest(w, err) render.NotFoundf(w, "Provided space wasn't found.")
log.Debug(). return
Err(err). } else if err != nil {
Msgf("Parent space with id '%s' doesn't exist.", in.SpaceId) log.Err(err).Msgf("Failed to get space with id '%s'.", in.SpaceId)
render.InternalError(w, errs.Internal)
return return
} }
// parentFqn is assumed to be valid, in.Name gets validated in check.Repo function // parentPath is assumed to be valid, in.Name gets validated in check.Repo function
parentFqn := parentSpace.Fqn parentPath := parentSpace.Path
fqn := parentFqn + "/" + in.Name
/* /*
* AUTHORIZATION * AUTHORIZATION
* Create is a special case - check permission without specific resource * Create is a special case - check permission without specific resource
*/ */
scope := &types.Scope{SpaceFqn: parentFqn} scope := &types.Scope{SpacePath: parentPath}
resource := &types.Resource{ resource := &types.Resource{
Type: enum.ResourceTypeRepo, Type: enum.ResourceTypeRepo,
Name: "", 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) // get current user (safe to be there, or enforce would fail)
usr, _ := request.UserFrom(ctx) usr, _ := request.UserFrom(ctx)
// create repo // create new repo object
repo := &types.Repository{ repo := &types.Repository{
Name: strings.ToLower(in.Name), Name: strings.ToLower(in.Name),
SpaceId: in.SpaceId, SpaceId: in.SpaceId,
Fqn: strings.ToLower(fqn),
DisplayName: in.DisplayName, DisplayName: in.DisplayName,
Description: in.Description, Description: in.Description,
IsPublic: in.IsPublic, IsPublic: in.IsPublic,
@ -99,20 +99,35 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore, repos store.RepoS
ForkId: in.ForkId, ForkId: in.ForkId,
} }
if ok, err := check.Repo(repo); !ok { // validate repo
if err := check.Repo(repo); err != nil {
render.BadRequest(w, err) render.BadRequest(w, err)
log.Debug().Err(err).
Msg("Repository validation failed.")
return return
} }
err = repos.Create(ctx, repo) // validate path (Due to racing conditions we can't be 100% sure on the path here, but that's okay)
if err != nil { path := paths.Concatinate(parentPath, repo.Name)
render.InternalError(w, err) if err = check.PathParams(path, false); err != nil {
log.Error().Err(err). render.BadRequest(w, err)
Msg("Repository creation failed") return
} else {
render.JSON(w, repo, 200)
} }
// 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)
} }
} }

View File

@ -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)
})
}

View File

@ -5,6 +5,7 @@
package repo package repo
import ( import (
"errors"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
@ -12,7 +13,8 @@ import (
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types/enum" "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, enum.PermissionRepoDelete,
false, false,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
// TODO: return 200 if repo confirmed doesn't exist
ctx := r.Context() ctx := r.Context()
rep, _ := request.RepoFrom(ctx) log := hlog.FromRequest(r)
repo, _ := request.RepoFrom(ctx)
err := repos.Delete(r.Context(), rep.ID) err := repos.Delete(r.Context(), repo.ID)
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
render.InternalError(w, err) render.NotFoundf(w, "Repository doesn't exist.")
log.Error().Err(err).
Int64("repo_id", rep.ID).
Str("repo_fqn", rep.Fqn).
Msg("Failed to delete repository.")
return return
} else if err != nil {
log.Err(err).Msgf("Failed to delete the Repository.")
render.InternalError(w, errs.Internal)
return
} }
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -5,25 +5,73 @@
package repo package repo
import ( import (
"errors" "encoding/json"
"net/http" "net/http"
"time"
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
"github.com/harness/gitness/internal/api/render" "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/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. * Updates an existing repository.
*/ */
func HandleUpdate(guard *guard.Guard) http.HandlerFunc { func HandleUpdate(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc {
return guard.Repo( return guard.Repo(
enum.PermissionRepoEdit, enum.PermissionRepoEdit,
false, false,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
/* ctx := r.Context()
* TO-DO: Add support for updating an existing repository. repo, _ := request.RepoFrom(ctx)
*/
render.BadRequest(w, errors.New("Updating an existing repo is not supported.")) 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)
}) })
} }

View File

@ -14,14 +14,16 @@ import (
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
) )
type spaceCreateInput struct { type spaceCreateRequest struct {
Name string `json:"name"` Name string `json:"name"`
ParentId int64 `json:"parentId"` ParentId int64 `json:"parentId"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
@ -37,37 +39,19 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc
ctx := r.Context() ctx := r.Context()
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
in := new(spaceCreateInput) in := new(spaceCreateRequest)
err := json.NewDecoder(r.Body).Decode(in) err := json.NewDecoder(r.Body).Decode(in)
if err != nil { if err != nil {
render.BadRequest(w, err) render.BadRequestf(w, "Invalid request body: %s.", err)
log.Debug().Err(err).
Msg("Decoding json body failed.")
return 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) // get current user (will be enforced to not be nil via explicit check or guard.Enforce)
usr, _ := request.UserFrom(ctx) usr, _ := request.UserFrom(ctx)
// Collect parent path along the way - needed for duplicate error message
parentPath := ""
/* /*
* AUTHORIZATION * AUTHORIZATION
* Can only be done once we know the parent space * 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 { if in.ParentId <= 0 {
// TODO: Restrict top level space creation. // TODO: Restrict top level space creation.
if usr == nil { if usr == nil {
render.Unauthorized(w, errors.New("Authentication required.")) render.Unauthorized(w, errs.NotAuthenticated)
return return
} }
} else { } else {
// Create is a special case - check permission without specific resource // Create is a special case - we need the parent path
scope := &types.Scope{SpaceFqn: parentFqn} 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{ resource := &types.Resource{
Type: enum.ResourceTypeSpace, Type: enum.ResourceTypeSpace,
Name: "", Name: "",
@ -88,12 +83,14 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc
if !guard.Enforce(w, r, scope, resource, enum.PermissionSpaceCreate) { if !guard.Enforce(w, r, scope, resource, enum.PermissionSpaceCreate) {
return return
} }
parentPath = parent.Path
} }
// create new space object
space := &types.Space{ space := &types.Space{
Name: strings.ToLower(in.Name), Name: strings.ToLower(in.Name),
ParentId: in.ParentId, ParentId: in.ParentId,
Fqn: strings.ToLower(fqn),
DisplayName: in.DisplayName, DisplayName: in.DisplayName,
Description: in.Description, Description: in.Description,
IsPublic: in.IsPublic, IsPublic: in.IsPublic,
@ -102,20 +99,35 @@ func HandleCreate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc
Updated: time.Now().UnixMilli(), Updated: time.Now().UnixMilli(),
} }
if ok, err := check.Space(space); !ok { // validate space
if err := check.Space(space); err != nil {
render.BadRequest(w, err) render.BadRequest(w, err)
log.Debug().Err(err).
Msg("Space validation failed.")
return return
} }
err = spaces.Create(ctx, space) // validate path (Due to racing conditions we can't be 100% sure on the path here, but that's okay)
if err != nil { path := paths.Concatinate(parentPath, space.Name)
render.InternalError(w, err) if err = check.PathParams(path, true); err != nil {
log.Error().Err(err). render.BadRequest(w, err)
Msg("Space creation failed") return
} else {
render.JSON(w, space, 200)
} }
// 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)
} }
} }

View File

@ -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)
})
}

View File

@ -5,6 +5,7 @@
package space package space
import ( import (
"errors"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
@ -12,7 +13,8 @@ import (
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types/enum" "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, enum.PermissionSpaceDelete,
false, false,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
// TODO: return 200 if space confirmed doesn't exist
ctx := r.Context() ctx := r.Context()
log := hlog.FromRequest(r)
s, _ := request.SpaceFrom(ctx) s, _ := request.SpaceFrom(ctx)
err := spaces.Delete(r.Context(), s.ID) err := spaces.Delete(r.Context(), s.ID)
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
render.InternalError(w, err) render.NotFoundf(w, "Space not found.")
log.Error().Err(err).
Str("space_fqn", s.Fqn).
Msg("Failed to delete space.")
return return
} else if err != nil {
log.Err(err).Msgf("Failed to delete the space.")
render.InternalError(w, errs.Internal)
return
} }
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View File

@ -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)
})
}

View File

@ -13,7 +13,8 @@ import (
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "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, true,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
s, _ := request.SpaceFrom(ctx) log := hlog.FromRequest(r)
space, _ := request.SpaceFrom(ctx)
params := request.ParseSpaceFilter(r) params := request.ParseSpaceFilter(r)
if params.Order == enum.OrderDefault { if params.Order == enum.OrderDefault {
params.Order = enum.OrderAsc params.Order = enum.OrderAsc
} }
count, err := spaces.Count(ctx, s.ID) count, err := spaces.Count(ctx, space.ID)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).Msgf("Failed to count child spaces.")
log.Error().Err(err).
Str("space_fqn", s.Fqn). render.InternalError(w, errs.Internal)
Msg("Failed to retrieve count of child spaces.")
return return
} }
allSpaces, err := spaces.List(ctx, s.ID, params) allSpaces, err := spaces.List(ctx, space.ID, params)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).Msgf("Failed to list child spaces.")
log.Error().Err(err).
Str("space_fqn", s.Fqn). render.InternalError(w, errs.Internal)
Msg("Failed to retrieve list of child spaces.")
return return
} }
@ -58,10 +58,10 @@ func HandleList(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc {
result := make([]*types.Space, 0, len(allSpaces)) result := make([]*types.Space, 0, len(allSpaces))
for _, cs := range allSpaces { for _, cs := range allSpaces {
if !cs.IsPublic { if !cs.IsPublic {
err := guard.CheckSpace(r, enum.PermissionSpaceView, cs.Fqn) err := guard.CheckSpace(r, enum.PermissionSpaceView, cs.Path)
if err != nil { if err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Msgf("Skip space '%s' in output.", cs.Fqn) Msgf("Skip space '%s' in output.", cs.Path)
continue continue
} }
} }

View File

@ -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)
})
}

View File

@ -13,7 +13,8 @@ import (
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "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, true,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
s, _ := request.SpaceFrom(ctx) log := hlog.FromRequest(r)
space, _ := request.SpaceFrom(ctx)
params := request.ParseRepoFilter(r) params := request.ParseRepoFilter(r)
if params.Order == enum.OrderDefault { if params.Order == enum.OrderDefault {
params.Order = enum.OrderAsc params.Order = enum.OrderAsc
} }
count, err := repos.Count(ctx, s.ID) count, err := repos.Count(ctx, space.ID)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).Msgf("Failed to count child repos.")
log.Error().Err(err).
Str("space_fqn", s.Fqn). render.InternalError(w, errs.Internal)
Msg("Failed to retrieve count of repos.")
return return
} }
allRepos, err := repos.List(ctx, s.ID, params) allRepos, err := repos.List(ctx, space.ID, params)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).Msgf("Failed to list child repos.")
log.Error().Err(err).
Str("space_fqn", s.Fqn). render.InternalError(w, errs.Internal)
Msg("Failed to retrieve list of repos.")
return return
} }
@ -58,10 +58,10 @@ func HandleListRepos(guard *guard.Guard, repos store.RepoStore) http.HandlerFunc
result := make([]*types.Repository, 0, len(allRepos)) result := make([]*types.Repository, 0, len(allRepos))
for _, rep := range allRepos { for _, rep := range allRepos {
if !rep.IsPublic { if !rep.IsPublic {
err := guard.CheckRepo(r, enum.PermissionRepoView, rep.Fqn) err := guard.CheckRepo(r, enum.PermissionRepoView, rep.Path)
if err != nil { if err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Msgf("Skip repo '%s' in output.", rep.Fqn) Msgf("Skip repo '%s' in output.", rep.Path)
continue continue
} }

View File

@ -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)
})
}

View File

@ -5,28 +5,73 @@
package space package space
import ( import (
"errors" "encoding/json"
"net/http" "net/http"
"time"
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
"github.com/harness/gitness/internal/api/render" "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/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. * Updates an existing space.
*/ */
func HandleUpdate(guard *guard.Guard) http.HandlerFunc { func HandleUpdate(guard *guard.Guard, spaces store.SpaceStore) http.HandlerFunc {
return guard.Space( return guard.Space(
enum.PermissionSpaceEdit, enum.PermissionSpaceEdit,
false, false,
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
/* ctx := r.Context()
* TO-DO: Add support for updating an existing space. space, _ := request.SpaceFrom(ctx)
* Requires Solving:
* - Update all FQNs of child spaces (or change design) in := new(spaceUpdateRequest)
* - Update all acl permissions? (or change design) err := json.NewDecoder(r.Body).Decode(in)
*/ if err != nil {
render.BadRequest(w, errors.New("Updating an existing space is not supported.")) 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)
}) })
} }

View File

@ -17,8 +17,8 @@ import (
func HandleFind() http.HandlerFunc { func HandleFind() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
viewer, _ := request.UserFrom(ctx) user, _ := request.UserFrom(ctx)
render.JSON(w, viewer, 200) render.JSON(w, user, 200)
} }
} }
@ -28,7 +28,7 @@ func HandleFind() http.HandlerFunc {
func HandleCurrent() http.HandlerFunc { func HandleCurrent() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
viewer, _ := request.UserFrom(ctx) user, _ := request.UserFrom(ctx)
platform.RenderResource(w, viewer, 200) platform.RenderResource(w, user, 200)
} }
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/token" "github.com/harness/gitness/internal/token"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
) )
@ -19,15 +20,15 @@ import (
// writes a json-encoded token to the http.Response body. // writes a json-encoded token to the http.Response body.
func HandleToken(users store.UserStore) http.HandlerFunc { func HandleToken(users store.UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
render.InternalErrorf(w, "Failed to generate token") log.Err(err).
hlog.FromRequest(r).
Error().Err(err).
Str("user", viewer.Email).
Msg("failed to generate token") Msg("failed to generate token")
render.InternalError(w, errs.Internal)
return return
} }

View File

@ -28,45 +28,42 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
viewer, _ := request.UserFrom(ctx) user, _ := request.UserFrom(ctx)
in := new(types.UserInput) in := new(types.UserInput)
err := json.NewDecoder(r.Body).Decode(in) err := json.NewDecoder(r.Body).Decode(in)
if err != nil { if err != nil {
render.BadRequest(w, err) render.BadRequestf(w, "Invalid request body: %s.", err)
log.Error().Err(err).
Str("email", viewer.Email).
Msg("cannot unmarshal request")
return return
} }
if in.Password != nil { if in.Password != nil {
hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to hash password.")
render.InternalError(w, err) render.InternalError(w, err)
log.Debug().Err(err).
Msg("cannot hash password")
return return
} }
viewer.Password = string(hash) user.Password = string(hash)
} }
if in.Name != nil { if in.Name != nil {
viewer.Name = ptr.ToString(in.Name) user.Name = ptr.ToString(in.Name)
} }
if in.Company != nil { 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 { if err != nil {
log.Err(err).Msg("Failed to update the user.")
render.InternalError(w, err) render.InternalError(w, err)
log.Error().Err(err). return
Str("email", viewer.Email).
Msg("cannot update user")
} else {
render.JSON(w, viewer, 200)
} }
render.JSON(w, user, 200)
} }
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -35,17 +36,17 @@ func HandleCreate(users store.UserStore) http.HandlerFunc {
in := new(userCreateInput) in := new(userCreateInput)
err := json.NewDecoder(r.Body).Decode(in) err := json.NewDecoder(r.Body).Decode(in)
if err != nil { if err != nil {
render.BadRequest(w, err) render.BadRequestf(w, "Invalid request body: %s.", err)
log.Debug().Err(err).
Msg("cannot unmarshal json request")
return return
} }
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).
log.Debug().Err(err). Str("email", in.Username).
Msg("cannot hash password") Msg("Failed to hash password")
render.InternalError(w, errs.Internal)
return return
} }
@ -59,22 +60,24 @@ func HandleCreate(users store.UserStore) http.HandlerFunc {
} }
if ok, err := check.User(user); !ok { if ok, err := check.User(user); !ok {
render.BadRequest(w, err)
log.Debug().Err(err). log.Debug().Err(err).
Str("user_email", user.Email). Str("email", user.Email).
Msg("cannot validate user") Msg("invalid user input")
render.BadRequest(w, err)
return return
} }
err = users.Create(ctx, user) err = users.Create(ctx, user)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).
log.Error().Err(err). Str("email", user.Email).
Int64("user_id", user.ID). Msg("failed to create user")
Str("user_email", user.Email).
Msg("cannot create user") render.InternalError(w, errs.Internal)
} else { return
render.JSON(w, user, 200)
} }
render.JSON(w, user, 200)
} }
} }

View File

@ -5,10 +5,12 @@
package users package users
import ( import (
"errors"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -23,23 +25,28 @@ func HandleDelete(users store.UserStore) http.HandlerFunc {
key := chi.URLParam(r, "user") key := chi.URLParam(r, "user")
user, err := users.FindKey(ctx, key) user, err := users.FindKey(ctx, key)
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
render.NotFound(w, err) render.NotFoundf(w, "User not found.")
log.Debug().Err(err). return
Str("user_key", key). } else if err != nil {
Msg("cannot find user") log.Err(err).Msgf("Failed to get user using key '%s'.", key)
render.InternalError(w, errs.Internal)
return return
} }
err = users.Delete(ctx, user) err = users.Delete(ctx, user)
if err != nil { if err != nil {
render.InternalError(w, err)
log.Error().Err(err). log.Error().Err(err).
Int64("user_id", user.ID). Int64("user_id", user.ID).
Str("user_email", user.Email). 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)
} }
} }

View File

@ -5,10 +5,12 @@
package users package users
import ( import (
"errors"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -23,13 +25,16 @@ func HandleFind(users store.UserStore) http.HandlerFunc {
key := chi.URLParam(r, "user") key := chi.URLParam(r, "user")
user, err := users.FindKey(ctx, key) user, err := users.FindKey(ctx, key)
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
render.NotFound(w, err) render.NotFoundf(w, "User doesn't exist.")
log.Debug().Err(err). return
Str("user_key", key). } else if err != nil {
Msg("cannot find user") log.Err(err).Msgf("Failed to get user using key '%s'.", key)
} else {
render.JSON(w, user, 200) render.InternalError(w, errs.Internal)
return
} }
render.JSON(w, user, 200)
} }
} }

View File

@ -18,10 +18,8 @@ import (
// list of all registered system users to the response body. // list of all registered system users to the response body.
func HandleList(users store.UserStore) http.HandlerFunc { func HandleList(users store.UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var ( ctx := r.Context()
ctx = r.Context() log := hlog.FromRequest(r)
log = hlog.FromRequest(r)
)
params := request.ParseUserFilter(r) params := request.ParseUserFilter(r)
if params.Order == enum.OrderDefault { if params.Order == enum.OrderDefault {
@ -30,15 +28,16 @@ func HandleList(users store.UserStore) http.HandlerFunc {
count, err := users.Count(ctx) count, err := users.Count(ctx)
if err != nil { if err != nil {
log.Error().Err(err). log.Err(err).
Msg("cannot retrieve user count") Msg("Failed to retrieve user count")
} }
list, err := users.List(ctx, params) list, err := users.List(ctx, params)
if err != nil { if err != nil {
log.Err(err).
Msg("Failed to retrieve user list")
render.InternalError(w, err) render.InternalError(w, err)
log.Error().Err(err).
Msg("cannot retrieve user list")
return return
} }

View File

@ -6,6 +6,7 @@ package users
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"time" "time"
@ -14,6 +15,7 @@ import (
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/errs"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -33,32 +35,31 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc {
key := chi.URLParam(r, "user") key := chi.URLParam(r, "user")
user, err := users.FindKey(ctx, key) user, err := users.FindKey(ctx, key)
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
render.NotFound(w, err) render.NotFoundf(w, "User not found.")
log.Debug().Err(err). return
Str("user_key", key). } else if err != nil {
Msg("cannot find user") log.Err(err).Msgf("Failed to get user using key '%s'.", key)
render.InternalError(w, errs.Internal)
return return
} }
in := new(types.UserInput) in := new(types.UserInput)
if err := json.NewDecoder(r.Body).Decode(in); err != nil { if err := json.NewDecoder(r.Body).Decode(in); err != nil {
render.BadRequest(w, err) render.BadRequestf(w, "Invalid request body: %s.", err)
log.Debug().Err(err).
Int64("user_id", user.ID).
Str("user_email", user.Email).
Msg("cannot unmarshal request")
return return
} }
if in.Password != nil { if in.Password != nil {
hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).
log.Debug().Err(err).
Int64("user_id", user.ID). Int64("user_id", user.ID).
Str("user_email", user.Email). Str("user_email", user.Email).
Msg("cannot hash password") Msg("Failed to hash password")
render.InternalError(w, errs.Internal)
return return
} }
user.Password = string(hash) user.Password = string(hash)
@ -76,23 +77,28 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc {
user.Admin = ptr.ToBool(in.Admin) user.Admin = ptr.ToBool(in.Admin)
} }
// TODO: why are we overwriting the password twice?
if in.Password != nil { if in.Password != nil {
hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).
log.Debug().Err(err). Int64("user_id", user.ID).
Msg("cannot hash password") Str("user_email", user.Email).
Msg("Failed to hash password")
render.InternalError(w, errs.Internal)
return return
} }
user.Password = string(hash) user.Password = string(hash)
} }
if ok, err := check.User(user); !ok { if ok, err := check.User(user); !ok {
render.BadRequest(w, err)
log.Debug().Err(err). log.Debug().Err(err).
Int64("user_id", user.ID). Int64("user_id", user.ID).
Str("user_email", user.Email). Str("user_email", user.Email).
Msg("cannot update user") Msg("invalid user input")
render.BadRequest(w, err)
return return
} }
@ -100,13 +106,15 @@ func HandleUpdate(users store.UserStore) http.HandlerFunc {
err = users.Update(ctx, user) err = users.Update(ctx, user)
if err != nil { if err != nil {
render.InternalError(w, err) log.Err(err).
log.Error().Err(err).
Int64("user_id", user.ID). Int64("user_id", user.ID).
Str("user_email", user.Email). Str("user_email", user.Email).
Msg("cannot update user") Msg("Failed to update the usser")
} else {
render.JSON(w, user, 200) render.InternalError(w, errs.Internal)
return
} }
render.JSON(w, user, 200)
} }
} }

View File

@ -34,10 +34,10 @@ func Attempt(authenticator authn.Authenticator) func(http.Handler) http.Handler
return return
} }
// otherwise update the logging context and inject user in context // Update the logging context and inject user in context
ctx := r.Context() ctx := r.Context()
log.Ctx(ctx).UpdateContext(func(c zerolog.Context) zerolog.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( next.ServeHTTP(w, r.WithContext(

View File

@ -5,31 +5,33 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "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. * (e.g. "space1/repo.git") before executing the provided http.HandlerFunc.
* The first prefix that matches the URL.Path will be used during encoding. * 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) { return func(w http.ResponseWriter, r *http.Request) {
r, _ = encodeFQNWithMarker(r, "", ".git", false) r, _ = pathTerminatedWithMarker(r, "", ".git", false)
h.ServeHTTP(w, r) 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. * before executing the provided http.HandlerFunc.
* The first prefix that matches the URL.Path will be used during encoding. * 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) { return func(w http.ResponseWriter, r *http.Request) {
for _, p := range prefixes { for _, p := range prefixes {
// IMPORTANT: define changed separately to avoid overshadowing r // IMPORTANT: define changed separately to avoid overshadowing r
changed := false changed := false
if r, changed = encodeFQNWithMarker(r, p, "/+", false); changed { if r, changed = pathTerminatedWithMarker(r, p, "/+", false); changed {
break 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. * 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" * 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: "" Path: "/space1/space2.git" => "/space1%2Fspace2"
* Prefix: "/spaces" Path: "/spaces/space1/space2/+/authToken" => "/spaces/space1%2Fspace2/authToken" * 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 // 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 { if len(r.URL.Path) < len(prefix) || r.URL.Path[0:len(prefix)] != prefix {
return r, false return r, false
} }
originalSubPath := r.URL.Path[len(prefix):] 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 we don't find a marker - nothing to encode
if !found { 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 '/') // 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 { if keepMarker {
escapedFqn += marker escapedPath += marker
} }
updatedSubPath := escapedFqn + suffix updatedSubPath := escapedPath + suffix
// TODO: Proper Logging // TODO: Proper Logging
fmt.Printf( fmt.Printf(

View File

@ -5,6 +5,7 @@
package repo package repo
import ( import (
"errors"
"net/http" "net/http"
"strconv" "strconv"
@ -12,6 +13,9 @@ import (
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "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() ctx := r.Context()
var rep *types.Repository var repo *types.Repository
// check if ref is repoId - ASSUMPTION: digit only is no valid repo name // check if ref is repoId - ASSUMPTION: digit only is no valid repo name
id, err := strconv.ParseInt(ref, 10, 64) id, err := strconv.ParseInt(ref, 10, 64)
if err == nil { if err == nil {
rep, err = repos.Find(ctx, id) repo, err = repos.Find(ctx, id)
} else { } else {
rep, err = repos.FindFqn(ctx, ref) repo, err = repos.FindByPath(ctx, ref)
} }
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
// TODO: what about errors that aren't notfound? render.NotFoundf(w, "Repository doesn't exist.")
render.NotFoundf(w, "Resolving repository reference '%s' failed: %s", ref, err) return
} else if err != nil {
log.Err(err).Msgf("Failed to get repo using ref '%s'.", ref)
render.InternalError(w, errs.Internal)
return 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( next.ServeHTTP(w, r.WithContext(
request.WithRepo(ctx, rep), request.WithRepo(ctx, repo),
)) ))
}) })
} }

View File

@ -5,6 +5,7 @@
package space package space
import ( import (
"errors"
"net/http" "net/http"
"strconv" "strconv"
@ -12,6 +13,9 @@ import (
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "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() ctx := r.Context()
var s *types.Space var space *types.Space
// check if ref is spaceId - ASSUMPTION: digit only is no valid space name // check if ref is spaceId - ASSUMPTION: digit only is no valid space name
id, err := strconv.ParseInt(ref, 10, 64) id, err := strconv.ParseInt(ref, 10, 64)
if err == nil { if err == nil {
s, err = spaces.Find(ctx, id) space, err = spaces.Find(ctx, id)
} else { } else {
s, err = spaces.FindFqn(ctx, ref) space, err = spaces.FindByPath(ctx, ref)
} }
if err != nil { if errors.Is(err, errs.ResourceNotFound) {
// TODO: what about errors that aren't notfound? render.NotFoundf(w, "Space not found.")
render.NotFoundf(w, "Resolving space reference '%s' failed: %s", ref, err) return
} else if err != nil {
log.Err(err).Msgf("Failed to get space using ref '%s'.", ref)
render.InternalError(w, errs.Internal)
return 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( next.ServeHTTP(w, r.WithContext(
request.WithSpace(ctx, s), request.WithSpace(ctx, space),
)) ))
}) })
} }

View File

@ -56,12 +56,24 @@ func Unauthorized(w http.ResponseWriter, err error) {
ErrorCode(w, err, 401) 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 // Forbidden writes the json-encoded error message to the response
// with a 403 forbidden status code. // with a 403 forbidden status code.
func Forbidden(w http.ResponseWriter, err error) { func Forbidden(w http.ResponseWriter, err error) {
ErrorCode(w, err, 403) 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 // BadRequest writes the json-encoded error message to the response
// with a 400 bad request status code. // with a 400 bad request status code.
func BadRequest(w http.ResponseWriter, err error) { func BadRequest(w http.ResponseWriter, err error) {

View File

@ -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
}

View File

@ -1,12 +1,12 @@
package request package request
import ( import (
"errors"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/harness/gitness/types/errs"
) )
const ( const (
@ -16,10 +16,10 @@ const (
func GetRepoRef(r *http.Request) (string, error) { func GetRepoRef(r *http.Request) (string, error) {
rawRef := chi.URLParam(r, RepoRefParamName) rawRef := chi.URLParam(r, RepoRefParamName)
if rawRef == "" { 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) ref, err := url.PathUnescape(rawRef)
return strings.ToLower(ref), err return strings.ToLower(ref), err
} }

View File

@ -1,12 +1,12 @@
package request package request
import ( import (
"errors"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/harness/gitness/types/errs"
) )
const ( const (
@ -16,10 +16,10 @@ const (
func GetSpaceRef(r *http.Request) (string, error) { func GetSpaceRef(r *http.Request) (string, error) {
rawRef := chi.URLParam(r, SpaceRefParamName) rawRef := chi.URLParam(r, SpaceRefParamName)
if rawRef == "" { 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) ref, err := url.PathUnescape(rawRef)
return strings.ToLower(ref), err return strings.ToLower(ref), err
} }

View File

@ -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. // ParseParams extracts the query parameter from the url.
func ParseParams(r *http.Request) types.Params { func ParseParams(r *http.Request) types.Params {
return 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. // ParseUserFilter extracts the user query parameter from the url.
func ParseUserFilter(r *http.Request) types.UserFilter { func ParseUserFilter(r *http.Request) *types.UserFilter {
return types.UserFilter{ return &types.UserFilter{
Order: ParseOrder(r), Order: ParseOrder(r),
Page: ParsePage(r), Page: ParsePage(r),
Sort: ParseSortUser(r), Sort: ParseSortUser(r),
@ -88,8 +95,8 @@ func ParseUserFilter(r *http.Request) types.UserFilter {
} }
// ParseSpaceFilter extracts the space query parameter from the url. // ParseSpaceFilter extracts the space query parameter from the url.
func ParseSpaceFilter(r *http.Request) types.SpaceFilter { func ParseSpaceFilter(r *http.Request) *types.SpaceFilter {
return types.SpaceFilter{ return &types.SpaceFilter{
Order: ParseOrder(r), Order: ParseOrder(r),
Page: ParsePage(r), Page: ParsePage(r),
Sort: ParseSortSpace(r), Sort: ParseSortSpace(r),
@ -98,11 +105,21 @@ func ParseSpaceFilter(r *http.Request) types.SpaceFilter {
} }
// ParseRepoFilter extracts the repository query parameter from the url. // ParseRepoFilter extracts the repository query parameter from the url.
func ParseRepoFilter(r *http.Request) types.RepoFilter { func ParseRepoFilter(r *http.Request) *types.RepoFilter {
return types.RepoFilter{ return &types.RepoFilter{
Order: ParseOrder(r), Order: ParseOrder(r),
Page: ParsePage(r), Page: ParsePage(r),
Sort: ParseSortRepo(r), Sort: ParseSortRepo(r),
Size: ParseSize(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),
}
}

View File

@ -10,6 +10,17 @@ import (
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
/*
* An abstraction of an entity thats responsible for authenticating users
* that are making calls via HTTP.
*/
type Authenticator interface { 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) Authenticate(r *http.Request) (*types.User, error)
} }

View File

@ -13,6 +13,9 @@ import (
var _ authn.Authenticator = (*Authenticator)(nil) var _ authn.Authenticator = (*Authenticator)(nil)
/*
* An authenticator that validates access token provided by harness SAAS.
*/
type Authenticator struct { type Authenticator struct {
// some config to validate jwt // some config to validate jwt
} }

View File

@ -21,6 +21,10 @@ import (
var _ Authenticator = (*TokenAuthenticator)(nil) 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 { type TokenAuthenticator struct {
users store.UserStore users store.UserStore
} }

View File

@ -9,7 +9,25 @@ import (
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
/*
* An abstraction of an entity responsible for authorizing access to resources.
*/
type Authorizer interface { 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) 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) CheckAll(principalType enum.PrincipalType, principalId string, permissionChecks ...*types.PermissionCheck) (bool, error)
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/harness/gitness/types/errs"
) )
var _ authz.Authorizer = (*Authorizer)(nil) 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) { func (a *Authorizer) CheckAll(principalType enum.PrincipalType, principalId string, permissionChecks ...*types.PermissionCheck) (bool, error) {
if len(permissionChecks) == 0 { if len(permissionChecks) == 0 {
return false, fmt.Errorf("No permission checks provided.") return false, errs.NoPermissionCheckProvided
} }
requestDto, err := createAclRequest(principalType, principalId, permissionChecks) requestDto, err := createAclRequest(principalType, principalId, permissionChecks)
@ -147,8 +148,7 @@ func checkAclResponse(permissionChecks []*types.PermissionCheck, responseDto acl
} }
if !permissionPermitted { if !permissionPermitted {
return false, fmt.Errorf( return false, fmt.Errorf("Permission '%s' is not permitted according to ACL (correlationId: '%s')",
"Permission '%s' is not permitted according to ACL (correlationId: '%s').",
check.Permission, check.Permission,
responseDto.CorrelationID) responseDto.CorrelationID)
} }
@ -164,7 +164,7 @@ func mapScope(scope types.Scope) (*aclResourceScope, error) {
* Harness embeded structure is mapped to the following scm space: * Harness embeded structure is mapped to the following scm space:
* {Account}/{Organization}/{Project} * {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. * 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 * For controlling access to any child resources of a repository, harness doesn't have a matching
* structure out of the box (e.g. branches, ...) * 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 * TODO: Handle scope.Repository in harness embedded mode
*/ */
harnessIdentifiers := strings.Split(scope.SpaceFqn, "/") harnessIdentifiers := strings.Split(scope.SpacePath, "/")
if len(harnessIdentifiers) > 3 { 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{} aclScope := &aclResourceScope{}

View File

@ -13,12 +13,15 @@ import (
var _ Authorizer = (*UnsafeAuthorizer)(nil) var _ Authorizer = (*UnsafeAuthorizer)(nil)
/*
* An unsafe authorizer that gives permits any action and simply logs the permission request.
*/
type UnsafeAuthorizer struct{}
func NewUnsafeAuthorizer() Authorizer { func NewUnsafeAuthorizer() Authorizer {
return &UnsafeAuthorizer{} 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) { func (a *UnsafeAuthorizer) Check(principalType enum.PrincipalType, principalId string, scope *types.Scope, resource *types.Resource, permission enum.Permission) (bool, error) {
fmt.Printf( fmt.Printf(
"[Authz] %s '%s' requests %s for %s '%s' in scope %v\n", "[Authz] %s '%s' requests %s for %s '%s' in scope %v\n",

48
internal/paths/paths.go Normal file
View File

@ -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
}

View File

@ -28,7 +28,7 @@ import (
/* /*
* Mounts the Rest API Router under mountPath (path has to end with ). * 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( func newApiHandler(
mountPath string, mountPath string,
@ -77,13 +77,13 @@ func newApiHandler(
}) })
}) })
// Generate list of all path prefixes that expect terminated FQNs // Generate list of all path prefixes that expect terminated Paths
terminatedFQNPrefixes := []string{ terminatedPathPrefixes := []string{
mountPath + "/v1/spaces", mountPath + "/v1/spaces",
mountPath + "/v1/repos", mountPath + "/v1/repos",
} }
return encode.TerminatedFqnBefore(terminatedFQNPrefixes, r.ServeHTTP), nil return encode.TerminatedPathBefore(terminatedPathPrefixes, r.ServeHTTP), nil
} }
func setupRoutesV1( func setupRoutesV1(
@ -97,27 +97,38 @@ func setupRoutesV1(
// SPACES // SPACES
r.Route("/spaces", func(r chi.Router) { 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.Post("/", handler_space.HandleCreate(guard, spaceStore))
r.Route(fmt.Sprintf("/{%s}", request.SpaceRefParamName), func(r chi.Router) { r.Route(fmt.Sprintf("/{%s}", request.SpaceRefParamName), func(r chi.Router) {
// resolves the space and stores in the context // resolves the space and stores in the context
r.Use(space.Required(spaceStore)) r.Use(space.Required(spaceStore))
// space level operations // space operations
r.Get("/", handler_space.HandleFind(guard, spaceStore)) 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)) 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("/spaces", handler_space.HandleList(guard, spaceStore))
r.Get("/repos", handler_space.HandleListRepos(guard, repoStore)) 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 // REPOS
r.Route("/repos", func(r chi.Router) { 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.Post("/", handler_repo.HandleCreate(guard, spaceStore, repoStore))
r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) { r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) {
@ -126,8 +137,21 @@ func setupRoutesV1(
// repo level operations // repo level operations
r.Get("/", handler_repo.HandleFind(guard, repoStore)) 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.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))
})
})
}) })
}) })

View File

@ -22,7 +22,7 @@ import (
/* /*
* Mounts the GIT Router under mountPath. * 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( func newGitHandler(
mountPath string, 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) { func stubGitHandler(w http.ResponseWriter, r *http.Request) {
@ -97,7 +97,7 @@ func stubGitHandler(w http.ResponseWriter, r *http.Request) {
" Method: '%s'\n"+ " Method: '%s'\n"+
" Path: '%s'\n"+ " Path: '%s'\n"+
" Query: '%s'", " Query: '%s'",
rep.DisplayName, rep.Fqn, rep.DisplayName, rep.Path,
r.Method, r.Method,
r.URL.Path, r.URL.Path,
r.URL.RawQuery, r.URL.RawQuery,

View File

@ -14,7 +14,7 @@ import (
/* /*
* Mounts the WEB Router under mountPath. * 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( func newWebHandler(
mountPath string, mountPath string,
@ -61,6 +61,6 @@ func newWebHandler(
) )
}) })
// web doesn't have any prefixes for terminated fqns // web doesn't have any prefixes for terminated paths
return encode.TerminatedFqnBefore([]string{""}, r.ServeHTTP), nil return encode.TerminatedPathBefore([]string{""}, r.ServeHTTP), nil
} }

View File

@ -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)
);

View File

@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS repositories (
repo_id SERIAL PRIMARY KEY repo_id SERIAL PRIMARY KEY
,repo_name TEXT ,repo_name TEXT
,repo_spaceId INTEGER ,repo_spaceId INTEGER
,repo_fqn TEXT ,repo_path TEXT
,repo_displayName TEXT ,repo_displayName TEXT
,repo_description TEXT ,repo_description TEXT
,repo_isPublic BOOLEAN ,repo_isPublic BOOLEAN
@ -14,5 +14,5 @@ CREATE TABLE IF NOT EXISTS repositories (
,repo_numPulls INTEGER ,repo_numPulls INTEGER
,repo_numClosedPulls INTEGER ,repo_numClosedPulls INTEGER
,repo_numOpenPulls INTEGER ,repo_numOpenPulls INTEGER
,UNIQUE(repo_fqn) ,UNIQUE(repo_path)
); );

View File

@ -1,7 +1,6 @@
CREATE TABLE IF NOT EXISTS spaces ( CREATE TABLE IF NOT EXISTS spaces (
space_id SERIAL PRIMARY KEY space_id SERIAL PRIMARY KEY
,space_name TEXT ,space_name TEXT
,space_fqn TEXT
,space_parentId INTEGER ,space_parentId INTEGER
,space_displayName TEXT ,space_displayName TEXT
,space_description TEXT ,space_description TEXT
@ -9,5 +8,4 @@ CREATE TABLE IF NOT EXISTS spaces (
,space_createdBy INTEGER ,space_createdBy INTEGER
,space_created INTEGER ,space_created INTEGER
,space_updated INTEGER ,space_updated INTEGER
,UNIQUE(space_fqn)
); );

View File

@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS paths_targetType_targetId
ON paths(path_targetType, path_targetId);

View File

@ -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)
);

View File

@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS repositories (
repo_id INTEGER PRIMARY KEY AUTOINCREMENT repo_id INTEGER PRIMARY KEY AUTOINCREMENT
,repo_name TEXT COLLATE NOCASE ,repo_name TEXT COLLATE NOCASE
,repo_spaceId INTEGER ,repo_spaceId INTEGER
,repo_fqn TEXT COLLATE NOCASE ,repo_path TEXT COLLATE NOCASE
,repo_displayName TEXT ,repo_displayName TEXT
,repo_description TEXT ,repo_description TEXT
,repo_isPublic BOOLEAN ,repo_isPublic BOOLEAN
@ -14,5 +14,5 @@ CREATE TABLE IF NOT EXISTS repositories (
,repo_numPulls INTEGER ,repo_numPulls INTEGER
,repo_numClosedPulls INTEGER ,repo_numClosedPulls INTEGER
,repo_numOpenPulls INTEGER ,repo_numOpenPulls INTEGER
,UNIQUE(repo_fqn COLLATE NOCASE) ,UNIQUE(repo_path COLLATE NOCASE)
); );

View File

@ -1,7 +1,6 @@
CREATE TABLE IF NOT EXISTS spaces ( CREATE TABLE IF NOT EXISTS spaces (
space_id INTEGER PRIMARY KEY AUTOINCREMENT space_id INTEGER PRIMARY KEY AUTOINCREMENT
,space_name TEXT COLLATE NOCASE ,space_name TEXT COLLATE NOCASE
,space_fqn TEXT COLLATE NOCASE
,space_parentId INTEGER ,space_parentId INTEGER
,space_displayName TEXT ,space_displayName TEXT
,space_description TEXT ,space_description TEXT
@ -9,5 +8,4 @@ CREATE TABLE IF NOT EXISTS spaces (
,space_createdBy INTEGER ,space_createdBy INTEGER
,space_created INTEGER ,space_created INTEGER
,space_updated INTEGER ,space_updated INTEGER
,UNIQUE(space_fqn COLLATE NOCASE)
); );

View File

@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS paths_targetType_targetId
ON paths(path_targetType, path_targetId);

View File

@ -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
`

View File

@ -7,10 +7,14 @@ package database
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/harness/gitness/types/errs"
"github.com/pkg/errors"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -30,66 +34,204 @@ type RepoStore struct {
// Finds the repo by id. // Finds the repo by id.
func (s *RepoStore) Find(ctx context.Context, id int64) (*types.Repository, error) { func (s *RepoStore) Find(ctx context.Context, id int64) (*types.Repository, error) {
dst := new(types.Repository) dst := new(types.Repository)
err := s.db.Get(dst, repoSelectID, id) if err := s.db.GetContext(ctx, dst, repoSelectById, id); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Select query failed")
}
return dst, nil
} }
// Finds the repo by the full qualified repo name. // Finds the repo by path.
func (s *RepoStore) FindFqn(ctx context.Context, fqn string) (*types.Repository, error) { func (s *RepoStore) FindByPath(ctx context.Context, path string) (*types.Repository, error) {
dst := new(types.Repository) dst := new(types.Repository)
err := s.db.Get(dst, repoSelectFqn, fqn) if err := s.db.GetContext(ctx, dst, repoSelectByPath, path); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Select query failed")
}
return dst, nil
} }
// Creates a new repo // Creates a new repo
func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error { func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error {
// TODO: Ensure parent exists!! tx, err := s.db.BeginTxx(ctx, nil)
// TODO: Ensure forkId exists! 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) query, arg, err := s.db.BindNamed(repoInsert, repo)
if err != nil { 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. // Updates the repo details.
func (s *RepoStore) Update(ctx context.Context, repo *types.Repository) error { func (s *RepoStore) Update(ctx context.Context, repo *types.Repository) error {
query, arg, err := s.db.BindNamed(repoUpdate, repo) query, arg, err := s.db.BindNamed(repoUpdate, repo)
if err != nil { 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. // Deletes the repo.
func (s *RepoStore) Delete(ctx context.Context, id int64) error { 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 { if err != nil {
return err return wrapSqlErrorf(err, "Failed to start a new transaction")
} }
defer tx.Rollback() defer tx.Rollback()
// delete the repo // delete all paths
if _, err := tx.Exec(repoDelete, id); err != nil { err = DeleteAllPaths(ctx, tx, enum.PathTargetTypeRepo, id)
return err 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. // 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{} dst := []*types.Repository{}
// if the user does not provide any customer filter // if the user does not provide any customer filter
// or sorting we use the default select statement. // or sorting we use the default select statement.
if opts.Sort == enum.RepoAttrNone { if opts.Sort == enum.RepoAttrNone {
err := s.db.Select(&dst, repoSelect, spaceId, limit(opts.Size), offset(opts.Page, opts.Size)) err := s.db.SelectContext(ctx, &dst, repoSelect, spaceId, limit(opts.Size), offset(opts.Page, opts.Size))
return dst, err if err != nil {
return nil, wrapSqlErrorf(err, "Failed executing default list query")
}
return dst, nil
} }
// else we construct the sql statement. // 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.Limit(uint64(limit(opts.Size)))
stmt = stmt.Offset(uint64(offset(opts.Page, 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()) stmt = stmt.OrderBy("repo_id " + opts.Order.String())
case enum.RepoAttrName: case enum.RepoAttrName:
stmt = stmt.OrderBy("repo_name " + opts.Order.String()) stmt = stmt.OrderBy("repo_name " + opts.Order.String())
case enum.RepoAttrFqn: case enum.RepoAttrDisplayName:
stmt = stmt.OrderBy("repo_fqn " + opts.Order.String()) stmt = stmt.OrderBy("repo_displayName " + opts.Order.String())
case enum.RepoAttrPath:
stmt = stmt.OrderBy("repo_path " + opts.Order.String())
} }
sql, _, err := stmt.ToSql() sql, _, err := stmt.ToSql()
if err != nil { if err != nil {
return dst, err return nil, errors.Wrap(err, "Failed to convert query to sql")
} }
err = s.db.Select(&dst, sql) if err = s.db.SelectContext(ctx, &dst, sql); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Failed executing custom list query")
}
return dst, nil
} }
// Count of repos in a space. // List returns a list of all paths of a repo.
func (s *RepoStore) Count(ctx context.Context, spaceId int64) (int64, error) { func (s *RepoStore) ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) {
var count int64 return ListPaths(ctx, s.db, enum.PathTargetTypeRepo, id, opts)
err := s.db.QueryRow(repoCount, spaceId).Scan(&count)
return count, err
} }
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 SELECT
repo_id repo_id
,repo_name ,repo_name
,repo_spaceId ,repo_spaceId
,repo_fqn ,paths.path_value AS repo_path
,repo_displayName ,repo_displayName
,repo_description ,repo_description
,repo_isPublic ,repo_isPublic
@ -142,11 +309,17 @@ repo_id
,repo_numPulls ,repo_numPulls
,repo_numClosedPulls ,repo_numClosedPulls
,repo_numOpenPulls ,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 WHERE repo_spaceId = $1
ORDER BY repo_fqn ASC ORDER BY repo_name ASC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
` `
@ -156,12 +329,14 @@ FROM repositories
WHERE repo_spaceId = $1 WHERE repo_spaceId = $1
` `
const repoSelectID = repoBase + ` const repoSelectById = repoSelectBaseWithJoin + `
WHERE repo_id = $1 WHERE repo_id = $1
` `
const repoSelectFqn = repoBase + ` const repoSelectByPath = repoSelectBase + `
WHERE repo_fqn = $1 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 = ` const repoDelete = `
@ -173,7 +348,6 @@ const repoInsert = `
INSERT INTO repositories ( INSERT INTO repositories (
repo_name repo_name
,repo_spaceId ,repo_spaceId
,repo_fqn
,repo_displayName ,repo_displayName
,repo_description ,repo_description
,repo_isPublic ,repo_isPublic
@ -188,7 +362,6 @@ INSERT INTO repositories (
) values ( ) values (
:repo_name :repo_name
,:repo_spaceId ,:repo_spaceId
,:repo_fqn
,:repo_displayName ,:repo_displayName
,:repo_description ,:repo_description
,:repo_isPublic ,:repo_isPublic
@ -216,3 +389,11 @@ SET
,repo_numOpenPulls = :repo_numOpenPulls ,repo_numOpenPulls = :repo_numOpenPulls
WHERE repo_id = :repo_id WHERE repo_id = :repo_id
` `
const repoUpdateNameAndSpaceId = `
UPDATE repositories
SET
repo_name = $1
,repo_spaceId = $2
WHERE repo_id = $3
`

View File

@ -33,11 +33,11 @@ func (s *RepoStoreSync) Find(ctx context.Context, id int64) (*types.Repository,
return s.base.Find(ctx, id) return s.base.Find(ctx, id)
} }
// Finds the repo by the full qualified repo name. // Finds the repo by path.
func (s *RepoStoreSync) FindFqn(ctx context.Context, fqn string) (*types.Repository, error) { func (s *RepoStoreSync) FindByPath(ctx context.Context, path string) (*types.Repository, error) {
mutex.RLock() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()
return s.base.FindFqn(ctx, fqn) return s.base.FindByPath(ctx, path)
} }
// Creates a new repo // 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) 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. // Updates the repo details.
func (s *RepoStoreSync) Update(ctx context.Context, repo *types.Repository) error { func (s *RepoStoreSync) Update(ctx context.Context, repo *types.Repository) error {
mutex.RLock() mutex.RLock()
@ -61,16 +68,35 @@ func (s *RepoStoreSync) Delete(ctx context.Context, id int64) error {
return s.base.Delete(ctx, id) 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. // Count of repos in a space.
func (s *RepoStoreSync) Count(ctx context.Context, spaceId int64) (int64, error) { func (s *RepoStoreSync) Count(ctx context.Context, spaceId int64) (int64, error) {
mutex.RLock() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()
return s.base.Count(ctx, spaceId) 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)
}

View File

@ -6,12 +6,16 @@ package database
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"time"
"github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/harness/gitness/types/errs"
"github.com/pkg/errors"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -31,74 +35,233 @@ type SpaceStore struct {
// Finds the space by id. // Finds the space by id.
func (s *SpaceStore) Find(ctx context.Context, id int64) (*types.Space, error) { func (s *SpaceStore) Find(ctx context.Context, id int64) (*types.Space, error) {
dst := new(types.Space) dst := new(types.Space)
err := s.db.Get(dst, spaceSelectID, id) if err := s.db.GetContext(ctx, dst, spaceSelectById, id); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Select query failed")
}
return dst, nil
} }
// Finds the space by the full qualified space name. // Finds the space by path.
func (s *SpaceStore) FindFqn(ctx context.Context, fqn string) (*types.Space, error) { func (s *SpaceStore) FindByPath(ctx context.Context, path string) (*types.Space, error) {
dst := new(types.Space) dst := new(types.Space)
err := s.db.Get(dst, spaceSelectFqn, fqn) if err := s.db.GetContext(ctx, dst, spaceSelectByPath, path); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Select query failed")
}
return dst, nil
} }
// Creates a new space // Creates a new space
func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error { 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) query, arg, err := s.db.BindNamed(spaceInsert, space)
if err != nil { 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. // Updates the space details.
func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error { func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
query, arg, err := s.db.BindNamed(spaceUpdate, space) query, arg, err := s.db.BindNamed(spaceUpdate, space)
if err != nil { 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. // Deletes the space.
func (s *SpaceStore) Delete(ctx context.Context, id int64) error { 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 { if err != nil {
return err return wrapSqlErrorf(err, "Failed to start a new transaction")
} }
defer tx.Rollback() defer tx.Rollback()
// ensure there are no child spaces // get primary path
var count int64 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 { 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 { } else if count > 0 {
// TODO: still returns 500 // 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 // delete the space
if _, err := tx.Exec(spaceDelete, id); err != nil { 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. // 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{} dst := []*types.Space{}
// if the user does not provide any customer filter // if the user does not provide any customer filter
// or sorting we use the default select statement. // or sorting we use the default select statement.
if opts.Sort == enum.SpaceAttrNone { if opts.Sort == enum.SpaceAttrNone {
err := s.db.Select(&dst, spaceSelect, id, limit(opts.Size), offset(opts.Page, opts.Size)) err := s.db.SelectContext(ctx, &dst, spaceSelect, id, limit(opts.Size), offset(opts.Page, opts.Size))
return dst, err if err != nil {
return nil, wrapSqlErrorf(err, "Failed executing default list query")
}
return dst, nil
} }
// else we construct the sql statement. // 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.Limit(uint64(limit(opts.Size)))
stmt = stmt.Offset(uint64(offset(opts.Page, 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()) stmt = stmt.OrderBy("space_id " + opts.Order.String())
case enum.SpaceAttrName: case enum.SpaceAttrName:
stmt = stmt.OrderBy("space_name " + opts.Order.String()) stmt = stmt.OrderBy("space_name " + opts.Order.String())
case enum.SpaceAttrFqn: case enum.SpaceAttrPath:
stmt = stmt.OrderBy("space_fqn " + opts.Order.String()) stmt = stmt.OrderBy("space_path " + opts.Order.String())
} }
sql, _, err := stmt.ToSql() sql, _, err := stmt.ToSql()
if err != nil { if err != nil {
return dst, err return nil, errors.Wrap(err, "Failed to convert query to sql")
} }
err = s.db.Select(&dst, sql) if err = s.db.SelectContext(ctx, &dst, sql); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Failed executing custom list query")
}
return dst, nil
} }
// Count the child spaces of a space. // List returns a list of all paths of a space.
func (s *SpaceStore) Count(ctx context.Context, id int64) (int64, error) { func (s *SpaceStore) ListAllPaths(ctx context.Context, id int64, opts *types.PathFilter) ([]*types.Path, error) {
var count int64 return ListPaths(ctx, s.db, enum.PathTargetTypeSpace, id, opts)
err := s.db.QueryRow(spaceCount, id).Scan(&count)
return count, err
} }
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 SELECT
space_id space_id
,space_name ,space_name
,space_fqn ,paths.path_value AS space_path
,space_parentId ,space_parentId
,space_displayName ,space_displayName
,space_description ,space_description
@ -146,12 +332,17 @@ SELECT
,space_createdBy ,space_createdBy
,space_created ,space_created
,space_updated ,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 WHERE space_parentId = $1
ORDER BY space_fqn ASC ORDER BY space_name ASC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
` `
@ -161,12 +352,14 @@ FROM spaces
WHERE space_parentId = $1 WHERE space_parentId = $1
` `
const spaceSelectID = spaceBase + ` const spaceSelectById = spaceSelectBaseWithJoin + `
WHERE space_id = $1 WHERE space_id = $1
` `
const spaceSelectFqn = spaceBase + ` const spaceSelectByPath = spaceSelectBase + `
WHERE space_fqn = $1 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 = ` const spaceDelete = `
@ -177,7 +370,6 @@ WHERE space_id = $1
const spaceInsert = ` const spaceInsert = `
INSERT INTO spaces ( INSERT INTO spaces (
space_name space_name
,space_fqn
,space_parentId ,space_parentId
,space_displayName ,space_displayName
,space_description ,space_description
@ -187,7 +379,6 @@ INSERT INTO spaces (
,space_updated ,space_updated
) values ( ) values (
:space_name :space_name
,:space_fqn
,:space_parentId ,:space_parentId
,:space_displayName ,:space_displayName
,:space_description ,:space_description
@ -207,3 +398,11 @@ space_displayName = :space_displayName
,space_updated = :space_updated ,space_updated = :space_updated
WHERE space_id = :space_id WHERE space_id = :space_id
` `
const spaceUpdateNameAndParentId = `
UPDATE spaces
SET
space_name = $1
,space_parentId = $2
WHERE space_id = $3
`

View File

@ -33,11 +33,11 @@ func (s *SpaceStoreSync) Find(ctx context.Context, id int64) (*types.Space, erro
return s.base.Find(ctx, id) return s.base.Find(ctx, id)
} }
// Finds the space by the full qualified space name. // Finds the space by path.
func (s *SpaceStoreSync) FindFqn(ctx context.Context, fqn string) (*types.Space, error) { func (s *SpaceStoreSync) FindByPath(ctx context.Context, path string) (*types.Space, error) {
mutex.RLock() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()
return s.base.FindFqn(ctx, fqn) return s.base.FindByPath(ctx, path)
} }
// Creates a new space // 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) 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. // Updates the space details.
func (s *SpaceStoreSync) Update(ctx context.Context, space *types.Space) error { func (s *SpaceStoreSync) Update(ctx context.Context, space *types.Space) error {
mutex.RLock() mutex.RLock()
@ -61,16 +68,35 @@ func (s *SpaceStoreSync) Delete(ctx context.Context, id int64) error {
return s.base.Delete(ctx, id) 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. // Count the child spaces of a space.
func (s *SpaceStoreSync) Count(ctx context.Context, id int64) (int64, error) { func (s *SpaceStoreSync) Count(ctx context.Context, id int64) (int64, error) {
mutex.RLock() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()
return s.base.Count(ctx, id) 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)
}

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/harness/gitness/internal/store/database/migrate" "github.com/harness/gitness/internal/store/database/migrate"
"github.com/pkg/errors"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -24,14 +25,14 @@ var builder = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
func Connect(driver, datasource string) (*sqlx.DB, error) { func Connect(driver, datasource string) (*sqlx.DB, error) {
db, err := sql.Open(driver, datasource) db, err := sql.Open(driver, datasource)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "Failed to open the db")
} }
dbx := sqlx.NewDb(db, driver) dbx := sqlx.NewDb(db, driver)
if err := pingDatabase(dbx); err != nil { 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 { if err := setupDatabase(dbx); err != nil {
return nil, err return nil, errors.Wrap(err, "Failed to setup the db")
} }
return dbx, nil return dbx, nil
} }

View File

@ -3,7 +3,6 @@
"id": 1, "id": 1,
"name": "repo1", "name": "repo1",
"spaceId": 1, "spaceId": 1,
"fqn": "space1/repo1",
"displayName": "Repository 1", "displayName": "Repository 1",
"description": "Some repository.", "description": "Some repository.",
"isPublic": true, "isPublic": true,
@ -20,7 +19,6 @@
"id": 2, "id": 2,
"name": "repo2", "name": "repo2",
"spaceId": 2, "spaceId": 2,
"fqn": "space1/space2/repo2",
"displayName": "Repository 2", "displayName": "Repository 2",
"description": "Some other repository.", "description": "Some other repository.",
"isPublic": true, "isPublic": true,

View File

@ -2,7 +2,6 @@
{ {
"id": 1, "id": 1,
"name": "space1", "name": "space1",
"fqn": "space1",
"parentId": 0, "parentId": 0,
"displayName": "Space 1", "displayName": "Space 1",
"description": "Some space.", "description": "Some space.",
@ -14,7 +13,6 @@
{ {
"id": 2, "id": 2,
"name": "space2", "name": "space2",
"fqn": "space1/space2",
"parentId": 1, "parentId": 1,
"displayName": "Space 2", "displayName": "Space 2",
"description": "Some subspace.", "description": "Some subspace.",

View File

@ -11,6 +11,7 @@ import (
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/pkg/errors"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -31,15 +32,19 @@ type UserStore struct {
// Find finds the user by id. // Find finds the user by id.
func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) { func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) {
dst := new(types.User) dst := new(types.User)
err := s.db.Get(dst, userSelectID, id) if err := s.db.GetContext(ctx, dst, userSelectID, id); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Select by id query failed")
}
return dst, nil
} }
// FindEmail finds the user by email. // FindEmail finds the user by email.
func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) { func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) {
dst := new(types.User) dst := new(types.User)
err := s.db.Get(dst, userSelectEmail, email) if err := s.db.GetContext(ctx, dst, userSelectEmail, email); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Select by email query failed")
}
return dst, nil
} }
// FindKey finds the user unique key (email or id). // 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. // 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{} dst := []*types.User{}
// if the user does not provide any customer filter // if the user does not provide any customer filter
// or sorting we use the default select statement. // or sorting we use the default select statement.
if opts.Sort == enum.UserAttrNone { if opts.Sort == enum.UserAttrNone {
err := s.db.Select(&dst, userSelect, limit(opts.Size), offset(opts.Page, opts.Size)) err := s.db.SelectContext(ctx, &dst, userSelect, limit(opts.Size), offset(opts.Page, opts.Size))
return dst, err if err != nil {
return nil, wrapSqlErrorf(err, "Failed executing default list query")
}
return dst, nil
} }
// else we construct the sql statement. // 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() sql, _, err := stmt.ToSql()
if err != nil { if err != nil {
return dst, err return nil, errors.Wrap(err, "Failed to convert query to sql")
} }
err = s.db.Select(&dst, sql) if err = s.db.SelectContext(ctx, &dst, sql); err != nil {
return dst, err return nil, wrapSqlErrorf(err, "Failed executing custom list query")
}
return dst, nil
} }
// Create saves the user details. // Create saves the user details.
func (s *UserStore) Create(ctx context.Context, user *types.User) error { func (s *UserStore) Create(ctx context.Context, user *types.User) error {
query, arg, err := s.db.BindNamed(userInsert, user) query, arg, err := s.db.BindNamed(userInsert, user)
if err != nil { 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. // Update updates the user details.
func (s *UserStore) Update(ctx context.Context, user *types.User) error { func (s *UserStore) Update(ctx context.Context, user *types.User) error {
query, arg, err := s.db.BindNamed(userUpdate, user) query, arg, err := s.db.BindNamed(userUpdate, user)
if err != nil { 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 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 { func (s *UserStore) Delete(ctx context.Context, user *types.User) error {
tx, err := s.db.BeginTx(ctx, nil) tx, err := s.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
return err return wrapSqlErrorf(err, "Failed to start a new transaction")
} }
defer tx.Rollback() defer tx.Rollback()
// delete the user // delete the user
if _, err := tx.Exec(userDelete, user.ID); err != nil { if _, err := tx.ExecContext(ctx, userDelete, user.ID); err != nil {
return err return wrapSqlErrorf(err, "The delete query failed")
} }
return tx.Commit() return tx.Commit()
} }
// Count returns a count of users. // 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 var count int64
err := s.db.QueryRow(userCount).Scan(&count) err := s.db.QueryRowContext(ctx, userCount).Scan(&count)
return count, err if err != nil {
return 0, wrapSqlErrorf(err, "Failed executing count query")
}
return count, nil
} }
const userCount = ` const userCount = `

View File

@ -46,7 +46,7 @@ func (s *UserStoreSync) FindKey(ctx context.Context, key string) (*types.User, e
} }
// List returns a list of users. // 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() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()
return s.base.List(ctx, opts) return s.base.List(ctx, opts)

View File

@ -194,7 +194,7 @@ func testUserList(store store.UserStore) func(t *testing.T) {
t.Error(err) t.Error(err)
return 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 { if err != nil {
t.Error(err) t.Error(err)
return return

View File

@ -4,6 +4,14 @@
package database package database
import (
"database/sql"
"github.com/harness/gitness/types/errs"
"github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
)
// default query range limit. // default query range limit.
const defaultLimit = 100 const defaultLimit = 100
@ -26,3 +34,18 @@ func offset(page, size int) int {
page = page - 1 page = page - 1
return page * size 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)
}

View File

@ -23,9 +23,6 @@ type (
// FindKey finds the user by unique key (email or id). // FindKey finds the user by unique key (email or id).
FindKey(ctx context.Context, key string) (*types.User, error) 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 saves the user details.
Create(ctx context.Context, user *types.User) error Create(ctx context.Context, user *types.User) error
@ -35,6 +32,9 @@ type (
// Delete deletes the user. // Delete deletes the user.
Delete(ctx context.Context, user *types.User) error 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 returns a count of users.
Count(ctx context.Context) (int64, error) Count(ctx context.Context) (int64, error)
} }
@ -44,23 +44,35 @@ type (
// Finds the space by id. // Finds the space by id.
Find(ctx context.Context, id int64) (*types.Space, error) Find(ctx context.Context, id int64) (*types.Space, error)
// Finds the space by the full qualified space name. // Finds the space by its path.
FindFqn(ctx context.Context, fqn string) (*types.Space, error) FindByPath(ctx context.Context, path string) (*types.Space, error)
// Creates a new space // Creates a new space
Create(ctx context.Context, space *types.Space) error 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. // Updates the space details.
Update(ctx context.Context, space *types.Space) error Update(ctx context.Context, space *types.Space) error
// Deletes the space. // Deletes the space.
Delete(ctx context.Context, id int64) error 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 the child spaces of a space.
Count(ctx context.Context, id int64) (int64, error) 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. // RepoStore defines the repository data storage.
@ -68,23 +80,35 @@ type (
// Finds the repo by id. // Finds the repo by id.
Find(ctx context.Context, id int64) (*types.Repository, error) Find(ctx context.Context, id int64) (*types.Repository, error)
// Finds the repo by the full qualified space name. // Finds the repo by path.
FindFqn(ctx context.Context, fqn string) (*types.Repository, error) FindByPath(ctx context.Context, path string) (*types.Repository, error)
// Creates a new repo // Creates a new repo
Create(ctx context.Context, repo *types.Repository) error 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. // Updates the repo details.
Update(ctx context.Context, repo *types.Repository) error Update(ctx context.Context, repo *types.Repository) error
// Deletes the repo. // Deletes the repo.
Delete(ctx context.Context, id int64) error 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 of repos in a space.
Count(ctx context.Context, spaceId int64) (int64, error) 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. // SystemStore defines internal system metadata storage.

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/pkg/errors"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
) )
@ -29,7 +30,13 @@ func Generate(user *types.User, secret string) (string, error) {
IssuedAt: time.Now().Unix(), 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. // 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(), 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
} }

View File

@ -161,7 +161,7 @@ func (mr *MockUserStoreMockRecorder) FindKey(arg0, arg1 interface{}) *gomock.Cal
} }
// List mocks base method. // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1) ret := m.ctrl.Call(m, "List", arg0, arg1)
ret0, _ := ret[0].([]*types.User) 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) 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. // Delete mocks base method.
func (m *MockSpaceStore) Delete(arg0 context.Context, arg1 int64) error { func (m *MockSpaceStore) Delete(arg0 context.Context, arg1 int64) error {
m.ctrl.T.Helper() 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) 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. // Find mocks base method.
func (m *MockSpaceStore) Find(arg0 context.Context, arg1 int64) (*types.Space, error) { func (m *MockSpaceStore) Find(arg0 context.Context, arg1 int64) (*types.Space, error) {
m.ctrl.T.Helper() 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) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockSpaceStore)(nil).Find), arg0, arg1)
} }
// FindFqn mocks base method. // FindByPath mocks base method.
func (m *MockSpaceStore) FindFqn(arg0 context.Context, arg1 string) (*types.Space, error) { func (m *MockSpaceStore) FindByPath(arg0 context.Context, arg1 string) (*types.Space, error) {
m.ctrl.T.Helper() 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) ret0, _ := ret[0].(*types.Space)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// FindFqn indicates an expected call of FindFqn. // FindByPath indicates an expected call of FindByPath.
func (mr *MockSpaceStoreMockRecorder) FindFqn(arg0, arg1 interface{}) *gomock.Call { func (mr *MockSpaceStoreMockRecorder) FindByPath(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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. // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
ret0, _ := ret[0].([]*types.Space) 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) 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. // Update mocks base method.
func (m *MockSpaceStore) Update(arg0 context.Context, arg1 *types.Space) error { func (m *MockSpaceStore) Update(arg0 context.Context, arg1 *types.Space) error {
m.ctrl.T.Helper() 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) 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. // Delete mocks base method.
func (m *MockRepoStore) Delete(arg0 context.Context, arg1 int64) error { func (m *MockRepoStore) Delete(arg0 context.Context, arg1 int64) error {
m.ctrl.T.Helper() 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) 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. // Find mocks base method.
func (m *MockRepoStore) Find(arg0 context.Context, arg1 int64) (*types.Repository, error) { func (m *MockRepoStore) Find(arg0 context.Context, arg1 int64) (*types.Repository, error) {
m.ctrl.T.Helper() 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) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepoStore)(nil).Find), arg0, arg1)
} }
// FindFqn mocks base method. // FindByPath mocks base method.
func (m *MockRepoStore) FindFqn(arg0 context.Context, arg1 string) (*types.Repository, error) { func (m *MockRepoStore) FindByPath(arg0 context.Context, arg1 string) (*types.Repository, error) {
m.ctrl.T.Helper() 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) ret0, _ := ret[0].(*types.Repository)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// FindFqn indicates an expected call of FindFqn. // FindByPath indicates an expected call of FindByPath.
func (mr *MockRepoStoreMockRecorder) FindFqn(arg0, arg1 interface{}) *gomock.Call { func (mr *MockRepoStoreMockRecorder) FindByPath(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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. // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
ret0, _ := ret[0].([]*types.Repository) 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) 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. // Update mocks base method.
func (m *MockRepoStore) Update(arg0 context.Context, arg1 *types.Repository) error { func (m *MockRepoStore) Update(arg0 context.Context, arg1 *types.Repository) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -28,10 +28,10 @@ type Resource struct {
* Represents the scope of a permission check. * Represents the scope of a permission check.
* Notes: * 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 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) * - 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 { type Scope struct {
SpaceFqn string SpacePath string
Repo string Repo string
} }

56
types/check/common.go Normal file
View File

@ -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
}

61
types/check/path.go Normal file
View File

@ -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
}

View File

@ -5,50 +5,31 @@
package check package check
import ( import (
"errors"
"fmt" "fmt"
"regexp"
"github.com/harness/gitness/types" "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 ( var (
ErrRepoNameLength = errors.New(fmt.Sprintf("Repository name has to be between %d and %d in length.", minRepoNameLength, maxRepoNameLength)) RepositoryRequiresSpaceIdError = fmt.Errorf("SpaceId required - Repositories don't exist outside of a space.")
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-_ ].")
) )
// Repo returns true if the Repo if valid. // Repo checks the provided repository and returns an error in it isn't valid.
func Repo(repo *types.Repository) (bool, error) { func Repo(repo *types.Repository) error {
l := len(repo.Name) // validate name
if l < minRepoNameLength || l > maxRepoNameLength { if err := Name(repo.Name); err != nil {
return false, ErrRepoNameLength return err
} }
if ok, _ := regexp.Match(repoNameRegex, []byte(repo.Name)); !ok { // validate display name
return false, ErrRepoNameRegex if err := DisplayName(repo.DisplayName); err != nil {
return err
} }
l = len(repo.DisplayName) // validate repo within a space
if l < minRepoDisplayNameLength || l > maxRepoDisplayNameLength { if repo.SpaceId <= 0 {
return false, ErrRepoDisplayNameLength return RepositoryRequiresSpaceIdError
} }
if ok, _ := regexp.Match(repoDisplayNameRegex, []byte(repo.DisplayName)); !ok { return nil
return false, ErrRepoDisplayNameRegex
}
return true, nil
} }

View File

@ -5,69 +5,43 @@
package check package check
import ( import (
"errors"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/harness/gitness/types" "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 ( var (
ErrSpaceNameLength = errors.New(fmt.Sprintf("Space name has to be between %d and %d in length.", minSpaceNameLength, maxSpaceNameLength)) illegalRootSpaceNames = []string{"api"}
ErrSpaceNameRegex = errors.New("Space name has start with a letter and only contain the following [a-z0-9-_].")
ErrSpaceDisplayNameLength = errors.New(fmt.Sprintf("Space display name has to be between %d and %d in length.", minSpaceDisplayNameLength, maxSpaceDisplayNameLength)) ErrRootSpaceNameNotAllowed = fmt.Errorf("The following names are not allowed for a root space: %v", illegalRootSpaceNames)
ErrSpaceDisplayNameRegex = errors.New("Space display name has start with a letter and only contain the following [a-zA-Z0-9-_ ].") ErrInvalidParentSpaceId = fmt.Errorf("Parent space ID has to be either zero for a root space or greater than zero for a child space.")
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.")
) )
// User returns true if the User if valid. // Repo checks the provided space and returns an error in it isn't valid.
func Space(space *types.Space) (bool, error) { func Space(space *types.Space) error {
l := len(space.Name) // validate name
if l < minSpaceNameLength || l > maxSpaceNameLength { if err := Name(space.Name); err != nil {
return false, ErrSpaceNameLength return err
} }
if ok, _ := regexp.Match(spaceNameRegex, []byte(space.Name)); !ok { // validate display name
return false, ErrSpaceNameRegex if err := DisplayName(space.DisplayName); err != nil {
} return err
l = len(space.DisplayName)
if l < minSpaceDisplayNameLength || l > maxSpaceDisplayNameLength {
return false, ErrSpaceDisplayNameLength
}
if ok, _ := regexp.Match(spaceDisplayNameRegex, []byte(space.DisplayName)); !ok {
return false, ErrSpaceDisplayNameRegex
} }
if space.ParentId < 0 { if space.ParentId < 0 {
return false, ErrInvalidParentSpaceId return ErrInvalidParentSpaceId
} }
// root space specific validations // root space specific validations
if space.ParentId == 0 { if space.ParentId == 0 {
for _, p := range illegalRootSpaceNames { for _, p := range illegalRootSpaceNames {
if strings.HasPrefix(space.Name, p) { if strings.HasPrefix(space.Name, p) {
return false, ErrRootSpaceNameNotAllowed return ErrRootSpaceNameNotAllowed
} }
} }
} }
return true, nil return nil
} }

View File

@ -5,20 +5,27 @@
package check package check
import ( import (
"errors" "fmt"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
const (
minEmailLength = 1
maxEmailLength = 250
)
var ( var (
// ErrEmailLen is returned when the email address // ErrEmailLen is returned when the email address
// exceeds the maximum number of characters. // 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. // User returns true if the User if valid.
func User(user *types.User) (bool, error) { 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 false, ErrEmailLen
} }
return true, nil return true, nil

View File

@ -4,6 +4,7 @@
package enum package enum
// Represents the different types of resources that can be guarded with permissions.
type ResourceType string type ResourceType string
const ( const (
@ -12,6 +13,7 @@ const (
// ResourceType_Branch ResourceType = "BRANCH" // ResourceType_Branch ResourceType = "BRANCH"
) )
// Represents the available permissions
type Permission string type Permission string
const ( const (
@ -34,9 +36,13 @@ const (
// PermissionBranchDelete Permission = "branch_delete" // PermissionBranchDelete Permission = "branch_delete"
) )
// Represents the type of the entity requesting permission
type PrincipalType string type PrincipalType string
const ( 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" PrincipalTypeApiKey PrincipalType = "API_KEY"
) )

56
types/enum/path.go Normal file
View File

@ -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
}
}

View File

@ -14,7 +14,7 @@ const (
RepoAttrNone RepoAttr = iota RepoAttrNone RepoAttr = iota
RepoAttrId RepoAttrId
RepoAttrName RepoAttrName
RepoAttrFqn RepoAttrPath
RepoAttrDisplayName RepoAttrDisplayName
RepoAttrCreated RepoAttrCreated
RepoAttrUpdated RepoAttrUpdated
@ -28,8 +28,8 @@ func ParseRepoAtrr(s string) RepoAttr {
return RepoAttrId return RepoAttrId
case "name": case "name":
return RepoAttrName return RepoAttrName
case "fqn": case "path":
return RepoAttrFqn return RepoAttrPath
case "displayName": case "displayName":
return RepoAttrDisplayName return RepoAttrDisplayName
case "created", "created_at": case "created", "created_at":

View File

@ -14,7 +14,7 @@ const (
SpaceAttrNone SpaceAttr = iota SpaceAttrNone SpaceAttr = iota
SpaceAttrId SpaceAttrId
SpaceAttrName SpaceAttrName
SpaceAttrFqn SpaceAttrPath
SpaceAttrDisplayName SpaceAttrDisplayName
SpaceAttrCreated SpaceAttrCreated
SpaceAttrUpdated SpaceAttrUpdated
@ -28,9 +28,9 @@ func ParseSpaceAttr(s string) SpaceAttr {
return SpaceAttrId return SpaceAttrId
case "name": case "name":
return SpaceAttrName return SpaceAttrName
case "fqn": case "path":
return SpaceAttrFqn return SpaceAttrPath
case "displayName": case "displayname", "display_name":
return SpaceAttrDisplayName return SpaceAttrDisplayName
case "created", "created_at": case "created", "created_at":
return SpaceAttrCreated return SpaceAttrCreated

65
types/errs/dynamic.go Normal file
View File

@ -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}
}

25
types/errs/static.go Normal file
View File

@ -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")
)

41
types/path.go Normal file
View File

@ -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"`
}

View File

@ -8,12 +8,13 @@ import (
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
// Represents a code repository
type Repository struct { type Repository struct {
// Core properties // Core properties
ID int64 `db:"repo_id" json:"id"` ID int64 `db:"repo_id" json:"id"`
Name string `db:"repo_name" json:"name"` Name string `db:"repo_name" json:"name"`
SpaceId int64 `db:"repo_spaceId" json:"spaceId"` 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"` DisplayName string `db:"repo_displayName" json:"displayName"`
Description string `db:"repo_description" json:"description"` Description string `db:"repo_description" json:"description"`
IsPublic bool `db:"repo_isPublic" json:"isPublic"` IsPublic bool `db:"repo_isPublic" json:"isPublic"`

View File

@ -5,37 +5,16 @@
package types package types
import ( import (
"errors"
"strings"
"github.com/harness/gitness/types/enum" "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. * Represents a space.
* There isn't a one-solves-all hierarchical data structure for DBs, * 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, * 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. * 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) * PRO: Quick lookup of childs, quick lookup based on fqdn (apis)
* CON: Changing a space name requires changing all its ancestors' FQNs. * CON: Changing a space name requires changing all its ancestors' Paths.
* *
* Interesting reads: * Interesting reads:
* https://stackoverflow.com/questions/4048151/what-are-the-options-for-storing-hierarchical-data-in-a-relational-database * 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 { type Space struct {
ID int64 `db:"space_id" json:"id"` ID int64 `db:"space_id" json:"id"`
Name string `db:"space_name" json:"name"` 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"` ParentId int64 `db:"space_parentId" json:"parentId"`
DisplayName string `db:"space_displayName" json:"displayName"` DisplayName string `db:"space_displayName" json:"displayName"`
Description string `db:"space_description" json:"description"` Description string `db:"space_description" json:"description"`

View File

@ -20,49 +20,10 @@ type (
Order enum.Order `json:"direction"` 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 stores token details.
Token struct { Token struct {
Value string `json:"access_token"` Value string `json:"access_token"`
Address string `json:"uri,omitempty"` Address string `json:"uri,omitempty"`
Expires time.Time `json:"expires_at,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"`
}
) )

51
types/user.go Normal file
View File

@ -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"`
}
)