From 39a998eacd87ff3af35ba694e8d33e7e66cfaa3d Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Thu, 28 Mar 2024 03:36:15 +0000 Subject: [PATCH] Adding Repo Level Settings (#1145) --- app/api/controller/githook/controller.go | 8 +- app/api/controller/githook/pre_receive.go | 2 +- app/api/controller/repo/controller.go | 42 ++-- app/api/controller/repo/helper.go | 73 +++++++ app/api/controller/repo/wire.go | 4 +- app/api/controller/reposettings/controller.go | 65 ++++++ app/api/controller/reposettings/security.go | 46 +++++ .../controller/reposettings/security_find.go | 44 ++++ .../reposettings/security_update.go | 51 +++++ app/api/controller/reposettings/wire.go | 36 ++++ app/api/handler/reposettings/security_find.go | 43 ++++ .../handler/reposettings/security_update.go | 51 +++++ app/githook/wire.go | 3 + app/router/api.go | 14 +- app/router/wire.go | 4 +- app/services/settings/helpers.go | 62 ++++++ app/services/settings/mapping.go | 72 +++++++ app/services/settings/service.go | 169 +++++++++++++++ app/services/settings/service_repo.go | 81 ++++++++ app/services/settings/settings.go | 23 +++ app/services/settings/wire.go | 31 +++ app/store/database.go | 30 +++ .../0047_create_table_settings.down.sql | 1 + .../0047_create_table_settings.up.sql | 24 +++ .../0047_create_table_settings.down.sql | 1 + .../sqlite/0047_create_table_settings.up.sql | 24 +++ app/store/database/settings.go | 194 ++++++++++++++++++ app/store/database/wire.go | 6 + cmd/gitness/wire.go | 4 + cmd/gitness/wire_gen.go | 11 +- types/enum/settings.go | 37 ++++ 31 files changed, 1221 insertions(+), 35 deletions(-) create mode 100644 app/api/controller/repo/helper.go create mode 100644 app/api/controller/reposettings/controller.go create mode 100644 app/api/controller/reposettings/security.go create mode 100644 app/api/controller/reposettings/security_find.go create mode 100644 app/api/controller/reposettings/security_update.go create mode 100644 app/api/controller/reposettings/wire.go create mode 100644 app/api/handler/reposettings/security_find.go create mode 100644 app/api/handler/reposettings/security_update.go create mode 100644 app/services/settings/helpers.go create mode 100644 app/services/settings/mapping.go create mode 100644 app/services/settings/service.go create mode 100644 app/services/settings/service_repo.go create mode 100644 app/services/settings/settings.go create mode 100644 app/services/settings/wire.go create mode 100644 app/store/database/migrate/postgres/0047_create_table_settings.down.sql create mode 100644 app/store/database/migrate/postgres/0047_create_table_settings.up.sql create mode 100644 app/store/database/migrate/sqlite/0047_create_table_settings.down.sql create mode 100644 app/store/database/migrate/sqlite/0047_create_table_settings.up.sql create mode 100644 app/store/database/settings.go create mode 100644 types/enum/settings.go diff --git a/app/api/controller/githook/controller.go b/app/api/controller/githook/controller.go index 5fc3b3fab..613c3563d 100644 --- a/app/api/controller/githook/controller.go +++ b/app/api/controller/githook/controller.go @@ -24,6 +24,7 @@ import ( "github.com/harness/gitness/app/auth/authz" eventsgit "github.com/harness/gitness/app/events/git" "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" "github.com/harness/gitness/types" @@ -38,7 +39,8 @@ type Controller struct { pullreqStore store.PullReqStore urlProvider url.Provider protectionManager *protection.Manager - resourceLimiter limiter.ResourceLimiter + limiter limiter.ResourceLimiter + settings *settings.Service preReceiveExtender PreReceiveExtender updateExtender UpdateExtender postReceiveExtender PostReceiveExtender @@ -53,6 +55,7 @@ func NewController( urlProvider url.Provider, protectionManager *protection.Manager, limiter limiter.ResourceLimiter, + settings *settings.Service, preReceiveExtender PreReceiveExtender, updateExtender UpdateExtender, postReceiveExtender PostReceiveExtender, @@ -66,7 +69,8 @@ func NewController( pullreqStore: pullreqStore, urlProvider: urlProvider, protectionManager: protectionManager, - resourceLimiter: limiter, + limiter: limiter, + settings: settings, preReceiveExtender: preReceiveExtender, updateExtender: updateExtender, postReceiveExtender: postReceiveExtender, diff --git a/app/api/controller/githook/pre_receive.go b/app/api/controller/githook/pre_receive.go index baf41aaf4..5697acf5b 100644 --- a/app/api/controller/githook/pre_receive.go +++ b/app/api/controller/githook/pre_receive.go @@ -46,7 +46,7 @@ func (c *Controller) PreReceive( return hook.Output{}, err } - if err := c.resourceLimiter.RepoSize(ctx, in.RepoID); err != nil { + if err := c.limiter.RepoSize(ctx, in.RepoID); err != nil { return hook.Output{}, fmt.Errorf( "resource limit exceeded: %w", limiter.ErrMaxRepoSizeReached) diff --git a/app/api/controller/repo/controller.go b/app/api/controller/repo/controller.go index a7ea23160..b0f971389 100644 --- a/app/api/controller/repo/controller.go +++ b/app/api/controller/repo/controller.go @@ -30,6 +30,7 @@ import ( "github.com/harness/gitness/app/services/importer" "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" "github.com/harness/gitness/git" @@ -56,6 +57,7 @@ type Controller struct { pipelineStore store.PipelineStore principalStore store.PrincipalStore ruleStore store.RuleStore + settings *settings.Service principalInfoCache store.PrincipalInfoCache protectionManager *protection.Manager git git.Interface @@ -79,6 +81,7 @@ func NewController( pipelineStore store.PipelineStore, principalStore store.PrincipalStore, ruleStore store.RuleStore, + settings *settings.Service, principalInfoCache store.PrincipalInfoCache, protectionManager *protection.Manager, git git.Interface, @@ -102,6 +105,7 @@ func NewController( pipelineStore: pipelineStore, principalStore: principalStore, ruleStore: ruleStore, + settings: settings, principalInfoCache: principalInfoCache, protectionManager: protectionManager, git: git, @@ -121,20 +125,11 @@ func (c *Controller) getRepo( ctx context.Context, repoRef string, ) (*types.Repository, error) { - if repoRef == "" { - return nil, usererror.BadRequest("A valid repository reference must be provided.") - } - - repo, err := c.repoStore.FindByRef(ctx, repoRef) - if err != nil { - return nil, fmt.Errorf("failed to find repository: %w", err) - } - - if repo.Importing { - return nil, usererror.BadRequest("Repository import is in progress.") - } - - return repo, nil + return GetRepo( + ctx, + c.repoStore, + repoRef, + ) } // getRepoCheckAccess fetches an active repo (not one that is currently being imported) @@ -146,16 +141,15 @@ func (c *Controller) getRepoCheckAccess( reqPermission enum.Permission, orPublic bool, ) (*types.Repository, error) { - repo, err := c.getRepo(ctx, repoRef) - if err != nil { - return nil, fmt.Errorf("failed to find repo: %w", err) - } - - if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, reqPermission, orPublic); err != nil { - return nil, fmt.Errorf("access check failed: %w", err) - } - - return repo, nil + return GetRepoCheckAccess( + ctx, + c.repoStore, + c.authorizer, + session, + repoRef, + reqPermission, + orPublic, + ) } func (c *Controller) validateParentRef(parentRef string) error { diff --git a/app/api/controller/repo/helper.go b/app/api/controller/repo/helper.go new file mode 100644 index 000000000..5d941069a --- /dev/null +++ b/app/api/controller/repo/helper.go @@ -0,0 +1,73 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/auth/authz" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// GetRepo fetches an active repo (not one that is currently being imported). +func GetRepo( + ctx context.Context, + repoStore store.RepoStore, + repoRef string, +) (*types.Repository, error) { + if repoRef == "" { + return nil, usererror.BadRequest("A valid repository reference must be provided.") + } + + repo, err := repoStore.FindByRef(ctx, repoRef) + if err != nil { + return nil, fmt.Errorf("failed to find repository: %w", err) + } + + if repo.Importing { + return nil, usererror.BadRequest("Repository import is in progress.") + } + + return repo, nil +} + +// GetRepoCheckAccess fetches an active repo (not one that is currently being imported) +// and checks if the current user has permission to access it. +func GetRepoCheckAccess( + ctx context.Context, + repoStore store.RepoStore, + authorizer authz.Authorizer, + session *auth.Session, + repoRef string, + reqPermission enum.Permission, + orPublic bool, +) (*types.Repository, error) { + repo, err := GetRepo(ctx, repoStore, repoRef) + if err != nil { + return nil, fmt.Errorf("failed to find repo: %w", err) + } + + if err = apiauth.CheckRepo(ctx, authorizer, session, repo, reqPermission, orPublic); err != nil { + return nil, fmt.Errorf("access check failed: %w", err) + } + + return repo, nil +} diff --git a/app/api/controller/repo/wire.go b/app/api/controller/repo/wire.go index 508d2ce63..288d77ae3 100644 --- a/app/api/controller/repo/wire.go +++ b/app/api/controller/repo/wire.go @@ -22,6 +22,7 @@ import ( "github.com/harness/gitness/app/services/importer" "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" "github.com/harness/gitness/git" @@ -48,6 +49,7 @@ func ProvideController( pipelineStore store.PipelineStore, principalStore store.PrincipalStore, ruleStore store.RuleStore, + settings *settings.Service, principalInfoCache store.PrincipalInfoCache, protectionManager *protection.Manager, rpcClient git.Interface, @@ -63,7 +65,7 @@ func ProvideController( return NewController(config, tx, urlProvider, authorizer, repoStore, spaceStore, pipelineStore, - principalStore, ruleStore, principalInfoCache, protectionManager, + principalStore, ruleStore, settings, principalInfoCache, protectionManager, rpcClient, importer, codeOwners, reporeporter, indexer, limiter, mtxManager, identifierCheck, repoChecks) } diff --git a/app/api/controller/reposettings/controller.go b/app/api/controller/reposettings/controller.go new file mode 100644 index 000000000..c98e99f43 --- /dev/null +++ b/app/api/controller/reposettings/controller.go @@ -0,0 +1,65 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "context" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/auth/authz" + "github.com/harness/gitness/app/services/settings" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +type Controller struct { + authorizer authz.Authorizer + repoStore store.RepoStore + settings *settings.Service +} + +func NewController( + authorizer authz.Authorizer, + repoStore store.RepoStore, + settings *settings.Service, +) *Controller { + return &Controller{ + authorizer: authorizer, + repoStore: repoStore, + settings: settings, + } +} + +// getRepoCheckAccess fetches an active repo (not one that is currently being imported) +// and checks if the current user has permission to access it. +func (c *Controller) getRepoCheckAccess( + ctx context.Context, + session *auth.Session, + repoRef string, + reqPermission enum.Permission, + orPublic bool, +) (*types.Repository, error) { + return repo.GetRepoCheckAccess( + ctx, + c.repoStore, + c.authorizer, + session, + repoRef, + reqPermission, + orPublic, + ) +} diff --git a/app/api/controller/reposettings/security.go b/app/api/controller/reposettings/security.go new file mode 100644 index 000000000..1b50104b2 --- /dev/null +++ b/app/api/controller/reposettings/security.go @@ -0,0 +1,46 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "github.com/harness/gitness/app/services/settings" + + "github.com/gotidy/ptr" +) + +// SecuritySettings represents the security related part of repository settings as exposed externally. +type SecuritySettings struct { + SecretScanningEnabled *bool `json:"secret_scanning_enabled"` +} + +func GetDefaultSecuritySettings() *SecuritySettings { + return &SecuritySettings{ + SecretScanningEnabled: ptr.Bool(settings.DefaultSecretScanningEnabled), + } +} + +func GetSecuritySettingsMappings(s *SecuritySettings) []settings.SettingHandler { + return []settings.SettingHandler{ + settings.Mapping(settings.KeySecretScanningEnabled, s.SecretScanningEnabled), + } +} + +func GetSecuritySettingsAsKeyValues(s *SecuritySettings) []settings.KeyValue { + kvs := make([]settings.KeyValue, 0, 1) + if s.SecretScanningEnabled != nil { + kvs = append(kvs, settings.KeyValue{Key: settings.KeySecretScanningEnabled, Value: *s.SecretScanningEnabled}) + } + return kvs +} diff --git a/app/api/controller/reposettings/security_find.go b/app/api/controller/reposettings/security_find.go new file mode 100644 index 000000000..f6c422f6c --- /dev/null +++ b/app/api/controller/reposettings/security_find.go @@ -0,0 +1,44 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// SecurityFind returns the security settings of a repo. +func (c *Controller) SecurityFind( + ctx context.Context, + session *auth.Session, + repoRef string, +) (*SecuritySettings, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true) + if err != nil { + return nil, err + } + + out := GetDefaultSecuritySettings() + mappings := GetSecuritySettingsMappings(out) + err = c.settings.RepoMap(ctx, repo.ID, mappings...) + if err != nil { + return nil, fmt.Errorf("failed to map settings: %w", err) + } + + return out, nil +} diff --git a/app/api/controller/reposettings/security_update.go b/app/api/controller/reposettings/security_update.go new file mode 100644 index 000000000..9d9e8cceb --- /dev/null +++ b/app/api/controller/reposettings/security_update.go @@ -0,0 +1,51 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// SecurityUpdate updates the security settings of the repo. +func (c *Controller) SecurityUpdate( + ctx context.Context, + session *auth.Session, + repoRef string, + in *SecuritySettings, +) (*SecuritySettings, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit, false) + if err != nil { + return nil, err + } + + err = c.settings.RepoSetMany(ctx, repo.ID, GetSecuritySettingsAsKeyValues(in)...) + if err != nil { + return nil, fmt.Errorf("failed to set settings: %w", err) + } + + // read all settings and return complete config + out := GetDefaultSecuritySettings() + mappings := GetSecuritySettingsMappings(out) + err = c.settings.RepoMap(ctx, repo.ID, mappings...) + if err != nil { + return nil, fmt.Errorf("failed to map settings: %w", err) + } + + return out, nil +} diff --git a/app/api/controller/reposettings/wire.go b/app/api/controller/reposettings/wire.go new file mode 100644 index 000000000..10776c9a3 --- /dev/null +++ b/app/api/controller/reposettings/wire.go @@ -0,0 +1,36 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "github.com/harness/gitness/app/auth/authz" + "github.com/harness/gitness/app/services/settings" + "github.com/harness/gitness/app/store" + + "github.com/google/wire" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideController, +) + +func ProvideController( + authorizer authz.Authorizer, + repoStore store.RepoStore, + settings *settings.Service, +) *Controller { + return NewController(authorizer, repoStore, settings) +} diff --git a/app/api/handler/reposettings/security_find.go b/app/api/handler/reposettings/security_find.go new file mode 100644 index 000000000..da2cfd672 --- /dev/null +++ b/app/api/handler/reposettings/security_find.go @@ -0,0 +1,43 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/reposettings" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleSecurityFind(repoSettingCtrl *reposettings.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + settings, err := repoSettingCtrl.SecurityFind(ctx, session, repoRef) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, settings) + } +} diff --git a/app/api/handler/reposettings/security_update.go b/app/api/handler/reposettings/security_update.go new file mode 100644 index 000000000..634bdceed --- /dev/null +++ b/app/api/handler/reposettings/security_update.go @@ -0,0 +1,51 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reposettings + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/reposettings" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleSecurityUpdate(repoSettingCtrl *reposettings.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(reposettings.SecuritySettings) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + settings, err := repoSettingCtrl.SecurityUpdate(ctx, session, repoRef, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, settings) + } +} diff --git a/app/githook/wire.go b/app/githook/wire.go index a14a8e5d7..44a2a9d28 100644 --- a/app/githook/wire.go +++ b/app/githook/wire.go @@ -20,6 +20,7 @@ import ( "github.com/harness/gitness/app/auth/authz" eventsgit "github.com/harness/gitness/app/events/git" "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" "github.com/harness/gitness/git" @@ -52,6 +53,7 @@ func ProvideController( protectionManager *protection.Manager, githookFactory hook.ClientFactory, limiter limiter.ResourceLimiter, + settings *settings.Service, preReceiveExtender githook.PreReceiveExtender, updateExtender githook.UpdateExtender, postReceiveExtender githook.PostReceiveExtender, @@ -65,6 +67,7 @@ func ProvideController( urlProvider, protectionManager, limiter, + settings, preReceiveExtender, updateExtender, postReceiveExtender, diff --git a/app/router/api.go b/app/router/api.go index 67b274d21..7c3f14bc9 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -30,6 +30,7 @@ import ( "github.com/harness/gitness/app/api/controller/principal" "github.com/harness/gitness/app/api/controller/pullreq" "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/controller/reposettings" "github.com/harness/gitness/app/api/controller/secret" "github.com/harness/gitness/app/api/controller/serviceaccount" "github.com/harness/gitness/app/api/controller/space" @@ -51,6 +52,7 @@ import ( handlerprincipal "github.com/harness/gitness/app/api/handler/principal" handlerpullreq "github.com/harness/gitness/app/api/handler/pullreq" handlerrepo "github.com/harness/gitness/app/api/handler/repo" + handlerreposettings "github.com/harness/gitness/app/api/handler/reposettings" "github.com/harness/gitness/app/api/handler/resource" handlersecret "github.com/harness/gitness/app/api/handler/secret" handlerserviceaccount "github.com/harness/gitness/app/api/handler/serviceaccount" @@ -97,6 +99,7 @@ func NewAPIHandler( config *types.Config, authenticator authn.Authenticator, repoCtrl *repo.Controller, + repoSettingsCtrl *reposettings.Controller, executionCtrl *execution.Controller, logCtrl *logs.Controller, spaceCtrl *space.Controller, @@ -139,7 +142,7 @@ func NewAPIHandler( r.Use(middlewareauthn.Attempt(authenticator)) r.Route("/v1", func(r chi.Router) { - setupRoutesV1(r, appCtx, config, repoCtrl, executionCtrl, triggerCtrl, logCtrl, pipelineCtrl, + setupRoutesV1(r, appCtx, config, repoCtrl, repoSettingsCtrl, executionCtrl, triggerCtrl, logCtrl, pipelineCtrl, connectorCtrl, templateCtrl, pluginCtrl, secretCtrl, spaceCtrl, pullreqCtrl, webhookCtrl, githookCtrl, git, saCtrl, userCtrl, principalCtrl, checkCtrl, sysCtrl, uploadCtrl, searchCtrl) @@ -167,6 +170,7 @@ func setupRoutesV1(r chi.Router, appCtx context.Context, config *types.Config, repoCtrl *repo.Controller, + repoSettingsCtrl *reposettings.Controller, executionCtrl *execution.Controller, triggerCtrl *trigger.Controller, logCtrl *logs.Controller, @@ -189,8 +193,8 @@ func setupRoutesV1(r chi.Router, searchCtrl *keywordsearch.Controller, ) { setupSpaces(r, appCtx, spaceCtrl) - setupRepos(r, repoCtrl, pipelineCtrl, executionCtrl, triggerCtrl, logCtrl, pullreqCtrl, webhookCtrl, checkCtrl, - uploadCtrl) + setupRepos(r, repoCtrl, repoSettingsCtrl, pipelineCtrl, executionCtrl, triggerCtrl, + logCtrl, pullreqCtrl, webhookCtrl, checkCtrl, uploadCtrl) setupConnectors(r, connectorCtrl) setupTemplates(r, templateCtrl) setupSecrets(r, secretCtrl) @@ -248,6 +252,7 @@ func setupSpaces(r chi.Router, appCtx context.Context, spaceCtrl *space.Controll func setupRepos(r chi.Router, repoCtrl *repo.Controller, + repoSettingsCtrl *reposettings.Controller, pipelineCtrl *pipeline.Controller, executionCtrl *execution.Controller, triggerCtrl *trigger.Controller, @@ -269,6 +274,9 @@ func setupRepos(r chi.Router, r.Post("/purge", handlerrepo.HandlePurge(repoCtrl)) r.Post("/restore", handlerrepo.HandleRestore(repoCtrl)) + r.Get("/settings/security", handlerreposettings.HandleSecurityFind(repoSettingsCtrl)) + r.Patch("/settings/security", handlerreposettings.HandleSecurityUpdate(repoSettingsCtrl)) + r.Post("/move", handlerrepo.HandleMove(repoCtrl)) r.Get("/service-accounts", handlerrepo.HandleListServiceAccounts(repoCtrl)) diff --git a/app/router/wire.go b/app/router/wire.go index 5cbb07750..09b7524b7 100644 --- a/app/router/wire.go +++ b/app/router/wire.go @@ -29,6 +29,7 @@ import ( "github.com/harness/gitness/app/api/controller/principal" "github.com/harness/gitness/app/api/controller/pullreq" "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/controller/reposettings" "github.com/harness/gitness/app/api/controller/secret" "github.com/harness/gitness/app/api/controller/serviceaccount" "github.com/harness/gitness/app/api/controller/space" @@ -92,6 +93,7 @@ func ProvideAPIHandler( config *types.Config, authenticator authn.Authenticator, repoCtrl *repo.Controller, + repoSettingsCtrl *reposettings.Controller, executionCtrl *execution.Controller, logCtrl *logs.Controller, spaceCtrl *space.Controller, @@ -114,7 +116,7 @@ func ProvideAPIHandler( searchCtrl *keywordsearch.Controller, ) APIHandler { return NewAPIHandler(appCtx, config, - authenticator, repoCtrl, executionCtrl, logCtrl, spaceCtrl, pipelineCtrl, + authenticator, repoCtrl, repoSettingsCtrl, executionCtrl, logCtrl, spaceCtrl, pipelineCtrl, secretCtrl, triggerCtrl, connectorCtrl, templateCtrl, pluginCtrl, pullreqCtrl, webhookCtrl, githookCtrl, git, saCtrl, userCtrl, principalCtrl, checkCtrl, sysCtrl, blobCtrl, searchCtrl) } diff --git a/app/services/settings/helpers.go b/app/services/settings/helpers.go new file mode 100644 index 000000000..e266d99be --- /dev/null +++ b/app/services/settings/helpers.go @@ -0,0 +1,62 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package settings + +import ( + "context" + "fmt" +) + +// RepoGet is a helper method for getting a setting of a specific type for a repo. +func RepoGet[T any]( + ctx context.Context, + s *Service, + repoID int64, + key Key, + dflt T, +) (T, error) { + var out T + ok, err := s.RepoGet(ctx, repoID, key, &out) + if err != nil { + return out, err + } + + if !ok { + return dflt, nil + } + + return out, nil +} + +// RepoGetRequired is a helper method for getting a setting of a specific type for a repo. +// If the setting isn't found, an error is returned. +func RepoGetRequired[T any]( + ctx context.Context, + s *Service, + repoID int64, + key Key, +) (T, error) { + var out T + ok, err := s.RepoGet(ctx, repoID, key, &out) + if err != nil { + return out, err + } + + if !ok { + return out, fmt.Errorf("setting %q not found", key) + } + + return out, nil +} diff --git a/app/services/settings/mapping.go b/app/services/settings/mapping.go new file mode 100644 index 000000000..61bfcd4dd --- /dev/null +++ b/app/services/settings/mapping.go @@ -0,0 +1,72 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package settings + +import ( + "context" + "encoding/json" + "fmt" +) + +// Mapping returns a SettingHandler that maps the value of the setting with the given key to the target. +func Mapping[T any](key Key, target *T) SettingHandler { + if target == nil { + panic("mapping target can't be nil") + } + return &settingHandlerMapping[T]{ + key: key, + required: false, + target: target, + } +} + +// MappingRequired returns a SettingHandler that maps the value of the setting with the given key to the target. +// If the setting wasn't found an error is returned. +func MappingRequired[T any](key Key, target *T) SettingHandler { + if target == nil { + panic("mapping target can't be nil") + } + return &settingHandlerMapping[T]{ + key: key, + required: true, + target: target, + } +} + +var _ SettingHandler = (*settingHandlerMapping[any])(nil) + +// settingHandlerMapping is a setting handler that maps the value of a setting to the provided target. +type settingHandlerMapping[T any] struct { + key Key + required bool + target *T +} + +func (q *settingHandlerMapping[T]) Key() Key { + return q.key +} + +func (q *settingHandlerMapping[T]) Required() bool { + return q.required +} + +func (q *settingHandlerMapping[T]) Handle(_ context.Context, raw []byte) error { + err := json.Unmarshal(raw, q.target) + if err != nil { + return fmt.Errorf("failed to unmarshal setting value: %w", err) + } + + return nil +} diff --git a/app/services/settings/service.go b/app/services/settings/service.go new file mode 100644 index 000000000..78412a243 --- /dev/null +++ b/app/services/settings/service.go @@ -0,0 +1,169 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package settings + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + appstore "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store" + "github.com/harness/gitness/types/enum" +) + +// KeyValue is a struct used for upserting many entries. +type KeyValue struct { + Key Key + Value any +} + +// SettingHandler is an abstraction of a component that's handling a single setting value as part of +// calling service.Map. +type SettingHandler interface { + Key() Key + Required() bool + Handle(ctx context.Context, raw []byte) error +} + +// Service is used to enhance interaction with the settings store. +type Service struct { + settingsStore appstore.SettingsStore +} + +func NewService( + settingsStore appstore.SettingsStore, +) *Service { + return &Service{ + settingsStore: settingsStore, + } +} + +// Set sets the value of the setting with the given key for the given scope. +func (s *Service) Set( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + key Key, + value any, +) error { + raw, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("failed to marshal setting value: %w", err) + } + + err = s.settingsStore.Upsert( + ctx, + scope, + scopeID, + string(key), + raw, + ) + if err != nil { + return fmt.Errorf("failed to upsert setting in store: %w", err) + } + + return nil +} + +// SetMany sets the value of the settings with the given keys for the given scope. +func (s *Service) SetMany( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + keyValues ...KeyValue, +) error { + // TODO: batch upsert + for _, kv := range keyValues { + if err := s.Set(ctx, scope, scopeID, kv.Key, kv.Value); err != nil { + return fmt.Errorf("failed to set setting for key %q: %w", kv.Key, err) + } + } + + return nil +} + +// Get returns the value of the setting with the given key for the given scope. +func (s *Service) Get( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + key Key, + out any, +) (bool, error) { + raw, err := s.settingsStore.Find( + ctx, + scope, + scopeID, + string(key), + ) + if errors.Is(err, store.ErrResourceNotFound) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("failed to find setting in store: %w", err) + } + + err = json.Unmarshal(raw, &out) + if err != nil { + return false, fmt.Errorf("failed to unmarshal setting value: %w", err) + } + + return true, nil +} + +// Map maps all available settings using the provided handlers for the given scope. +func (s *Service) Map( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + handlers ...SettingHandler, +) error { + if len(handlers) == 0 { + return nil + } + + keys := make([]string, len(handlers)) + for i, m := range handlers { + keys[i] = string(m.Key()) + } + + rawValues, err := s.settingsStore.FindMany( + ctx, + scope, + scopeID, + keys..., + ) + if err != nil { + return fmt.Errorf("failed to find settings in store: %w", err) + } + + for _, m := range handlers { + rawValue, found := rawValues[string(m.Key())] + if !found && m.Required() { + return fmt.Errorf("required setting %q not found", m.Key()) + } + if !found { + continue + } + + if err = m.Handle(ctx, rawValue); err != nil { + return fmt.Errorf("failed to handle value for setting %q: %w", m.Key(), err) + } + } + + return nil +} diff --git a/app/services/settings/service_repo.go b/app/services/settings/service_repo.go new file mode 100644 index 000000000..d4dd956be --- /dev/null +++ b/app/services/settings/service_repo.go @@ -0,0 +1,81 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package settings + +import ( + "context" + + "github.com/harness/gitness/types/enum" +) + +// RepoSet sets the value of the setting with the given key for the given repo. +func (s *Service) RepoSet( + ctx context.Context, + repoID int64, + key Key, + value any, +) error { + return s.Set( + ctx, + enum.SettingsScopeRepo, + repoID, + key, + value, + ) +} + +// RepoSetMany sets the value of the settings with the given keys for the given repo. +func (s *Service) RepoSetMany( + ctx context.Context, + repoID int64, + keyValues ...KeyValue, +) error { + return s.SetMany( + ctx, + enum.SettingsScopeRepo, + repoID, + keyValues..., + ) +} + +// RepoGet returns the value of the setting with the given key for the given repo. +func (s *Service) RepoGet( + ctx context.Context, + repoID int64, + key Key, + out any, +) (bool, error) { + return s.Get( + ctx, + enum.SettingsScopeRepo, + repoID, + key, + out, + ) +} + +// RepoMap maps all available settings using the provided handlers for the given repo. +func (s *Service) RepoMap( + ctx context.Context, + repoID int64, + handlers ...SettingHandler, +) error { + return s.Map( + ctx, + enum.SettingsScopeRepo, + repoID, + handlers..., + ) +} diff --git a/app/services/settings/settings.go b/app/services/settings/settings.go new file mode 100644 index 000000000..dc978075c --- /dev/null +++ b/app/services/settings/settings.go @@ -0,0 +1,23 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package settings + +type Key string + +var ( + // KeySecretScanningEnabled [bool] enables secret scanning if set to true. + KeySecretScanningEnabled Key = "secret_scanning_enabled" + DefaultSecretScanningEnabled = false +) diff --git a/app/services/settings/wire.go b/app/services/settings/wire.go new file mode 100644 index 000000000..b50747b89 --- /dev/null +++ b/app/services/settings/wire.go @@ -0,0 +1,31 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package settings + +import ( + "github.com/harness/gitness/app/store" + + "github.com/google/wire" +) + +var WireSet = wire.NewSet( + ProvideService, +) + +func ProvideService( + settingsStore store.SettingsStore, +) *Service { + return NewService(settingsStore) +} diff --git a/app/store/database.go b/app/store/database.go index 72253222d..fac1cd63e 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -17,6 +17,7 @@ package store import ( "context" + "encoding/json" "time" "github.com/harness/gitness/types" @@ -239,6 +240,35 @@ type ( ListSizeInfos(ctx context.Context) ([]*types.RepositorySizeInfo, error) } + // SettingsStore defines the settings storage. + SettingsStore interface { + // Find returns the value of the setting with the given key for the provided scope. + Find( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + key string, + ) (json.RawMessage, error) + + // FindMany returns the values of the settings with the given keys for the provided scope. + // NOTE: if a setting key doesn't exist the map just won't contain an entry for it (no error returned). + FindMany( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + keys ...string, + ) (map[string]json.RawMessage, error) + + // Upsert upserts the value of the setting with the given key for the provided scope. + Upsert( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + key string, + value json.RawMessage, + ) error + } + // RepoGitInfoView defines the repository GitUID view. RepoGitInfoView interface { Find(ctx context.Context, id int64) (*types.RepositoryGitInfo, error) diff --git a/app/store/database/migrate/postgres/0047_create_table_settings.down.sql b/app/store/database/migrate/postgres/0047_create_table_settings.down.sql new file mode 100644 index 000000000..bdb2b6349 --- /dev/null +++ b/app/store/database/migrate/postgres/0047_create_table_settings.down.sql @@ -0,0 +1 @@ +DROP TABLE settings; \ No newline at end of file diff --git a/app/store/database/migrate/postgres/0047_create_table_settings.up.sql b/app/store/database/migrate/postgres/0047_create_table_settings.up.sql new file mode 100644 index 000000000..4be80b530 --- /dev/null +++ b/app/store/database/migrate/postgres/0047_create_table_settings.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE settings ( + setting_id SERIAL PRIMARY KEY +,setting_space_id INTEGER +,setting_repo_id INTEGER +,setting_key TEXT NOT NULL +,setting_value JSON + +,CONSTRAINT fk_settings_space_id FOREIGN KEY (setting_space_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +,CONSTRAINT fk_settings_repo_id FOREIGN KEY (setting_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX settings_space_id_key + ON settings(setting_space_id, LOWER(setting_key)) + WHERE setting_space_id IS NOT NULL; + +CREATE UNIQUE INDEX settings_repo_id_key + ON settings(setting_repo_id, LOWER(setting_key)) + WHERE setting_repo_id IS NOT NULL; \ No newline at end of file diff --git a/app/store/database/migrate/sqlite/0047_create_table_settings.down.sql b/app/store/database/migrate/sqlite/0047_create_table_settings.down.sql new file mode 100644 index 000000000..bdb2b6349 --- /dev/null +++ b/app/store/database/migrate/sqlite/0047_create_table_settings.down.sql @@ -0,0 +1 @@ +DROP TABLE settings; \ No newline at end of file diff --git a/app/store/database/migrate/sqlite/0047_create_table_settings.up.sql b/app/store/database/migrate/sqlite/0047_create_table_settings.up.sql new file mode 100644 index 000000000..027db3b8a --- /dev/null +++ b/app/store/database/migrate/sqlite/0047_create_table_settings.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE settings ( + setting_id INTEGER PRIMARY KEY AUTOINCREMENT +,setting_space_id INTEGER +,setting_repo_id INTEGER +,setting_key TEXT NOT NULL +,setting_value TEXT + +,CONSTRAINT fk_settings_space_id FOREIGN KEY (setting_space_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +,CONSTRAINT fk_settings_repo_id FOREIGN KEY (setting_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX settings_space_id_key + ON settings(setting_space_id, LOWER(setting_key)) + WHERE setting_space_id IS NOT NULL; + +CREATE UNIQUE INDEX settings_repo_id_key + ON settings(setting_repo_id, LOWER(setting_key)) + WHERE setting_repo_id IS NOT NULL; \ No newline at end of file diff --git a/app/store/database/settings.go b/app/store/database/settings.go new file mode 100644 index 000000000..5dd9769bd --- /dev/null +++ b/app/store/database/settings.go @@ -0,0 +1,194 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package database + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types/enum" + + "github.com/Masterminds/squirrel" + "github.com/guregu/null" + "github.com/jmoiron/sqlx" +) + +var _ store.SettingsStore = (*SettingsStore)(nil) + +// NewSettingsStore returns a new SettingsStore. +func NewSettingsStore(db *sqlx.DB) *SettingsStore { + return &SettingsStore{ + db: db, + } +} + +// SettingsStore implements store.SettingsStore backed by a relational database. +type SettingsStore struct { + db *sqlx.DB +} + +// setting is an internal representation used to store setting data in the database. +type setting struct { + ID int64 `db:"setting_id"` + SpaceID null.Int `db:"setting_space_id"` + RepoID null.Int `db:"setting_repo_id"` + Key string `db:"setting_key"` + Value json.RawMessage `db:"setting_value"` +} + +const ( + settingsColumns = ` + setting_id + ,setting_space_id + ,setting_repo_id + ,setting_key + ,setting_value` +) + +func (s *SettingsStore) Find( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + key string, +) (json.RawMessage, error) { + stmt := database.Builder. + Select(settingsColumns). + From("settings"). + Where("LOWER(setting_key) = ?", strings.ToLower(key)) + + switch scope { + case enum.SettingsScopeSpace: + stmt = stmt.Where("setting_space_id = ?", scopeID) + case enum.SettingsScopeRepo: + stmt = stmt.Where("setting_repo_id = ?", scopeID) + default: + return nil, fmt.Errorf("setting scope %q is not supported", scope) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + dst := &setting{} + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Select query failed") + } + + return dst.Value, nil +} + +func (s *SettingsStore) FindMany( + ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + keys ...string, +) (map[string]json.RawMessage, error) { + if len(keys) == 0 { + return map[string]json.RawMessage{}, nil + } + + keysLower := make([]string, len(keys)) + for i, k := range keys { + keysLower[i] = strings.ToLower(k) + } + + stmt := database.Builder. + Select(settingsColumns). + From("settings"). + Where(squirrel.Eq{"LOWER(setting_key)": keysLower}) + + switch scope { + case enum.SettingsScopeSpace: + stmt = stmt.Where("setting_space_id = ?", scopeID) + case enum.SettingsScopeRepo: + stmt = stmt.Where("setting_repo_id = ?", scopeID) + default: + return nil, fmt.Errorf("setting scope %q is not supported", scope) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + dst := []*setting{} + if err := db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Select query failed") + } + + out := map[string]json.RawMessage{} + for _, d := range dst { + out[d.Key] = d.Value + } + + return out, nil +} + +func (s *SettingsStore) Upsert(ctx context.Context, + scope enum.SettingsScope, + scopeID int64, + key string, + value json.RawMessage, +) error { + stmt := database.Builder. + Insert(""). + Into("settings"). + Columns( + "setting_space_id", + "setting_repo_id", + "setting_key", + "setting_value", + ) + + switch scope { + case enum.SettingsScopeSpace: + stmt = stmt.Values(null.IntFrom(scopeID), null.Int{}, key, value) + stmt = stmt.Suffix(`ON CONFLICT (setting_space_id, LOWER(setting_key)) WHERE setting_space_id IS NOT NULL DO`) + case enum.SettingsScopeRepo: + stmt = stmt.Values(null.Int{}, null.IntFrom(scopeID), key, value) + stmt = stmt.Suffix(`ON CONFLICT (setting_repo_id, LOWER(setting_key)) WHERE setting_repo_id IS NOT NULL DO`) + default: + return fmt.Errorf("setting scope %q is not supported", scope) + } + + stmt = stmt.Suffix(` + UPDATE SET + setting_value = EXCLUDED.setting_value + WHERE + settings.setting_value <> EXCLUDED.setting_value`) + + sql, args, err := stmt.ToSql() + if err != nil { + return fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, sql, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Upsert query failed") + } + + return nil +} diff --git a/app/store/database/wire.go b/app/store/database/wire.go index 31a2baba8..369b46f23 100644 --- a/app/store/database/wire.go +++ b/app/store/database/wire.go @@ -52,6 +52,7 @@ var WireSet = wire.NewSet( ProvidePullReqFileViewStore, ProvideWebhookStore, ProvideWebhookExecutionStore, + ProvideSettingsStore, ProvideCheckStore, ProvideConnectorStore, ProvideTemplateStore, @@ -241,3 +242,8 @@ func ProvideCheckStore(db *sqlx.DB, ) store.CheckStore { return NewCheckStore(db, principalInfoCache) } + +// ProvideSettingsStore provides a settings store. +func ProvideSettingsStore(db *sqlx.DB) store.SettingsStore { + return NewSettingsStore(db) +} diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index a18e7668b..4f5d94964 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -22,6 +22,7 @@ import ( "github.com/harness/gitness/app/api/controller/principal" "github.com/harness/gitness/app/api/controller/pullreq" "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/controller/reposettings" "github.com/harness/gitness/app/api/controller/secret" "github.com/harness/gitness/app/api/controller/service" "github.com/harness/gitness/app/api/controller/serviceaccount" @@ -64,6 +65,7 @@ import ( "github.com/harness/gitness/app/services/protection" pullreqservice "github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/services/reposize" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/services/trigger" "github.com/harness/gitness/app/services/usergroup" "github.com/harness/gitness/app/services/webhook" @@ -112,6 +114,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e space.WireSet, limiter.WireSet, repo.WireSet, + reposettings.WireSet, pullreq.WireSet, controllerwebhook.WireSet, serviceaccount.WireSet, @@ -181,6 +184,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e cliserver.ProvideKeywordSearchConfig, keywordsearch.WireSet, controllerkeywordsearch.WireSet, + settings.WireSet, usergroup.WireSet, openapi.WireSet, repo.ProvideRepoCheck, diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index b9f910ed4..47f14ce0e 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -21,6 +21,7 @@ import ( "github.com/harness/gitness/app/api/controller/principal" pullreq2 "github.com/harness/gitness/app/api/controller/pullreq" "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/controller/reposettings" "github.com/harness/gitness/app/api/controller/secret" "github.com/harness/gitness/app/api/controller/service" "github.com/harness/gitness/app/api/controller/serviceaccount" @@ -63,6 +64,7 @@ import ( "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/services/reposize" + "github.com/harness/gitness/app/services/settings" trigger2 "github.com/harness/gitness/app/services/trigger" "github.com/harness/gitness/app/services/usergroup" "github.com/harness/gitness/app/services/webhook" @@ -126,6 +128,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro repoStore := database.ProvideRepoStore(db, spacePathCache, spacePathStore, spaceStore) pipelineStore := database.ProvidePipelineStore(db) ruleStore := database.ProvideRuleStore(db, principalInfoCache) + settingsStore := database.ProvideSettingsStore(db) + settingsService := settings.ProvideService(settingsStore) protectionManager, err := protection.ProvideManager(ruleStore) if err != nil { return nil, err @@ -190,7 +194,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro } repoIdentifier := check.ProvideRepoIdentifierCheck() repoCheck := repo.ProvideRepoCheck() - repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, mutexManager, repoIdentifier, repoCheck) + repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, mutexManager, repoIdentifier, repoCheck) + reposettingsController := reposettings.ProvideController(authorizer, repoStore, settingsService) executionStore := database.ProvideExecutionStore(db) checkStore := database.ProvideCheckStore(db, principalInfoCache) stageStore := database.ProvideStageStore(db) @@ -274,7 +279,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } - githookController := githook.ProvideController(authorizer, principalStore, repoStore, reporter2, gitInterface, pullReqStore, provider, protectionManager, clientFactory, resourceLimiter, preReceiveExtender, updateExtender, postReceiveExtender) + githookController := githook.ProvideController(authorizer, principalStore, repoStore, reporter2, gitInterface, pullReqStore, provider, protectionManager, clientFactory, resourceLimiter, settingsService, preReceiveExtender, updateExtender, postReceiveExtender) serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore) principalController := principal.ProvideController(principalStore) v := check2.ProvideCheckSanitizers() @@ -291,7 +296,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro uploadController := upload.ProvideController(authorizer, repoStore, blobStore) searcher := keywordsearch.ProvideSearcher(localIndexSearcher) keywordsearchController := keywordsearch2.ProvideController(authorizer, searcher, repoController, spaceController) - apiHandler := router.ProvideAPIHandler(ctx, config, authenticator, repoController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, checkController, systemController, uploadController, keywordsearchController) + apiHandler := router.ProvideAPIHandler(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, checkController, systemController, uploadController, keywordsearchController) gitHandler := router.ProvideGitHandler(provider, authenticator, repoController) openapiService := openapi.ProvideOpenAPIService() webHandler := router.ProvideWebHandler(config, openapiService) diff --git a/types/enum/settings.go b/types/enum/settings.go new file mode 100644 index 000000000..6213d278b --- /dev/null +++ b/types/enum/settings.go @@ -0,0 +1,37 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enum + +// SettingsScope defines the different scopes of a setting. +type SettingsScope string + +func (SettingsScope) Enum() []interface{} { + return toInterfaceSlice(GetAllSettingsScopes()) +} + +var ( + // SettingsScopeSpace defines settings stored on a space level. + SettingsScopeSpace SettingsScope = "space" + + // SettingsScopeRepo defines settings stored on a repo level. + SettingsScopeRepo SettingsScope = "repo" +) + +func GetAllSettingsScopes() []SettingsScope { + return []SettingsScope{ + SettingsScopeSpace, + SettingsScopeRepo, + } +}