From 4668e940278fa3e23b17b65a25d4fae464bb2a27 Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Fri, 30 Sep 2022 16:22:12 -0700 Subject: [PATCH] [Harness] Adding JWT/PAT/SAT Support, Harness Clients, Inline User/ServiceAccount Creation, harness Build flag, ... (#22) This change adds the initial stepping stones for harness integration: - Authentication: JWT/PAT/SAT support - Authorization: ACL integration (acl currently denies requests as gitness hasn't been integrated yet) - Remote Clients for Token, User, ServiceAccount, ACL - User Integration: Syncs harness users during authentication if unknown - SA integration: syncs harness service accounts during authentication if unknown - Initial harness API: THIS WILL BE CHANGED IN THE FUTURE! - single harness subpackage (all marked with harness build flag) - harness & standalone wire + make build commands --- .harness.env | 18 ++ .local.env | 1 + Makefile | 24 +- cli/operations/account/register.go | 4 +- cli/operations/user/user.go | 2 + cli/server/config.go | 16 -- cli/server/harness.wire.go | 41 +++ cli/server/harness.wire_gen.go | 73 ++++++ cli/server/{wire.go => standalone.wire.go} | 8 +- .../{wire_gen.go => standalone.wire_gen.go} | 15 +- cli/util/util.go | 25 ++ client/client.go | 5 +- client/interface.go | 2 +- internal/api/handler/account/login.go | 14 +- internal/api/handler/account/register.go | 21 +- internal/api/handler/common/common.go | 1 + internal/api/handler/serviceaccount/create.go | 1 + internal/api/handler/users/create.go | 14 +- internal/api/handler/users/delete.go | 5 +- internal/api/handler/users/update.go | 12 +- .../api/middleware/accesslog/accesslog.go | 2 - internal/api/middleware/authn/authn.go | 2 +- internal/api/middleware/encode/encode.go | 89 +++---- .../api/middleware/resolve/serviceAccount.go | 10 +- internal/api/middleware/resolve/space.go | 1 - internal/api/middleware/resolve/user.go | 8 +- internal/api/request/principal.go | 12 +- internal/api/request/util.go | 17 +- internal/auth/authn/harness/harness.go | 28 -- internal/auth/authn/token.go | 2 +- internal/auth/authn/wire.go | 8 +- internal/auth/authz/harness/authorizer.go | 243 ------------------ internal/auth/authz/harness/types.go | 52 ---- internal/auth/authz/unsafe.go | 2 +- internal/auth/authz/wire.go | 6 +- internal/auth/harness.go/harness.go | 18 -- internal/request/request.go | 63 +++++ internal/router/api.go | 50 ++-- internal/router/git.go | 80 +++--- internal/router/router.go | 102 ++++++-- .../router/translator/requestTranslator.go | 25 ++ .../translator/terminatedPathTranslator.go | 49 ++++ internal/router/translator/wire.go | 18 ++ internal/router/web.go | 76 +++--- internal/router/wire.go | 23 +- .../0001_create_table_principals.up.sql | 3 +- .../0001_create_table_principals.up.sql | 3 +- internal/store/database/serviceAccount.go | 20 +- .../store/database/serviceAccount_sync.go | 7 + internal/store/database/testdata/users.json | 2 + internal/store/database/user.go | 38 +-- internal/store/database/user_sync.go | 18 +- internal/store/database/user_test.go | 40 +-- internal/store/database/util.go | 2 +- internal/store/store.go | 11 +- mocks/mock_client.go | 8 +- mocks/mock_store.go | 14 +- types/check/common.go | 30 ++- types/check/serviceAccount.go | 5 + types/check/user.go | 5 + types/config.go | 64 ++--- types/enum/user.go | 4 +- types/enum/user_test.go | 2 +- types/principal.go | 59 +++-- types/service.go | 18 +- types/serviceAccount.go | 16 +- types/user.go | 16 +- 67 files changed, 903 insertions(+), 770 deletions(-) create mode 100644 .harness.env create mode 100644 cli/server/harness.wire.go create mode 100644 cli/server/harness.wire_gen.go rename cli/server/{wire.go => standalone.wire.go} (86%) rename cli/server/{wire_gen.go => standalone.wire_gen.go} (68%) delete mode 100644 internal/auth/authn/harness/harness.go delete mode 100644 internal/auth/authz/harness/authorizer.go delete mode 100644 internal/auth/authz/harness/types.go delete mode 100644 internal/auth/harness.go/harness.go create mode 100644 internal/request/request.go create mode 100644 internal/router/translator/requestTranslator.go create mode 100644 internal/router/translator/terminatedPathTranslator.go create mode 100644 internal/router/translator/wire.go diff --git a/.harness.env b/.harness.env new file mode 100644 index 000000000..c3a0424d5 --- /dev/null +++ b/.harness.env @@ -0,0 +1,18 @@ +GITNESS_TRACE=true +HARNESS_JWT_IDENTITY="gitness" +HARNESS_JWT_SECRET="IC04LYMBf1lDP5oeY4hupxd4HJhLmN6azUku3xEbeE3SUx5G3ZYzhbiwVtK4i7AmqyU9OZkwB4v8E9qM" +HARNESS_JWT_VALIDINMIN=1440 +HARNESS_JWT_BEARER_IDENTITY="Bearer" +HARNESS_JWT_BEARER_SECRET="dOkdsVqdRPPRJG31XU0qY4MPqmBBMk0PTAGIKM6O7TGqhjyxScIdJe80mwh5Yb5zF3KxYBHw6B3Lfzlq" +HARNESS_JWT_IDENTITY_SERVICE_IDENTITY="IdentityService" +HARNESS_JWT_IDENTITY_SERVICE_SECRET="HVSKUYqD4e5Rxu12hFDdCJKGM64sxgEynvdDhaOHaTHhwwn0K4Ttr0uoOxSsEVYNrUU" +HARNESS_JWT_MANAGER_IDENTITY="Manager" +HARNESS_JWT_MANAGER_SECRET="dOkdsVqdRPPRJG31XU0qY4MPqmBBMk0PTAGIKM6O7TGqhjyxScIdJe80mwh5Yb5zF3KxYBHw6B3Lfzlq" +HARNESS_JWT_NGMANAGER_IDENTITY="NextGenManager" +HARNESS_JWT_NGMANAGER_SECRET="IC04LYMBf1lDP5oeY4hupxd4HJhLmN6azUku3xEbeE3SUx5G3ZYzhbiwVtK4i7AmqyU9OZkwB4v8E9qM" +HARNESS_CLIENTS_ACL_SECURE=false +HARNESS_CLIENTS_ACL_BASEURL="http://localhost:9006/api" +HARNESS_CLIENTS_MANAGER_SECURE=false +HARNESS_CLIENTS_MANAGER_BASEURL="http://localhost:3457/api" +HARNESS_CLIENTS_NGMANAGER_SECURE=false +HARNESS_CLIENTS_NGMANAGER_BASEURL="http://localhost:7457" diff --git a/.local.env b/.local.env index e69de29bb..52271eb0c 100644 --- a/.local.env +++ b/.local.env @@ -0,0 +1 @@ +GITNESS_TRACE=true \ No newline at end of file diff --git a/Makefile b/Makefile index 54b809317..6afafa30b 100644 --- a/Makefile +++ b/Makefile @@ -35,13 +35,19 @@ tools: $(tools) ## Install tools required for the build mocks: $(mocks) @echo "Generating Test Mocks" -generate: $(mocks) cli/server/wire_gen.go mocks/mock_client.go +wire: cli/server/harness.wire_gen.go cli/server/standalone.wire_gen.go + +generate: $(mocks) wire mocks/mock_client.go @echo "Generating Code" build: generate ## Build the gitness service binary @echo "Building Gitness Server" go build -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./gitness . +harness-build: generate ## Build the gitness service binary for harness embedded mode + @echo "Building Gitness Server for Harness" + go build -tags=harness -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./gitness . + test: generate ## Run the go tests @echo "Running tests" go test -v -coverprofile=coverage.out ./internal/... @@ -114,9 +120,19 @@ lint: tools generate # lint the golang code # Some code generation can be slow, so we only run it if # the source file has changed. ########################################### -cli/server/wire_gen.go: cli/server/wire.go ## Update the wire dependency injection if wire.go has changed. - @echo "Updating wire_gen.go" - go generate ./cli/server/wire_gen.go +cli/server/harness.wire_gen.go: cli/server/harness.wire.go ## Update the wire dependency injection if harness.wire.go has changed. + @echo "Updating harness.wire_gen.go" + @go run github.com/google/wire/cmd/wire gen -tags=harness -output_file_prefix="harness." github.com/harness/gitness/cli/server + @perl -ni -e 'print unless /go:generate/' cli/server/harness.wire_gen.go + @perl -i -pe's/\+build !wireinject/\+build !wireinject,harness/g' cli/server/harness.wire_gen.go + @perl -i -pe's/go:build !wireinject/go:build !wireinject && harness/g' cli/server/harness.wire_gen.go + +cli/server/standalone.wire_gen.go: cli/server/standalone.wire.go ## Update the wire dependency injection if standalone.wire.go has changed. + @echo "Updating standalone.wire_gen.go" + @go run github.com/google/wire/cmd/wire gen -tags= -output_file_prefix="standalone." github.com/harness/gitness/cli/server + @perl -ni -e 'print unless /go:generate/' cli/server/standalone.wire_gen.go + @perl -i -pe's/\+build !wireinject/\+build !wireinject,!harness/g' cli/server/standalone.wire_gen.go + @perl -i -pe's/go:build !wireinject/go:build !wireinject && !harness/g' cli/server/standalone.wire_gen.go mocks/mock_client.go: internal/store/store.go client/client.go go generate mocks/mock.go diff --git a/cli/operations/account/register.go b/cli/operations/account/register.go index 3b54702d7..824cd8ef3 100644 --- a/cli/operations/account/register.go +++ b/cli/operations/account/register.go @@ -19,11 +19,11 @@ type registerCommand struct { } func (c *registerCommand) run(*kingpin.ParseContext) error { - username, password := util.Credentials() + username, name, email, password := util.Registration() httpClient := client.New(c.server) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - ts, err := httpClient.Register(ctx, username, password) + ts, err := httpClient.Register(ctx, username, name, email, password) if err != nil { return err } diff --git a/cli/operations/user/user.go b/cli/operations/user/user.go index 08b34314d..e8ecd6c0c 100644 --- a/cli/operations/user/user.go +++ b/cli/operations/user/user.go @@ -18,6 +18,8 @@ import ( ) const userTmpl = ` +uid: {{ .UID }} +name: {{ .Name }} email: {{ .Email }} admin: {{ .Admin }} ` diff --git a/cli/server/config.go b/cli/server/config.go index 094e8026b..2701e19d3 100644 --- a/cli/server/config.go +++ b/cli/server/config.go @@ -5,30 +5,14 @@ package server import ( - "os" - "github.com/harness/gitness/types" "github.com/kelseyhightower/envconfig" ) -// legacy environment variables. the key is the legacy -// variable name, and the value is the new variable name. -var legacy = map[string]string{ - // none defined -} - // load returns the system configuration from the // host environment. func load() (*types.Config, error) { - // loop through legacy environment variable and, if set - // rewrite to the new variable name. - for k, v := range legacy { - if s, ok := os.LookupEnv(k); ok { - os.Setenv(v, s) - } - } - config := new(types.Config) // read the configuration from the environment and // populate the configuration structure. diff --git a/cli/server/harness.wire.go b/cli/server/harness.wire.go new file mode 100644 index 000000000..c203f4ce8 --- /dev/null +++ b/cli/server/harness.wire.go @@ -0,0 +1,41 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +//go:build wireinject && harness +// +build wireinject,harness + +package server + +import ( + "github.com/harness/gitness/harness" + "github.com/harness/gitness/harness/auth/authn" + "github.com/harness/gitness/harness/auth/authz" + "github.com/harness/gitness/harness/client" + "github.com/harness/gitness/harness/router/translator" + "github.com/harness/gitness/internal/cron" + "github.com/harness/gitness/internal/router" + "github.com/harness/gitness/internal/server" + "github.com/harness/gitness/internal/store/database" + "github.com/harness/gitness/internal/store/memory" + "github.com/harness/gitness/types" + + "github.com/google/wire" +) + +func initSystem(config *types.Config) (*system, error) { + wire.Build( + newSystem, + database.WireSet, + memory.WireSet, + router.WireSet, + server.WireSet, + cron.WireSet, + harness.LoadConfig, + authn.WireSet, + authz.WireSet, + client.WireSet, + translator.WireSet, + ) + return &system{}, nil +} diff --git a/cli/server/harness.wire_gen.go b/cli/server/harness.wire_gen.go new file mode 100644 index 000000000..5bcd7645c --- /dev/null +++ b/cli/server/harness.wire_gen.go @@ -0,0 +1,73 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:build !wireinject && harness +// +build !wireinject,harness + +package server + +import ( + "github.com/harness/gitness/harness" + "github.com/harness/gitness/harness/auth/authn" + "github.com/harness/gitness/harness/auth/authz" + "github.com/harness/gitness/harness/client" + "github.com/harness/gitness/harness/router/translator" + "github.com/harness/gitness/internal/cron" + "github.com/harness/gitness/internal/router" + "github.com/harness/gitness/internal/server" + "github.com/harness/gitness/internal/store/database" + "github.com/harness/gitness/internal/store/memory" + "github.com/harness/gitness/types" +) + +// Injectors from harness.wire.go: + +func initSystem(config *types.Config) (*system, error) { + requestTranslator := translator.ProvideRequestTranslator() + systemStore := memory.New(config) + db, err := database.ProvideDatabase(config) + if err != nil { + return nil, err + } + userStore := database.ProvideUserStore(db) + spaceStore := database.ProvideSpaceStore(db) + repoStore := database.ProvideRepoStore(db) + tokenStore := database.ProvideTokenStore(db) + serviceAccountStore := database.ProvideServiceAccountStore(db) + harnessConfig, err := harness.LoadConfig() + if err != nil { + return nil, err + } + serviceJWTProvider, err := client.ProvideServiceJWTProvider(harnessConfig) + if err != nil { + return nil, err + } + tokenClient, err := client.ProvideTokenClient(serviceJWTProvider, harnessConfig) + if err != nil { + return nil, err + } + userClient, err := client.ProvideUserClient(serviceJWTProvider, harnessConfig) + if err != nil { + return nil, err + } + serviceAccountClient, err := client.ProvideServiceAccountClient(serviceJWTProvider, harnessConfig) + if err != nil { + return nil, err + } + authenticator, err := authn.ProvideAuthenticator(userStore, tokenClient, userClient, harnessConfig, serviceAccountClient, serviceAccountStore) + if err != nil { + return nil, err + } + aclClient, err := client.ProvideACLClient(serviceJWTProvider, harnessConfig) + if err != nil { + return nil, err + } + authorizer := authz.ProvideAuthorizer(aclClient) + handler, err := router.ProvideHTTPHandler(requestTranslator, systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer) + if err != nil { + return nil, err + } + serverServer := server.ProvideServer(config, handler) + nightly := cron.NewNightly() + serverSystem := newSystem(serverServer, nightly) + return serverSystem, nil +} diff --git a/cli/server/wire.go b/cli/server/standalone.wire.go similarity index 86% rename from cli/server/wire.go rename to cli/server/standalone.wire.go index 143d01454..29ced8668 100644 --- a/cli/server/wire.go +++ b/cli/server/standalone.wire.go @@ -2,8 +2,8 @@ // Use of this source code is governed by the Polyform Free Trial License // that can be found in the LICENSE.md file for this repository. -//go:build wireinject -// +build wireinject +//go:build wireinject && !harness +// +build wireinject,!harness package server @@ -14,6 +14,7 @@ import ( "github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/router" + "github.com/harness/gitness/internal/router/translator" "github.com/harness/gitness/internal/server" "github.com/harness/gitness/internal/store/database" "github.com/harness/gitness/internal/store/memory" @@ -24,14 +25,15 @@ import ( func initSystem(ctx context.Context, config *types.Config) (*system, error) { wire.Build( + newSystem, database.WireSet, memory.WireSet, router.WireSet, server.WireSet, cron.WireSet, - newSystem, authn.WireSet, authz.WireSet, + translator.WireSet, ) return &system{}, nil } diff --git a/cli/server/wire_gen.go b/cli/server/standalone.wire_gen.go similarity index 68% rename from cli/server/wire_gen.go rename to cli/server/standalone.wire_gen.go index 9d351bd11..b9dddd23a 100644 --- a/cli/server/wire_gen.go +++ b/cli/server/standalone.wire_gen.go @@ -1,8 +1,7 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire -//go:build !wireinject -// +build !wireinject +//go:build !wireinject && !harness +// +build !wireinject,!harness package server @@ -13,15 +12,17 @@ import ( "github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/router" + "github.com/harness/gitness/internal/router/translator" "github.com/harness/gitness/internal/server" "github.com/harness/gitness/internal/store/database" "github.com/harness/gitness/internal/store/memory" "github.com/harness/gitness/types" ) -// Injectors from wire.go: +// Injectors from standalone.wire.go: func initSystem(ctx context.Context, config *types.Config) (*system, error) { + requestTranslator := translator.ProvideRequestTranslator() systemStore := memory.New(config) db, err := database.ProvideDatabase(ctx, config) if err != nil { @@ -32,9 +33,9 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) { repoStore := database.ProvideRepoStore(db) tokenStore := database.ProvideTokenStore(db) serviceAccountStore := database.ProvideServiceAccountStore(db) - authenticator := authn.NewTokenAuthenticator(userStore, serviceAccountStore, tokenStore) - authorizer := authz.NewUnsafeAuthorizer() - handler, err := router.New(systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer) + authenticator := authn.ProvideAuthenticator(userStore, serviceAccountStore, tokenStore) + authorizer := authz.ProvideAuthorizer() + handler, err := router.ProvideHTTPHandler(requestTranslator, systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer) if err != nil { return nil, err } diff --git a/cli/util/util.go b/cli/util/util.go index 6088f74bd..d014deb03 100644 --- a/cli/util/util.go +++ b/cli/util/util.go @@ -101,6 +101,11 @@ func Config() (string, error) { ) } +// Registration returns the username, name, email and password from stdin. +func Registration() (string, string, string, string) { + return Username(), Name(), Email(), Password() +} + // Credentials returns the username and password from stdin. func Credentials() (string, string) { return Username(), Password() @@ -116,6 +121,26 @@ func Username() string { return strings.TrimSpace(username) } +// Name returns the name from stdin. +func Name() string { + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Enter Name: ") + name, _ := reader.ReadString('\n') + + return strings.TrimSpace(name) +} + +// Email returns the email from stdin. +func Email() string { + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Enter Email: ") + email, _ := reader.ReadString('\n') + + return strings.TrimSpace(email) +} + // Password returns the password from stdin. func Password() string { fmt.Print("Enter Password: ") diff --git a/client/client.go b/client/client.go index 3e6e91c76..464336b2b 100644 --- a/client/client.go +++ b/client/client.go @@ -69,9 +69,12 @@ func (c *HTTPClient) Login(ctx context.Context, username, password string) (*typ } // Register registers a new user and returns a JWT token. -func (c *HTTPClient) Register(ctx context.Context, username, password string) (*types.TokenResponse, error) { +func (c *HTTPClient) Register(ctx context.Context, + username, name, email, password string) (*types.TokenResponse, error) { form := &url.Values{} form.Add("username", username) + form.Add("name", name) + form.Add("email", email) form.Add("password", password) out := new(types.TokenResponse) uri := fmt.Sprintf("%s/api/v1/register", c.base) diff --git a/client/interface.go b/client/interface.go index 3218997b0..3ca1456e3 100644 --- a/client/interface.go +++ b/client/interface.go @@ -16,7 +16,7 @@ type Client interface { Login(ctx context.Context, username, password string) (*types.TokenResponse, error) // Register registers a new user and returns a JWT token. - Register(ctx context.Context, username, password string) (*types.TokenResponse, error) + Register(ctx context.Context, username, name, email, password string) (*types.TokenResponse, error) // Self returns the currently authenticated user. Self(ctx context.Context) (*types.User, error) diff --git a/internal/api/handler/account/login.go b/internal/api/handler/account/login.go index 7e001db15..98b5df64f 100644 --- a/internal/api/handler/account/login.go +++ b/internal/api/handler/account/login.go @@ -5,6 +5,7 @@ package account import ( + "errors" "net/http" "github.com/harness/gitness/internal/api/render" @@ -25,11 +26,14 @@ func HandleLogin(userStore store.UserStore, system store.SystemStore, tokenStore username := r.FormValue("username") password := r.FormValue("password") - user, err := userStore.FindEmail(ctx, username) + user, err := userStore.FindUID(ctx, username) + if errors.Is(err, store.ErrResourceNotFound) { + user, err = userStore.FindEmail(ctx, username) + } + if err != nil { log.Debug().Err(err). - Str("user", username). - Msg("cannot find user") + Msgf("cannot find user with '%s'", username) // always give not found error as extra security measurement. render.NotFound(w) @@ -42,7 +46,7 @@ func HandleLogin(userStore store.UserStore, system store.SystemStore, tokenStore ) if err != nil { log.Debug().Err(err). - Str("user", username). + Str("user_uid", user.UID). Msg("invalid password") render.NotFound(w) @@ -52,7 +56,7 @@ func HandleLogin(userStore store.UserStore, system store.SystemStore, tokenStore token, jwtToken, err := token.CreateUserSession(ctx, tokenStore, user, "login") if err != nil { log.Err(err). - Str("user", username). + Str("user_uid", user.UID). Msg("failed to generate token") render.InternalError(w) diff --git a/internal/api/handler/account/register.go b/internal/api/handler/account/register.go index 06ccb3d8c..7d6f91cd1 100644 --- a/internal/api/handler/account/register.go +++ b/internal/api/handler/account/register.go @@ -26,23 +26,25 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt ctx := r.Context() log := hlog.FromRequest(r) - username := r.FormValue("username") + uid := r.FormValue("username") + name := r.FormValue("name") + email := r.FormValue("email") password := r.FormValue("password") hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { log.Err(err). - Str("email", username). + Str("uid", uid). Msg("Failed to hash password") render.InternalError(w) return } - // TODO: allow to provide email and name separately ... user := &types.User{ - Name: username, - Email: username, + UID: uid, + Name: name, + Email: email, Password: string(hash), Salt: uniuri.NewLen(uniuri.UUIDLen), Created: time.Now().UnixMilli(), @@ -51,7 +53,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt if err = check.User(user); err != nil { log.Debug().Err(err). - Str("email", username). + Str("uid", uid). Msg("invalid user input") render.UserfiedErrorOrInternal(w, err) @@ -60,7 +62,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt if err = userStore.Create(ctx, user); err != nil { log.Err(err). - Str("email", username). + Str("uid", uid). Msg("Failed to create user") render.InternalError(w) @@ -74,8 +76,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt user.Admin = true if err = userStore.Update(ctx, user); err != nil { log.Err(err). - Str("email", username). - Int64("user_id", user.ID). + Str("user_uid", user.UID). Msg("Failed to enable admin user") render.InternalError(w) @@ -86,7 +87,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt token, jwtToken, err := token.CreateUserSession(ctx, tokenStore, user, "register") if err != nil { log.Err(err). - Str("user", username). + Str("user", uid). Msg("failed to generate token") render.InternalError(w) diff --git a/internal/api/handler/common/common.go b/internal/api/handler/common/common.go index edcac9f59..a6cd19b1e 100644 --- a/internal/api/handler/common/common.go +++ b/internal/api/handler/common/common.go @@ -13,6 +13,7 @@ type CreatePathRequest struct { // CreateServiceAccountRequest used for service account creation apis. type CreateServiceAccountRequest struct { + UID string `json:"uid"` Name string `json:"name"` ParentType enum.ParentResourceType `json:"parentType"` ParentID int64 `json:"parentId"` diff --git a/internal/api/handler/serviceaccount/create.go b/internal/api/handler/serviceaccount/create.go index 0588b4c6c..5a3f7f138 100644 --- a/internal/api/handler/serviceaccount/create.go +++ b/internal/api/handler/serviceaccount/create.go @@ -36,6 +36,7 @@ func HandleCreate(guard *guard.Guard, saStore store.ServiceAccountStore) http.Ha } sa := &types.ServiceAccount{ + UID: in.UID, Name: in.Name, Salt: uniuri.NewLen(uniuri.UUIDLen), Created: time.Now().UnixMilli(), diff --git a/internal/api/handler/users/create.go b/internal/api/handler/users/create.go index f86498698..9e5085fed 100644 --- a/internal/api/handler/users/create.go +++ b/internal/api/handler/users/create.go @@ -20,7 +20,9 @@ import ( ) type userCreateInput struct { - Username string `json:"email"` + UID string `json:"uid"` + Name string `json:"name"` + Email string `json:"email"` Password string `json:"password"` Admin bool `json:"admin"` } @@ -42,7 +44,7 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc { hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) if err != nil { log.Err(err). - Str("email", in.Username). + Str("uid", in.UID). Msg("Failed to hash password") render.InternalError(w) @@ -50,7 +52,9 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc { } user := &types.User{ - Email: in.Username, + UID: in.UID, + Name: in.Name, + Email: in.Email, Admin: in.Admin, Password: string(hash), Salt: uniuri.NewLen(uniuri.UUIDLen), @@ -59,7 +63,7 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc { } if err = check.User(user); err != nil { log.Debug().Err(err). - Str("email", user.Email). + Str("uid", user.UID). Msg("invalid user input") render.UserfiedErrorOrInternal(w, err) @@ -69,7 +73,7 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc { err = userStore.Create(ctx, user) if err != nil { log.Err(err). - Str("email", user.Email). + Str("uid", user.UID). Msg("failed to create user") render.UserfiedErrorOrInternal(w, err) diff --git a/internal/api/handler/users/delete.go b/internal/api/handler/users/delete.go index 7402d98c6..10f300492 100644 --- a/internal/api/handler/users/delete.go +++ b/internal/api/handler/users/delete.go @@ -30,11 +30,10 @@ func HandleDelete(userStore store.UserStore, tokenStore store.TokenStore) http.H return } - err = userStore.Delete(ctx, user) + err = userStore.Delete(ctx, user.ID) if err != nil { log.Error().Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). + Str("user_uid", user.UID). Msg("failed to delete user") render.UserfiedErrorOrInternal(w, err) diff --git a/internal/api/handler/users/update.go b/internal/api/handler/users/update.go index f718673af..66b944862 100644 --- a/internal/api/handler/users/update.go +++ b/internal/api/handler/users/update.go @@ -43,8 +43,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc { hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) if err != nil { log.Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). + Str("user_uid", user.UID). Msg("Failed to hash password") render.InternalError(w) @@ -67,8 +66,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc { hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) if err != nil { log.Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). + Str("user_uid", user.UID). Msg("Failed to hash password") render.InternalError(w) @@ -78,8 +76,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc { } if err := check.User(user); err != nil { log.Debug().Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). + Str("user_uid", user.UID). Msg("invalid user input") render.UserfiedErrorOrInternal(w, err) @@ -91,8 +88,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc { err := userStore.Update(ctx, user) if err != nil { log.Err(err). - Int64("user_id", user.ID). - Str("user_email", user.Email). + Str("user_uid", user.UID). Msg("Failed to update the usser") render.UserfiedErrorOrInternal(w, err) diff --git a/internal/api/middleware/accesslog/accesslog.go b/internal/api/middleware/accesslog/accesslog.go index e4907f9f5..12cbfc623 100644 --- a/internal/api/middleware/accesslog/accesslog.go +++ b/internal/api/middleware/accesslog/accesslog.go @@ -18,8 +18,6 @@ func HlogHandler() func(http.Handler) http.Handler { return hlog.AccessHandler( func(r *http.Request, status, size int, duration time.Duration) { hlog.FromRequest(r).Info(). - Str("method", r.Method). - Stringer("url", r.URL). Int("status_code", status). Int("response_size_bytes", size). Dur("elapsed_ms", duration). diff --git a/internal/api/middleware/authn/authn.go b/internal/api/middleware/authn/authn.go index ac5639f9f..caa0dac43 100644 --- a/internal/api/middleware/authn/authn.go +++ b/internal/api/middleware/authn/authn.go @@ -51,7 +51,7 @@ func Attempt(authenticator authn.Authenticator) func(http.Handler) http.Handler // Update the logging context and inject principal in context log.UpdateContext(func(c zerolog.Context) zerolog.Context { return c. - Int64("principal_id", session.Principal.ID). + Str("principal_uid", session.Principal.UID). Str("principal_type", string(session.Principal.Type)). Bool("principal_admin", session.Principal.Admin) }) diff --git a/internal/api/middleware/encode/encode.go b/internal/api/middleware/encode/encode.go index 3b34a49c1..2b00a28de 100644 --- a/internal/api/middleware/encode/encode.go +++ b/internal/api/middleware/encode/encode.go @@ -2,39 +2,42 @@ package encode import ( "net/http" - "net/url" "strings" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog/hlog" + "github.com/harness/gitness/internal/request" "github.com/harness/gitness/types" ) -// GitPathBefore 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 +const ( + EncodedPathSeparator = "%252F" +) + +// GitPath encodes Paths coming as part of the GIT api (e.g. "space1/repo.git") // The first prefix that matches the URL.Path will be used during encoding. -func GitPathBefore(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - r, _ = pathTerminatedWithMarker(r, "", ".git", false) - h.ServeHTTP(w, r) - } +func GitPath(r *http.Request) error { + _, err := pathTerminatedWithMarker(r, "", ".git", false) + return err } -// TerminatedPathBefore wraps an http.HandlerFunc in a layer that encodes a terminated path (e.g. "/space1/space2/+") +// TerminatedPath wraps an http.HandlerFunc in a layer that encodes a terminated path (e.g. "/space1/space2/+") // before executing the provided http.HandlerFunc. The first prefix that matches the URL.Path will -// be used during encoding. -func TerminatedPathBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - for _, p := range prefixes { - // IMPORTANT: define changed separately to avoid overshadowing r - var changed bool - if r, changed = pathTerminatedWithMarker(r, p, "/+", false); changed { - break - } +// be used during encoding (prefix is ignored during encoding). +func TerminatedPath(prefixes []string, r *http.Request) error { + for _, p := range prefixes { + changed, err := pathTerminatedWithMarker(r, p, "/+", false) + if err != nil { + return err } - h.ServeHTTP(w, r) + // first prefix that leads to success we can stop + if changed { + break + } } + + return nil } // pathTerminatedWithMarker function encodes a path followed by a custom marker and returns a request with an @@ -46,49 +49,41 @@ func TerminatedPathBefore(prefixes []string, h http.HandlerFunc) http.HandlerFun // Prefix: "" Path: "/space1/space2/+" => "/space1%2Fspace2" // Prefix: "" Path: "/space1/space2.git" => "/space1%2Fspace2" // Prefix: "/spaces" Path: "/spaces/space1/space2/+/authToken" => "/spaces/space1%2Fspace2/authToken". -func pathTerminatedWithMarker(r *http.Request, prefix string, marker string, keepMarker bool) (*http.Request, bool) { +func pathTerminatedWithMarker(r *http.Request, prefix string, marker string, keepMarker bool) (bool, error) { // In case path doesn't start with prefix - nothing to encode if len(r.URL.Path) < len(prefix) || r.URL.Path[0:len(prefix)] != prefix { - return r, false + return false, nil } originalSubPath := r.URL.Path[len(prefix):] - path, suffix, found := strings.Cut(originalSubPath, marker) + path, _, found := strings.Cut(originalSubPath, marker) // If we don't find a marker - nothing to encode if !found { - return r, false + return false, nil } - // if marker was found - convert to escaped version (skip first character in case path starts with '/') - escapedPath := path[0:1] + strings.ReplaceAll(path[1:], types.PathSeparator, "%2F") + // if marker was found - convert to escaped version (skip first character in case path starts with '/'). + // Since replacePrefix unescapes the strings, we have to double escape. + escapedPath := path[0:1] + strings.ReplaceAll(path[1:], types.PathSeparator, EncodedPathSeparator) if keepMarker { escapedPath += marker } - updatedSubPath := escapedPath + suffix - // TODO: Proper Logging - log.Debug().Msgf( - "[Encode] prefix: '%s', marker: '%s', original: '%s', updated: '%s'.\n", + prefixWithPath := prefix + path + marker + prefixWithEscapedPath := prefix + escapedPath + + hlog.FromRequest(r).Trace().Msgf( + "[Encode] prefix: '%s', marker: '%s', original: '%s', escaped: '%s'.\n", prefix, marker, - originalSubPath, - updatedSubPath) + prefixWithPath, + prefixWithEscapedPath) - /* - * Return shallow clone with updated URL, similar to http.StripPrefix or earlier version of request.WithContext - * https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/net/http/server.go;l=2138 - * https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/net/http/request.go;l=355 - * - * http.StripPrefix initially changed the path only, but that was updated because of official recommendations: - * https://github.com/golang/go/issues/18952 - */ - r2 := new(http.Request) - *r2 = *r - r2.URL = new(url.URL) - *r2.URL = *r.URL - r2.URL.Path = prefix + updatedSubPath - r2.URL.RawPath = "" + err := request.ReplacePrefix(r, prefixWithPath, prefixWithEscapedPath) + if err != nil { + return false, err + } - return r2, true + return true, nil } diff --git a/internal/api/middleware/resolve/serviceAccount.go b/internal/api/middleware/resolve/serviceAccount.go index 309bc5a28..165b1ef5e 100644 --- a/internal/api/middleware/resolve/serviceAccount.go +++ b/internal/api/middleware/resolve/serviceAccount.go @@ -25,17 +25,17 @@ func ServiceAccount(saStore store.ServiceAccountStore) func(http.Handler) http.H ctx := r.Context() log := hlog.FromRequest(r) - id, err := request.GetServiceAccountID(r) + uid, err := request.GetServiceAccountUID(r) if err != nil { - log.Info().Err(err).Msgf("Receieved no or invalid service account id") + log.Info().Err(err).Msgf("Receieved no or invalid service account uid") render.BadRequest(w) return } - sa, err := saStore.Find(ctx, id) + sa, err := saStore.FindUID(ctx, uid) if err != nil { - log.Warn().Err(err).Msgf("Failed to get service account with id '%d'.", id) + log.Warn().Err(err).Msgf("Failed to get service account with uid '%s'.", uid) render.UserfiedErrorOrInternal(w, err) return @@ -43,7 +43,7 @@ func ServiceAccount(saStore store.ServiceAccountStore) func(http.Handler) http.H // Update the logging context and inject repo in context log.UpdateContext(func(c zerolog.Context) zerolog.Context { - return c.Int64("sa_id", sa.ID).Str("sa_name", sa.Name) + return c.Str("sa_uid", sa.UID) }) next.ServeHTTP(w, r.WithContext( diff --git a/internal/api/middleware/resolve/space.go b/internal/api/middleware/resolve/space.go index ca062dd11..2084c2148 100644 --- a/internal/api/middleware/resolve/space.go +++ b/internal/api/middleware/resolve/space.go @@ -47,7 +47,6 @@ func Space(spaceStore store.SpaceStore) func(http.Handler) http.Handler { if err != nil { log.Debug().Err(err).Msgf("Failed to get space using ref '%s'.", ref) - render.UserfiedErrorOrInternal(w, err) return } diff --git a/internal/api/middleware/resolve/user.go b/internal/api/middleware/resolve/user.go index fb484ee81..226dc5a47 100644 --- a/internal/api/middleware/resolve/user.go +++ b/internal/api/middleware/resolve/user.go @@ -25,7 +25,7 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler { ctx := r.Context() log := hlog.FromRequest(r) - id, err := request.GetUserID(r) + uid, err := request.GetUserUID(r) if err != nil { log.Info().Err(err).Msgf("Receieved no or invalid user id") @@ -33,9 +33,9 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler { return } - user, err := userStore.Find(ctx, id) + user, err := userStore.FindUID(ctx, uid) if err != nil { - log.Info().Err(err).Msgf("Failed to get user with id '%d'.", id) + log.Info().Err(err).Msgf("Failed to get user with uid '%s'.", uid) render.UserfiedErrorOrInternal(w, err) return @@ -43,7 +43,7 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler { // Update the logging context and inject repo in context log.UpdateContext(func(c zerolog.Context) zerolog.Context { - return c.Int64("user_id", user.ID).Str("user_name", user.Name) + return c.Str("user_uid", user.UID) }) next.ServeHTTP(w, r.WithContext( diff --git a/internal/api/request/principal.go b/internal/api/request/principal.go index 42ec6df99..369c4ff49 100644 --- a/internal/api/request/principal.go +++ b/internal/api/request/principal.go @@ -5,14 +5,14 @@ import ( ) const ( - UserIDParamName = "userId" - ServiceAccountIDParamName = "saId" + UserUIDParamName = "userUID" + ServiceAccountUIDParamName = "saUID" ) -func GetUserID(r *http.Request) (int64, error) { - return ParseAsInt64(r, UserIDParamName) +func GetUserUID(r *http.Request) (string, error) { + return ParamOrError(r, UserUIDParamName) } -func GetServiceAccountID(r *http.Request) (int64, error) { - return ParseAsInt64(r, ServiceAccountIDParamName) +func GetServiceAccountUID(r *http.Request) (string, error) { + return ParamOrError(r, ServiceAccountUIDParamName) } diff --git a/internal/api/request/util.go b/internal/api/request/util.go index d4b6c3abb..3cfebc112 100644 --- a/internal/api/request/util.go +++ b/internal/api/request/util.go @@ -14,11 +14,22 @@ import ( "github.com/harness/gitness/types/enum" ) +// ParamOrError tries to retrieve the parameter from the request and +// returns the parameter if it exists and is not empty, otherwise returns an error. +func ParamOrError(r *http.Request, paramName string) (string, error) { + value := chi.URLParam(r, paramName) + if value == "" { + return "", fmt.Errorf("parameter '%s' not found in request", paramName) + } + + return value, nil +} + // ParseAsInt64 tries to retrieve the parameter from the request and parse it to in64. func ParseAsInt64(r *http.Request, paramName string) (int64, error) { - rawID := chi.URLParam(r, paramName) - if rawID == "" { - return 0, fmt.Errorf("parameter '%s' not found in request", paramName) + rawID, err := ParamOrError(r, paramName) + if err != nil { + return 0, err } id, err := strconv.ParseInt(rawID, 10, 64) diff --git a/internal/auth/authn/harness/harness.go b/internal/auth/authn/harness/harness.go deleted file mode 100644 index 24e25c8b1..000000000 --- a/internal/auth/authn/harness/harness.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2022 Harness Inc. All rights reserved. -// Use of this source code is governed by the Polyform Free Trial License -// that can be found in the LICENSE.md file for this repository. - -package harness - -import ( - "net/http" - - "github.com/harness/gitness/internal/auth" - "github.com/harness/gitness/internal/auth/authn" - "github.com/harness/gitness/types" -) - -var _ authn.Authenticator = (*Authenticator)(nil) - -// Authenticator that validates access token provided by harness SAAS. -type Authenticator struct { - // some config to validate jwt -} - -func NewAuthenticator() (authn.Authenticator, error) { - return &Authenticator{}, nil -} - -func (a *Authenticator) Authenticate(r *http.Request) (*auth.Session, error) { - return &auth.Session{Principal: types.Principal{}, Metadata: &auth.EmptyMetadata{}}, nil -} diff --git a/internal/auth/authn/token.go b/internal/auth/authn/token.go index c93725ac9..f5f823ce1 100644 --- a/internal/auth/authn/token.go +++ b/internal/auth/authn/token.go @@ -35,7 +35,7 @@ type TokenAuthenticator struct { func NewTokenAuthenticator( userStore store.UserStore, saStore store.ServiceAccountStore, - tokenStore store.TokenStore) Authenticator { + tokenStore store.TokenStore) *TokenAuthenticator { return &TokenAuthenticator{ userStore: userStore, saStore: saStore, diff --git a/internal/auth/authn/wire.go b/internal/auth/authn/wire.go index e03190105..bc8ca0ca8 100644 --- a/internal/auth/authn/wire.go +++ b/internal/auth/authn/wire.go @@ -6,9 +6,15 @@ package authn import ( "github.com/google/wire" + "github.com/harness/gitness/internal/store" ) // WireSet provides a wire set for this package. var WireSet = wire.NewSet( - NewTokenAuthenticator, + ProvideAuthenticator, ) + +func ProvideAuthenticator(userStore store.UserStore, saStore store.ServiceAccountStore, + tokenStore store.TokenStore) Authenticator { + return NewTokenAuthenticator(userStore, saStore, tokenStore) +} diff --git a/internal/auth/authz/harness/authorizer.go b/internal/auth/authz/harness/authorizer.go deleted file mode 100644 index ad405da9a..000000000 --- a/internal/auth/authz/harness/authorizer.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2022 Harness Inc. All rights reserved. -// Use of this source code is governed by the Polyform Free Trial License -// that can be found in the LICENSE.md file for this repository. - -package harness - -import ( - "bytes" - "context" - "crypto/tls" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - - "github.com/harness/gitness/internal/auth" - "github.com/harness/gitness/internal/auth/authz" - "github.com/harness/gitness/types" - "github.com/harness/gitness/types/enum" -) - -var _ authz.Authorizer = (*Authorizer)(nil) - -type Authorizer struct { - client *http.Client - aclEndpoint string - authToken string -} - -func NewAuthorizer(aclEndpoint, authToken string) (authz.Authorizer, error) { - // build http client - could be injected, too - tr := &http.Transport{ - // TODO: expose InsecureSkipVerify in config - TLSClientConfig: &tls.Config{ - //nolint:gosec // accept any host cert - InsecureSkipVerify: true, - }, - } - client := &http.Client{Transport: tr} - - return &Authorizer{ - client: client, - aclEndpoint: aclEndpoint, - authToken: authToken, - }, nil -} - -func (a *Authorizer) Check(ctx context.Context, session *auth.Session, - scope *types.Scope, resource *types.Resource, permission enum.Permission) (bool, error) { - return a.CheckAll(ctx, session, types.PermissionCheck{ - Scope: *scope, - Resource: *resource, - Permission: permission, - }) -} - -func (a *Authorizer) CheckAll(ctx context.Context, session *auth.Session, - permissionChecks ...types.PermissionCheck) (bool, error) { - if len(permissionChecks) == 0 { - return false, authz.ErrNoPermissionCheckProvided - } - - // TODO: Ensure that we also handle HarnessMetadata! - requestDto, err := createACLRequest(&session.Principal, permissionChecks) - if err != nil { - return false, err - } - byt, err := json.Marshal(requestDto) - if err != nil { - return false, err - } - - // TODO: accountId might be different! - url := a.aclEndpoint + "?routingId=" + requestDto.Permissions[0].ResourceScope.AccountIdentifier - httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(byt)) - if err != nil { - return false, err - } - - httpRequest.Header = http.Header{ - "Content-Type": []string{"application/json"}, - "Authorization": []string{"Bearer " + a.authToken}, - } - - response, err := a.client.Do(httpRequest) - if response != nil { - defer response.Body.Close() - } - if err != nil { - return false, err - } - - if response.StatusCode != http.StatusOK { - return false, fmt.Errorf("got unexpected status code '%d' - assume unauthorized", response.StatusCode) - } - - bodyByte, err := io.ReadAll(response.Body) - if err != nil { - return false, err - } - - var responseDto aclResponse - err = json.Unmarshal(bodyByte, &responseDto) - if err != nil { - return false, err - } - - return checkACLResponse(permissionChecks, responseDto) -} - -func createACLRequest(principal *types.Principal, - permissionChecks []types.PermissionCheck) (*aclRequest, error) { - // Generate ACL req - req := aclRequest{ - Permissions: []aclPermission{}, - Principal: aclPrincipal{ - PrincipalIdentifier: principal.ExternalID, - }, - } - - // map principaltype - actualPrincipalType, err := mapPrincipalType(principal.Type) - if err != nil { - return nil, err - } - req.Principal.PrincipalType = actualPrincipalType - - // map all permissionchecks to ACL permission checks - for _, c := range permissionChecks { - mappedPermission := mapPermission(c.Permission) - - var mappedResourceScope *aclResourceScope - mappedResourceScope, err = mapScope(c.Scope) - if err != nil { - return nil, err - } - - req.Permissions = append(req.Permissions, aclPermission{ - Permission: mappedPermission, - ResourceScope: *mappedResourceScope, - ResourceType: string(c.Resource.Type), - ResourceIdentifier: c.Resource.Name, - }) - } - - return &req, nil -} - -func checkACLResponse(permissionChecks []types.PermissionCheck, responseDto aclResponse) (bool, error) { - /* - * We are assuming two things: - * - All permission checks were made for the same principal. - * - Permissions inherit down the hierarchy (Account -> Organization -> Project -> Repository) - * - No two checks are for the same permission - is similar to ff implementation: - * https://github.com/wings-software/ff-server/blob/master/pkg/rbac/client.go#L88 - * - * Based on that, if there's any permitted result for a permission check the permission is allowed. - * Now we just have to ensure that all permissions are allowed - * - * TODO: Use resource name + scope for verifying results. - */ - - for _, check := range permissionChecks { - permissionPermitted := false - for _, ace := range responseDto.Data.AccessControlList { - if string(check.Permission) == ace.Permission && ace.Permitted { - permissionPermitted = true - break - } - } - - if !permissionPermitted { - return false, fmt.Errorf("permission '%s' is not permitted according to ACL (correlationId: '%s')", - check.Permission, - responseDto.CorrelationID) - } - } - - return true, nil -} - -func mapScope(scope types.Scope) (*aclResourceScope, error) { - /* - * ASSUMPTION: - * Harness embeded structure is mapped to the following scm space: - * {Account}/{Organization}/{Project} - * - * We can use that assumption to translate back from scope.spacePath to harness scope. - * However, this only works as long as resources exist within spaces only. - * For controlling access to any child resources of a repository, harness doesn't have a matching - * structure out of the box (e.g. branches, ...) - * - * IMPORTANT: - * For now harness embedded doesn't support scope.Repository (has to be configured on space level ...) - * - * TODO: Handle scope.Repository in harness embedded mode - */ - - const ( - accIndex = 0 - orgIndex = 1 - projectIndex = 2 - scopes = 3 - ) - - harnessIdentifiers := strings.Split(scope.SpacePath, "/") - if len(harnessIdentifiers) > scopes { - return nil, fmt.Errorf("unable to convert '%s' to harness resource scope "+ - "(expected {Account}/{Organization}/{Project} or a sub scope)", scope.SpacePath) - } - - aclScope := &aclResourceScope{} - if len(harnessIdentifiers) > accIndex { - aclScope.AccountIdentifier = harnessIdentifiers[accIndex] - } - if len(harnessIdentifiers) > orgIndex { - aclScope.OrgIdentifier = harnessIdentifiers[orgIndex] - } - if len(harnessIdentifiers) > projectIndex { - aclScope.ProjectIdentifier = harnessIdentifiers[projectIndex] - } - - return aclScope, nil -} - -func mapPermission(permission enum.Permission) string { - // harness has multiple modules - add scm prefix - return "scm_" + string(permission) -} - -func mapPrincipalType(principalType enum.PrincipalType) (string, error) { - switch principalType { - case enum.PrincipalTypeUser: - return "USER", nil - case enum.PrincipalTypeServiceAccount: - return "SERVICE_ACCOUNT", nil - case enum.PrincipalTypeService: - return "SERVICE", nil - default: - return "", fmt.Errorf("unknown principaltype '%s'", principalType) - } -} diff --git a/internal/auth/authz/harness/types.go b/internal/auth/authz/harness/types.go deleted file mode 100644 index b10bcd25c..000000000 --- a/internal/auth/authz/harness/types.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2022 Harness Inc. All rights reserved. -// Use of this source code is governed by the Polyform Free Trial License -// that can be found in the LICENSE.md file for this repository. - -package harness - -/* - * Classes required for harness ACL. - * For now keep it here, as it shouldn't even be part of the code base in the first place - * (should be in its own harness wide client library). - */ -type aclRequest struct { - Principal aclPrincipal `json:"principal"` - Permissions []aclPermission `json:"permissions"` -} - -type aclResponse struct { - Status string `json:"status"` - CorrelationID string `json:"correlationId"` - Data aclResponseData `json:"data"` -} - -type aclResponseData struct { - Principal aclPrincipal `json:"principal"` - AccessControlList []aclControlElement `json:"accessControlList"` -} - -type aclControlElement struct { - Permission string `json:"permission"` - ResourceScope aclResourceScope `json:"resourceScope,omitempty"` - ResourceType string `json:"resourceType"` - ResourceIdentifier string `json:"resourceIdentifier"` - Permitted bool `json:"permitted"` -} - -type aclResourceScope struct { - AccountIdentifier string `json:"accountIdentifier"` - OrgIdentifier string `json:"orgIdentifier,omitempty"` - ProjectIdentifier string `json:"projectIdentifier,omitempty"` -} - -type aclPermission struct { - ResourceScope aclResourceScope `json:"resourceScope,omitempty"` - ResourceType string `json:"resourceType"` - ResourceIdentifier string `json:"resourceIdentifier"` - Permission string `json:"permission"` -} - -type aclPrincipal struct { - PrincipalIdentifier string `json:"principalIdentifier"` - PrincipalType string `json:"principalType"` -} diff --git a/internal/auth/authz/unsafe.go b/internal/auth/authz/unsafe.go index ada6af118..d0cf5977f 100644 --- a/internal/auth/authz/unsafe.go +++ b/internal/auth/authz/unsafe.go @@ -21,7 +21,7 @@ var _ Authorizer = (*UnsafeAuthorizer)(nil) */ type UnsafeAuthorizer struct{} -func NewUnsafeAuthorizer() Authorizer { +func NewUnsafeAuthorizer() *UnsafeAuthorizer { return &UnsafeAuthorizer{} } diff --git a/internal/auth/authz/wire.go b/internal/auth/authz/wire.go index abfd88082..a2b54fabf 100644 --- a/internal/auth/authz/wire.go +++ b/internal/auth/authz/wire.go @@ -10,5 +10,9 @@ import ( // WireSet provides a wire set for this package. var WireSet = wire.NewSet( - NewUnsafeAuthorizer, + ProvideAuthorizer, ) + +func ProvideAuthorizer() Authorizer { + return NewUnsafeAuthorizer() +} diff --git a/internal/auth/harness.go/harness.go b/internal/auth/harness.go/harness.go deleted file mode 100644 index 851f5a1c3..000000000 --- a/internal/auth/harness.go/harness.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2022 Harness Inc. All rights reserved. -// Use of this source code is governed by the Polyform Free Trial License -// that can be found in the LICENSE.md file for this repository. - -package harness - -import "github.com/harness/gitness/types/enum" - -// Metadata is used for all harness embedded auths (apart from ssh). -type Metadata struct { - ExecutingPrincipalType enum.PrincipalType - ExecutingPrincipalID int64 -} - -// RequiresEnforcement returns true if the metadata contains authz related info. -func (m *Metadata) RequiresEnforcement() bool { - return true -} diff --git a/internal/request/request.go b/internal/request/request.go new file mode 100644 index 000000000..f6eccd467 --- /dev/null +++ b/internal/request/request.go @@ -0,0 +1,63 @@ +// 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 router provides http handlers for serving the +// web applicationa and API endpoints. +package request + +import ( + "fmt" + "net/http" + "net/url" +) + +// ReplacePrefix replaces the path of the request. +// IMPORTANT: +// - both prefix are unescaped for path, and used as is for RawPath! +// - only called by top level handler!! +func ReplacePrefix(r *http.Request, oldPrefix string, newPrefix string) error { + /* + * According to official documentation, we can change anything in the request but the body: + * https://pkg.go.dev/net/http#Handler + * + * ASSUMPTION: + * This is called by a top level handler (no router or middleware above it) + * Therefore, we don't have to worry about getting any routing metadata out of sync. + * + * This is different to returning a shallow clone with updated URL, which is what + * http.StripPrefix or earlier versions of request.WithContext are doing: + * https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/net/http/server.go;l=2138 + * https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/net/http/request.go;l=355 + * + * http.StripPrefix initially changed the path only, but that was updated because of official recommendations: + * https://github.com/golang/go/issues/18952 + */ + unOldPrefix, err := url.PathUnescape(oldPrefix) + if err != nil { + return fmt.Errorf("failed to unescape old prefix '%s'", oldPrefix) + } + unNewPrefix, err := url.PathUnescape(newPrefix) + if err != nil { + return fmt.Errorf("failed to unescape new prefix '%s'", newPrefix) + } + + unl := len(unOldPrefix) + if len(r.URL.Path) < unl || r.URL.Path[0:unl] != unOldPrefix { + return fmt.Errorf("path '%s' doesn't contain prefix '%s'", r.URL.Path, unOldPrefix) + } + + // only change RawPath if it exists + if r.URL.RawPath != "" { + l := len(oldPrefix) + if len(r.URL.RawPath) < l || r.URL.RawPath[0:l] != oldPrefix { + return fmt.Errorf("raw path '%s' doesn't contain prefix '%s'", r.URL.RawPath, oldPrefix) + } + + r.URL.RawPath = newPrefix + r.URL.RawPath[l:] + } + + r.URL.Path = unNewPrefix + r.URL.Path[unl:] + + return nil +} diff --git a/internal/router/api.go b/internal/router/api.go index 8f581cb10..2f4a5f9da 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -14,7 +14,6 @@ import ( handleruser "github.com/harness/gitness/internal/api/handler/user" "github.com/harness/gitness/internal/api/middleware/accesslog" middlewareauthn "github.com/harness/gitness/internal/api/middleware/authn" - "github.com/harness/gitness/internal/api/middleware/encode" "github.com/harness/gitness/internal/api/middleware/resolve" "github.com/harness/gitness/internal/api/request" @@ -27,15 +26,12 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/cors" "github.com/rs/zerolog/hlog" - "github.com/rs/zerolog/log" ) /* - * Mounts the Rest API Router under mountPath (path has to end with ). - * The handler is wrapped within a layer that handles encoding terminated Paths. + * newAPIHandler returns a new http handler for handling API calls. */ func newAPIHandler( - mountPath string, systemStore store.SystemStore, userStore store.UserStore, spaceStore store.SpaceStore, @@ -50,36 +46,28 @@ func newAPIHandler( // Use go-chi router for inner routing (restricted to mountPath!) r := chi.NewRouter() - r.Route(mountPath, func(r chi.Router) { - // Apply common api middleware - r.Use(middleware.NoCache) - r.Use(middleware.Recoverer) - // configure logging middleware. - r.Use(hlog.NewHandler(log.Logger)) - r.Use(hlog.URLHandler("path")) - r.Use(hlog.MethodHandler("method")) - r.Use(hlog.RequestIDHandler("request", "Request-Id")) - r.Use(accesslog.HlogHandler()) + // Apply common api middleware + r.Use(middleware.NoCache) + r.Use(middleware.Recoverer) - // configure cors middleware - r.Use(corsHandler(config)) + // configure logging middleware. + r.Use(hlog.URLHandler("url")) + r.Use(hlog.MethodHandler("method")) + r.Use(hlog.RequestIDHandler("request", "Request-Id")) + r.Use(accesslog.HlogHandler()) - // for now always attempt auth - enforced per operation - r.Use(middlewareauthn.Attempt(authenticator)) + // configure cors middleware + r.Use(corsHandler(config)) - r.Route("/v1", func(r chi.Router) { - setupRoutesV1(r, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, g) - }) + // for now always attempt auth - enforced per operation + r.Use(middlewareauthn.Attempt(authenticator)) + + r.Route("/v1", func(r chi.Router) { + setupRoutesV1(r, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, g) }) - // Generate list of all path prefixes that expect terminated Paths - terminatedPathPrefixes := []string{ - mountPath + "/v1/spaces", - mountPath + "/v1/repos", - } - - return encode.TerminatedPathBefore(terminatedPathPrefixes, r.ServeHTTP) + return r } func corsHandler(config *types.Config) func(http.Handler) http.Handler { @@ -214,7 +202,7 @@ func setupServiceAccounts(r chi.Router, saStore store.ServiceAccountStore, token // create takes parent information via body r.Post("/", handlerserviceaccount.HandleCreate(guard, saStore)) - r.Route(fmt.Sprintf("/{%s}", request.ServiceAccountIDParamName), func(r chi.Router) { + r.Route(fmt.Sprintf("/{%s}", request.ServiceAccountUIDParamName), func(r chi.Router) { // resolves the service account and stores it in the context r.Use(resolve.ServiceAccount(saStore)) @@ -251,7 +239,7 @@ func setupAdmin(r chi.Router, userStore store.UserStore, guard *guard.Guard) { _, _ = w.Write([]byte(fmt.Sprintf("Create user '%s'", chi.URLParam(r, "rref")))) }) - r.Route(fmt.Sprintf("/{%s}", request.UserIDParamName), func(r chi.Router) { + r.Route(fmt.Sprintf("/{%s}", request.UserUIDParamName), func(r chi.Router) { // resolves the user and stores it in the context resolve.User(userStore) diff --git a/internal/router/git.go b/internal/router/git.go index 24e00fa66..4dd372664 100644 --- a/internal/router/git.go +++ b/internal/router/git.go @@ -7,7 +7,6 @@ import ( "github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/middleware/accesslog" middleware_authn "github.com/harness/gitness/internal/api/middleware/authn" - "github.com/harness/gitness/internal/api/middleware/encode" "github.com/harness/gitness/internal/api/middleware/resolve" "github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/auth/authn" @@ -18,15 +17,12 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/rs/zerolog/hlog" - "github.com/rs/zerolog/log" ) /* - * Mounts the GIT Router under mountPath. - * The handler is wrapped within a layer that handles encoding Paths. + * newGitHandler returns a new http handler for handling GIT calls. */ func newGitHandler( - mountPath string, _ store.SystemStore, _ store.UserStore, spaceStore store.SpaceStore, @@ -34,55 +30,53 @@ func newGitHandler( authenticator authn.Authenticator, authorizer authz.Authorizer) http.Handler { guard := guard.New(authorizer, spaceStore, repoStore) + // Use go-chi router for inner routing (restricted to mountPath!) r := chi.NewRouter() - r.Route(mountPath, func(r chi.Router) { - // Apply common api middleware - r.Use(middleware.NoCache) - r.Use(middleware.Recoverer) + // Apply common api middleware + r.Use(middleware.NoCache) + r.Use(middleware.Recoverer) - // configure logging middleware. - r.Use(hlog.NewHandler(log.Logger)) - r.Use(hlog.URLHandler("path")) - r.Use(hlog.MethodHandler("method")) - r.Use(hlog.RequestIDHandler("request", "Request-Id")) - r.Use(accesslog.HlogHandler()) + // configure logging middleware. + r.Use(hlog.URLHandler("url")) + r.Use(hlog.MethodHandler("method")) + r.Use(hlog.RequestIDHandler("request", "Request-Id")) + r.Use(accesslog.HlogHandler()) - // for now always attempt auth - enforced per operation - r.Use(middleware_authn.Attempt(authenticator)) + // for now always attempt auth - enforced per operation + r.Use(middleware_authn.Attempt(authenticator)) - r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) { - // resolves the repo and stores in the context - r.Use(resolve.Repo(repoStore)) + r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) { + // resolves the repo and stores in the context + r.Use(resolve.Repo(repoStore)) - // Write operations (need auth) - r.Group(func(r chi.Router) { - // TODO: specific permission for pushing code? - r.Use(guard.ForRepo(enum.PermissionRepoEdit, false)) + // Write operations (need auth) + r.Group(func(r chi.Router) { + // TODO: specific permission for pushing code? + r.Use(guard.ForRepo(enum.PermissionRepoEdit, false)) - r.Handle("/git-upload-pack", http.HandlerFunc(stubGitHandler)) - }) + r.Handle("/git-upload-pack", http.HandlerFunc(stubGitHandler)) + }) - // Read operations (only need of it not public) - r.Group(func(r chi.Router) { - // middlewares - r.Use(guard.ForRepo(enum.PermissionRepoView, true)) - // handlers - r.Post("/git-receive-pack", stubGitHandler) - r.Get("/info/refs", stubGitHandler) - r.Get("/HEAD", stubGitHandler) - r.Get("/objects/info/alternates", stubGitHandler) - r.Get("/objects/info/http-alternates", stubGitHandler) - r.Get("/objects/info/packs", stubGitHandler) - r.Get("/objects/info/{file:[^/]*}", stubGitHandler) - r.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", stubGitHandler) - r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", stubGitHandler) - r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler) - }) + // Read operations (only need of it not public) + r.Group(func(r chi.Router) { + // middlewares + r.Use(guard.ForRepo(enum.PermissionRepoView, true)) + // handlers + r.Post("/git-receive-pack", stubGitHandler) + r.Get("/info/refs", stubGitHandler) + r.Get("/HEAD", stubGitHandler) + r.Get("/objects/info/alternates", stubGitHandler) + r.Get("/objects/info/http-alternates", stubGitHandler) + r.Get("/objects/info/packs", stubGitHandler) + r.Get("/objects/info/{file:[^/]*}", stubGitHandler) + r.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", stubGitHandler) + r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", stubGitHandler) + r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler) }) }) - return encode.GitPathBefore(r.ServeHTTP) + return r } func stubGitHandler(w http.ResponseWriter, r *http.Request) { diff --git a/internal/router/router.go b/internal/router/router.go index 7b1fc4914..fe9a5d54d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -10,25 +10,33 @@ import ( "net/http" "strings" + "github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/auth/authn" "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/request" + "github.com/harness/gitness/internal/router/translator" "github.com/harness/gitness/internal/store" + "github.com/rs/zerolog" + "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" ) const ( - restMount = "/api" + APIMount = "/api" gitUserAgentPrefix = "git/" ) type Router struct { - api http.Handler - git http.Handler - web http.Handler + translator translator.RequestTranslator + api http.Handler + git http.Handler + web http.Handler } -// New returns a new http.Handler that routes traffic +// NewRouter returns a new http.Handler that routes traffic // to the appropriate http.Handlers. -func New( +func NewRouter( + translator translator.RequestTranslator, systemStore store.SystemStore, userStore store.UserStore, spaceStore store.SpaceStore, @@ -37,20 +45,38 @@ func New( saStore store.ServiceAccountStore, authenticator authn.Authenticator, authorizer authz.Authorizer, -) (http.Handler, error) { - api := newAPIHandler(restMount, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, +) (*Router, error) { + api := newAPIHandler(systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, authorizer) - git := newGitHandler("/", systemStore, userStore, spaceStore, repoStore, authenticator, authorizer) - web := newWebHandler("/", systemStore) + git := newGitHandler(systemStore, userStore, spaceStore, repoStore, authenticator, authorizer) + web := newWebHandler(systemStore) return &Router{ - api: api, - git: git, - web: web, + translator: translator, + api: api, + git: git, + web: web, }, nil } func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + var err error + // setup logger for request + log := log.Logger.With().Logger() + req = req.WithContext(log.WithContext(req.Context())) + log.UpdateContext(func(c zerolog.Context) zerolog.Context { + return c. + Str("original_url", req.URL.String()) + }) + + // Initial translation of the request before any routing. + req, err = r.translator.TranslatePreRouting(req) + if err != nil { + log.Err(err).Msgf("Failed pre-routing translation of request.") + render.InternalError(w) + return + } + /* * 1. GIT * @@ -60,6 +86,18 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { */ ua := req.Header.Get("user-agent") if strings.HasPrefix(ua, gitUserAgentPrefix) { + log.UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Str("handler", "git") + }) + + // Translate git request + req, err = r.translator.TranslateGit(req) + if err != nil { + hlog.FromRequest(req).Err(err).Msgf("Failed GIT translation of request.") + render.InternalError(w) + return + } + r.git.ServeHTTP(w, req) return } @@ -68,10 +106,28 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { * 2. REST API * * All Rest API calls start with "/api/", and thus can be uniquely identified. - * Note: This assumes that we are blocking "api" as a space name! */ p := req.URL.Path - if strings.HasPrefix(p, restMount) { + if strings.HasPrefix(p, APIMount) { + log.UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Str("handler", "api") + }) + + // remove matched prefix to simplify API handlers + if err = stripPrefix(APIMount, req); err != nil { + hlog.FromRequest(req).Err(err).Msgf("Failed striping of prefix for api request.") + render.InternalError(w) + return + } + + // Translate API request + req, err = r.translator.TranslateAPI(req) + if err != nil { + hlog.FromRequest(req).Err(err).Msgf("Failed API translation of request.") + render.InternalError(w) + return + } + r.api.ServeHTTP(w, req) return } @@ -81,5 +137,21 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { * * Everything else will be routed to web (or return 404) */ + log.UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Str("handler", "web") + }) + + req, err = r.translator.TranslateWeb(req) + if err != nil { + hlog.FromRequest(req).Err(err).Msgf("Failed Web translation of request.") + render.InternalError(w) + return + } + r.web.ServeHTTP(w, req) } + +// stripPrefix removes the prefix from the request path (expected to be there). +func stripPrefix(prefix string, r *http.Request) error { + return request.ReplacePrefix(r, r.URL.Path[:len(prefix)], "") +} diff --git a/internal/router/translator/requestTranslator.go b/internal/router/translator/requestTranslator.go new file mode 100644 index 000000000..bff29c6a8 --- /dev/null +++ b/internal/router/translator/requestTranslator.go @@ -0,0 +1,25 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package translator + +import ( + "net/http" +) + +// RequestTranslator is responsible to translate an incomming request +// before it's getting routed and handled. +type RequestTranslator interface { + // TranslatePreRouting is called before any routing decisions are made. + TranslatePreRouting(*http.Request) (*http.Request, error) + + // TranslateGit is called for a git related request. + TranslateGit(*http.Request) (*http.Request, error) + + // TranslateAPI is called for an API related request. + TranslateAPI(*http.Request) (*http.Request, error) + + // TranslateWeb is called for an web related request. + TranslateWeb(*http.Request) (*http.Request, error) +} diff --git a/internal/router/translator/terminatedPathTranslator.go b/internal/router/translator/terminatedPathTranslator.go new file mode 100644 index 000000000..ae412698e --- /dev/null +++ b/internal/router/translator/terminatedPathTranslator.go @@ -0,0 +1,49 @@ +// 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 translator + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/middleware/encode" +) + +var ( + terminatedPathPrefixesAPI = []string{"/v1/spaces/", "/v1/repos/"} +) + +var _ RequestTranslator = (*TerminatedPathTranslator)(nil) + +// TerminatedPathTranslator translates encoded paths. +// For example: +// - /space1/space2/+ -> /space1%2Fspace2 +// - /space1/rep1.git -> /space1%2Frepo1 +// +// Note: paths are terminated after initial routing. +type TerminatedPathTranslator struct{} + +func NewTerminatedPathTranslator() *TerminatedPathTranslator { + return &TerminatedPathTranslator{} +} + +// TranslatePreRouting is called before any routing decisions are made. +func (t *TerminatedPathTranslator) TranslatePreRouting(r *http.Request) (*http.Request, error) { + return r, nil +} + +// TranslateGit is called for a git related request. +func (t *TerminatedPathTranslator) TranslateGit(r *http.Request) (*http.Request, error) { + return r, encode.GitPath(r) +} + +// TranslateAPI is called for an API related request. +func (t *TerminatedPathTranslator) TranslateAPI(r *http.Request) (*http.Request, error) { + return r, encode.TerminatedPath(terminatedPathPrefixesAPI, r) +} + +// TranslateWeb is called for an web related request. +func (t *TerminatedPathTranslator) TranslateWeb(r *http.Request) (*http.Request, error) { + return r, nil +} diff --git a/internal/router/translator/wire.go b/internal/router/translator/wire.go new file mode 100644 index 000000000..e92a3916e --- /dev/null +++ b/internal/router/translator/wire.go @@ -0,0 +1,18 @@ +// 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 translator + +import ( + "github.com/google/wire" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideRequestTranslator, +) + +func ProvideRequestTranslator() RequestTranslator { + return NewTerminatedPathTranslator() +} diff --git a/internal/router/web.go b/internal/router/web.go index 78946c8e8..f5e928e87 100644 --- a/internal/router/web.go +++ b/internal/router/web.go @@ -4,7 +4,6 @@ import ( "context" "net/http" - "github.com/harness/gitness/internal/api/middleware/encode" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/web" "github.com/swaggest/swgui/v3emb" @@ -14,53 +13,46 @@ import ( ) /* - * Mounts the WEB Router under mountPath. - * The handler is wrapped within a layer that handles encoding Paths. + * newWebHandler returns a new http handler for handling WEB calls. */ -func newWebHandler( - mountPath string, - systemStore store.SystemStore) http.Handler { - // +func newWebHandler(systemStore store.SystemStore) http.Handler { config := systemStore.Config(context.Background()) // Use go-chi router for inner routing (restricted to mountPath!) r := chi.NewRouter() - r.Route(mountPath, func(r chi.Router) { - // create middleware to enforce security best practices for - // the user interface. note that theis middleware is only used - // when serving the user interface (not found handler, below). - sec := secure.New( - secure.Options{ - AllowedHosts: config.Secure.AllowedHosts, - HostsProxyHeaders: config.Secure.HostsProxyHeaders, - SSLRedirect: config.Secure.SSLRedirect, - SSLTemporaryRedirect: config.Secure.SSLTemporaryRedirect, - SSLHost: config.Secure.SSLHost, - SSLProxyHeaders: config.Secure.SSLProxyHeaders, - STSSeconds: config.Secure.STSSeconds, - STSIncludeSubdomains: config.Secure.STSIncludeSubdomains, - STSPreload: config.Secure.STSPreload, - ForceSTSHeader: config.Secure.ForceSTSHeader, - FrameDeny: config.Secure.FrameDeny, - ContentTypeNosniff: config.Secure.ContentTypeNosniff, - BrowserXssFilter: config.Secure.BrowserXSSFilter, - ContentSecurityPolicy: config.Secure.ContentSecurityPolicy, - ReferrerPolicy: config.Secure.ReferrerPolicy, - }, - ) + // create middleware to enforce security best practices for + // the user interface. note that theis middleware is only used + // when serving the user interface (not found handler, below). + sec := secure.New( + secure.Options{ + AllowedHosts: config.Secure.AllowedHosts, + HostsProxyHeaders: config.Secure.HostsProxyHeaders, + SSLRedirect: config.Secure.SSLRedirect, + SSLTemporaryRedirect: config.Secure.SSLTemporaryRedirect, + SSLHost: config.Secure.SSLHost, + SSLProxyHeaders: config.Secure.SSLProxyHeaders, + STSSeconds: config.Secure.STSSeconds, + STSIncludeSubdomains: config.Secure.STSIncludeSubdomains, + STSPreload: config.Secure.STSPreload, + ForceSTSHeader: config.Secure.ForceSTSHeader, + FrameDeny: config.Secure.FrameDeny, + ContentTypeNosniff: config.Secure.ContentTypeNosniff, + BrowserXssFilter: config.Secure.BrowserXSSFilter, + ContentSecurityPolicy: config.Secure.ContentSecurityPolicy, + ReferrerPolicy: config.Secure.ReferrerPolicy, + }, + ) - // openapi playground endpoints - swagger := v3emb.NewHandler("API Definition", "/api/v1/swagger.yaml", "/swagger") - r.With(sec.Handler).Handle("/swagger", swagger) - r.With(sec.Handler).Handle("/swagger/*", swagger) + // openapi playground endpoints + swagger := v3emb.NewHandler("API Definition", "/api/v1/swagger.yaml", "/swagger") + r.With(sec.Handler).Handle("/swagger", swagger) + r.With(sec.Handler).Handle("/swagger/*", swagger) - // serve all other routes from the embedded filesystem, - // which in turn serves the user interface. - r.With(sec.Handler).NotFound( - web.Handler(), - ) - }) + // serve all other routes from the embedded filesystem, + // which in turn serves the user interface. + r.With(sec.Handler).NotFound( + web.Handler(), + ) - // web doesn't have any prefixes for terminated paths - return encode.TerminatedPathBefore([]string{""}, r.ServeHTTP) + return r } diff --git a/internal/router/wire.go b/internal/router/wire.go index 76712be75..7d2f65707 100644 --- a/internal/router/wire.go +++ b/internal/router/wire.go @@ -5,8 +5,29 @@ package router import ( + "net/http" + "github.com/google/wire" + "github.com/harness/gitness/internal/auth/authn" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/router/translator" + "github.com/harness/gitness/internal/store" ) // WireSet provides a wire set for this package. -var WireSet = wire.NewSet(New) +var WireSet = wire.NewSet(ProvideHTTPHandler) + +func ProvideHTTPHandler( + translator translator.RequestTranslator, + systemStore store.SystemStore, + userStore store.UserStore, + spaceStore store.SpaceStore, + repoStore store.RepoStore, + tokenStore store.TokenStore, + saStore store.ServiceAccountStore, + authenticator authn.Authenticator, + authorizer authz.Authorizer, +) (http.Handler, error) { + return NewRouter(translator, systemStore, userStore, spaceStore, + repoStore, tokenStore, saStore, authenticator, authorizer) +} diff --git a/internal/store/database/migrate/postgres/0001_create_table_principals.up.sql b/internal/store/database/migrate/postgres/0001_create_table_principals.up.sql index acdb48fc3..27aa22f1a 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_principals.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_principals.up.sql @@ -1,9 +1,9 @@ CREATE TABLE IF NOT EXISTS principals ( principal_id SERIAL PRIMARY KEY +,principal_uid TEXT ,principal_type TEXT ,principal_name TEXT ,principal_admin BOOLEAN -,principal_externalId TEXT ,principal_blocked BOOLEAN ,principal_salt TEXT ,principal_created INTEGER @@ -15,6 +15,7 @@ principal_id SERIAL PRIMARY KEY ,principal_sa_parentType TEXT ,principal_sa_parentId INTEGER +,UNIQUE(principal_uid) ,UNIQUE(principal_salt) ,UNIQUE(principal_user_email) ); diff --git a/internal/store/database/migrate/sqlite/0001_create_table_principals.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_principals.up.sql index 404c5df9f..fd015c9eb 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_principals.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_principals.up.sql @@ -1,9 +1,9 @@ CREATE TABLE IF NOT EXISTS principals ( principal_id INTEGER PRIMARY KEY AUTOINCREMENT +,principal_uid TEXT ,principal_type TEXT ,principal_name TEXT ,principal_admin BOOLEAN -,principal_externalId TEXT ,principal_blocked BOOLEAN ,principal_salt TEXT ,principal_created INTEGER @@ -15,6 +15,7 @@ principal_id INTEGER PRIMARY KEY AUTOINCREMENT ,principal_sa_parentType TEXT ,principal_sa_parentId INTEGER +,UNIQUE(principal_uid) ,UNIQUE(principal_salt) ,UNIQUE(principal_user_email COLLATE NOCASE) ); diff --git a/internal/store/database/serviceAccount.go b/internal/store/database/serviceAccount.go index a86289675..4ccabe4d6 100644 --- a/internal/store/database/serviceAccount.go +++ b/internal/store/database/serviceAccount.go @@ -37,6 +37,15 @@ func (s *ServiceAccountStore) Find(ctx context.Context, id int64) (*types.Servic return dst, nil } +// FindUID finds the service account by uid. +func (s *ServiceAccountStore) FindUID(ctx context.Context, uid string) (*types.ServiceAccount, error) { + dst := new(types.ServiceAccount) + if err := s.db.GetContext(ctx, dst, serviceAccountSelectUID, uid); err != nil { + return nil, processSQLErrorf(err, "Select by uid query failed") + } + return dst, nil +} + // Create saves the service account. func (s *ServiceAccountStore) Create(ctx context.Context, sa *types.ServiceAccount) error { query, arg, err := s.db.BindNamed(serviceAccountInsert, sa) @@ -113,8 +122,8 @@ WHERE principal_type = "serviceaccount" and principal_sa_parentType = $1 and pri const serviceAccountBase = ` SELECT principal_id +,principal_uid ,principal_name -,principal_externalId ,principal_blocked ,principal_salt ,principal_created @@ -133,6 +142,10 @@ const serviceAccountSelectID = serviceAccountBase + ` WHERE principal_type = "serviceaccount" AND principal_id = $1 ` +const serviceAccountSelectUID = serviceAccountBase + ` +WHERE principal_type = "serviceaccount" AND principal_uid = $1 +` + const serviceAccountDelete = ` DELETE FROM principals WHERE principal_type = "serviceaccount" AND principal_id = $1 @@ -141,9 +154,9 @@ WHERE principal_type = "serviceaccount" AND principal_id = $1 const serviceAccountInsert = ` INSERT INTO principals ( principal_type +,principal_uid ,principal_name ,principal_admin -,principal_externalId ,principal_blocked ,principal_salt ,principal_created @@ -152,9 +165,9 @@ principal_type ,principal_sa_parentId ) values ( "serviceaccount" +,:principal_uid ,:principal_name ,false -,:principal_externalId ,:principal_blocked ,:principal_salt ,:principal_created @@ -168,7 +181,6 @@ const serviceAccountUpdate = ` UPDATE principals SET principal_name = :principal_name -,:principal_externalId = :principal_externalId ,:principal_blocked = :principal_blocked ,:principal_salt = :principal_salt ,:principal_updated = :principal_updated diff --git a/internal/store/database/serviceAccount_sync.go b/internal/store/database/serviceAccount_sync.go index 611d87e19..cf2d4899c 100644 --- a/internal/store/database/serviceAccount_sync.go +++ b/internal/store/database/serviceAccount_sync.go @@ -34,6 +34,13 @@ func (s *ServiceAccountStoreSync) Find(ctx context.Context, id int64) (*types.Se return s.base.Find(ctx, id) } +// FindUID finds the service account by uid. +func (s *ServiceAccountStoreSync) FindUID(ctx context.Context, uid string) (*types.ServiceAccount, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindUID(ctx, uid) +} + // Create saves the service account. func (s *ServiceAccountStoreSync) Create(ctx context.Context, sa *types.ServiceAccount) error { mutex.RLock() diff --git a/internal/store/database/testdata/users.json b/internal/store/database/testdata/users.json index 3a54c80b9..d87a14cf9 100644 --- a/internal/store/database/testdata/users.json +++ b/internal/store/database/testdata/users.json @@ -1,6 +1,7 @@ [ { "id": 1, + "uid": "jane21", "email": "jane@example.com", "name": "jane", "company": "acme", @@ -12,6 +13,7 @@ }, { "id": 2, + "uid": "john21", "email": "john@example.com", "name": "john", "company": "acme", diff --git a/internal/store/database/user.go b/internal/store/database/user.go index dab71dc4a..2b8b9a5f6 100644 --- a/internal/store/database/user.go +++ b/internal/store/database/user.go @@ -7,7 +7,6 @@ package database import ( "context" "database/sql" - "strconv" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" @@ -39,6 +38,15 @@ func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) { return dst, nil } +// FindUID finds the user by uid. +func (s *UserStore) FindUID(ctx context.Context, uid string) (*types.User, error) { + dst := new(types.User) + if err := s.db.GetContext(ctx, dst, userSelectUID, uid); err != nil { + return nil, processSQLErrorf(err, "Select by uid query failed") + } + return dst, nil +} + // FindEmail finds the user by email. func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) { dst := new(types.User) @@ -48,15 +56,6 @@ func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, e return dst, nil } -// FindKey finds the user unique key (email or id). -func (s *UserStore) FindKey(ctx context.Context, key string) (*types.User, error) { - id, err := strconv.ParseInt(key, 10, 64) - if err == nil { - return s.Find(ctx, id) - } - return s.FindEmail(ctx, key) -} - // List returns a list of users. func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) { dst := []*types.User{} @@ -88,8 +87,8 @@ func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types. stmt = stmt.OrderBy("principal_updated " + opts.Order.String()) case enum.UserAttrEmail: stmt = stmt.OrderBy("principal_user_email " + opts.Order.String()) - case enum.UserAttrID: - stmt = stmt.OrderBy("principal_id " + opts.Order.String()) + case enum.UserAttrUID: + stmt = stmt.OrderBy("principal_uid " + opts.Order.String()) case enum.UserAttrAdmin: stmt = stmt.OrderBy("principal_admin " + opts.Order.String()) } @@ -135,7 +134,7 @@ func (s *UserStore) Update(ctx context.Context, user *types.User) error { } // Delete deletes the user. -func (s *UserStore) Delete(ctx context.Context, user *types.User) error { +func (s *UserStore) Delete(ctx context.Context, id int64) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return processSQLErrorf(err, "Failed to start a new transaction") @@ -144,7 +143,7 @@ func (s *UserStore) Delete(ctx context.Context, user *types.User) error { _ = tx.Rollback() }(tx) // delete the user - if _, err = tx.ExecContext(ctx, userDelete, user.ID); err != nil { + if _, err = tx.ExecContext(ctx, userDelete, id); err != nil { return processSQLErrorf(err, "The delete query failed") } return tx.Commit() @@ -169,9 +168,9 @@ WHERE principal_type = "user" const userBase = ` SELECT principal_id +,principal_uid ,principal_name ,principal_admin -,principal_externalId ,principal_blocked ,principal_salt ,principal_created @@ -191,6 +190,10 @@ const userSelectID = userBase + ` WHERE principal_type = "user" AND principal_id = $1 ` +const userSelectUID = userBase + ` +WHERE principal_type = "user" AND principal_uid = $1 +` + const userSelectEmail = userBase + ` WHERE principal_type = "user" AND principal_user_email = $1 ` @@ -203,9 +206,9 @@ WHERE principal_type = "user" AND principal_id = $1 const userInsert = ` INSERT INTO principals ( principal_type +,principal_uid ,principal_name ,principal_admin -,principal_externalId ,principal_blocked ,principal_salt ,principal_created @@ -214,9 +217,9 @@ principal_type ,principal_user_password ) values ( "user" +,:principal_uid ,:principal_name ,:principal_admin -,:principal_externalId ,:principal_blocked ,:principal_salt ,:principal_created @@ -231,7 +234,6 @@ UPDATE principals SET principal_name = :principal_name ,principal_admin = :principal_admin -,principal_externalId = :principal_externalId ,principal_blocked = :principal_blocked ,principal_salt = :principal_salt ,principal_updated = :principal_updated diff --git a/internal/store/database/user_sync.go b/internal/store/database/user_sync.go index d8379b831..154866bbf 100644 --- a/internal/store/database/user_sync.go +++ b/internal/store/database/user_sync.go @@ -31,6 +31,13 @@ func (s *UserStoreSync) Find(ctx context.Context, id int64) (*types.User, error) return s.base.Find(ctx, id) } +// FindUID finds the user by uid. +func (s *UserStoreSync) FindUID(ctx context.Context, uid string) (*types.User, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindUID(ctx, uid) +} + // FindEmail finds the user by email. func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.User, error) { mutex.RLock() @@ -38,13 +45,6 @@ func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.Use return s.base.FindEmail(ctx, email) } -// FindKey finds the user unique key (email or id). -func (s *UserStoreSync) FindKey(ctx context.Context, key string) (*types.User, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.FindKey(ctx, key) -} - // List returns a list of users. func (s *UserStoreSync) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) { mutex.RLock() @@ -67,10 +67,10 @@ func (s *UserStoreSync) Update(ctx context.Context, user *types.User) error { } // Delete deletes the user. -func (s *UserStoreSync) Delete(ctx context.Context, user *types.User) error { +func (s *UserStoreSync) Delete(ctx context.Context, id int64) error { mutex.Lock() defer mutex.Unlock() - return s.base.Delete(ctx, user) + return s.base.Delete(ctx, id) } // Count returns a count of users. diff --git a/internal/store/database/user_test.go b/internal/store/database/user_test.go index 961d9f9ee..a40113ac7 100644 --- a/internal/store/database/user_test.go +++ b/internal/store/database/user_test.go @@ -141,6 +141,18 @@ func testUserFind(store store.UserStore) func(t *testing.T) { } }) + t.Run("uid", func(t *testing.T) { + got, err := store.FindUID(ctx, "jane21") + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + t.Run("email", func(t *testing.T) { got, err := store.FindEmail(ctx, want.Email) if err != nil { @@ -164,30 +176,6 @@ func testUserFind(store store.UserStore) func(t *testing.T) { return } }) - - t.Run("key/id", func(t *testing.T) { - got, err := store.FindKey(ctx, "1") - if err != nil { - t.Error(err) - return - } - if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { - t.Errorf(diff) - return - } - }) - - t.Run("key/email", func(t *testing.T) { - got, err := store.FindKey(ctx, want.Email) - if err != nil { - t.Error(err) - return - } - if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { - t.Errorf(diff) - return - } - }) } } @@ -247,12 +235,12 @@ func testUserUpdate(store store.UserStore) func(t *testing.T) { func testUserDelete(s store.UserStore) func(t *testing.T) { return func(t *testing.T) { ctx := context.Background() - v, err := s.Find(ctx, 1) + _, err := s.Find(ctx, 1) if err != nil { t.Error(err) return } - if err = s.Delete(ctx, v); err != nil { + if err = s.Delete(ctx, 1); err != nil { t.Error(err) return } diff --git a/internal/store/database/util.go b/internal/store/database/util.go index a09056df2..f97ee9429 100644 --- a/internal/store/database/util.go +++ b/internal/store/database/util.go @@ -40,7 +40,7 @@ func offset(page, size int) int { // Logs the error and message, returns either the original error or a store equivalent if possible. func processSQLErrorf(err error, format string, args ...interface{}) error { // always log DB error (print formated message) - log.Warn().Msgf("%s %s", fmt.Sprintf(format, args...), err) + log.Debug().Msgf("%s %s", fmt.Sprintf(format, args...), err) // If it's a known error, return converted error instead. if errors.Is(err, sql.ErrNoRows) { diff --git a/internal/store/store.go b/internal/store/store.go index e2897e5fb..bbfb70fcd 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -18,12 +18,12 @@ type ( // Find finds the user by id. Find(ctx context.Context, id int64) (*types.User, error) + // FindUID finds the user by uid. + FindUID(ctx context.Context, uid string) (*types.User, error) + // FindEmail finds the user by email. FindEmail(ctx context.Context, email string) (*types.User, error) - // FindKey finds the user by unique key (email or id). - FindKey(ctx context.Context, key string) (*types.User, error) - // Create saves the user details. Create(ctx context.Context, user *types.User) error @@ -31,7 +31,7 @@ type ( Update(ctx context.Context, user *types.User) error // Delete deletes the user. - Delete(ctx context.Context, user *types.User) error + Delete(ctx context.Context, id int64) error // List returns a list of users. List(ctx context.Context, params *types.UserFilter) ([]*types.User, error) @@ -45,6 +45,9 @@ type ( // Find finds the service account by id. Find(ctx context.Context, id int64) (*types.ServiceAccount, error) + // FindUID finds the service account by uid. + FindUID(ctx context.Context, uid string) (*types.ServiceAccount, error) + // Create saves the service account. Create(ctx context.Context, sa *types.ServiceAccount) error diff --git a/mocks/mock_client.go b/mocks/mock_client.go index 0146aad95..dca3bd1f7 100644 --- a/mocks/mock_client.go +++ b/mocks/mock_client.go @@ -51,18 +51,18 @@ func (mr *MockClientMockRecorder) Login(arg0, arg1, arg2 interface{}) *gomock.Ca } // Register mocks base method. -func (m *MockClient) Register(arg0 context.Context, arg1, arg2 string) (*types.TokenResponse, error) { +func (m *MockClient) Register(arg0 context.Context, arg1, arg2, arg3, arg4 string) (*types.TokenResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Register", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Register", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*types.TokenResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // Register indicates an expected call of Register. -func (mr *MockClientMockRecorder) Register(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) Register(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1, arg2, arg3, arg4) } // Self mocks base method. diff --git a/mocks/mock_store.go b/mocks/mock_store.go index 7f679498e..a1e502b7f 100644 --- a/mocks/mock_store.go +++ b/mocks/mock_store.go @@ -102,7 +102,7 @@ func (mr *MockUserStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call } // Delete mocks base method. -func (m *MockUserStore) Delete(arg0 context.Context, arg1 *types.User) error { +func (m *MockUserStore) Delete(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", arg0, arg1) ret0, _ := ret[0].(error) @@ -145,19 +145,19 @@ func (mr *MockUserStoreMockRecorder) FindEmail(arg0, arg1 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEmail", reflect.TypeOf((*MockUserStore)(nil).FindEmail), arg0, arg1) } -// FindKey mocks base method. -func (m *MockUserStore) FindKey(arg0 context.Context, arg1 string) (*types.User, error) { +// FindUID mocks base method. +func (m *MockUserStore) FindUID(arg0 context.Context, arg1 string) (*types.User, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindKey", arg0, arg1) + ret := m.ctrl.Call(m, "FindUID", arg0, arg1) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) return ret0, ret1 } -// FindKey indicates an expected call of FindKey. -func (mr *MockUserStoreMockRecorder) FindKey(arg0, arg1 interface{}) *gomock.Call { +// FindUID indicates an expected call of FindUID. +func (mr *MockUserStoreMockRecorder) FindUID(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindKey", reflect.TypeOf((*MockUserStore)(nil).FindKey), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUID", reflect.TypeOf((*MockUserStore)(nil).FindUID), arg0, arg1) } // List mocks base method. diff --git a/types/check/common.go b/types/check/common.go index 9576780f6..6f0c47f8a 100644 --- a/types/check/common.go +++ b/types/check/common.go @@ -17,20 +17,32 @@ const ( minNameLength = 1 maxNameLength = 256 nameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$" + + minUIDLength = 2 + maxUIDLength = 64 + uidRegex = "^[a-z][a-z0-9\\-\\_]*$" ) var ( ErrPathNameLength = &ValidationError{ fmt.Sprintf("Path name has to be between %d and %d in length.", minPathNameLength, maxPathNameLength), } - ErrPathNameRegex = &ValidationError{"Path name has start with a letter and only contain the following [a-z0-9-_]."} + ErrPathNameRegex = &ValidationError{"Path name has to start with a letter and only contain the following [a-z0-9-_]."} ErrNameLength = &ValidationError{ fmt.Sprintf("Name has to be between %d and %d in length.", minNameLength, maxNameLength), } ErrNameRegex = &ValidationError{ - "Name has start with a letter and only contain the following [a-zA-Z0-9-_ ].", + "Name has to start with a letter and only contain the following [a-zA-Z0-9-_ ].", + } + + ErrUIDLength = &ValidationError{ + fmt.Sprintf("UID has to be between %d and %d in length.", + minUIDLength, maxUIDLength), + } + ErrUIDRegex = &ValidationError{ + "UID has to start with a letter and only contain the following [a-z0-9-_].", } ) @@ -61,3 +73,17 @@ func Name(name string) error { return nil } + +// UID checks the provided uid and returns an error in it isn't valid. +func UID(uid string) error { + l := len(uid) + if l < minUIDLength || l > maxUIDLength { + return ErrUIDLength + } + + if ok, _ := regexp.Match(uidRegex, []byte(uid)); !ok { + return ErrUIDRegex + } + + return nil +} diff --git a/types/check/serviceAccount.go b/types/check/serviceAccount.go index 36b7684a6..fdf083a08 100644 --- a/types/check/serviceAccount.go +++ b/types/check/serviceAccount.go @@ -17,6 +17,11 @@ var ( // ServiceAccount returns true if the ServiceAccount if valid. func ServiceAccount(sa *types.ServiceAccount) error { + // validate UID + if err := UID(sa.UID); err != nil { + return err + } + // verify name if err := Name(sa.Name); err != nil { return err diff --git a/types/check/user.go b/types/check/user.go index 46c45d69f..b9c6a9fa7 100644 --- a/types/check/user.go +++ b/types/check/user.go @@ -25,6 +25,11 @@ var ( // User returns true if the User if valid. func User(user *types.User) error { + // validate UID + if err := UID(user.UID); err != nil { + return err + } + // validate name if err := Name(user.Name); err != nil { return err diff --git a/types/config.go b/types/config.go index 5bf5bb59d..a98c45918 100644 --- a/types/config.go +++ b/types/config.go @@ -8,60 +8,60 @@ import "time" // Config stores the system configuration. type Config struct { - Debug bool `envconfig:"APP_DEBUG"` - Trace bool `envconfig:"APP_TRACE"` + Debug bool `envconfig:"GITNESS_DEBUG"` + Trace bool `envconfig:"GITNESS_TRACE"` // Server defines the server configuration parameters. Server struct { - Bind string `envconfig:"APP_HTTP_BIND" default:":3000"` - Proto string `envconfig:"APP_HTTP_PROTO"` - Host string `envconfig:"APP_HTTP_HOST"` + Bind string `envconfig:"GITNESS_HTTP_BIND" default:":3000"` + Proto string `envconfig:"GITNESS_HTTP_PROTO"` + Host string `envconfig:"GITNESS_HTTP_HOST"` // Acme defines Acme configuration parameters. Acme struct { - Enabled bool `envconfig:"APP_ACME_ENABLED"` - Endpont string `envconfig:"APP_ACME_ENDPOINT"` - Email bool `envconfig:"APP_ACME_EMAIL"` + Enabled bool `envconfig:"GITNESS_ACME_ENABLED"` + Endpont string `envconfig:"GITNESS_ACME_ENDPOINT"` + Email bool `envconfig:"GITNESS_ACME_EMAIL"` } } // Database defines the database configuration parameters. Database struct { - Driver string `envconfig:"APP_DATABASE_DRIVER" default:"sqlite3"` - Datasource string `envconfig:"APP_DATABASE_DATASOURCE" default:"database.sqlite3"` + Driver string `envconfig:"GITNESS_DATABASE_DRIVER" default:"sqlite3"` + Datasource string `envconfig:"GITNESS_DATABASE_DATASOURCE" default:"database.sqlite3"` } // Token defines token configuration parameters. Token struct { - Expire time.Duration `envconfig:"APP_TOKEN_EXPIRE" default:"720h"` + Expire time.Duration `envconfig:"GITNESS_TOKEN_EXPIRE" default:"720h"` } // Cors defines http cors parameters Cors struct { - AllowedOrigins []string `envconfig:"APP_CORS_ALLOWED_ORIGINS" default:"*"` - AllowedMethods []string `envconfig:"APP_CORS_ALLOWED_METHODS" default:"GET,POST,PATCH,PUT,DELETE,OPTIONS"` - AllowedHeaders []string `envconfig:"APP_CORS_ALLOWED_HEADERS" default:"Origin,Accept,Accept-Language,Authorization,Content-Type,Content-Language,X-Requested-With,X-Request-Id"` //nolint:lll // struct tags can't be multiline - ExposedHeaders []string `envconfig:"APP_CORS_EXPOSED_HEADERS" default:"Link"` - AllowCredentials bool `envconfig:"APP_CORS_ALLOW_CREDENTIALS" default:"true"` - MaxAge int `envconfig:"APP_CORS_MAX_AGE" default:"300"` + AllowedOrigins []string `envconfig:"GITNESS_CORS_ALLOWED_ORIGINS" default:"*"` + AllowedMethods []string `envconfig:"GITNESS_CORS_ALLOWED_METHODS" default:"GET,POST,PATCH,PUT,DELETE,OPTIONS"` + AllowedHeaders []string `envconfig:"GITNESS_CORS_ALLOWED_HEADERS" default:"Origin,Accept,Accept-Language,Authorization,Content-Type,Content-Language,X-Requested-With,X-Request-Id"` //nolint:lll // struct tags can't be multiline + ExposedHeaders []string `envconfig:"GITNESS_CORS_EXPOSED_HEADERS" default:"Link"` + AllowCredentials bool `envconfig:"GITNESS_CORS_ALLOW_CREDENTIALS" default:"true"` + MaxAge int `envconfig:"GITNESS_CORS_MAX_AGE" default:"300"` } // Secure defines http security parameters. Secure struct { - AllowedHosts []string `envconfig:"APP_HTTP_ALLOWED_HOSTS"` - HostsProxyHeaders []string `envconfig:"APP_HTTP_PROXY_HEADERS"` - SSLRedirect bool `envconfig:"APP_HTTP_SSL_REDIRECT"` - SSLTemporaryRedirect bool `envconfig:"APP_HTTP_SSL_TEMPORARY_REDIRECT"` - SSLHost string `envconfig:"APP_HTTP_SSL_HOST"` - SSLProxyHeaders map[string]string `envconfig:"APP_HTTP_SSL_PROXY_HEADERS"` - STSSeconds int64 `envconfig:"APP_HTTP_STS_SECONDS"` - STSIncludeSubdomains bool `envconfig:"APP_HTTP_STS_INCLUDE_SUBDOMAINS"` - STSPreload bool `envconfig:"APP_HTTP_STS_PRELOAD"` - ForceSTSHeader bool `envconfig:"APP_HTTP_STS_FORCE_HEADER"` - BrowserXSSFilter bool `envconfig:"APP_HTTP_BROWSER_XSS_FILTER" default:"true"` - FrameDeny bool `envconfig:"APP_HTTP_FRAME_DENY" default:"true"` - ContentTypeNosniff bool `envconfig:"APP_HTTP_CONTENT_TYPE_NO_SNIFF"` - ContentSecurityPolicy string `envconfig:"APP_HTTP_CONTENT_SECURITY_POLICY"` - ReferrerPolicy string `envconfig:"APP_HTTP_REFERRER_POLICY"` + AllowedHosts []string `envconfig:"GITNESS_HTTP_ALLOWED_HOSTS"` + HostsProxyHeaders []string `envconfig:"GITNESS_HTTP_PROXY_HEADERS"` + SSLRedirect bool `envconfig:"GITNESS_HTTP_SSL_REDIRECT"` + SSLTemporaryRedirect bool `envconfig:"GITNESS_HTTP_SSL_TEMPORARY_REDIRECT"` + SSLHost string `envconfig:"GITNESS_HTTP_SSL_HOST"` + SSLProxyHeaders map[string]string `envconfig:"GITNESS_HTTP_SSL_PROXY_HEADERS"` + STSSeconds int64 `envconfig:"GITNESS_HTTP_STS_SECONDS"` + STSIncludeSubdomains bool `envconfig:"GITNESS_HTTP_STS_INCLUDE_SUBDOMAINS"` + STSPreload bool `envconfig:"GITNESS_HTTP_STS_PRELOAD"` + ForceSTSHeader bool `envconfig:"GITNESS_HTTP_STS_FORCE_HEADER"` + BrowserXSSFilter bool `envconfig:"GITNESS_HTTP_BROWSER_XSS_FILTER" default:"true"` + FrameDeny bool `envconfig:"GITNESS_HTTP_FRAME_DENY" default:"true"` + ContentTypeNosniff bool `envconfig:"GITNESS_HTTP_CONTENT_TYPE_NO_SNIFF"` + ContentSecurityPolicy string `envconfig:"GITNESS_HTTP_CONTENT_SECURITY_POLICY"` + ReferrerPolicy string `envconfig:"GITNESS_HTTP_REFERRER_POLICY"` } } diff --git a/types/enum/user.go b/types/enum/user.go index 29a86665f..ff890735d 100644 --- a/types/enum/user.go +++ b/types/enum/user.go @@ -13,7 +13,7 @@ type UserAttr int // Order enumeration. const ( UserAttrNone UserAttr = iota - UserAttrID + UserAttrUID UserAttrName UserAttrEmail UserAttrAdmin @@ -26,7 +26,7 @@ const ( func ParseUserAttr(s string) UserAttr { switch strings.ToLower(s) { case "id": - return UserAttrID + return UserAttrUID case "name": return UserAttrName case "email": diff --git a/types/enum/user_test.go b/types/enum/user_test.go index 6cfb2a137..c16c135ec 100644 --- a/types/enum/user_test.go +++ b/types/enum/user_test.go @@ -11,7 +11,7 @@ func TestParseUserAttr(t *testing.T) { text string want UserAttr }{ - {"id", UserAttrID}, + {"id", UserAttrUID}, {"name", UserAttrName}, {"email", UserAttrEmail}, {"created", UserAttrCreated}, diff --git a/types/principal.go b/types/principal.go index 7121b261a..7f7d4bbee 100644 --- a/types/principal.go +++ b/types/principal.go @@ -10,15 +10,16 @@ import "github.com/harness/gitness/types/enum" type ( // Represents the identity of an acting entity (User, ServiceAccount, Service). Principal struct { - ID int64 `db:"principal_id" json:"id"` + // ID is the internal identifier of a principal (primary key) + ID int64 `db:"principal_id" json:"-"` + UID string `db:"principal_uid" json:"uid"` Type enum.PrincipalType `db:"principal_type" json:"type"` Name string `db:"principal_name" json:"name"` Admin bool `db:"principal_admin" json:"admin"` // Should be part of principal or not? - ExternalID string `db:"principal_externalId" json:"externalId"` - Blocked bool `db:"principal_blocked" json:"blocked"` - Salt string `db:"principal_salt" json:"-"` + Blocked bool `db:"principal_blocked" json:"blocked"` + Salt string `db:"principal_salt" json:"-"` // Other info Created int64 `db:"principal_created" json:"created"` @@ -28,28 +29,42 @@ type ( func PrincipalFromUser(user *User) *Principal { return &Principal{ - ID: user.ID, - Type: enum.PrincipalTypeUser, - Name: user.Name, - Admin: user.Admin, - ExternalID: user.ExternalID, - Blocked: user.Blocked, - Salt: user.Salt, - Created: user.Created, - Updated: user.Updated, + ID: user.ID, + UID: user.UID, + Type: enum.PrincipalTypeUser, + Name: user.Name, + Admin: user.Admin, + Blocked: user.Blocked, + Salt: user.Salt, + Created: user.Created, + Updated: user.Updated, } } func PrincipalFromServiceAccount(sa *ServiceAccount) *Principal { return &Principal{ - ID: sa.ID, - Type: enum.PrincipalTypeServiceAccount, - Name: sa.Name, - Admin: false, - ExternalID: sa.ExternalID, - Blocked: sa.Blocked, - Salt: sa.Salt, - Created: sa.Created, - Updated: sa.Updated, + ID: sa.ID, + UID: sa.UID, + Type: enum.PrincipalTypeServiceAccount, + Name: sa.Name, + Admin: false, + Blocked: sa.Blocked, + Salt: sa.Salt, + Created: sa.Created, + Updated: sa.Updated, + } +} + +func PrincipalFromService(s *Service) *Principal { + return &Principal{ + ID: s.ID, + UID: s.UID, + Type: enum.PrincipalTypeService, + Name: s.Name, + Admin: true, + Blocked: s.Blocked, + Salt: s.Salt, + Created: s.Created, + Updated: s.Updated, } } diff --git a/types/service.go b/types/service.go index 81eaca74a..d5e75ab93 100644 --- a/types/service.go +++ b/types/service.go @@ -10,15 +10,15 @@ import "github.com/harness/gitness/types/enum" type ( // Service is a principal representing a different internal service that runs alongside gitness. Service struct { - // Fields from Principal (without admin) - ID int64 `db:"principal_id" json:"id"` - Name string `db:"principal_name" json:"name"` - Admin bool `db:"principal_admin" json:"admin"` - ExternalID string `db:"principal_externalId" json:"externalId"` - Blocked bool `db:"principal_blocked" json:"blocked"` - Salt string `db:"principal_salt" json:"-"` - Created int64 `db:"principal_created" json:"created"` - Updated int64 `db:"principal_updated" json:"updated"` + // Fields from Principal (without admin, as it's always admin for now) + ID int64 `db:"principal_id" json:"-"` + UID string `db:"principal_uid" json:"uid"` + Name string `db:"principal_name" json:"name"` + Admin bool `db:"principal_admin" json:"admin"` + Blocked bool `db:"principal_blocked" json:"blocked"` + Salt string `db:"principal_salt" json:"-"` + Created int64 `db:"principal_created" json:"created"` + Updated int64 `db:"principal_updated" json:"updated"` } // ServiceAccountInput store details used to diff --git a/types/serviceAccount.go b/types/serviceAccount.go index 87800ea2b..1ed4900cc 100644 --- a/types/serviceAccount.go +++ b/types/serviceAccount.go @@ -10,14 +10,14 @@ import "github.com/harness/gitness/types/enum" type ( // ServiceAccount is a principal representing a service account. ServiceAccount struct { - // Fields from Principal (without admin) - ID int64 `db:"principal_id" json:"id"` - Name string `db:"principal_name" json:"name"` - ExternalID string `db:"principal_externalId" json:"externalId"` - Blocked bool `db:"principal_blocked" json:"blocked"` - Salt string `db:"principal_salt" json:"-"` - Created int64 `db:"principal_created" json:"created"` - Updated int64 `db:"principal_updated" json:"updated"` + // Fields from Principal (without admin, as it's never an admin) + ID int64 `db:"principal_id" json:"-"` + UID string `db:"principal_uid" json:"uid"` + Name string `db:"principal_name" json:"name"` + Blocked bool `db:"principal_blocked" json:"blocked"` + Salt string `db:"principal_salt" json:"-"` + Created int64 `db:"principal_created" json:"created"` + Updated int64 `db:"principal_updated" json:"updated"` // ServiceAccount specific fields ParentType enum.ParentResourceType `db:"principal_sa_parentType" json:"parentType"` diff --git a/types/user.go b/types/user.go index a43d877c1..0cfe5833b 100644 --- a/types/user.go +++ b/types/user.go @@ -13,14 +13,14 @@ type ( // User is a principal representing an end user. User struct { // Fields from Principal - ID int64 `db:"principal_id" json:"id"` - Name string `db:"principal_name" json:"name"` - Admin bool `db:"principal_admin" json:"admin"` - ExternalID string `db:"principal_externalId" json:"externalId"` - Blocked bool `db:"principal_blocked" json:"blocked"` - Salt string `db:"principal_salt" json:"-"` - Created int64 `db:"principal_created" json:"created"` - Updated int64 `db:"principal_updated" json:"updated"` + ID int64 `db:"principal_id" json:"-"` + UID string `db:"principal_uid" json:"uid"` + Name string `db:"principal_name" json:"name"` + Admin bool `db:"principal_admin" json:"admin"` + Blocked bool `db:"principal_blocked" json:"blocked"` + Salt string `db:"principal_salt" json:"-"` + Created int64 `db:"principal_created" json:"created"` + Updated int64 `db:"principal_updated" json:"updated"` // User specific fields Email string `db:"principal_user_email" json:"email"`