From 5786ad2409567b57d3fc96858baf30974270bd05 Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Mon, 10 Oct 2022 21:32:14 -0700 Subject: [PATCH] [Embedded] Harness Router, Inline Space Creation, Bootstrap, Harness/Admin User Setup (#28) Adds the basic for harness embedded mode: - Harness dedicated router with custom APIHandler - Inline Space Creation - Client for Account/Org/Project - Bootstrap (Allows for automated creation of admin user and gitness service (used for all platform required ops)) - Inline harness service principal creation - Ignore flag for ACL. --- .harness.env | 36 ++-- .local.env | 6 +- cli/server/harness.wire.go | 18 +- cli/server/harness.wire_gen.go | 62 +++--- cli/server/server.go | 12 +- cli/server/standalone.wire.go | 2 + cli/server/standalone.wire_gen.go | 16 +- cli/server/system.go | 13 +- internal/api/auth/service.go | 31 +++ internal/api/controller/service/controller.go | 22 +++ internal/api/controller/service/create.go | 68 +++++++ internal/api/controller/service/delete.go | 31 +++ internal/api/controller/service/find.go | 40 ++++ internal/api/controller/service/helper.go | 17 ++ internal/api/controller/service/list.go | 41 ++++ internal/api/controller/service/update.go | 55 ++++++ .../api/controller/service/updateAdmin.go | 41 ++++ internal/api/controller/service/wire.go | 20 ++ .../api/controller/space/listRepositories.go | 2 +- internal/api/controller/user/create.go | 18 +- internal/api/controller/user/find.go | 10 +- internal/api/controller/user/register.go | 4 +- internal/api/handler/repo/createPath.go | 2 +- internal/api/handler/repo/delete.go | 2 +- internal/api/handler/repo/deletePath.go | 4 +- internal/api/handler/repo/find.go | 2 +- internal/api/handler/repo/listPaths.go | 2 +- .../api/handler/repo/listServiceAccounts.go | 2 +- internal/api/handler/repo/move.go | 2 +- internal/api/handler/repo/update.go | 2 +- .../api/handler/serviceaccount/createToken.go | 2 +- internal/api/handler/serviceaccount/delete.go | 2 +- .../api/handler/serviceaccount/deleteToken.go | 4 +- internal/api/handler/serviceaccount/find.go | 2 +- .../api/handler/serviceaccount/listTokens.go | 2 +- internal/api/handler/space/createPath.go | 2 +- internal/api/handler/space/delete.go | 2 +- internal/api/handler/space/deletePath.go | 4 +- internal/api/handler/space/find.go | 2 +- internal/api/handler/space/list.go | 2 +- internal/api/handler/space/listPaths.go | 2 +- internal/api/handler/space/listRepos.go | 2 +- .../api/handler/space/listServiceAccounts.go | 2 +- internal/api/handler/space/move.go | 2 +- internal/api/handler/space/update.go | 2 +- internal/api/handler/user/deleteToken.go | 2 +- internal/api/handler/users/delete.go | 2 +- internal/api/handler/users/find.go | 2 +- internal/api/handler/users/update.go | 2 +- internal/api/middleware/authn/authn.go | 18 ++ internal/api/request/path.go | 4 +- internal/api/request/principal.go | 8 +- internal/api/request/repo.go | 4 +- internal/api/request/space.go | 4 +- internal/api/request/token.go | 4 +- internal/api/request/util.go | 10 +- internal/api/usererror/translate.go | 2 +- internal/bootstrap/bootstrap.go | 74 ++++++++ internal/bootstrap/wire.go | 18 ++ internal/router/git.go | 2 +- .../postgres/0001_create_table_paths.up.sql | 4 +- .../0001_create_table_principals.up.sql | 4 +- .../0001_create_table_repositories.up.sql | 4 +- .../postgres/0001_create_table_spaces.up.sql | 4 +- .../postgres/0001_create_table_tokens.up.sql | 6 +- .../sqlite/0001_create_table_paths.up.sql | 4 +- .../0001_create_table_principals.up.sql | 4 +- .../0001_create_table_repositories.up.sql | 4 +- .../sqlite/0001_create_table_spaces.up.sql | 4 +- .../sqlite/0001_create_table_tokens.up.sql | 6 +- internal/store/database/service.go | 179 ++++++++++++++++++ internal/store/database/serviceAccount.go | 6 +- internal/store/database/service_sync.go | 76 ++++++++ internal/store/database/wire.go | 13 ++ internal/store/store.go | 24 +++ types/check/password.go | 34 ++++ types/check/service.go | 24 +++ types/check/serviceAccount.go | 4 +- types/check/user.go | 4 +- types/config.go | 7 + types/enum/permission.go | 14 +- types/path.go | 1 + types/principal.go | 4 +- types/repo.go | 2 +- types/service.go | 12 +- types/space.go | 1 + types/token.go | 1 + 87 files changed, 1061 insertions(+), 162 deletions(-) create mode 100644 internal/api/auth/service.go create mode 100644 internal/api/controller/service/controller.go create mode 100644 internal/api/controller/service/create.go create mode 100644 internal/api/controller/service/delete.go create mode 100644 internal/api/controller/service/find.go create mode 100644 internal/api/controller/service/helper.go create mode 100644 internal/api/controller/service/list.go create mode 100644 internal/api/controller/service/update.go create mode 100644 internal/api/controller/service/updateAdmin.go create mode 100644 internal/api/controller/service/wire.go create mode 100644 internal/bootstrap/bootstrap.go create mode 100644 internal/bootstrap/wire.go create mode 100644 internal/store/database/service.go create mode 100644 internal/store/database/service_sync.go create mode 100644 types/check/password.go create mode 100644 types/check/service.go diff --git a/.harness.env b/.harness.env index c3a0424d5..d57f1ee41 100644 --- a/.harness.env +++ b/.harness.env @@ -1,18 +1,28 @@ +# Gitness values GITNESS_TRACE=true +GITNESS_ADMIN_NAME=Administrator +GITNESS_ADMIN_EMAIL=gitness@harness.io +GITNESS_ADMIN_PASSWORD=changeit + +# Harness specifc values 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" + +HARNESS_SERVICES_IDENTITY_JWT_IDENTITY="IdentityService" +HARNESS_SERVICES_IDENTITY_JWT_SECRET="HVSKUYqD4e5Rxu12hFDdCJKGM64sxgEynvdDhaOHaTHhwwn0K4Ttr0uoOxSsEVYNrUU" + +HARNESS_SERVICES_ACL_IGNORE=true +HARNESS_SERVICES_ACL_CLIENT_SECURE=false +HARNESS_SERVICES_ACL_CLIENT_BASEURL="http://localhost:9006/api" + +HARNESS_SERVICES_MANAGER_CLIENT_SECURE=false +HARNESS_SERVICES_MANAGER_CLIENT_BASEURL="http://localhost:3457/api" +HARNESS_SERVICES_MANAGER_JWT_IDENTITY="Manager" +HARNESS_SERVICES_MANAGER_JWT_SECRET="dOkdsVqdRPPRJG31XU0qY4MPqmBBMk0PTAGIKM6O7TGqhjyxScIdJe80mwh5Yb5zF3KxYBHw6B3Lfzlq" + +HARNESS_SERVICES_NGMANAGER_CLIENT_SECURE=false +HARNESS_SERVICES_NGMANAGER_CLIENT_BASEURL="http://localhost:7457" +HARNESS_SERVICES_NGMANAGER_JWT_IDENTITY="NextGenManager" +HARNESS_SERVICES_NGMANAGER_JWT_SECRET="IC04LYMBf1lDP5oeY4hupxd4HJhLmN6azUku3xEbeE3SUx5G3ZYzhbiwVtK4i7AmqyU9OZkwB4v8E9qM" diff --git a/.local.env b/.local.env index 52271eb0c..b1a2e180d 100644 --- a/.local.env +++ b/.local.env @@ -1 +1,5 @@ -GITNESS_TRACE=true \ No newline at end of file +GITNESS_TRACE=true +GITNESS_ADMIN_UID=admin +GITNESS_ADMIN_NAME=Administrator +GITNESS_ADMIN_EMAIL=admin@gitness.io +GITNESS_ADMIN_PASSWORD=changeit \ No newline at end of file diff --git a/cli/server/harness.wire.go b/cli/server/harness.wire.go index 8d899f738..435321f25 100644 --- a/cli/server/harness.wire.go +++ b/cli/server/harness.wire.go @@ -10,37 +10,39 @@ package server import ( "context" - "github.com/harness/gitness/harness" "github.com/harness/gitness/harness/auth/authn" "github.com/harness/gitness/harness/auth/authz" + "github.com/harness/gitness/harness/bootstrap" "github.com/harness/gitness/harness/client" + "github.com/harness/gitness/harness/router" + "github.com/harness/gitness/harness/types" "github.com/harness/gitness/internal/api/controller/repo" - "github.com/harness/gitness/internal/api/controller/serviceaccount" + "github.com/harness/gitness/internal/api/controller/service" "github.com/harness/gitness/internal/api/controller/space" "github.com/harness/gitness/internal/api/controller/user" "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" + gitnessTypes "github.com/harness/gitness/types" "github.com/google/wire" ) -func initSystem(ctx context.Context, config *types.Config) (*system, error) { +func initSystem(ctx context.Context, config *gitnessTypes.Config) (*system, error) { wire.Build( newSystem, + bootstrap.WireSet, database.WireSet, memory.WireSet, - router.WireSet, server.WireSet, cron.WireSet, repo.WireSet, - serviceaccount.WireSet, space.WireSet, user.WireSet, - harness.LoadConfig, + service.WireSet, + types.LoadConfig, + router.WireSet, authn.WireSet, authz.WireSet, client.WireSet, diff --git a/cli/server/harness.wire_gen.go b/cli/server/harness.wire_gen.go index 9ac7623ff..b4dcae56a 100644 --- a/cli/server/harness.wire_gen.go +++ b/cli/server/harness.wire_gen.go @@ -8,16 +8,18 @@ package server import ( "context" - "github.com/harness/gitness/harness" "github.com/harness/gitness/harness/auth/authn" "github.com/harness/gitness/harness/auth/authz" + "github.com/harness/gitness/harness/bootstrap" "github.com/harness/gitness/harness/client" + "github.com/harness/gitness/harness/router" + types2 "github.com/harness/gitness/harness/types" "github.com/harness/gitness/internal/api/controller/repo" - "github.com/harness/gitness/internal/api/controller/serviceaccount" + "github.com/harness/gitness/internal/api/controller/service" "github.com/harness/gitness/internal/api/controller/space" "github.com/harness/gitness/internal/api/controller/user" "github.com/harness/gitness/internal/cron" - "github.com/harness/gitness/internal/router" + router2 "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" @@ -27,55 +29,61 @@ import ( // Injectors from harness.wire.go: func initSystem(ctx context.Context, config *types.Config) (*system, error) { - systemStore := memory.New(config) + typesConfig, err := types2.LoadConfig() + if err != nil { + return nil, err + } + serviceJWTProvider, err := client.ProvideServiceJWTProvider(typesConfig) + if err != nil { + return nil, err + } + aclClient, err := client.ProvideACLClient(serviceJWTProvider, typesConfig) + if err != nil { + return nil, err + } + authorizer := authz.ProvideAuthorizer(typesConfig, aclClient) db, err := database.ProvideDatabase(ctx, config) if err != nil { return nil, err } userStore := database.ProvideUserStore(db) - harnessConfig, err := harness.LoadConfig() + tokenStore := database.ProvideTokenStore(db) + controller := user.NewController(authorizer, userStore, tokenStore) + serviceStore := database.ProvideServiceStore(db) + serviceController := service.NewController(authorizer, serviceStore) + bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller, serviceController) + systemStore := memory.New(config) + tokenClient, err := client.ProvideTokenClient(serviceJWTProvider, typesConfig) if err != nil { return nil, err } - serviceJWTProvider, err := client.ProvideServiceJWTProvider(harnessConfig) + userClient, err := client.ProvideUserClient(serviceJWTProvider, typesConfig) 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) + serviceAccountClient, err := client.ProvideServiceAccountClient(serviceJWTProvider, typesConfig) if err != nil { return nil, err } serviceAccountStore := database.ProvideServiceAccountStore(db) - authenticator, err := authn.ProvideAuthenticator(userStore, tokenClient, userClient, harnessConfig, serviceAccountClient, serviceAccountStore) + authenticator, err := authn.ProvideAuthenticator(userStore, tokenClient, userClient, typesConfig, serviceAccountClient, serviceAccountStore, serviceStore) if err != nil { return nil, err } - aclClient, err := client.ProvideACLClient(serviceJWTProvider, harnessConfig) + accountClient, err := client.ProvideAccountClient(serviceJWTProvider, typesConfig) if err != nil { return nil, err } - authorizer := authz.ProvideAuthorizer(aclClient) spaceStore := database.ProvideSpaceStore(db) repoStore := database.ProvideRepoStore(db) - controller := repo.NewController(authorizer, spaceStore, repoStore, serviceAccountStore) spaceController := space.NewController(authorizer, spaceStore, repoStore, serviceAccountStore) - tokenStore := database.ProvideTokenStore(db) - serviceaccountController := serviceaccount.NewController(authorizer, serviceAccountStore, spaceStore, repoStore, tokenStore) - userController := user.NewController(authorizer, userStore, tokenStore) - apiHandler := router.ProvideAPIHandler(systemStore, authenticator, controller, spaceController, serviceaccountController, userController) - gitHandler := router.ProvideGitHandler(repoStore, authenticator) - webHandler := router.ProvideWebHandler(systemStore) - routerRouter := router.ProvideRouter(apiHandler, gitHandler, webHandler) + repoController := repo.NewController(authorizer, spaceStore, repoStore, serviceAccountStore) + apiHandler := router.ProvideAPIHandler(systemStore, authenticator, accountClient, spaceController, repoController) + gitHandler := router2.ProvideGitHandler(repoStore, authenticator) + webHandler := router2.ProvideWebHandler(systemStore) + routerRouter := router2.ProvideRouter(apiHandler, gitHandler, webHandler) serverServer := server.ProvideServer(config, routerRouter) nightly := cron.NewNightly() - serverSystem := newSystem(serverServer, nightly) + serverSystem := newSystem(bootstrapBootstrap, serverServer, nightly) return serverSystem, nil } diff --git a/cli/server/server.go b/cli/server/server.go index 017daa040..974a18db1 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -54,10 +54,20 @@ func (c *command) run(*kingpin.ParseContext) error { // configure the log level setupLogger(config) + // add logger to context + log := log.Logger.With().Logger() + ctx = log.WithContext(ctx) + // initialize system system, err := initSystem(ctx, config) if err != nil { - return fmt.Errorf("encountered an error while initializing the system: %w", err) + return fmt.Errorf("encountered an error while wiring the system: %w", err) + } + + // bootstrap the system + err = system.bootstrap(ctx) + if err != nil { + return fmt.Errorf("encountered an error while bootstrapping the system: %w", err) } // collects all go routines - gCTX cancels if any go routine encounters an error diff --git a/cli/server/standalone.wire.go b/cli/server/standalone.wire.go index d07dab43c..cae7a15cc 100644 --- a/cli/server/standalone.wire.go +++ b/cli/server/standalone.wire.go @@ -16,6 +16,7 @@ import ( "github.com/harness/gitness/internal/api/controller/user" "github.com/harness/gitness/internal/auth/authn" "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/router" "github.com/harness/gitness/internal/server" @@ -29,6 +30,7 @@ import ( func initSystem(ctx context.Context, config *types.Config) (*system, error) { wire.Build( newSystem, + bootstrap.WireSet, database.WireSet, memory.WireSet, router.WireSet, diff --git a/cli/server/standalone.wire_gen.go b/cli/server/standalone.wire_gen.go index baa0c1478..fa5598ec6 100644 --- a/cli/server/standalone.wire_gen.go +++ b/cli/server/standalone.wire_gen.go @@ -14,6 +14,7 @@ import ( "github.com/harness/gitness/internal/api/controller/user" "github.com/harness/gitness/internal/auth/authn" "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/router" "github.com/harness/gitness/internal/server" @@ -25,28 +26,29 @@ import ( // Injectors from standalone.wire.go: func initSystem(ctx context.Context, config *types.Config) (*system, error) { - systemStore := memory.New(config) + authorizer := authz.ProvideAuthorizer() db, err := database.ProvideDatabase(ctx, config) if err != nil { return nil, err } userStore := database.ProvideUserStore(db) - serviceAccountStore := database.ProvideServiceAccountStore(db) tokenStore := database.ProvideTokenStore(db) + controller := user.NewController(authorizer, userStore, tokenStore) + bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller) + systemStore := memory.New(config) + serviceAccountStore := database.ProvideServiceAccountStore(db) authenticator := authn.ProvideAuthenticator(userStore, serviceAccountStore, tokenStore) - authorizer := authz.ProvideAuthorizer() spaceStore := database.ProvideSpaceStore(db) repoStore := database.ProvideRepoStore(db) - controller := repo.NewController(authorizer, spaceStore, repoStore, serviceAccountStore) + repoController := repo.NewController(authorizer, spaceStore, repoStore, serviceAccountStore) spaceController := space.NewController(authorizer, spaceStore, repoStore, serviceAccountStore) serviceaccountController := serviceaccount.NewController(authorizer, serviceAccountStore, spaceStore, repoStore, tokenStore) - userController := user.NewController(authorizer, userStore, tokenStore) - apiHandler := router.ProvideAPIHandler(systemStore, authenticator, controller, spaceController, serviceaccountController, userController) + apiHandler := router.ProvideAPIHandler(systemStore, authenticator, repoController, spaceController, serviceaccountController, controller) gitHandler := router.ProvideGitHandler(repoStore, authenticator) webHandler := router.ProvideWebHandler(systemStore) routerRouter := router.ProvideRouter(apiHandler, gitHandler, webHandler) serverServer := server.ProvideServer(config, routerRouter) nightly := cron.NewNightly() - serverSystem := newSystem(serverServer, nightly) + serverSystem := newSystem(bootstrapBootstrap, serverServer, nightly) return serverSystem, nil } diff --git a/cli/server/system.go b/cli/server/system.go index 29ff3f348..e5d21ef8e 100644 --- a/cli/server/system.go +++ b/cli/server/system.go @@ -5,20 +5,23 @@ package server import ( + "github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/server" ) // system stores high level system sub-routines. type system struct { - server *server.Server - nightly *cron.Nightly + bootstrap bootstrap.Bootstrap + server *server.Server + nightly *cron.Nightly } // newSystem returns a new system structure. -func newSystem(server *server.Server, nightly *cron.Nightly) *system { +func newSystem(bootstrap bootstrap.Bootstrap, server *server.Server, nightly *cron.Nightly) *system { return &system{ - server: server, - nightly: nightly, + bootstrap: bootstrap, + server: server, + nightly: nightly, } } diff --git a/internal/api/auth/service.go b/internal/api/auth/service.go new file mode 100644 index 000000000..8c034f7cf --- /dev/null +++ b/internal/api/auth/service.go @@ -0,0 +1,31 @@ +// 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 auth + +import ( + "context" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +/* + * CheckService checks if a service specific permission is granted for the current auth session. + * Returns nil if the permission is granted, otherwise returns an error. + * NotAuthenticated, NotAuthorized, or any unerlaying error. + */ +func CheckService(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, + svc *types.Service, permission enum.Permission) error { + // a service exists outside of any scope + scope := &types.Scope{} + resource := &types.Resource{ + Type: enum.ResourceTypeService, + Name: svc.UID, + } + + return Check(ctx, authorizer, session, scope, resource, permission) +} diff --git a/internal/api/controller/service/controller.go b/internal/api/controller/service/controller.go new file mode 100644 index 000000000..1729e5816 --- /dev/null +++ b/internal/api/controller/service/controller.go @@ -0,0 +1,22 @@ +// 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 service + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" +) + +type Controller struct { + authorizer authz.Authorizer + serviceStore store.ServiceStore +} + +func NewController(authorizer authz.Authorizer, serviceStore store.ServiceStore) *Controller { + return &Controller{ + authorizer: authorizer, + serviceStore: serviceStore, + } +} diff --git a/internal/api/controller/service/create.go b/internal/api/controller/service/create.go new file mode 100644 index 000000000..4d7a16fd0 --- /dev/null +++ b/internal/api/controller/service/create.go @@ -0,0 +1,68 @@ +// 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 service + +import ( + "context" + "time" + + "github.com/dchest/uniuri" + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +// CreateInput is the input used for create operations. +type CreateInput struct { + UID string + Name string +} + +/* + * Create creates a new service. + */ +func (c *Controller) Create(ctx context.Context, session *auth.Session, in *CreateInput) (*types.Service, error) { + // Ensure principal has required permissions (service is global, no explicit resource) + scope := &types.Scope{} + resource := &types.Resource{ + Type: enum.ResourceTypeService, + } + if err := apiauth.Check(ctx, c.authorizer, session, scope, resource, enum.PermissionServiceCreate); err != nil { + return nil, err + } + + return c.CreateNoAuth(ctx, in, false) +} + +/* + * CreateNoAuth creates a new service without auth checks. + * WARNING: Never call as part of user flow. + * + * Note: take admin separately to avoid potential vulnerabilities for user calls. + */ +func (c *Controller) CreateNoAuth(ctx context.Context, in *CreateInput, admin bool) (*types.Service, error) { + svc := &types.Service{ + UID: in.UID, + Name: in.Name, + Admin: admin, + Salt: uniuri.NewLen(uniuri.UUIDLen), + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // validate service + if err := check.Service(svc); err != nil { + return nil, err + } + + err := c.serviceStore.Create(ctx, svc) + if err != nil { + return nil, err + } + + return svc, nil +} diff --git a/internal/api/controller/service/delete.go b/internal/api/controller/service/delete.go new file mode 100644 index 000000000..5775fdb77 --- /dev/null +++ b/internal/api/controller/service/delete.go @@ -0,0 +1,31 @@ +// 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 service + +import ( + "context" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types/enum" +) + +/* + * Delete deletes a service. + */ +func (c *Controller) Delete(ctx context.Context, session *auth.Session, + serviceUID string) error { + svc, err := findServiceFromUID(ctx, c.serviceStore, serviceUID) + if err != nil { + return err + } + + // Ensure principal has required permissions on parent + if err = apiauth.CheckService(ctx, c.authorizer, session, svc, enum.PermissionServiceDelete); err != nil { + return err + } + + return c.serviceStore.Delete(ctx, svc.ID) +} diff --git a/internal/api/controller/service/find.go b/internal/api/controller/service/find.go new file mode 100644 index 000000000..9d95d0705 --- /dev/null +++ b/internal/api/controller/service/find.go @@ -0,0 +1,40 @@ +// 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 service + +import ( + "context" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +/* + * Find tries to find the provided service. + */ +func (c *Controller) Find(ctx context.Context, session *auth.Session, + serviceUID string) (*types.Service, error) { + svc, err := c.FindNoAuth(ctx, serviceUID) + if err != nil { + return nil, err + } + + // Ensure principal has required permissions on parent. + if err = apiauth.CheckService(ctx, c.authorizer, session, svc, enum.PermissionServiceView); err != nil { + return nil, err + } + + return svc, nil +} + +/* + * FindNoAuth finds a service without auth checks. + * WARNING: Never call as part of user flow. + */ +func (c *Controller) FindNoAuth(ctx context.Context, serviceUID string) (*types.Service, error) { + return findServiceFromUID(ctx, c.serviceStore, serviceUID) +} diff --git a/internal/api/controller/service/helper.go b/internal/api/controller/service/helper.go new file mode 100644 index 000000000..59986a718 --- /dev/null +++ b/internal/api/controller/service/helper.go @@ -0,0 +1,17 @@ +// 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 service + +import ( + "context" + + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" +) + +func findServiceFromUID(ctx context.Context, + serviceStore store.ServiceStore, serviceUID string) (*types.Service, error) { + return serviceStore.FindUID(ctx, serviceUID) +} diff --git a/internal/api/controller/service/list.go b/internal/api/controller/service/list.go new file mode 100644 index 000000000..cda5e577a --- /dev/null +++ b/internal/api/controller/service/list.go @@ -0,0 +1,41 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package service + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +/* + * List lists all services of the system. + */ +func (c *Controller) List(ctx context.Context, session *auth.Session) (int64, []*types.Service, error) { + // Ensure principal has required permissions (service is global, no explicit resource) + scope := &types.Scope{} + resource := &types.Resource{ + Type: enum.ResourceTypeService, + } + if err := apiauth.Check(ctx, c.authorizer, session, scope, resource, enum.PermissionServiceView); err != nil { + return 0, nil, err + } + + count, err := c.serviceStore.Count(ctx) + if err != nil { + return 0, nil, fmt.Errorf("failed to count services: %w", err) + } + + repos, err := c.serviceStore.List(ctx) + if err != nil { + return 0, nil, fmt.Errorf("failed to list services: %w", err) + } + + return count, repos, nil +} diff --git a/internal/api/controller/service/update.go b/internal/api/controller/service/update.go new file mode 100644 index 000000000..aa0c849ae --- /dev/null +++ b/internal/api/controller/service/update.go @@ -0,0 +1,55 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package service + +import ( + "context" + "time" + + "github.com/gotidy/ptr" + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +// UpdateInput store infos to update an existing service. +type UpdateInput struct { + Name *string `json:"name"` +} + +/* + * Update updates the provided service. + */ +func (c *Controller) Update(ctx context.Context, session *auth.Session, + serviceUID string, in *UpdateInput) (*types.Service, error) { + svc, err := findServiceFromUID(ctx, c.serviceStore, serviceUID) + if err != nil { + return nil, err + } + + // Ensure principal has required permissions on parent. + if err = apiauth.CheckService(ctx, c.authorizer, session, svc, enum.PermissionServiceEdit); err != nil { + return nil, err + } + + if in.Name != nil { + svc.Name = ptr.ToString(in.Name) + } + svc.Updated = time.Now().UnixMilli() + + // validate service + if err = check.Service(svc); err != nil { + return nil, err + } + + err = c.serviceStore.Update(ctx, svc) + if err != nil { + return nil, err + } + + return svc, nil +} diff --git a/internal/api/controller/service/updateAdmin.go b/internal/api/controller/service/updateAdmin.go new file mode 100644 index 000000000..a24812471 --- /dev/null +++ b/internal/api/controller/service/updateAdmin.go @@ -0,0 +1,41 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package service + +import ( + "context" + "time" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +/* + * UpdateAdmin updates the admin state of a service. + */ +func (c *Controller) UpdateAdmin(ctx context.Context, session *auth.Session, + serviceUID string, admin bool) (*types.Service, error) { + sbc, err := findServiceFromUID(ctx, c.serviceStore, serviceUID) + if err != nil { + return nil, err + } + + // Ensure principal has required permissions on parent. + if err = apiauth.CheckService(ctx, c.authorizer, session, sbc, enum.PermissionServiceEditAdmin); err != nil { + return nil, err + } + + sbc.Admin = admin + sbc.Updated = time.Now().UnixMilli() + + err = c.serviceStore.Update(ctx, sbc) + if err != nil { + return nil, err + } + + return sbc, nil +} diff --git a/internal/api/controller/service/wire.go b/internal/api/controller/service/wire.go new file mode 100644 index 000000000..d7b9e4fea --- /dev/null +++ b/internal/api/controller/service/wire.go @@ -0,0 +1,20 @@ +// 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 service + +import ( + "github.com/google/wire" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + NewController, +) + +func ProvideController(authorizer authz.Authorizer, serviceStore store.ServiceStore) *Controller { + return NewController(authorizer, serviceStore) +} diff --git a/internal/api/controller/space/listRepositories.go b/internal/api/controller/space/listRepositories.go index c20a3a567..cecf5465c 100644 --- a/internal/api/controller/space/listRepositories.go +++ b/internal/api/controller/space/listRepositories.go @@ -24,7 +24,7 @@ func (c *Controller) ListRepositories(ctx context.Context, session *auth.Session return 0, nil, err } - if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceView, true); err != nil { + if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionRepoView, true); err != nil { return 0, nil, err } diff --git a/internal/api/controller/user/create.go b/internal/api/controller/user/create.go index 283c72769..a8b812bfb 100644 --- a/internal/api/controller/user/create.go +++ b/internal/api/controller/user/create.go @@ -18,6 +18,8 @@ import ( "golang.org/x/crypto/bcrypt" ) +// CreateInput is the input used for create operations. +// On purpose don't expose admin, has to be enabled explicitly. type CreateInput struct { UID string `json:"uid"` Name string `json:"name"` @@ -38,13 +40,21 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea return nil, err } - return c.createNoAuth(ctx, in) + return c.CreateNoAuth(ctx, in, false) } /* - * createNoAuth creates a new user without auth checks. + * CreateNoAuth creates a new user without auth checks. + * WARNING: Never call as part of user flow. + * + * Note: take admin separately to avoid potential vulnerabilities for user calls. */ -func (c *Controller) createNoAuth(ctx context.Context, in *CreateInput) (*types.User, error) { +func (c *Controller) CreateNoAuth(ctx context.Context, in *CreateInput, admin bool) (*types.User, error) { + // validate password before hashing + if err := check.Password(in.Password); err != nil { + return nil, err + } + hash, err := hashPassword([]byte(in.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("failed to create hash: %w", err) @@ -58,6 +68,7 @@ func (c *Controller) createNoAuth(ctx context.Context, in *CreateInput) (*types. Salt: uniuri.NewLen(uniuri.UUIDLen), Created: time.Now().UnixMilli(), Updated: time.Now().UnixMilli(), + Admin: admin, } // validate user @@ -71,6 +82,7 @@ func (c *Controller) createNoAuth(ctx context.Context, in *CreateInput) (*types. } // first user will be admin by default. + // TODO: move responsibility somewhere else. if user.ID == 1 { user.Admin = true err = c.userStore.Update(ctx, user) diff --git a/internal/api/controller/user/find.go b/internal/api/controller/user/find.go index bfec71b04..2d516d080 100644 --- a/internal/api/controller/user/find.go +++ b/internal/api/controller/user/find.go @@ -18,7 +18,7 @@ import ( */ func (c *Controller) Find(ctx context.Context, session *auth.Session, userUID string) (*types.User, error) { - user, err := findUserFromUID(ctx, c.userStore, userUID) + user, err := c.FindNoAuth(ctx, userUID) if err != nil { return nil, err } @@ -30,3 +30,11 @@ func (c *Controller) Find(ctx context.Context, session *auth.Session, return user, nil } + +/* + * FindNoAuth finds a user without auth checks. + * WARNING: Never call as part of user flow. + */ +func (c *Controller) FindNoAuth(ctx context.Context, userUID string) (*types.User, error) { + return findUserFromUID(ctx, c.userStore, userUID) +} diff --git a/internal/api/controller/user/register.go b/internal/api/controller/user/register.go index e27314e61..13b662f82 100644 --- a/internal/api/controller/user/register.go +++ b/internal/api/controller/user/register.go @@ -18,9 +18,9 @@ import ( * functionalities (unable to create admin user for example). */ func (c *Controller) Register(ctx context.Context, in *CreateInput) (*types.TokenResponse, error) { - // TODO: allows to configure if open register is allowed. + // TODO: allow to configure if open register is allowed. - user, err := c.createNoAuth(ctx, in) + user, err := c.CreateNoAuth(ctx, in, false) if err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } diff --git a/internal/api/handler/repo/createPath.go b/internal/api/handler/repo/createPath.go index 96d9d87df..a70237645 100644 --- a/internal/api/handler/repo/createPath.go +++ b/internal/api/handler/repo/createPath.go @@ -20,7 +20,7 @@ func HandleCreatePath(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/delete.go b/internal/api/handler/repo/delete.go index 61776c75d..3ce23da99 100644 --- a/internal/api/handler/repo/delete.go +++ b/internal/api/handler/repo/delete.go @@ -19,7 +19,7 @@ func HandleDelete(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/deletePath.go b/internal/api/handler/repo/deletePath.go index f72a91dca..6fe08243d 100644 --- a/internal/api/handler/repo/deletePath.go +++ b/internal/api/handler/repo/deletePath.go @@ -19,12 +19,12 @@ func HandleDeletePath(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return } - pathID, err := request.GetPathID(r) + pathID, err := request.GetPathIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/find.go b/internal/api/handler/repo/find.go index 72e88b8db..c8a01d34f 100644 --- a/internal/api/handler/repo/find.go +++ b/internal/api/handler/repo/find.go @@ -19,7 +19,7 @@ func HandleFind(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/listPaths.go b/internal/api/handler/repo/listPaths.go index 092ed9fc5..ef3aa0f4d 100644 --- a/internal/api/handler/repo/listPaths.go +++ b/internal/api/handler/repo/listPaths.go @@ -20,7 +20,7 @@ func HandleListPaths(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/listServiceAccounts.go b/internal/api/handler/repo/listServiceAccounts.go index efd437f2d..ced3d47d4 100644 --- a/internal/api/handler/repo/listServiceAccounts.go +++ b/internal/api/handler/repo/listServiceAccounts.go @@ -19,7 +19,7 @@ func HandleListServiceAccounts(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/move.go b/internal/api/handler/repo/move.go index fd77c95d1..c350f61df 100644 --- a/internal/api/handler/repo/move.go +++ b/internal/api/handler/repo/move.go @@ -18,7 +18,7 @@ func HandleMove(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/repo/update.go b/internal/api/handler/repo/update.go index 01188aeba..38d062ae2 100644 --- a/internal/api/handler/repo/update.go +++ b/internal/api/handler/repo/update.go @@ -20,7 +20,7 @@ func HandleUpdate(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - repoRef, err := request.GetRepoRef(r) + repoRef, err := request.GetRepoRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/serviceaccount/createToken.go b/internal/api/handler/serviceaccount/createToken.go index 0a7fb3750..bac8e8031 100644 --- a/internal/api/handler/serviceaccount/createToken.go +++ b/internal/api/handler/serviceaccount/createToken.go @@ -19,7 +19,7 @@ func HandleCreateToken(saCrl *serviceaccount.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - saUID, err := request.GetServiceAccountUID(r) + saUID, err := request.GetServiceAccountUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/serviceaccount/delete.go b/internal/api/handler/serviceaccount/delete.go index bb8c00873..7c0291765 100644 --- a/internal/api/handler/serviceaccount/delete.go +++ b/internal/api/handler/serviceaccount/delete.go @@ -19,7 +19,7 @@ func HandleDelete(saCrl *serviceaccount.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - saUID, err := request.GetServiceAccountUID(r) + saUID, err := request.GetServiceAccountUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/serviceaccount/deleteToken.go b/internal/api/handler/serviceaccount/deleteToken.go index c062c86f3..0b1013f5f 100644 --- a/internal/api/handler/serviceaccount/deleteToken.go +++ b/internal/api/handler/serviceaccount/deleteToken.go @@ -18,12 +18,12 @@ func HandleDeleteToken(saCrl *serviceaccount.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - saUID, err := request.GetServiceAccountUID(r) + saUID, err := request.GetServiceAccountUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return } - tokenID, err := request.GetTokenID(r) + tokenID, err := request.GetTokenIDFromPath(r) if err != nil { render.BadRequest(w) return diff --git a/internal/api/handler/serviceaccount/find.go b/internal/api/handler/serviceaccount/find.go index f0dc2afb7..5e14c224b 100644 --- a/internal/api/handler/serviceaccount/find.go +++ b/internal/api/handler/serviceaccount/find.go @@ -18,7 +18,7 @@ func HandleFind(saCrl *serviceaccount.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - saUID, err := request.GetServiceAccountUID(r) + saUID, err := request.GetServiceAccountUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/serviceaccount/listTokens.go b/internal/api/handler/serviceaccount/listTokens.go index e96f1d1fe..6785a67d1 100644 --- a/internal/api/handler/serviceaccount/listTokens.go +++ b/internal/api/handler/serviceaccount/listTokens.go @@ -18,7 +18,7 @@ func HandleListTokens(saCrl *serviceaccount.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - saUID, err := request.GetServiceAccountUID(r) + saUID, err := request.GetServiceAccountUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/createPath.go b/internal/api/handler/space/createPath.go index 08afc8203..ca49e9500 100644 --- a/internal/api/handler/space/createPath.go +++ b/internal/api/handler/space/createPath.go @@ -20,7 +20,7 @@ func HandleCreatePath(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/delete.go b/internal/api/handler/space/delete.go index 4af9c21d7..4b6f970b7 100644 --- a/internal/api/handler/space/delete.go +++ b/internal/api/handler/space/delete.go @@ -19,7 +19,7 @@ func HandleDelete(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/deletePath.go b/internal/api/handler/space/deletePath.go index 1f6fd4101..e61a1b94c 100644 --- a/internal/api/handler/space/deletePath.go +++ b/internal/api/handler/space/deletePath.go @@ -19,13 +19,13 @@ func HandleDeletePath(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return } - pathID, err := request.GetPathID(r) + pathID, err := request.GetPathIDFromPath(r) if err != nil { render.BadRequest(w) return diff --git a/internal/api/handler/space/find.go b/internal/api/handler/space/find.go index 8c975e448..f255b2771 100644 --- a/internal/api/handler/space/find.go +++ b/internal/api/handler/space/find.go @@ -19,7 +19,7 @@ func HandleFind(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/list.go b/internal/api/handler/space/list.go index 1a0d2230d..a8e7968cf 100644 --- a/internal/api/handler/space/list.go +++ b/internal/api/handler/space/list.go @@ -18,7 +18,7 @@ func HandleListSpaces(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/listPaths.go b/internal/api/handler/space/listPaths.go index 5024f58e4..e33bbf420 100644 --- a/internal/api/handler/space/listPaths.go +++ b/internal/api/handler/space/listPaths.go @@ -20,7 +20,7 @@ func HandleListPaths(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/listRepos.go b/internal/api/handler/space/listRepos.go index 93d714ad6..c35cc1faa 100644 --- a/internal/api/handler/space/listRepos.go +++ b/internal/api/handler/space/listRepos.go @@ -18,7 +18,7 @@ func HandleListRepos(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/listServiceAccounts.go b/internal/api/handler/space/listServiceAccounts.go index c24cd40ab..839b7bb06 100644 --- a/internal/api/handler/space/listServiceAccounts.go +++ b/internal/api/handler/space/listServiceAccounts.go @@ -19,7 +19,7 @@ func HandleListServiceAccounts(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/move.go b/internal/api/handler/space/move.go index 06bb9b601..2d25e76e8 100644 --- a/internal/api/handler/space/move.go +++ b/internal/api/handler/space/move.go @@ -18,7 +18,7 @@ func HandleMove(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/space/update.go b/internal/api/handler/space/update.go index cb6c70d2b..f40238ae4 100644 --- a/internal/api/handler/space/update.go +++ b/internal/api/handler/space/update.go @@ -20,7 +20,7 @@ func HandleUpdate(spaceCtrl *space.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - spaceRef, err := request.GetSpaceRef(r) + spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/user/deleteToken.go b/internal/api/handler/user/deleteToken.go index beb2864da..47e406375 100644 --- a/internal/api/handler/user/deleteToken.go +++ b/internal/api/handler/user/deleteToken.go @@ -21,7 +21,7 @@ func HandleDeleteToken(userCtrl *user.Controller, tokenType enum.TokenType) http session, _ := request.AuthSessionFrom(ctx) userUID := session.Principal.UID - tokenID, err := request.GetTokenID(r) + tokenID, err := request.GetTokenIDFromPath(r) if err != nil { render.BadRequest(w) return diff --git a/internal/api/handler/users/delete.go b/internal/api/handler/users/delete.go index 4f2a16b3b..58bd75d9b 100644 --- a/internal/api/handler/users/delete.go +++ b/internal/api/handler/users/delete.go @@ -18,7 +18,7 @@ func HandleDelete(userCtrl *user.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - userUID, err := request.GetUserUID(r) + userUID, err := request.GetUserUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/users/find.go b/internal/api/handler/users/find.go index 0e0dbdda2..b064a55a2 100644 --- a/internal/api/handler/users/find.go +++ b/internal/api/handler/users/find.go @@ -18,7 +18,7 @@ func HandleFind(userCtrl *user.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - userUID, err := request.GetUserUID(r) + userUID, err := request.GetUserUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/handler/users/update.go b/internal/api/handler/users/update.go index 5c0b608be..2151a90a1 100644 --- a/internal/api/handler/users/update.go +++ b/internal/api/handler/users/update.go @@ -19,7 +19,7 @@ func HandleUpdate(userCtrl *user.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) - userUID, err := request.GetUserUID(r) + userUID, err := request.GetUserUIDFromPath(r) if err != nil { render.TranslatedUserError(w, err) return diff --git a/internal/api/middleware/authn/authn.go b/internal/api/middleware/authn/authn.go index caa0dac43..5fa292f80 100644 --- a/internal/api/middleware/authn/authn.go +++ b/internal/api/middleware/authn/authn.go @@ -19,6 +19,19 @@ import ( // Attempt returns an http.HandlerFunc middleware that authenticates // the http.Request if authentication payload is available. func Attempt(authenticator authn.Authenticator) func(http.Handler) http.Handler { + return performAuthentication(authenticator, false) +} + +// Required returns an http.HandlerFunc middleware that authenticates +// the http.Request and fails the request if no auth data was available. +func Required(authenticator authn.Authenticator) func(http.Handler) http.Handler { + return performAuthentication(authenticator, true) +} + +// performAuthentication returns an http.HandlerFunc middleware that authenticates +// the http.Request if authentication payload is available. +// Depending on whether it is required or not, the request will be failed. +func performAuthentication(authenticator authn.Authenticator, required bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -27,6 +40,11 @@ func Attempt(authenticator authn.Authenticator) func(http.Handler) http.Handler session, err := authenticator.Authenticate(r) if errors.Is(err, authn.ErrNoAuthData) { + if required { + render.Unauthorized(w) + return + } + // if there was no auth data in the request - continue as is next.ServeHTTP(w, r) return diff --git a/internal/api/request/path.go b/internal/api/request/path.go index ac9eb7a83..fd7dbc103 100644 --- a/internal/api/request/path.go +++ b/internal/api/request/path.go @@ -8,6 +8,6 @@ const ( PathParamPathID = "pathID" ) -func GetPathID(r *http.Request) (int64, error) { - return ParseAsInt64(r, PathParamPathID) +func GetPathIDFromPath(r *http.Request) (int64, error) { + return PathParamAsInt64(r, PathParamPathID) } diff --git a/internal/api/request/principal.go b/internal/api/request/principal.go index 1bdcb7d5f..a6eed8beb 100644 --- a/internal/api/request/principal.go +++ b/internal/api/request/principal.go @@ -9,10 +9,10 @@ const ( PathParamServiceAccountUID = "saUID" ) -func GetUserUID(r *http.Request) (string, error) { - return ParamOrError(r, PathParamUserUID) +func GetUserUIDFromPath(r *http.Request) (string, error) { + return PathParamOrError(r, PathParamUserUID) } -func GetServiceAccountUID(r *http.Request) (string, error) { - return ParamOrError(r, PathParamServiceAccountUID) +func GetServiceAccountUIDFromPath(r *http.Request) (string, error) { + return PathParamOrError(r, PathParamServiceAccountUID) } diff --git a/internal/api/request/repo.go b/internal/api/request/repo.go index 338618473..a6762a43d 100644 --- a/internal/api/request/repo.go +++ b/internal/api/request/repo.go @@ -10,8 +10,8 @@ const ( PathParamRepoRef = "repositoryRef" ) -func GetRepoRef(r *http.Request) (string, error) { - rawRef, err := ParamOrError(r, PathParamRepoRef) +func GetRepoRefFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PathParamRepoRef) if err != nil { return "", err } diff --git a/internal/api/request/space.go b/internal/api/request/space.go index a47f22b2c..3e6b3e86d 100644 --- a/internal/api/request/space.go +++ b/internal/api/request/space.go @@ -10,8 +10,8 @@ const ( PathParamSpaceRef = "spaceRef" ) -func GetSpaceRef(r *http.Request) (string, error) { - rawRef, err := ParamOrError(r, PathParamSpaceRef) +func GetSpaceRefFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PathParamSpaceRef) if err != nil { return "", err } diff --git a/internal/api/request/token.go b/internal/api/request/token.go index adb6ea7ad..1dca09bc6 100644 --- a/internal/api/request/token.go +++ b/internal/api/request/token.go @@ -8,6 +8,6 @@ const ( PathParamTokenID = "tokenID" ) -func GetTokenID(r *http.Request) (int64, error) { - return ParseAsInt64(r, PathParamTokenID) +func GetTokenIDFromPath(r *http.Request) (int64, error) { + return PathParamAsInt64(r, PathParamTokenID) } diff --git a/internal/api/request/util.go b/internal/api/request/util.go index e22a146da..3eebe1531 100644 --- a/internal/api/request/util.go +++ b/internal/api/request/util.go @@ -15,9 +15,9 @@ import ( "github.com/harness/gitness/types/enum" ) -// ParamOrError tries to retrieve the parameter from the request and +// PathParamOrError 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) { +func PathParamOrError(r *http.Request, paramName string) (string, error) { value := chi.URLParam(r, paramName) if value == "" { return "", usererror.BadRequest(fmt.Sprintf("Parameter '%s' not found in request path.", paramName)) @@ -26,9 +26,9 @@ func ParamOrError(r *http.Request, paramName string) (string, error) { 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, err := ParamOrError(r, paramName) +// PathParamAsInt64 tries to retrieve the parameter from the request and parse it to in64. +func PathParamAsInt64(r *http.Request, paramName string) (int64, error) { + rawID, err := PathParamOrError(r, paramName) if err != nil { return 0, err } diff --git a/internal/api/usererror/translate.go b/internal/api/usererror/translate.go index ebced1093..086dddb26 100644 --- a/internal/api/usererror/translate.go +++ b/internal/api/usererror/translate.go @@ -45,7 +45,7 @@ func Translate(err error) *Error { // unknown error default: - log.Err(err).Msgf("Unable to translate error: %s", err) + log.Warn().Msgf("Unable to translate error: %s", err) return ErrInternal } } diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go new file mode 100644 index 000000000..dc1130441 --- /dev/null +++ b/internal/bootstrap/bootstrap.go @@ -0,0 +1,74 @@ +// 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 bootstrap + +import ( + "context" + "errors" + "fmt" + + "github.com/harness/gitness/internal/api/controller/user" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" + "github.com/rs/zerolog/log" +) + +const ( + adminUID = "admin" +) + +// Bootstrap is an abstraction of a function that bootstraps a system. +type Bootstrap func(context.Context) error + +func System(config *types.Config, userCtrl *user.Controller) func(context.Context) error { + return func(ctx context.Context) error { + return Admin(ctx, config, userCtrl) + } +} + +// Admin sets up the admin user based on the config (if provided) +// +// NOTE: We could just call update and ignore any duplicate error +// but then the duplicte might be due to email or name, not uid. +// Futhermore, it would create unnecesary error logs. +func Admin(ctx context.Context, config *types.Config, userCtrl *user.Controller) error { + if config.Admin.Name == "" { + return nil + } + + _, err := userCtrl.FindNoAuth(ctx, adminUID) + if err == nil || !errors.Is(err, store.ErrResourceNotFound) { + return err + } + + in := &user.CreateInput{ + UID: adminUID, + Name: config.Admin.Name, + Email: config.Admin.Email, + Password: config.Admin.Password, + } + + // create user as admin + usr, err := userCtrl.CreateNoAuth(ctx, in, true) + if errors.Is(err, store.ErrDuplicate) { + // user might've been created by another instance + // in which case we should find the user now. + _, err2 := userCtrl.FindNoAuth(ctx, adminUID) + if err2 != nil { + // return original error. + return err + } + + // user exists - perfect + return nil + } + if err != nil { + return fmt.Errorf("failed to create admin user: %w", err) + } + + log.Ctx(ctx).Info().Msgf("Created admin user (id: %d).", usr.ID) + + return nil +} diff --git a/internal/bootstrap/wire.go b/internal/bootstrap/wire.go new file mode 100644 index 000000000..2fa592425 --- /dev/null +++ b/internal/bootstrap/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 bootstrap + +import ( + "github.com/google/wire" + "github.com/harness/gitness/internal/api/controller/user" + "github.com/harness/gitness/types" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet(ProvideBootstrap) + +func ProvideBootstrap(config *types.Config, userCtrl *user.Controller) Bootstrap { + return System(config, userCtrl) +} diff --git a/internal/router/git.go b/internal/router/git.go index bab7d6bfd..379b802cb 100644 --- a/internal/router/git.go +++ b/internal/router/git.go @@ -76,7 +76,7 @@ func stubGitHandler(repoStore store.RepoStore) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) - repoPath, _ := request.GetRepoRef(r) + repoPath, _ := request.GetRepoRefFromPath(r) repo, err := repoStore.FindByPath(r.Context(), repoPath) if err != nil { _, _ = w.Write([]byte(fmt.Sprintf("Repo '%s' not found.", repoPath))) diff --git a/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql b/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql index 2c5176600..ac02a3a27 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_paths.up.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS paths ( ,path_targetType TEXT ,path_targetId INTEGER ,path_createdBy INTEGER -,path_created INTEGER -,path_updated INTEGER +,path_created BIGINT +,path_updated BIGINT ,UNIQUE(path_value) ); \ No newline at end of file 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 27aa22f1a..378f38eb3 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 @@ -6,8 +6,8 @@ principal_id SERIAL PRIMARY KEY ,principal_admin BOOLEAN ,principal_blocked BOOLEAN ,principal_salt TEXT -,principal_created INTEGER -,principal_updated INTEGER +,principal_created BIGINT +,principal_updated BIGINT ,principal_user_email CITEXT ,principal_user_password TEXT diff --git a/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql b/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql index 0ae8655ab..b377b0ec0 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_repositories.up.sql @@ -6,8 +6,8 @@ CREATE TABLE IF NOT EXISTS repositories ( ,repo_description TEXT ,repo_isPublic BOOLEAN ,repo_createdBy INTEGER -,repo_created INTEGER -,repo_updated INTEGER +,repo_created BIGINT +,repo_updated BIGINT ,repo_forkId INTEGER ,repo_numForks INTEGER ,repo_numPulls INTEGER diff --git a/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql b/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql index 6c4cd209a..a688a083c 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_spaces.up.sql @@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS spaces ( ,space_description TEXT ,space_isPublic BOOLEAN ,space_createdBy INTEGER -,space_created INTEGER -,space_updated INTEGER +,space_created BIGINT +,space_updated BIGINT ); \ No newline at end of file diff --git a/internal/store/database/migrate/postgres/0001_create_table_tokens.up.sql b/internal/store/database/migrate/postgres/0001_create_table_tokens.up.sql index 34ff991c4..5b841ab91 100644 --- a/internal/store/database/migrate/postgres/0001_create_table_tokens.up.sql +++ b/internal/store/database/migrate/postgres/0001_create_table_tokens.up.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS tokens ( ,token_type TEXT ,token_name TEXT ,token_principalId INTEGER -,token_expiresAt INTEGER -,token_grants INTEGER -,token_issuedAt INTEGER +,token_expiresAt BIGINT +,token_grants BIGINT +,token_issuedAt BIGINT ,token_createdBy INTEGER ); diff --git a/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql index cd5cf0d3a..c7a098f5d 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_paths.up.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS paths ( ,path_targetType TEXT ,path_targetId INTEGER ,path_createdBy INTEGER -,path_created INTEGER -,path_updated INTEGER +,path_created BIGINT +,path_updated BIGINT ,UNIQUE(path_value COLLATE NOCASE) ); \ No newline at end of file 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 fd015c9eb..d6c71406a 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 @@ -6,8 +6,8 @@ principal_id INTEGER PRIMARY KEY AUTOINCREMENT ,principal_admin BOOLEAN ,principal_blocked BOOLEAN ,principal_salt TEXT -,principal_created INTEGER -,principal_updated INTEGER +,principal_created BIGINT +,principal_updated BIGINT ,principal_user_email TEXT ,principal_user_password TEXT diff --git a/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql index f12375de8..5b71b494e 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_repositories.up.sql @@ -6,8 +6,8 @@ CREATE TABLE IF NOT EXISTS repositories ( ,repo_description TEXT ,repo_isPublic BOOLEAN ,repo_createdBy INTEGER -,repo_created INTEGER -,repo_updated INTEGER +,repo_created BIGINT +,repo_updated BIGINT ,repo_forkId INTEGER ,repo_numForks INTEGER ,repo_numPulls INTEGER diff --git a/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql index b0c3b0357..bc4e71f89 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_spaces.up.sql @@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS spaces ( ,space_description TEXT ,space_isPublic BOOLEAN ,space_createdBy INTEGER -,space_created INTEGER -,space_updated INTEGER +,space_created BIGINT +,space_updated BIGINT ); \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0001_create_table_tokens.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_tokens.up.sql index 098e08de9..f0bb42165 100644 --- a/internal/store/database/migrate/sqlite/0001_create_table_tokens.up.sql +++ b/internal/store/database/migrate/sqlite/0001_create_table_tokens.up.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS tokens ( ,token_type TEXT COLLATE NOCASE ,token_name TEXT ,token_principalId INTEGER -,token_expiresAt INTEGER -,token_grants INTEGER -,token_issuedAt INTEGER +,token_expiresAt BIGINT +,token_grants BIGINT +,token_issuedAt BIGINT ,token_createdBy INTEGER ); diff --git a/internal/store/database/service.go b/internal/store/database/service.go new file mode 100644 index 000000000..c851fea71 --- /dev/null +++ b/internal/store/database/service.go @@ -0,0 +1,179 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + "database/sql" + + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types" + + "github.com/jmoiron/sqlx" +) + +var _ store.ServiceStore = (*ServiceStore)(nil) + +// NewServiceStore returns a new ServiceStore. +func NewServiceStore(db *sqlx.DB) *ServiceStore { + return &ServiceStore{db} +} + +// ServiceStore implements a ServiceStore backed by a relational +// database. +type ServiceStore struct { + db *sqlx.DB +} + +// Find finds the service by id. +func (s *ServiceStore) Find(ctx context.Context, id int64) (*types.Service, error) { + dst := new(types.Service) + if err := s.db.GetContext(ctx, dst, serviceSelectID, id); err != nil { + return nil, processSQLErrorf(err, "Select by id query failed") + } + return dst, nil +} + +// FindUID finds the service by uid. +func (s *ServiceStore) FindUID(ctx context.Context, uid string) (*types.Service, error) { + dst := new(types.Service) + if err := s.db.GetContext(ctx, dst, serviceSelectUID, uid); err != nil { + return nil, processSQLErrorf(err, "Select by uid query failed") + } + return dst, nil +} + +// Create saves the service. +func (s *ServiceStore) Create(ctx context.Context, sa *types.Service) error { + query, arg, err := s.db.BindNamed(serviceInsert, sa) + if err != nil { + return processSQLErrorf(err, "Failed to bind service object") + } + + if err = s.db.QueryRowContext(ctx, query, arg...).Scan(&sa.ID); err != nil { + return processSQLErrorf(err, "Insert query failed") + } + + return nil +} + +// Update updates the service. +func (s *ServiceStore) Update(ctx context.Context, sa *types.Service) error { + query, arg, err := s.db.BindNamed(serviceUpdate, sa) + if err != nil { + return processSQLErrorf(err, "Failed to bind service object") + } + + if _, err = s.db.ExecContext(ctx, query, arg...); err != nil { + return processSQLErrorf(err, "Update query failed") + } + + return err +} + +// Delete deletes the service. +func (s *ServiceStore) 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") + } + defer func(tx *sql.Tx) { + _ = tx.Rollback() + }(tx) + // delete the service + if _, err = tx.ExecContext(ctx, serviceDelete, id); err != nil { + return processSQLErrorf(err, "The delete query failed") + } + return tx.Commit() +} + +// List returns a list of service for a specific parent. +func (s *ServiceStore) List(ctx context.Context) ([]*types.Service, error) { + dst := []*types.Service{} + + err := s.db.SelectContext(ctx, &dst, serviceSelect) + if err != nil { + return nil, processSQLErrorf(err, "Failed executing default list query") + } + return dst, nil +} + +// Count returns a count of service for a specific parent. +func (s *ServiceStore) Count(ctx context.Context) (int64, error) { + var count int64 + err := s.db.QueryRowContext(ctx, serviceCount).Scan(&count) + if err != nil { + return 0, processSQLErrorf(err, "Failed executing count query") + } + return count, nil +} + +const serviceCount = ` +SELECT count(*) +FROM principals +WHERE principal_type = "service" +` + +const serviceBase = ` +SELECT +principal_id +,principal_uid +,principal_name +,principal_blocked +,principal_salt +,principal_created +,principal_updated +FROM principals +` + +const serviceSelect = serviceBase + ` +WHERE principal_type = "service" +ORDER BY principal_name ASC +` + +const serviceSelectID = serviceBase + ` +WHERE principal_type = "service" AND principal_id = $1 +` + +const serviceSelectUID = serviceBase + ` +WHERE principal_type = "service" AND principal_uid = $1 +` + +const serviceDelete = ` +DELETE FROM principals +WHERE principal_type = "service" AND principal_id = $1 +` + +const serviceInsert = ` +INSERT INTO principals ( +principal_type +,principal_uid +,principal_name +,principal_admin +,principal_blocked +,principal_salt +,principal_created +,principal_updated +) values ( + "service" +,:principal_uid +,:principal_name +,:principal_admin +,:principal_blocked +,:principal_salt +,:principal_created +,:principal_updated +) RETURNING principal_id +` + +const serviceUpdate = ` +UPDATE principals +SET + principal_name = :principal_name +,principal_admin = :principal_admin +,principal_blocked = :principal_blocked +,principal_updated = :principal_updated +WHERE principal_type = "service" AND principal_id = :principal_id +` diff --git a/internal/store/database/serviceAccount.go b/internal/store/database/serviceAccount.go index 4ccabe4d6..9fd201721 100644 --- a/internal/store/database/serviceAccount.go +++ b/internal/store/database/serviceAccount.go @@ -181,8 +181,8 @@ const serviceAccountUpdate = ` UPDATE principals SET principal_name = :principal_name -,:principal_blocked = :principal_blocked -,:principal_salt = :principal_salt -,:principal_updated = :principal_updated +,principal_blocked = :principal_blocked +,principal_salt = :principal_salt +,principal_updated = :principal_updated WHERE principal_type = "serviceaccount" AND principal_id = :principal_id ` diff --git a/internal/store/database/service_sync.go b/internal/store/database/service_sync.go new file mode 100644 index 000000000..94ecb4f2c --- /dev/null +++ b/internal/store/database/service_sync.go @@ -0,0 +1,76 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/internal/store/database/mutex" + "github.com/harness/gitness/types" +) + +var _ store.ServiceStore = (*ServiceStoreSync)(nil) + +// NewServiceStoreSync returns a new ServiceStoreSync. +func NewServiceStoreSync(base *ServiceStore) *ServiceStoreSync { + return &ServiceStoreSync{base} +} + +// ServiceStoreSync synchronizes read and write access to the +// service store. This prevents race conditions when the database +// type is sqlite3. +type ServiceStoreSync struct { + base *ServiceStore +} + +// Find finds the service by id. +func (s *ServiceStoreSync) Find(ctx context.Context, id int64) (*types.Service, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Find(ctx, id) +} + +// FindUID finds the service by uid. +func (s *ServiceStoreSync) FindUID(ctx context.Context, uid string) (*types.Service, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindUID(ctx, uid) +} + +// Create saves the service. +func (s *ServiceStoreSync) Create(ctx context.Context, sa *types.Service) error { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Create(ctx, sa) +} + +// Update updates the service. +func (s *ServiceStoreSync) Update(ctx context.Context, sa *types.Service) error { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Update(ctx, sa) +} + +// Delete deletes the service. +func (s *ServiceStoreSync) Delete(ctx context.Context, id int64) error { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Delete(ctx, id) +} + +// List returns a list of service for a specific parent. +func (s *ServiceStoreSync) List(ctx context.Context) ([]*types.Service, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.List(ctx) +} + +// Count returns a count of service for a specific parent. +func (s *ServiceStoreSync) Count(ctx context.Context) (int64, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Count(ctx) +} diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go index b031f4a9e..04a9e1d11 100644 --- a/internal/store/database/wire.go +++ b/internal/store/database/wire.go @@ -23,6 +23,7 @@ var WireSet = wire.NewSet( ProvideDatabase, ProvideUserStore, ProvideServiceAccountStore, + ProvideServiceStore, ProvideSpaceStore, ProvideRepoStore, ProvideTokenStore, @@ -61,6 +62,18 @@ func ProvideServiceAccountStore(db *sqlx.DB) store.ServiceAccountStore { } } +// ProvideServiceStore provides a service store. +func ProvideServiceStore(db *sqlx.DB) store.ServiceStore { + switch db.DriverName() { + case postgres: + return NewServiceStore(db) + default: + return NewServiceStoreSync( + NewServiceStore(db), + ) + } +} + // ProvideSpaceStore provides a space store. func ProvideSpaceStore(db *sqlx.DB) store.SpaceStore { switch db.DriverName() { diff --git a/internal/store/store.go b/internal/store/store.go index bbfb70fcd..c5ba7cb7a 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -64,6 +64,30 @@ type ( Count(ctx context.Context, parentType enum.ParentResourceType, parentID int64) (int64, error) } + // ServiceStore defines the service data storage. + ServiceStore interface { + // Find finds the service by id. + Find(ctx context.Context, id int64) (*types.Service, error) + + // FindUID finds the service by uid. + FindUID(ctx context.Context, uid string) (*types.Service, error) + + // Create saves the service. + Create(ctx context.Context, sa *types.Service) error + + // Update updates the service details. + Update(ctx context.Context, sa *types.Service) error + + // Delete deletes the service. + Delete(ctx context.Context, id int64) error + + // List returns a list of all services. + List(ctx context.Context) ([]*types.Service, error) + + // Count returns a count of all services. + Count(ctx context.Context) (int64, error) + } + // SpaceStore defines the space data storage. SpaceStore interface { // Find the space by id. diff --git a/types/check/password.go b/types/check/password.go new file mode 100644 index 000000000..39eb7cedb --- /dev/null +++ b/types/check/password.go @@ -0,0 +1,34 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check + +import ( + "fmt" +) + +const ( + minPasswordLength = 1 + maxPasswordLength = 128 +) + +var ( + // ErrPasswordLength is returned when the password + // is outside of the allowed length. + ErrPasswordLength = &ValidationError{ + fmt.Sprintf("Password has to be within %d and %d characters", minPasswordLength, maxPasswordLength), + } +) + +// Password returns true if the Password is valid. +// TODO: add proper password checks. +func Password(pw string) error { + // validate length + l := len(pw) + if l < minPasswordLength || l > maxPasswordLength { + return ErrPasswordLength + } + + return nil +} diff --git a/types/check/service.go b/types/check/service.go new file mode 100644 index 000000000..f9e64c574 --- /dev/null +++ b/types/check/service.go @@ -0,0 +1,24 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check + +import ( + "github.com/harness/gitness/types" +) + +// Service returns true if the Service if valid. +func Service(sa *types.Service) error { + // validate UID + if err := UID(sa.UID); err != nil { + return err + } + + // validate name + if err := Name(sa.Name); err != nil { + return err + } + + return nil +} diff --git a/types/check/serviceAccount.go b/types/check/serviceAccount.go index fdf083a08..b2e2b0967 100644 --- a/types/check/serviceAccount.go +++ b/types/check/serviceAccount.go @@ -22,12 +22,12 @@ func ServiceAccount(sa *types.ServiceAccount) error { return err } - // verify name + // validate name if err := Name(sa.Name); err != nil { return err } - // verify parentType is valid + // validate parentType if sa.ParentType != enum.ParentResourceTypeRepo && sa.ParentType != enum.ParentResourceTypeSpace { return ErrServiceAccountParentTypeIsInvalid } diff --git a/types/check/user.go b/types/check/user.go index b9c6a9fa7..e1f6f925b 100644 --- a/types/check/user.go +++ b/types/check/user.go @@ -17,13 +17,13 @@ const ( var ( // ErrEmailLen is returned when the email address - // exceeds the maximum number of characters. + // exceeds the range of allowed number of characters. ErrEmailLen = &ValidationError{ fmt.Sprintf("Email address has to be within %d and %d characters", minEmailLength, maxEmailLength), } ) -// User returns true if the User if valid. +// User returns true if the User is valid. func User(user *types.User) error { // validate UID if err := UID(user.UID); err != nil { diff --git a/types/config.go b/types/config.go index a98c45918..ea1eff5fe 100644 --- a/types/config.go +++ b/types/config.go @@ -64,4 +64,11 @@ type Config struct { ContentSecurityPolicy string `envconfig:"GITNESS_HTTP_CONTENT_SECURITY_POLICY"` ReferrerPolicy string `envconfig:"GITNESS_HTTP_REFERRER_POLICY"` } + + // Admin defines admin user params (no admin setup if either is empty) + Admin struct { + Name string `envconfig:"GITNESS_ADMIN_NAME"` + Email string `envconfig:"GITNESS_ADMIN_EMAIL"` + Password string `envconfig:"GITNESS_ADMIN_PASSWORD"` + } } diff --git a/types/enum/permission.go b/types/enum/permission.go index ee36734e6..d5ba661ce 100644 --- a/types/enum/permission.go +++ b/types/enum/permission.go @@ -12,6 +12,7 @@ const ( ResourceTypeRepo ResourceType = "REPOSITORY" ResourceTypeUser ResourceType = "USER" ResourceTypeServiceAccount ResourceType = "SERVICEACCOUNT" + ResourceTypeService ResourceType = "SERVICE" // ResourceType_Branch ResourceType = "BRANCH" ) @@ -51,10 +52,21 @@ const ( const ( /* - ----- REPOSITORY ----- + ----- SERVICE ACCOUNT ----- */ PermissionServiceAccountCreate Permission = "serviceaccount_create" PermissionServiceAccountView Permission = "serviceaccount_view" PermissionServiceAccountEdit Permission = "serviceaccount_edit" PermissionServiceAccountDelete Permission = "serviceaccount_delete" ) + +const ( + /* + ----- SERVICE ----- + */ + PermissionServiceCreate Permission = "service_create" + PermissionServiceView Permission = "service_view" + PermissionServiceEdit Permission = "service_edit" + PermissionServiceDelete Permission = "service_delete" + PermissionServiceEditAdmin Permission = "service_editadmin" +) diff --git a/types/path.go b/types/path.go index 2b01b9432..77b537190 100644 --- a/types/path.go +++ b/types/path.go @@ -14,6 +14,7 @@ const ( // Represents a path to a resource (e.g. space) that can be used to address the resource. type Path struct { + // TODO: int64 ID doesn't match DB ID int64 `db:"path_id" json:"id"` Value string `db:"path_value" json:"value"` IsAlias bool `db:"path_isAlias" json:"isAlias"` diff --git a/types/principal.go b/types/principal.go index 7f7d4bbee..288d1c40b 100644 --- a/types/principal.go +++ b/types/principal.go @@ -10,7 +10,7 @@ import "github.com/harness/gitness/types/enum" type ( // Represents the identity of an acting entity (User, ServiceAccount, Service). Principal struct { - // ID is the internal identifier of a principal (primary key) + // TODO: int64 ID doesn't match DB ID int64 `db:"principal_id" json:"-"` UID string `db:"principal_uid" json:"uid"` Type enum.PrincipalType `db:"principal_type" json:"type"` @@ -61,7 +61,7 @@ func PrincipalFromService(s *Service) *Principal { UID: s.UID, Type: enum.PrincipalTypeService, Name: s.Name, - Admin: true, + Admin: s.Admin, Blocked: s.Blocked, Salt: s.Salt, Created: s.Created, diff --git a/types/repo.go b/types/repo.go index 4dcbb0587..52c61530e 100644 --- a/types/repo.go +++ b/types/repo.go @@ -10,7 +10,7 @@ import ( // Repository represents a code repository. type Repository struct { - // Core properties + // TODO: int64 ID doesn't match DB ID int64 `db:"repo_id" json:"id"` SpaceID int64 `db:"repo_spaceId" json:"spaceId"` PathName string `db:"repo_pathName" json:"pathName"` diff --git a/types/service.go b/types/service.go index d5e75ab93..387c49f21 100644 --- a/types/service.go +++ b/types/service.go @@ -5,12 +5,10 @@ // Package types defines common data structures. package types -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, as it's always admin for now) + // Fields from Principal ID int64 `db:"principal_id" json:"-"` UID string `db:"principal_uid" json:"uid"` Name string `db:"principal_name" json:"name"` @@ -20,12 +18,4 @@ type ( Created int64 `db:"principal_created" json:"created"` Updated int64 `db:"principal_updated" json:"updated"` } - - // ServiceAccountInput store details used to - // create or update a service account. - ServiceInput struct { - Name *string `json:"name"` - ParentType *enum.ParentResourceType `json:"parentType"` - ParentID *int64 `json:"parentId"` - } ) diff --git a/types/space.go b/types/space.go index 25f9e16e7..36753a5ec 100644 --- a/types/space.go +++ b/types/space.go @@ -21,6 +21,7 @@ https://stackoverflow.com/questions/4048151/what-are-the-options-for-storing-hie https://www.slideshare.net/billkarwin/models-for-hierarchical-data */ type Space struct { + // TODO: int64 ID doesn't match DB ID int64 `db:"space_id" json:"id"` ParentID int64 `db:"space_parentId" json:"parentId"` PathName string `db:"space_pathName" json:"pathName"` diff --git a/types/token.go b/types/token.go index c769bb3f9..1cd3f9a5a 100644 --- a/types/token.go +++ b/types/token.go @@ -10,6 +10,7 @@ import ( // Represents server side infos stored for tokens we distribute. type Token struct { + // TODO: int64 ID doesn't match DB ID int64 `db:"token_id" json:"id"` Type enum.TokenType `db:"token_type" json:"type"` Name string `db:"token_name" json:"name"`