diff --git a/app/api/controller/repo/controller.go b/app/api/controller/repo/controller.go index a7de5e99a..2257a9ea1 100644 --- a/app/api/controller/repo/controller.go +++ b/app/api/controller/repo/controller.go @@ -35,6 +35,7 @@ import ( "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/publicaccess" + "github.com/harness/gitness/app/services/rules" "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/services/usergroup" "github.com/harness/gitness/app/store" @@ -102,6 +103,7 @@ type Controller struct { publicAccess publicaccess.Service labelSvc *label.Service instrumentation instrument.Service + rulesSvc *rules.Service } func NewController( @@ -136,6 +138,7 @@ func NewController( instrumentation instrument.Service, userGroupStore store.UserGroupStore, userGroupService usergroup.SearchService, + rulesSvc *rules.Service, ) *Controller { return &Controller{ defaultBranch: config.Git.DefaultBranch, @@ -169,6 +172,7 @@ func NewController( instrumentation: instrumentation, userGroupStore: userGroupStore, userGroupService: userGroupService, + rulesSvc: rulesSvc, } } @@ -249,76 +253,3 @@ func (c *Controller) fetchRules( return protectionRules, isRepoOwner, nil } - -func (c *Controller) getRuleUserAndUserGroups( - ctx context.Context, - r *types.Rule, -) (map[int64]*types.PrincipalInfo, map[int64]*types.UserGroupInfo, error) { - rule, err := c.parseRule(r) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse rule: %w", err) - } - - userMap, err := c.getRuleUsers(ctx, rule) - if err != nil { - return nil, nil, fmt.Errorf("failed to get rule users: %w", err) - } - userGroupMap, err := c.getRuleUserGroups(ctx, rule) - if err != nil { - return nil, nil, fmt.Errorf("failed to get rule user groups: %w", err) - } - - return userMap, userGroupMap, nil -} - -func (c *Controller) getRuleUsers( - ctx context.Context, - rule protection.Protection, -) (map[int64]*types.PrincipalInfo, error) { - userIDs, err := rule.UserIDs() - if err != nil { - return nil, fmt.Errorf("failed to get user ID from rule: %w", err) - } - - userMap, err := c.principalInfoCache.Map(ctx, userIDs) - if err != nil { - return nil, fmt.Errorf("failed to get principal infos: %w", err) - } - - return userMap, nil -} - -func (c *Controller) getRuleUserGroups( - ctx context.Context, - rule protection.Protection, -) (map[int64]*types.UserGroupInfo, error) { - groupIDs, err := rule.UserGroupIDs() - if err != nil { - return nil, fmt.Errorf("failed to get group IDs from rule: %w", err) - } - - userGroupInfoMap := make(map[int64]*types.UserGroupInfo) - - if len(groupIDs) == 0 { - return userGroupInfoMap, nil - } - - groupMap, err := c.userGroupStore.Map(ctx, groupIDs) - if err != nil { - return nil, fmt.Errorf("failed to get userGroup infos: %w", err) - } - - for k, v := range groupMap { - userGroupInfoMap[k] = v.ToUserGroupInfo() - } - return userGroupInfoMap, nil -} - -func (c *Controller) parseRule(r *types.Rule) (protection.Protection, error) { - rule, err := c.protectionManager.FromJSON(r.Type, r.Definition, false) - if err != nil { - return nil, fmt.Errorf("failed to parse json rule definition: %w", err) - } - - return rule, nil -} diff --git a/app/api/controller/repo/rule_create.go b/app/api/controller/repo/rule_create.go index a054c301d..169d2274b 100644 --- a/app/api/controller/repo/rule_create.go +++ b/app/api/controller/repo/rule_create.go @@ -16,139 +16,34 @@ package repo import ( "context" - "encoding/json" "fmt" - "time" - "github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/auth" - "github.com/harness/gitness/app/paths" - "github.com/harness/gitness/app/services/instrument" - "github.com/harness/gitness/app/services/protection" - "github.com/harness/gitness/audit" + "github.com/harness/gitness/app/services/rules" "github.com/harness/gitness/types" - "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" - - "github.com/rs/zerolog/log" ) -type RuleCreateInput struct { - Type types.RuleType `json:"type"` - State enum.RuleState `json:"state"` - // TODO [CODE-1363]: remove after identifier migration. - UID string `json:"uid" deprecated:"true"` - Identifier string `json:"identifier"` - Description string `json:"description"` - Pattern protection.Pattern `json:"pattern"` - Definition json.RawMessage `json:"definition"` -} - -// sanitize validates and sanitizes the create rule input data. -func (in *RuleCreateInput) sanitize() error { - // TODO [CODE-1363]: remove after identifier migration. - if in.Identifier == "" { - in.Identifier = in.UID - } - - if err := check.Identifier(in.Identifier); err != nil { - return err - } - - if err := in.Pattern.Validate(); err != nil { - return usererror.BadRequestf("invalid pattern: %s", err) - } - - var ok bool - in.State, ok = in.State.Sanitize() - if !ok { - return usererror.BadRequest("rule state is invalid") - } - - if in.Type == "" { - in.Type = protection.TypeBranch - } - - if len(in.Definition) == 0 { - return usererror.BadRequest("rule definition missing") - } - - return nil -} - // RuleCreate creates a new protection rule for a repo. func (c *Controller) RuleCreate(ctx context.Context, session *auth.Session, repoRef string, - in *RuleCreateInput, + in *rules.CreateInput, ) (*types.Rule, error) { - if err := in.sanitize(); err != nil { - return nil, err - } - repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) if err != nil { return nil, err } - in.Definition, err = c.protectionManager.SanitizeJSON(in.Type, in.Definition) - if err != nil { - return nil, usererror.BadRequestf("invalid rule definition: %s", err.Error()) - } - - now := time.Now().UnixMilli() - r := &types.Rule{ - CreatedBy: session.Principal.ID, - Created: now, - Updated: now, - RepoID: &repo.ID, - SpaceID: nil, - Type: in.Type, - State: in.State, - Identifier: in.Identifier, - Description: in.Description, - Pattern: in.Pattern.JSON(), - Definition: in.Definition, - CreatedByInfo: types.PrincipalInfo{}, - } - - err = c.ruleStore.Create(ctx, r) - if err != nil { - return nil, fmt.Errorf("failed to create repository-level protection rule: %w", err) - } - - err = c.auditService.Log(ctx, - session.Principal, - audit.NewResource(audit.ResourceTypeBranchRule, r.Identifier, audit.RepoName, repo.Identifier), - audit.ActionCreated, - paths.Parent(repo.Path), - audit.WithNewObject(r), + rule, err := c.rulesSvc.Create( + ctx, &session.Principal, + enum.RuleParentRepo, repo.ID, + repo.Identifier, repo.Path, + in, ) if err != nil { - log.Ctx(ctx).Warn().Msgf("failed to insert audit log for create branch rule operation: %s", err) + return nil, fmt.Errorf("failed to create repo-level protection rule: %w", err) } - err = c.instrumentation.Track(ctx, instrument.Event{ - Type: instrument.EventTypeCreateBranchRule, - Principal: session.Principal.ToPrincipalInfo(), - Path: repo.Path, - Properties: map[instrument.Property]any{ - instrument.PropertyRepositoryID: repo.ID, - instrument.PropertyRepositoryName: repo.Identifier, - instrument.PropertyRuleID: r.ID, - }, - }) - if err != nil { - log.Ctx(ctx).Warn().Msgf("failed to insert instrumentation record for create branch rule operation: %s", err) - } - - userMap, userGroupMap, err := c.getRuleUserAndUserGroups(ctx, r) - if err != nil { - return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) - } - - r.Users = userMap - r.UserGroups = userGroupMap - - return r, nil + return rule, nil } diff --git a/app/api/controller/repo/rule_delete.go b/app/api/controller/repo/rule_delete.go index 88feadd92..2ff30114d 100644 --- a/app/api/controller/repo/rule_delete.go +++ b/app/api/controller/repo/rule_delete.go @@ -19,11 +19,7 @@ import ( "fmt" "github.com/harness/gitness/app/auth" - "github.com/harness/gitness/app/paths" - "github.com/harness/gitness/audit" "github.com/harness/gitness/types/enum" - - "github.com/rs/zerolog/log" ) // RuleDelete deletes a protection rule by identifier. @@ -37,25 +33,15 @@ func (c *Controller) RuleDelete(ctx context.Context, return err } - r, err := c.ruleStore.FindByIdentifier(ctx, nil, &repo.ID, identifier) - if err != nil { - return fmt.Errorf("failed to find repository-level protection rule by identifier: %w", err) - } - - err = c.ruleStore.Delete(ctx, r.ID) - if err != nil { - return fmt.Errorf("failed to delete repository-level protection rule: %w", err) - } - - err = c.auditService.Log(ctx, - session.Principal, - audit.NewResource(audit.ResourceTypeBranchRule, r.Identifier, audit.RepoName, repo.Identifier), - audit.ActionDeleted, - paths.Parent(repo.Path), - audit.WithOldObject(r), + err = c.rulesSvc.Delete( + ctx, + &session.Principal, + enum.RuleParentRepo, repo.ID, + repo.Identifier, repo.Path, + identifier, ) if err != nil { - log.Ctx(ctx).Warn().Msgf("failed to insert audit log for delete branch rule operation: %s", err) + return fmt.Errorf("failed to delete repo-level protection rule: %w", err) } return nil diff --git a/app/api/controller/repo/rule_find.go b/app/api/controller/repo/rule_find.go index bf95a8c1f..8ba09d6a7 100644 --- a/app/api/controller/repo/rule_find.go +++ b/app/api/controller/repo/rule_find.go @@ -34,18 +34,10 @@ func (c *Controller) RuleFind(ctx context.Context, return nil, err } - r, err := c.ruleStore.FindByIdentifier(ctx, nil, &repo.ID, identifier) + rule, err := c.rulesSvc.Find(ctx, enum.RuleParentRepo, repo.ID, identifier) if err != nil { - return nil, fmt.Errorf("failed to find repository-level protection rule by identifier: %w", err) + return nil, fmt.Errorf("failed to find repo-level protection rule by identifier: %w", err) } - userMap, userGroupMap, err := c.getRuleUserAndUserGroups(ctx, r) - if err != nil { - return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) - } - - r.Users = userMap - r.UserGroups = userGroupMap - - return r, nil + return rule, nil } diff --git a/app/api/controller/repo/rule_list.go b/app/api/controller/repo/rule_list.go index fa6c6c3be..9e4d2258d 100644 --- a/app/api/controller/repo/rule_list.go +++ b/app/api/controller/repo/rule_list.go @@ -19,7 +19,6 @@ import ( "fmt" "github.com/harness/gitness/app/auth" - "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) @@ -28,6 +27,7 @@ import ( func (c *Controller) RuleList(ctx context.Context, session *auth.Session, repoRef string, + inherited bool, filter *types.RuleFilter, ) ([]types.Rule, int64, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) @@ -35,36 +35,9 @@ func (c *Controller) RuleList(ctx context.Context, return nil, 0, err } - var list []types.Rule - var count int64 - - err = c.tx.WithTx(ctx, func(ctx context.Context) error { - list, err = c.ruleStore.List(ctx, nil, &repo.ID, filter) - if err != nil { - return fmt.Errorf("failed to list repository-level protection rules: %w", err) - } - - if filter.Page == 1 && len(list) < filter.Size { - count = int64(len(list)) - return nil - } - - count, err = c.ruleStore.Count(ctx, nil, &repo.ID, filter) - if err != nil { - return fmt.Errorf("failed to count repository-level protection rules: %w", err) - } - - return nil - }, dbtx.TxDefaultReadOnly) + list, count, err := c.rulesSvc.List(ctx, repo.ID, enum.RuleParentRepo, inherited, filter) if err != nil { - return nil, 0, err - } - - for i := range list { - list[i].Users, list[i].UserGroups, err = c.getRuleUserAndUserGroups(ctx, &list[i]) - if err != nil { - return nil, 0, err - } + return nil, 0, fmt.Errorf("failed to list repo-level protection rules: %w", err) } return list, count, nil diff --git a/app/api/controller/repo/rule_update.go b/app/api/controller/repo/rule_update.go index 68e65646d..080f3b758 100644 --- a/app/api/controller/repo/rule_update.go +++ b/app/api/controller/repo/rule_update.go @@ -16,146 +16,38 @@ package repo import ( "context" - "encoding/json" "fmt" - "github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/auth" - "github.com/harness/gitness/app/paths" - "github.com/harness/gitness/app/services/protection" - "github.com/harness/gitness/audit" + "github.com/harness/gitness/app/services/rules" "github.com/harness/gitness/types" - "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" - - "github.com/rs/zerolog/log" ) -type RuleUpdateInput struct { - // TODO [CODE-1363]: remove after identifier migration. - UID *string `json:"uid" deprecated:"true"` - Identifier *string `json:"identifier"` - State *enum.RuleState `json:"state"` - Description *string `json:"description"` - Pattern *protection.Pattern `json:"pattern"` - Definition *json.RawMessage `json:"definition"` -} - -// sanitize validates and sanitizes the update rule input data. -func (in *RuleUpdateInput) sanitize() error { - // TODO [CODE-1363]: remove after identifier migration. - if in.Identifier == nil { - in.Identifier = in.UID - } - - if in.Identifier != nil { - if err := check.Identifier(*in.Identifier); err != nil { - return err - } - } - - if in.State != nil { - state, ok := in.State.Sanitize() - if !ok { - return usererror.BadRequest("rule state is invalid") - } - - in.State = &state - } - - if in.Pattern != nil { - if err := in.Pattern.Validate(); err != nil { - return usererror.BadRequestf("invalid pattern: %s", err) - } - } - - if in.Definition != nil && len(*in.Definition) == 0 { - return usererror.BadRequest("rule definition missing") - } - - return nil -} - -func (in *RuleUpdateInput) isEmpty() bool { - return in.Identifier == nil && in.State == nil && in.Description == nil && in.Pattern == nil && in.Definition == nil -} - // RuleUpdate updates an existing protection rule for a repository. func (c *Controller) RuleUpdate(ctx context.Context, session *auth.Session, repoRef string, identifier string, - in *RuleUpdateInput, + in *rules.UpdateInput, ) (*types.Rule, error) { - if err := in.sanitize(); err != nil { - return nil, err - } - repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) if err != nil { return nil, err } - r, err := c.ruleStore.FindByIdentifier(ctx, nil, &repo.ID, identifier) - if err != nil { - return nil, fmt.Errorf("failed to get a repository rule by its identifier: %w", err) - } - oldRule := r.Clone() - if in.isEmpty() { - userMap, userGroupMap, err := c.getRuleUserAndUserGroups(ctx, r) - if err != nil { - return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) - } - - r.Users = userMap - r.UserGroups = userGroupMap - - return r, nil - } - - if in.Identifier != nil { - r.Identifier = *in.Identifier - } - if in.State != nil { - r.State = *in.State - } - if in.Description != nil { - r.Description = *in.Description - } - if in.Pattern != nil { - r.Pattern = in.Pattern.JSON() - } - if in.Definition != nil { - r.Definition, err = c.protectionManager.SanitizeJSON(r.Type, *in.Definition) - if err != nil { - return nil, usererror.BadRequestf("invalid rule definition: %s", err.Error()) - } - } - - userMap, userGroupMap, err := c.getRuleUserAndUserGroups(ctx, r) - if err != nil { - return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) - } - - r.Users = userMap - r.UserGroups = userGroupMap - - err = c.ruleStore.Update(ctx, r) - if err != nil { - return nil, fmt.Errorf("failed to update repository-level protection rule: %w", err) - } - - err = c.auditService.Log(ctx, - session.Principal, - audit.NewResource(audit.ResourceTypeBranchRule, r.Identifier, audit.RepoName, repo.Identifier), - audit.ActionUpdated, - paths.Parent(repo.Path), - audit.WithOldObject(oldRule), - audit.WithNewObject(r), + rule, err := c.rulesSvc.Update( + ctx, &session.Principal, + enum.RuleParentRepo, + repo.ID, + repo.Identifier, + repo.Path, + identifier, + in, ) if err != nil { - log.Ctx(ctx).Warn().Msgf("failed to insert audit log for update branch rule operation: %s", err) + return nil, fmt.Errorf("failed to update repo-level protection rule by identifier: %w", err) } - return r, nil + return rule, nil } diff --git a/app/api/controller/repo/wire.go b/app/api/controller/repo/wire.go index aabcc6411..3ab83c84c 100644 --- a/app/api/controller/repo/wire.go +++ b/app/api/controller/repo/wire.go @@ -26,6 +26,7 @@ import ( "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/publicaccess" + "github.com/harness/gitness/app/services/rules" "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/services/usergroup" "github.com/harness/gitness/app/store" @@ -77,6 +78,7 @@ func ProvideController( instrumentation instrument.Service, userGroupStore store.UserGroupStore, userGroupService usergroup.SearchService, + rulesSvc *rules.Service, ) *Controller { return NewController(config, tx, urlProvider, authorizer, @@ -84,7 +86,9 @@ func ProvideController( principalStore, ruleStore, checkStore, pullReqStore, settings, principalInfoCache, protectionManager, rpcClient, importer, codeOwners, reporeporter, indexer, limiter, locker, auditService, mtxManager, identifierCheck, - repoChecks, publicAccess, labelSvc, instrumentation, userGroupStore, userGroupService) + repoChecks, publicAccess, labelSvc, instrumentation, userGroupStore, userGroupService, + rulesSvc, + ) } func ProvideRepoCheck() Check { diff --git a/app/api/controller/space/controller.go b/app/api/controller/space/controller.go index 0d49e2d18..cb078e9b9 100644 --- a/app/api/controller/space/controller.go +++ b/app/api/controller/space/controller.go @@ -28,6 +28,7 @@ import ( "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/services/pullreq" + "github.com/harness/gitness/app/services/rules" "github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" @@ -90,6 +91,7 @@ type Controller struct { labelSvc *label.Service instrumentation instrument.Service executionStore store.ExecutionStore + rulesSvc *rules.Service } func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Provider, @@ -102,6 +104,7 @@ func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Pro limiter limiter.ResourceLimiter, publicAccess publicaccess.Service, auditService audit.Service, gitspaceSvc *gitspace.Service, labelSvc *label.Service, instrumentation instrument.Service, executionStore store.ExecutionStore, + rulesSvc *rules.Service, ) *Controller { return &Controller{ nestedSpacesEnabled: config.NestedSpacesEnabled, @@ -130,5 +133,6 @@ func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Pro labelSvc: labelSvc, instrumentation: instrumentation, executionStore: executionStore, + rulesSvc: rulesSvc, } } diff --git a/app/api/controller/space/rule_create.go b/app/api/controller/space/rule_create.go new file mode 100644 index 000000000..772d1e0be --- /dev/null +++ b/app/api/controller/space/rule_create.go @@ -0,0 +1,52 @@ +// 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 space + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/services/rules" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// RuleCreate creates a new protection rule for a space. +func (c *Controller) RuleCreate(ctx context.Context, + session *auth.Session, + spaceRef string, + in *rules.CreateInput, +) (*types.Rule, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + rule, err := c.rulesSvc.Create( + ctx, + &session.Principal, + enum.RuleParentSpace, + space.ID, + space.Identifier, + space.Path, + in, + ) + if err != nil { + return nil, fmt.Errorf("failed to create space-level protection rule: %w", err) + } + + return rule, nil +} diff --git a/app/api/controller/space/rule_delete.go b/app/api/controller/space/rule_delete.go new file mode 100644 index 000000000..451766dba --- /dev/null +++ b/app/api/controller/space/rule_delete.go @@ -0,0 +1,50 @@ +// 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 space + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// RuleDelete deletes a protection rule by identifier. +func (c *Controller) RuleDelete(ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, +) error { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return fmt.Errorf("failed to acquire access to space: %w", err) + } + + err = c.rulesSvc.Delete( + ctx, + &session.Principal, + enum.RuleParentSpace, + space.ID, + space.Identifier, + space.Path, + identifier, + ) + if err != nil { + return fmt.Errorf("failed to delete space-level protection rule: %w", err) + } + + return nil +} diff --git a/app/api/controller/space/rule_find.go b/app/api/controller/space/rule_find.go new file mode 100644 index 000000000..fb41675a0 --- /dev/null +++ b/app/api/controller/space/rule_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 space + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// RuleFind returns the protection rule by identifier. +func (c *Controller) RuleFind(ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, +) (*types.Rule, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceView) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + rule, err := c.rulesSvc.Find(ctx, enum.RuleParentSpace, space.ID, identifier) + if err != nil { + return nil, fmt.Errorf("failed to find space-level protection rule by identifier: %w", err) + } + + return rule, nil +} diff --git a/app/api/controller/space/rule_list.go b/app/api/controller/space/rule_list.go new file mode 100644 index 000000000..d11e8f2e1 --- /dev/null +++ b/app/api/controller/space/rule_list.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 space + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// RuleList returns protection rules for a repository. +func (c *Controller) RuleList(ctx context.Context, + session *auth.Session, + spaceRef string, + inherited bool, + filter *types.RuleFilter, +) ([]types.Rule, int64, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceView) + if err != nil { + return nil, 0, fmt.Errorf("failed to acquire access to space: %w", err) + } + + list, count, err := c.rulesSvc.List(ctx, space.ID, enum.RuleParentSpace, inherited, filter) + if err != nil { + return nil, 0, fmt.Errorf("failed to list space-level protection rules: %w", err) + } + + return list, count, nil +} diff --git a/app/api/controller/space/rule_update.go b/app/api/controller/space/rule_update.go new file mode 100644 index 000000000..9fd20fee1 --- /dev/null +++ b/app/api/controller/space/rule_update.go @@ -0,0 +1,54 @@ +// 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 space + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/services/rules" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// RuleUpdate updates an existing protection rule for a space. +func (c *Controller) RuleUpdate(ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, + in *rules.UpdateInput, +) (*types.Rule, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + rule, err := c.rulesSvc.Update( + ctx, + &session.Principal, + enum.RuleParentSpace, + space.ID, + space.Identifier, + space.Path, + identifier, + in, + ) + if err != nil { + return nil, fmt.Errorf("failed to update space-level protection rule by identifier: %w", err) + } + + return rule, nil +} diff --git a/app/api/controller/space/wire.go b/app/api/controller/space/wire.go index 120246aea..c8ce21078 100644 --- a/app/api/controller/space/wire.go +++ b/app/api/controller/space/wire.go @@ -25,6 +25,7 @@ import ( "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/services/pullreq" + "github.com/harness/gitness/app/services/rules" "github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" @@ -47,18 +48,21 @@ func ProvideController(config *types.Config, tx dbtx.Transactor, urlProvider url connectorStore store.ConnectorStore, templateStore store.TemplateStore, spaceStore store.SpaceStore, repoStore store.RepoStore, principalStore store.PrincipalStore, repoCtrl *repo.Controller, membershipStore store.MembershipStore, prListService *pullreq.ListService, - importer *importer.Repository, exporter *exporter.Repository, limiter limiter.ResourceLimiter, - publicAccess publicaccess.Service, auditService audit.Service, gitspaceService *gitspace.Service, + importer *importer.Repository, + exporter *exporter.Repository, limiter limiter.ResourceLimiter, publicAccess publicaccess.Service, + auditService audit.Service, gitspaceService *gitspace.Service, labelSvc *label.Service, instrumentation instrument.Service, executionStore store.ExecutionStore, + rulesSvc *rules.Service, ) *Controller { return NewController(config, tx, urlProvider, sseStreamer, identifierCheck, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, - repoCtrl, membershipStore, prListService, - importer, exporter, limiter, - publicAccess, auditService, gitspaceService, + repoCtrl, membershipStore, prListService, importer, + exporter, limiter, publicAccess, + auditService, gitspaceService, labelSvc, instrumentation, executionStore, + rulesSvc, ) } diff --git a/app/api/handler/repo/rule_create.go b/app/api/handler/repo/rule_create.go index 8a7258c31..32bd4fad1 100644 --- a/app/api/handler/repo/rule_create.go +++ b/app/api/handler/repo/rule_create.go @@ -21,9 +21,10 @@ import ( "github.com/harness/gitness/app/api/controller/repo" "github.com/harness/gitness/app/api/render" "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/app/services/rules" ) -// HandleRuleCreate handles API that adds a new protection rule to a repository. +// HandleRuleCreate adds a new protection rule to a repository. func HandleRuleCreate(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -35,7 +36,7 @@ func HandleRuleCreate(repoCtrl *repo.Controller) http.HandlerFunc { return } - in := new(repo.RuleCreateInput) + in := new(rules.CreateInput) err = json.NewDecoder(r.Body).Decode(in) if err != nil { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) diff --git a/app/api/handler/repo/rule_delete.go b/app/api/handler/repo/rule_delete.go index 903c839c5..b6eadfcb3 100644 --- a/app/api/handler/repo/rule_delete.go +++ b/app/api/handler/repo/rule_delete.go @@ -22,7 +22,7 @@ import ( "github.com/harness/gitness/app/api/request" ) -// HandleRuleDelete handles API that deletes a protection rule. +// HandleRuleDelete deletes a protection rule of a repository. func HandleRuleDelete(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/app/api/handler/repo/rule_find.go b/app/api/handler/repo/rule_find.go index 267de6609..b39ce9895 100644 --- a/app/api/handler/repo/rule_find.go +++ b/app/api/handler/repo/rule_find.go @@ -22,7 +22,7 @@ import ( "github.com/harness/gitness/app/api/request" ) -// HandleRuleFind handles API that returns a protection rule of a repository. +// HandleRuleFind finds a protection rule of a repository. func HandleRuleFind(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/app/api/handler/repo/rule_list.go b/app/api/handler/repo/rule_list.go index c4be5b77b..e4f42d5b6 100644 --- a/app/api/handler/repo/rule_list.go +++ b/app/api/handler/repo/rule_list.go @@ -22,7 +22,7 @@ import ( "github.com/harness/gitness/app/api/request" ) -// HandleRuleList handles API that lists a protection rules of a repository. +// HandleRuleList lists rotection rules of a repository. func HandleRuleList(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -36,7 +36,12 @@ func HandleRuleList(repoCtrl *repo.Controller) http.HandlerFunc { filter := request.ParseRuleFilter(r) - rules, rulesCount, err := repoCtrl.RuleList(ctx, session, repoRef, filter) + inherited, err := request.ParseInheritedFromQuery(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + } + + rules, rulesCount, err := repoCtrl.RuleList(ctx, session, repoRef, inherited, filter) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/handler/repo/rule_update.go b/app/api/handler/repo/rule_update.go index 9e36d381a..191a11797 100644 --- a/app/api/handler/repo/rule_update.go +++ b/app/api/handler/repo/rule_update.go @@ -21,9 +21,10 @@ import ( "github.com/harness/gitness/app/api/controller/repo" "github.com/harness/gitness/app/api/render" "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/app/services/rules" ) -// HandleRuleUpdate handles API that updates a protection rule of a repository. +// HandleRuleUpdate updates a protection rule of a repository. func HandleRuleUpdate(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -41,7 +42,7 @@ func HandleRuleUpdate(repoCtrl *repo.Controller) http.HandlerFunc { return } - in := new(repo.RuleUpdateInput) + in := new(rules.UpdateInput) err = json.NewDecoder(r.Body).Decode(in) if err != nil { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) diff --git a/app/api/handler/space/rule_create.go b/app/api/handler/space/rule_create.go new file mode 100644 index 000000000..a335022b1 --- /dev/null +++ b/app/api/handler/space/rule_create.go @@ -0,0 +1,54 @@ +// 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 space + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/space" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/app/services/rules" +) + +// HandleRuleCreate adds a new protection rule to a space. +func HandleRuleCreate(spaceCtrl *space.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + spaceRef, err := request.GetSpaceRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(rules.CreateInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } + + rule, err := spaceCtrl.RuleCreate(ctx, session, spaceRef, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, rule) + } +} diff --git a/app/api/handler/space/rule_delete.go b/app/api/handler/space/rule_delete.go new file mode 100644 index 000000000..c0ae020ee --- /dev/null +++ b/app/api/handler/space/rule_delete.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 space + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/space" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +// HandleRuleDelete deletes a protection rule of a space. +func HandleRuleDelete(spaceCtrl *space.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + spaceRef, err := request.GetSpaceRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + ruleIdentifier, err := request.GetRuleIdentifierFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + err = spaceCtrl.RuleDelete(ctx, session, spaceRef, ruleIdentifier) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/space/rule_find.go b/app/api/handler/space/rule_find.go new file mode 100644 index 000000000..590d2a5d4 --- /dev/null +++ b/app/api/handler/space/rule_find.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 space + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/space" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +// HandleRuleFind finds a protection rule of a space. +func HandleRuleFind(spaceCtrl *space.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + spaceRef, err := request.GetSpaceRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + ruleIdentifier, err := request.GetRuleIdentifierFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + rule, err := spaceCtrl.RuleFind(ctx, session, spaceRef, ruleIdentifier) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, rule) + } +} diff --git a/app/api/handler/space/rule_list.go b/app/api/handler/space/rule_list.go new file mode 100644 index 000000000..b8466a02a --- /dev/null +++ b/app/api/handler/space/rule_list.go @@ -0,0 +1,52 @@ +// 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 space + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/space" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +// HandleRuleList lists a protection rules of a space. +func HandleRuleList(spaceCtrl *space.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + spaceRef, err := request.GetSpaceRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + filter := request.ParseRuleFilter(r) + inherited, err := request.ParseInheritedFromQuery(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + } + + rules, rulesCount, err := spaceCtrl.RuleList(ctx, session, spaceRef, inherited, filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.Pagination(r, w, filter.Page, filter.Size, int(rulesCount)) + render.JSON(w, http.StatusOK, rules) + } +} diff --git a/app/api/handler/space/rule_update.go b/app/api/handler/space/rule_update.go new file mode 100644 index 000000000..f0151cfa4 --- /dev/null +++ b/app/api/handler/space/rule_update.go @@ -0,0 +1,60 @@ +// 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 space + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/space" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/app/services/rules" +) + +// HandleRuleUpdate updates a protection rule of a space. +func HandleRuleUpdate(spaceCtrl *space.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + spaceRef, err := request.GetSpaceRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + ruleIdentifier, err := request.GetRuleIdentifierFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(rules.UpdateInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } + + rule, err := spaceCtrl.RuleUpdate(ctx, session, spaceRef, ruleIdentifier, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, rule) + } +} diff --git a/app/api/openapi/openapi.go b/app/api/openapi/openapi.go index 73c0a4b72..5c0cd5590 100644 --- a/app/api/openapi/openapi.go +++ b/app/api/openapi/openapi.go @@ -61,6 +61,7 @@ func (*OpenAPI) Generate() *openapi3.Spec { spaceOperations(&reflector) pluginOperations(&reflector) repoOperations(&reflector) + rulesOperations(&reflector) pipelineOperations(&reflector) connectorOperations(&reflector) templateOperations(&reflector) diff --git a/app/api/openapi/repo.go b/app/api/openapi/repo.go index 3943525ec..fdfafe0ec 100644 --- a/app/api/openapi/repo.go +++ b/app/api/openapi/repo.go @@ -21,7 +21,6 @@ import ( "github.com/harness/gitness/app/api/controller/reposettings" "github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/api/usererror" - "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/git" gittypes "github.com/harness/gitness/git/api" "github.com/harness/gitness/types" @@ -175,31 +174,6 @@ type codeOwnersValidate struct { repoRequest } -// ruleType is a plugin for types.RuleType to allow using oneof. -type ruleType string - -func (ruleType) Enum() []interface{} { - return []interface{}{protection.TypeBranch} -} - -// ruleDefinition is a plugin for types.Rule Definition to allow using oneof. -type ruleDefinition struct{} - -func (ruleDefinition) JSONSchemaOneOf() []interface{} { - return []interface{}{protection.Branch{}} -} - -type rule struct { - types.Rule - - // overshadow Type and Definition to enable oneof. - Type ruleType `json:"type"` - Definition ruleDefinition `json:"definition"` - - // overshadow Pattern to correct the type - Pattern protection.Pattern `json:"pattern"` -} - type restoreRequest struct { repoRequest repo.RestoreInput @@ -1142,88 +1116,6 @@ func repoOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opMergeCheck, new(usererror.Error), http.StatusForbidden) _ = reflector.Spec.AddOperation(http.MethodPost, "/repos/{repo_ref}/merge-check/{range}", opMergeCheck) - opRuleAdd := openapi3.Operation{} - opRuleAdd.WithTags("repository") - opRuleAdd.WithMapOfAnything(map[string]interface{}{"operationId": "ruleAdd"}) - _ = reflector.SetRequest(&opRuleAdd, struct { - repoRequest - repo.RuleCreateInput - - // overshadow "definition" - Type ruleType `json:"type"` - Definition ruleDefinition `json:"definition"` - }{}, http.MethodPost) - _ = reflector.SetJSONResponse(&opRuleAdd, rule{}, http.StatusCreated) - _ = reflector.SetJSONResponse(&opRuleAdd, new(usererror.Error), http.StatusInternalServerError) - _ = reflector.SetJSONResponse(&opRuleAdd, new(usererror.Error), http.StatusUnauthorized) - _ = reflector.SetJSONResponse(&opRuleAdd, new(usererror.Error), http.StatusForbidden) - _ = reflector.SetJSONResponse(&opRuleAdd, new(usererror.Error), http.StatusNotFound) - _ = reflector.Spec.AddOperation(http.MethodPost, "/repos/{repo_ref}/rules", opRuleAdd) - - opRuleDelete := openapi3.Operation{} - opRuleDelete.WithTags("repository") - opRuleDelete.WithMapOfAnything(map[string]interface{}{"operationId": "ruleDelete"}) - _ = reflector.SetRequest(&opRuleDelete, struct { - repoRequest - RuleIdentifier string `path:"rule_identifier"` - }{}, http.MethodDelete) - _ = reflector.SetJSONResponse(&opRuleDelete, nil, http.StatusNoContent) - _ = reflector.SetJSONResponse(&opRuleDelete, new(usererror.Error), http.StatusInternalServerError) - _ = reflector.SetJSONResponse(&opRuleDelete, new(usererror.Error), http.StatusUnauthorized) - _ = reflector.SetJSONResponse(&opRuleDelete, new(usererror.Error), http.StatusForbidden) - _ = reflector.SetJSONResponse(&opRuleDelete, new(usererror.Error), http.StatusNotFound) - _ = reflector.Spec.AddOperation(http.MethodDelete, "/repos/{repo_ref}/rules/{rule_identifier}", opRuleDelete) - - opRuleUpdate := openapi3.Operation{} - opRuleUpdate.WithTags("repository") - opRuleUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "ruleUpdate"}) - _ = reflector.SetRequest(&opRuleUpdate, &struct { - repoRequest - Identifier string `path:"rule_identifier"` - repo.RuleUpdateInput - - // overshadow Type and Definition to enable oneof. - Type ruleType `json:"type"` - Definition ruleDefinition `json:"definition"` - }{}, http.MethodPatch) - _ = reflector.SetJSONResponse(&opRuleUpdate, rule{}, http.StatusOK) - _ = reflector.SetJSONResponse(&opRuleUpdate, new(usererror.Error), http.StatusInternalServerError) - _ = reflector.SetJSONResponse(&opRuleUpdate, new(usererror.Error), http.StatusUnauthorized) - _ = reflector.SetJSONResponse(&opRuleUpdate, new(usererror.Error), http.StatusForbidden) - _ = reflector.SetJSONResponse(&opRuleUpdate, new(usererror.Error), http.StatusNotFound) - _ = reflector.Spec.AddOperation(http.MethodPatch, "/repos/{repo_ref}/rules/{rule_identifier}", opRuleUpdate) - - opRuleList := openapi3.Operation{} - opRuleList.WithTags("repository") - opRuleList.WithMapOfAnything(map[string]interface{}{"operationId": "ruleList"}) - opRuleList.WithParameters( - queryParameterQueryRuleList, - queryParameterOrder, queryParameterSortRuleList, - QueryParameterPage, QueryParameterLimit) - _ = reflector.SetRequest(&opRuleList, &struct { - repoRequest - }{}, http.MethodGet) - _ = reflector.SetJSONResponse(&opRuleList, []rule{}, http.StatusOK) - _ = reflector.SetJSONResponse(&opRuleList, new(usererror.Error), http.StatusInternalServerError) - _ = reflector.SetJSONResponse(&opRuleList, new(usererror.Error), http.StatusUnauthorized) - _ = reflector.SetJSONResponse(&opRuleList, new(usererror.Error), http.StatusForbidden) - _ = reflector.SetJSONResponse(&opRuleList, new(usererror.Error), http.StatusNotFound) - _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/rules", opRuleList) - - opRuleGet := openapi3.Operation{} - opRuleGet.WithTags("repository") - opRuleGet.WithMapOfAnything(map[string]interface{}{"operationId": "ruleGet"}) - _ = reflector.SetRequest(&opRuleGet, &struct { - repoRequest - Identifier string `path:"rule_identifier"` - }{}, http.MethodGet) - _ = reflector.SetJSONResponse(&opRuleGet, rule{}, http.StatusOK) - _ = reflector.SetJSONResponse(&opRuleGet, new(usererror.Error), http.StatusInternalServerError) - _ = reflector.SetJSONResponse(&opRuleGet, new(usererror.Error), http.StatusUnauthorized) - _ = reflector.SetJSONResponse(&opRuleGet, new(usererror.Error), http.StatusForbidden) - _ = reflector.SetJSONResponse(&opRuleGet, new(usererror.Error), http.StatusNotFound) - _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/rules/{rule_identifier}", opRuleGet) - opCodeOwnerValidate := openapi3.Operation{} opCodeOwnerValidate.WithTags("repository") opCodeOwnerValidate.WithMapOfAnything(map[string]interface{}{"operationId": "codeOwnersValidate"}) diff --git a/app/api/openapi/rules.go b/app/api/openapi/rules.go new file mode 100644 index 000000000..0fec7e371 --- /dev/null +++ b/app/api/openapi/rules.go @@ -0,0 +1,217 @@ +// 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 openapi + +import ( + "net/http" + + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/rules" + "github.com/harness/gitness/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +// ruleType is a plugin for types.RuleType to allow using oneof. +type ruleType string + +func (ruleType) Enum() []interface{} { + return []interface{}{protection.TypeBranch} +} + +// ruleDefinition is a plugin for types.Rule Definition to allow using oneof. +type ruleDefinition struct{} + +func (ruleDefinition) JSONSchemaOneOf() []interface{} { + return []interface{}{protection.Branch{}} +} + +type rule struct { + types.Rule + + // overshadow Type and Definition to enable oneof. + Type ruleType `json:"type"` + Definition ruleDefinition `json:"definition"` + + // overshadow Pattern to correct the type + Pattern protection.Pattern `json:"pattern"` +} + +func rulesOperations(reflector *openapi3.Reflector) { + opSpaceRuleAdd := openapi3.Operation{} + opSpaceRuleAdd.WithTags("space") + opSpaceRuleAdd.WithMapOfAnything(map[string]interface{}{"operationId": "spaceRuleAdd"}) + _ = reflector.SetRequest(&opSpaceRuleAdd, struct { + spaceRequest + rules.CreateInput + + // overshadow "definition" + Type ruleType `json:"type"` + Definition ruleDefinition `json:"definition"` + }{}, http.MethodPost) + _ = reflector.SetJSONResponse(&opSpaceRuleAdd, rule{}, http.StatusCreated) + _ = reflector.SetJSONResponse(&opSpaceRuleAdd, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSpaceRuleAdd, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSpaceRuleAdd, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSpaceRuleAdd, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/rules", opSpaceRuleAdd) + + opSpaceRuleDelete := openapi3.Operation{} + opSpaceRuleDelete.WithTags("space") + opSpaceRuleDelete.WithMapOfAnything(map[string]interface{}{"operationId": "spaceRuleDelete"}) + _ = reflector.SetRequest(&opSpaceRuleDelete, struct { + spaceRequest + RuleIdentifier string `path:"rule_identifier"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opSpaceRuleDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opSpaceRuleDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSpaceRuleDelete, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSpaceRuleDelete, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSpaceRuleDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodDelete, "/spaces/{space_ref}/rules/{rule_identifier}", opSpaceRuleDelete) + + opSpaceRuleUpdate := openapi3.Operation{} + opSpaceRuleUpdate.WithTags("space") + opSpaceRuleUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "spaceRuleUpdate"}) + _ = reflector.SetRequest(&opSpaceRuleUpdate, &struct { + spaceRequest + Identifier string `path:"rule_identifier"` + rules.UpdateInput + + // overshadow Type and Definition to enable oneof. + Type ruleType `json:"type"` + Definition ruleDefinition `json:"definition"` + }{}, http.MethodPatch) + _ = reflector.SetJSONResponse(&opSpaceRuleUpdate, rule{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opSpaceRuleUpdate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSpaceRuleUpdate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSpaceRuleUpdate, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSpaceRuleUpdate, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, "/spaces/{space_ref}/rules/{rule_identifier}", opSpaceRuleUpdate) + + opSpaceRuleList := openapi3.Operation{} + opSpaceRuleList.WithTags("space") + opSpaceRuleList.WithMapOfAnything(map[string]interface{}{"operationId": "spaceRuleList"}) + opSpaceRuleList.WithParameters( + queryParameterQueryRuleList, + queryParameterOrder, queryParameterSortRuleList, + QueryParameterPage, QueryParameterLimit) + _ = reflector.SetRequest(&opSpaceRuleList, &struct { + spaceRequest + }{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opSpaceRuleList, []rule{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opSpaceRuleList, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSpaceRuleList, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSpaceRuleList, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSpaceRuleList, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/rules", opSpaceRuleList) + + opSpaceRuleGet := openapi3.Operation{} + opSpaceRuleGet.WithTags("space") + opSpaceRuleGet.WithMapOfAnything(map[string]interface{}{"operationId": "spaceRuleGet"}) + _ = reflector.SetRequest(&opSpaceRuleGet, &struct { + spaceRequest + Identifier string `path:"rule_identifier"` + }{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opSpaceRuleGet, rule{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opSpaceRuleGet, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSpaceRuleGet, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSpaceRuleGet, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSpaceRuleGet, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/rules/{rule_identifier}", opSpaceRuleGet) + + opRepoRuleAdd := openapi3.Operation{} + opRepoRuleAdd.WithTags("repository") + opRepoRuleAdd.WithMapOfAnything(map[string]interface{}{"operationId": "repoRuleAdd"}) + _ = reflector.SetRequest(&opRepoRuleAdd, struct { + repoRequest + rules.CreateInput + + // overshadow "definition" + Type ruleType `json:"type"` + Definition ruleDefinition `json:"definition"` + }{}, http.MethodPost) + _ = reflector.SetJSONResponse(&opRepoRuleAdd, rule{}, http.StatusCreated) + _ = reflector.SetJSONResponse(&opRepoRuleAdd, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opRepoRuleAdd, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opRepoRuleAdd, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opRepoRuleAdd, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPost, "/repos/{repo_ref}/rules", opRepoRuleAdd) + + opRepoRuleDelete := openapi3.Operation{} + opRepoRuleDelete.WithTags("repository") + opRepoRuleDelete.WithMapOfAnything(map[string]interface{}{"operationId": "repoRuleDelete"}) + _ = reflector.SetRequest(&opRepoRuleDelete, struct { + repoRequest + RuleIdentifier string `path:"rule_identifier"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opRepoRuleDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opRepoRuleDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opRepoRuleDelete, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opRepoRuleDelete, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opRepoRuleDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodDelete, "/repos/{repo_ref}/rules/{rule_identifier}", opRepoRuleDelete) + + opRepoRuleUpdate := openapi3.Operation{} + opRepoRuleUpdate.WithTags("repository") + opRepoRuleUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "repoRuleUpdate"}) + _ = reflector.SetRequest(&opRepoRuleUpdate, &struct { + repoRequest + Identifier string `path:"rule_identifier"` + rules.UpdateInput + + // overshadow Type and Definition to enable oneof. + Type ruleType `json:"type"` + Definition ruleDefinition `json:"definition"` + }{}, http.MethodPatch) + _ = reflector.SetJSONResponse(&opRepoRuleUpdate, rule{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opRepoRuleUpdate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opRepoRuleUpdate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opRepoRuleUpdate, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opRepoRuleUpdate, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, "/repos/{repo_ref}/rules/{rule_identifier}", opRepoRuleUpdate) + + opRepoRuleList := openapi3.Operation{} + opRepoRuleList.WithTags("repository") + opRepoRuleList.WithMapOfAnything(map[string]interface{}{"operationId": "repoRuleList"}) + opRepoRuleList.WithParameters( + queryParameterQueryRuleList, + queryParameterOrder, queryParameterSortRuleList, + QueryParameterPage, QueryParameterLimit) + _ = reflector.SetRequest(&opRepoRuleList, &struct { + repoRequest + }{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opRepoRuleList, []rule{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opRepoRuleList, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opRepoRuleList, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opRepoRuleList, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opRepoRuleList, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/rules", opRepoRuleList) + + opRepoRuleGet := openapi3.Operation{} + opRepoRuleGet.WithTags("repository") + opRepoRuleGet.WithMapOfAnything(map[string]interface{}{"operationId": "repoRuleGet"}) + _ = reflector.SetRequest(&opRepoRuleGet, &struct { + repoRequest + Identifier string `path:"rule_identifier"` + }{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opRepoRuleGet, rule{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opRepoRuleGet, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opRepoRuleGet, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opRepoRuleGet, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opRepoRuleGet, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/rules/{rule_identifier}", opRepoRuleGet) +} diff --git a/app/router/api.go b/app/router/api.go index 84833dad5..ba6a541d7 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -293,6 +293,7 @@ func setupSpaces( SetupSpaceLabels(r, spaceCtrl) SetupWebhookSpace(r, webhookCtrl) + SetupRulesSpace(r, spaceCtrl) }) }) } @@ -341,6 +342,19 @@ func SetupWebhookSpace(r chi.Router, webhookCtrl *webhook.Controller) { }) } +func SetupRulesSpace(r chi.Router, spaceCtrl *space.Controller) { + r.Route("/rules", func(r chi.Router) { + r.Post("/", handlerspace.HandleRuleCreate(spaceCtrl)) + r.Get("/", handlerspace.HandleRuleList(spaceCtrl)) + + r.Route(fmt.Sprintf("/{%s}", request.PathParamRuleIdentifier), func(r chi.Router) { + r.Patch("/", handlerspace.HandleRuleUpdate(spaceCtrl)) + r.Delete("/", handlerspace.HandleRuleDelete(spaceCtrl)) + r.Get("/", handlerspace.HandleRuleFind(spaceCtrl)) + }) + }) +} + func setupRepos(r chi.Router, repoCtrl *repo.Controller, repoSettingsCtrl *reposettings.Controller, @@ -460,7 +474,7 @@ func setupRepos(r chi.Router, SetupUploads(r, uploadCtrl) - SetupRules(r, repoCtrl) + SetupRulesRepo(r, repoCtrl) SetupRepoLabels(r, repoCtrl) }) @@ -742,7 +756,7 @@ func SetupChecks(r chi.Router, checkCtrl *check.Controller) { }) } -func SetupRules(r chi.Router, repoCtrl *repo.Controller) { +func SetupRulesRepo(r chi.Router, repoCtrl *repo.Controller) { r.Route("/rules", func(r chi.Router) { r.Post("/", handlerrepo.HandleRuleCreate(repoCtrl)) r.Get("/", handlerrepo.HandleRuleList(repoCtrl)) diff --git a/app/services/instrument/instrument.go b/app/services/instrument/instrument.go index 0cba0ab24..710562344 100644 --- a/app/services/instrument/instrument.go +++ b/app/services/instrument/instrument.go @@ -42,6 +42,9 @@ const ( PropertyIsDefaultBranch Property = "is_default_branch" PropertyDecision Property = "decision" PropertyRepositories Property = "repositories" + + PropertySpaceID Property = "space_id" + PropertySpaceName Property = "space_name" ) type EventType string diff --git a/app/services/rules/common.go b/app/services/rules/common.go new file mode 100644 index 000000000..9fa347b6b --- /dev/null +++ b/app/services/rules/common.go @@ -0,0 +1,96 @@ +// 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 rules + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/types" +) + +func (s *Service) getRuleUserAndUserGroups( + ctx context.Context, + rule *types.Rule, +) (map[int64]*types.PrincipalInfo, map[int64]*types.UserGroupInfo, error) { + protection, err := s.parseRule(rule) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse rule: %w", err) + } + + userMap, err := s.getRuleUsers(ctx, protection) + if err != nil { + return nil, nil, fmt.Errorf("failed to get rule users: %w", err) + } + userGroupMap, err := s.getRuleUserGroups(ctx, protection) + if err != nil { + return nil, nil, fmt.Errorf("failed to get rule user groups: %w", err) + } + + return userMap, userGroupMap, nil +} + +func (s *Service) getRuleUsers( + ctx context.Context, + protection protection.Protection, +) (map[int64]*types.PrincipalInfo, error) { + userIDs, err := protection.UserIDs() + if err != nil { + return nil, fmt.Errorf("failed to get user ID from rule: %w", err) + } + + userMap, err := s.principalInfoCache.Map(ctx, userIDs) + if err != nil { + return nil, fmt.Errorf("failed to get principal infos: %w", err) + } + + return userMap, nil +} + +func (s *Service) getRuleUserGroups( + ctx context.Context, + protection protection.Protection, +) (map[int64]*types.UserGroupInfo, error) { + groupIDs, err := protection.UserGroupIDs() + if err != nil { + return nil, fmt.Errorf("failed to get group IDs from rule: %w", err) + } + + userGroupInfoMap := make(map[int64]*types.UserGroupInfo) + + if len(groupIDs) == 0 { + return userGroupInfoMap, nil + } + + groupMap, err := s.userGroupStore.Map(ctx, groupIDs) + if err != nil { + return nil, fmt.Errorf("failed to get userGroup infos: %w", err) + } + + for k, v := range groupMap { + userGroupInfoMap[k] = v.ToUserGroupInfo() + } + return userGroupInfoMap, nil +} + +func (s *Service) parseRule(rule *types.Rule) (protection.Protection, error) { + protection, err := s.protectionManager.FromJSON(rule.Type, rule.Definition, false) + if err != nil { + return nil, fmt.Errorf("failed to parse json rule definition: %w", err) + } + + return protection, nil +} diff --git a/app/services/rules/create.go b/app/services/rules/create.go new file mode 100644 index 000000000..0e38c7a24 --- /dev/null +++ b/app/services/rules/create.go @@ -0,0 +1,197 @@ +// 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 rules + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/app/paths" + "github.com/harness/gitness/app/services/instrument" + "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/audit" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" +) + +type CreateInput struct { + Type types.RuleType `json:"type"` + State enum.RuleState `json:"state"` + // TODO [CODE-1363]: remove after identifier migration. + UID string `json:"uid" deprecated:"true"` + Identifier string `json:"identifier"` + Description string `json:"description"` + Pattern protection.Pattern `json:"pattern"` + Definition json.RawMessage `json:"definition"` +} + +// sanitize validates and sanitizes the create rule input data. +func (in *CreateInput) sanitize() error { + // TODO [CODE-1363]: remove after identifier migration. + if in.Identifier == "" { + in.Identifier = in.UID + } + + if err := check.Identifier(in.Identifier); err != nil { + return err + } + + if err := in.Pattern.Validate(); err != nil { + return usererror.BadRequestf("invalid pattern: %s", err) + } + + var ok bool + in.State, ok = in.State.Sanitize() + if !ok { + return usererror.BadRequest("rule state is invalid") + } + + if in.Type == "" { + in.Type = protection.TypeBranch + } + + if len(in.Definition) == 0 { + return usererror.BadRequest("rule definition missing") + } + + return nil +} + +// Create creates a new protection rule for a scope. +func (s *Service) Create(ctx context.Context, + principal *types.Principal, + parentType enum.RuleParent, + parentID int64, + scopeIdentifier string, + path string, + in *CreateInput, +) (*types.Rule, error) { + if err := in.sanitize(); err != nil { + return nil, err + } + + var err error + in.Definition, err = s.protectionManager.SanitizeJSON(in.Type, in.Definition) + if err != nil { + return nil, usererror.BadRequestf("invalid rule definition: %s", err.Error()) + } + + now := time.Now().UnixMilli() + rule := &types.Rule{ + CreatedBy: principal.ID, + Created: now, + Updated: now, + Type: in.Type, + State: in.State, + Identifier: in.Identifier, + Description: in.Description, + Pattern: in.Pattern.JSON(), + Definition: in.Definition, + CreatedByInfo: types.PrincipalInfo{}, + } + + nameKey := audit.RepoName + if parentType == enum.RuleParentRepo { + rule.RepoID = &parentID + } else if parentType == enum.RuleParentSpace { + nameKey = audit.SpaceName + rule.SpaceID = &parentID + } + + err = s.ruleStore.Create(ctx, rule) + if err != nil { + return nil, fmt.Errorf("failed to create protection rule: %w", err) + } + + err = s.auditService.Log(ctx, + *principal, + audit.NewResource(audit.ResourceTypeBranchRule, rule.Identifier, nameKey, scopeIdentifier), + audit.ActionCreated, + paths.Parent(path), + audit.WithNewObject(rule), + ) + if err != nil { + log.Ctx(ctx).Warn().Msgf("failed to insert audit log for create branch rule operation: %s", err) + } + + userMap, userGroupMap, err := s.getRuleUserAndUserGroups(ctx, rule) + if err != nil { + return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) + } + + rule.Users = userMap + rule.UserGroups = userGroupMap + + var event instrument.Event + if parentType == enum.RuleParentRepo { + event = instrumentEventRepo( + rule.ID, principal.ToPrincipalInfo(), parentID, scopeIdentifier, path, + ) + } else if parentType == enum.RuleParentSpace { + event = instrumentEventSpace( + rule.ID, principal.ToPrincipalInfo(), parentID, scopeIdentifier, path, + ) + } + err = s.instrumentation.Track(ctx, event) + if err != nil { + log.Ctx(ctx).Warn().Msgf("failed to insert instrumentation record for create branch rule operation: %s", err) + } + + return rule, nil +} + +func instrumentEventRepo( + ruleID int64, + principalInfo *types.PrincipalInfo, + scopeID int64, + scopeIdentifier string, + path string, +) instrument.Event { + return instrument.Event{ + Type: instrument.EventTypeCreateBranchRule, + Principal: principalInfo, + Path: path, + Properties: map[instrument.Property]any{ + instrument.PropertyRepositoryID: scopeID, + instrument.PropertyRepositoryName: scopeIdentifier, + instrument.PropertyRuleID: ruleID, + }, + } +} + +func instrumentEventSpace( + ruleID int64, + principalInfo *types.PrincipalInfo, + scopeID int64, + scopeIdentifier string, + path string, +) instrument.Event { + return instrument.Event{ + Type: instrument.EventTypeCreateBranchRule, + Principal: principalInfo, + Path: path, + Properties: map[instrument.Property]any{ + instrument.PropertySpaceID: scopeID, + instrument.PropertySpaceName: scopeIdentifier, + instrument.PropertyRuleID: ruleID, + }, + } +} diff --git a/app/services/rules/delete.go b/app/services/rules/delete.go new file mode 100644 index 000000000..82c53786e --- /dev/null +++ b/app/services/rules/delete.go @@ -0,0 +1,69 @@ +// 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 rules + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/paths" + "github.com/harness/gitness/audit" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" +) + +// Delete deletes a protection rule by identifier. +func (s *Service) Delete(ctx context.Context, + principal *types.Principal, + parentType enum.RuleParent, + parentID int64, + scopeIdentifier string, + path string, + identifier string, +) error { + rule, err := s.ruleStore.FindByIdentifier(ctx, parentType, parentID, identifier) + if err != nil { + return fmt.Errorf("failed to find protection rule by identifier: %w", err) + } + + err = s.ruleStore.Delete(ctx, rule.ID) + if err != nil { + return fmt.Errorf("failed to delete protection rule: %w", err) + } + + nameKey := audit.RepoName + if parentType == enum.RuleParentSpace { + nameKey = audit.SpaceName + } + err = s.auditService.Log(ctx, + *principal, + audit.NewResource( + audit.ResourceTypeBranchRule, + rule.Identifier, + nameKey, + scopeIdentifier, + ), + audit.ActionDeleted, + paths.Parent(path), + audit.WithOldObject(rule), + ) + if err != nil { + log.Ctx(ctx).Warn().Msgf("failed to insert audit log for delete branch rule operation: %s", err) + } + + return nil +} diff --git a/app/services/rules/find.go b/app/services/rules/find.go new file mode 100644 index 000000000..bd311bd4b --- /dev/null +++ b/app/services/rules/find.go @@ -0,0 +1,45 @@ +// 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 rules + +import ( + "context" + "fmt" + + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// Find returns the protection rule by identifier. +func (s *Service) Find(ctx context.Context, + parentType enum.RuleParent, + parentID int64, + identifier string, +) (*types.Rule, error) { + rule, err := s.ruleStore.FindByIdentifier(ctx, parentType, parentID, identifier) + if err != nil { + return nil, fmt.Errorf("failed to find protection rule by identifier: %w", err) + } + + userMap, userGroupMap, err := s.getRuleUserAndUserGroups(ctx, rule) + if err != nil { + return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) + } + + rule.Users = userMap + rule.UserGroups = userGroupMap + + return rule, nil +} diff --git a/app/services/rules/list.go b/app/services/rules/list.go new file mode 100644 index 000000000..d1e46c192 --- /dev/null +++ b/app/services/rules/list.go @@ -0,0 +1,147 @@ +// 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 rules + +import ( + "context" + "fmt" + + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// List returns protection rules for a scope. +func (s *Service) List(ctx context.Context, + parentID int64, + parentType enum.RuleParent, + inherited bool, + filter *types.RuleFilter, +) ([]types.Rule, int64, error) { + var parents []types.RuleParentInfo + var err error + + switch parentType { + case enum.RuleParentRepo: + parents, err = s.getParentInfoRepo(ctx, parentID, inherited) + if err != nil { + return nil, 0, err + } + case enum.RuleParentSpace: + parents, err = s.getParentInfoSpace(ctx, parentID, inherited) + if err != nil { + return nil, 0, err + } + default: + return nil, 0, fmt.Errorf("webhook type %s is not supported", parentType) + } + + var list []types.Rule + var count int64 + + err = s.tx.WithTx(ctx, func(ctx context.Context) error { + list, err = s.ruleStore.List(ctx, parents, filter) + if err != nil { + return fmt.Errorf("failed to list protection rules: %w", err) + } + + if filter.Page == 1 && len(list) < filter.Size { + count = int64(len(list)) + return nil + } + + count, err = s.ruleStore.Count(ctx, parents, filter) + if err != nil { + return fmt.Errorf("failed to count protection rules: %w", err) + } + + return nil + }, dbtx.TxDefaultReadOnly) + if err != nil { + return nil, 0, err + } + + for i := range list { + list[i].Users, list[i].UserGroups, err = s.getRuleUserAndUserGroups(ctx, &list[i]) + if err != nil { + return nil, 0, err + } + } + + return list, count, nil +} + +func (s *Service) getParentInfoRepo( + ctx context.Context, + repoID int64, + inherited bool, +) ([]types.RuleParentInfo, error) { + var parents []types.RuleParentInfo + + parents = append(parents, types.RuleParentInfo{ + ID: repoID, + Type: enum.RuleParentRepo, + }) + + if inherited { + repo, err := s.repoStore.Find(ctx, repoID) + if err != nil { + return nil, fmt.Errorf("failed to get repo: %w", err) + } + + ids, err := s.spaceStore.GetAncestorIDs(ctx, repo.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent space ids: %w", err) + } + + for _, id := range ids { + parents = append(parents, types.RuleParentInfo{ + Type: enum.RuleParentSpace, + ID: id, + }) + } + } + + return parents, nil +} + +func (s *Service) getParentInfoSpace( + ctx context.Context, + spaceID int64, + inherited bool, +) ([]types.RuleParentInfo, error) { + var parents []types.RuleParentInfo + + if inherited { + ids, err := s.spaceStore.GetAncestorIDs(ctx, spaceID) + if err != nil { + return nil, fmt.Errorf("failed to get parent space ids: %w", err) + } + + for _, id := range ids { + parents = append(parents, types.RuleParentInfo{ + Type: enum.RuleParentSpace, + ID: id, + }) + } + } else { + parents = append(parents, types.RuleParentInfo{ + Type: enum.RuleParentSpace, + ID: spaceID, + }) + } + + return parents, nil +} diff --git a/app/services/rules/service.go b/app/services/rules/service.go new file mode 100644 index 000000000..be0cb6d8f --- /dev/null +++ b/app/services/rules/service.go @@ -0,0 +1,66 @@ +// 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 rules + +import ( + "github.com/harness/gitness/app/services/instrument" + "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/usergroup" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/audit" + "github.com/harness/gitness/store/database/dbtx" +) + +// Service is responsible for processing rules. +type Service struct { + tx dbtx.Transactor + + ruleStore store.RuleStore + repoStore store.RepoStore + spaceStore store.SpaceStore + protectionManager *protection.Manager + auditService audit.Service + instrumentation instrument.Service + principalInfoCache store.PrincipalInfoCache + userGroupStore store.UserGroupStore + userGroupService usergroup.SearchService +} + +func NewService( + tx dbtx.Transactor, + ruleStore store.RuleStore, + repoStore store.RepoStore, + spaceStore store.SpaceStore, + protectionManager *protection.Manager, + auditService audit.Service, + instrumentation instrument.Service, + principalInfoCache store.PrincipalInfoCache, + userGroupStore store.UserGroupStore, + userGroupService usergroup.SearchService, + +) *Service { + return &Service{ + tx: tx, + ruleStore: ruleStore, + repoStore: repoStore, + spaceStore: spaceStore, + protectionManager: protectionManager, + auditService: auditService, + instrumentation: instrumentation, + principalInfoCache: principalInfoCache, + userGroupStore: userGroupStore, + userGroupService: userGroupService, + } +} diff --git a/app/services/rules/update.go b/app/services/rules/update.go new file mode 100644 index 000000000..3466f93b9 --- /dev/null +++ b/app/services/rules/update.go @@ -0,0 +1,167 @@ +// 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 rules + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/app/paths" + "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/audit" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" +) + +type UpdateInput struct { + // TODO [CODE-1363]: remove after identifier migration. + UID *string `json:"uid" deprecated:"true"` + Identifier *string `json:"identifier"` + State *enum.RuleState `json:"state"` + Description *string `json:"description"` + Pattern *protection.Pattern `json:"pattern"` + Definition *json.RawMessage `json:"definition"` +} + +// sanitize validates and sanitizes the update rule input data. +func (in *UpdateInput) sanitize() error { + // TODO [CODE-1363]: remove after identifier migration. + if in.Identifier == nil { + in.Identifier = in.UID + } + + if in.Identifier != nil { + if err := check.Identifier(*in.Identifier); err != nil { + return err + } + } + + if in.State != nil { + state, ok := in.State.Sanitize() + if !ok { + return usererror.BadRequest("rule state is invalid") + } + + in.State = &state + } + + if in.Pattern != nil { + if err := in.Pattern.Validate(); err != nil { + return usererror.BadRequestf("invalid pattern: %s", err) + } + } + + if in.Definition != nil && len(*in.Definition) == 0 { + return usererror.BadRequest("rule definition missing") + } + + return nil +} + +func (in *UpdateInput) isEmpty() bool { + return in.Identifier == nil && in.State == nil && in.Description == nil && in.Pattern == nil && in.Definition == nil +} + +// Update updates an existing protection rule for a repository. +func (s *Service) Update(ctx context.Context, + principal *types.Principal, + parentType enum.RuleParent, + parentID int64, + scopeIdentifier string, + path string, + identifier string, + in *UpdateInput, +) (*types.Rule, error) { + if err := in.sanitize(); err != nil { + return nil, err + } + + rule, err := s.ruleStore.FindByIdentifier(ctx, parentType, parentID, identifier) + if err != nil { + return nil, fmt.Errorf("failed to get a repository rule by its identifier: %w", err) + } + oldRule := rule.Clone() + + if in.isEmpty() { + userMap, userGroupMap, err := s.getRuleUserAndUserGroups(ctx, rule) + if err != nil { + return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) + } + + rule.Users = userMap + rule.UserGroups = userGroupMap + + return rule, nil + } + + if in.Identifier != nil { + rule.Identifier = *in.Identifier + } + if in.State != nil { + rule.State = *in.State + } + if in.Description != nil { + rule.Description = *in.Description + } + if in.Pattern != nil { + rule.Pattern = in.Pattern.JSON() + } + if in.Definition != nil { + rule.Definition, err = s.protectionManager.SanitizeJSON(rule.Type, *in.Definition) + if err != nil { + return nil, usererror.BadRequestf("invalid rule definition: %s", err.Error()) + } + } + + userMap, userGroupMap, err := s.getRuleUserAndUserGroups(ctx, rule) + if err != nil { + return nil, fmt.Errorf("failed to get rule users and user groups: %w", err) + } + + rule.Users = userMap + rule.UserGroups = userGroupMap + + if rule.IsEqual(&oldRule) { + return rule, nil + } + + err = s.ruleStore.Update(ctx, rule) + if err != nil { + return nil, fmt.Errorf("failed to update repository-level protection rule: %w", err) + } + + nameKey := audit.RepoName + if parentType == enum.RuleParentSpace { + nameKey = audit.SpaceName + } + err = s.auditService.Log(ctx, + *principal, + audit.NewResource(audit.ResourceTypeBranchRule, rule.Identifier, nameKey, scopeIdentifier), + audit.ActionUpdated, + paths.Parent(path), + audit.WithOldObject(oldRule), + audit.WithNewObject(rule), + ) + if err != nil { + log.Ctx(ctx).Warn().Msgf("failed to insert audit log for update branch rule operation: %s", err) + } + + return rule, nil +} diff --git a/app/services/rules/wire.go b/app/services/rules/wire.go new file mode 100644 index 000000000..6e2ceebd9 --- /dev/null +++ b/app/services/rules/wire.go @@ -0,0 +1,57 @@ +// 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 rules + +import ( + "github.com/harness/gitness/app/services/instrument" + "github.com/harness/gitness/app/services/protection" + "github.com/harness/gitness/app/services/usergroup" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/audit" + "github.com/harness/gitness/store/database/dbtx" + + "github.com/google/wire" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideService, +) + +func ProvideService( + tx dbtx.Transactor, + ruleStore store.RuleStore, + repoStore store.RepoStore, + spaceStore store.SpaceStore, + protectionManager *protection.Manager, + auditService audit.Service, + instrumentation instrument.Service, + principalInfoCache store.PrincipalInfoCache, + userGroupStore store.UserGroupStore, + userGroupService usergroup.SearchService, +) *Service { + return NewService( + tx, + ruleStore, + repoStore, + spaceStore, + protectionManager, + auditService, + instrumentation, + principalInfoCache, + userGroupStore, + userGroupService, + ) +} diff --git a/app/store/database.go b/app/store/database.go index 4ac2896f4..f9087e2a0 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -526,7 +526,12 @@ type ( Find(ctx context.Context, id int64) (*types.Rule, error) // FindByIdentifier finds a protection rule by parent ID and identifier. - FindByIdentifier(ctx context.Context, spaceID, repoID *int64, identifier string) (*types.Rule, error) + FindByIdentifier( + ctx context.Context, + parentType enum.RuleParent, + parentID int64, + identifier string, + ) (*types.Rule, error) // Create inserts a new protection rule. Create(ctx context.Context, rule *types.Rule) error @@ -537,14 +542,19 @@ type ( // Delete removes a protection rule by its ID. Delete(ctx context.Context, id int64) error - // DeleteByIdentifier removes a protection rule by its identifier. - DeleteByIdentifier(ctx context.Context, spaceID, repoID *int64, identifier string) error + // Count returns count of protection rules of a repository or a space. + Count( + ctx context.Context, + parents []types.RuleParentInfo, + filter *types.RuleFilter, + ) (int64, error) - // Count returns count of protection rules matching the provided criteria. - Count(ctx context.Context, spaceID, repoID *int64, filter *types.RuleFilter) (int64, error) - - // List returns a list of protection rules of a repository or a space that matches the provided criteria. - List(ctx context.Context, spaceID, repoID *int64, filter *types.RuleFilter) ([]types.Rule, error) + // List returns a list of protection rules of a repository or a space. + List( + ctx context.Context, + parents []types.RuleParentInfo, + filter *types.RuleFilter, + ) ([]types.Rule, error) // ListAllRepoRules returns a list of all protection rules that can be applied on a repository. ListAllRepoRules(ctx context.Context, repoID int64) ([]types.RuleInfoInternal, error) diff --git a/app/store/database/rule.go b/app/store/database/rule.go index c46b41818..d95fa5904 100644 --- a/app/store/database/rule.go +++ b/app/store/database/rule.go @@ -114,15 +114,23 @@ func (s *RuleStore) Find(ctx context.Context, id int64) (*types.Rule, error) { func (s *RuleStore) FindByIdentifier( ctx context.Context, - spaceID *int64, - repoID *int64, + parentType enum.RuleParent, + parentID int64, identifier string, ) (*types.Rule, error) { stmt := database.Builder. Select(ruleColumns). From("rules"). Where("LOWER(rule_uid) = ?", strings.ToLower(identifier)) - stmt = s.applyParentID(stmt, spaceID, repoID) + + switch parentType { + case enum.RuleParentRepo: + stmt = stmt.Where("rule_repo_id = ?", parentID) + case enum.RuleParentSpace: + stmt = stmt.Where("rule_space_id = ?", parentID) + default: + return nil, fmt.Errorf("rule parent type '%s' is not supported", parentType) + } sql, args, err := stmt.ToSql() if err != nil { @@ -252,40 +260,21 @@ func (s *RuleStore) Delete(ctx context.Context, id int64) error { return nil } -func (s *RuleStore) DeleteByIdentifier(ctx context.Context, spaceID, repoID *int64, identifier string) error { - stmt := database.Builder. - Delete("rules"). - Where("LOWER(rule_uid) = ?", strings.ToLower(identifier)) - - if spaceID != nil { - stmt = stmt.Where("rule_space_id = ?", *spaceID) - } - - if repoID != nil { - stmt = stmt.Where("rule_repo_id = ?", *repoID) - } - - sql, args, err := stmt.ToSql() - if err != nil { - return fmt.Errorf("failed to convert delete rule by identifier to sql: %w", err) - } - - db := dbtx.GetAccessor(ctx, s.db) - - if _, err = db.ExecContext(ctx, sql, args...); err != nil { - return database.ProcessSQLErrorf(ctx, err, "Failed executing delete rule by identifier query") - } - - return nil -} - // Count returns count of protection rules matching the provided criteria. -func (s *RuleStore) Count(ctx context.Context, spaceID, repoID *int64, filter *types.RuleFilter) (int64, error) { +func (s *RuleStore) Count( + ctx context.Context, + parents []types.RuleParentInfo, + filter *types.RuleFilter, +) (int64, error) { stmt := database.Builder. Select("count(*)"). From("rules") - stmt = s.applyParentID(stmt, spaceID, repoID) + err := selectRuleParents(parents, &stmt) + if err != nil { + return 0, fmt.Errorf("failed to select rule parents: %w", err) + } + stmt = s.applyFilter(stmt, filter) sql, args, err := stmt.ToSql() @@ -306,12 +295,20 @@ func (s *RuleStore) Count(ctx context.Context, spaceID, repoID *int64, filter *t } // List returns a list of protection rules of a repository or a space. -func (s *RuleStore) List(ctx context.Context, spaceID, repoID *int64, filter *types.RuleFilter) ([]types.Rule, error) { +func (s *RuleStore) List( + ctx context.Context, + parents []types.RuleParentInfo, + filter *types.RuleFilter, +) ([]types.Rule, error) { stmt := database.Builder. Select(ruleColumns). From("rules") - stmt = s.applyParentID(stmt, spaceID, repoID) + err := selectRuleParents(parents, &stmt) + if err != nil { + return nil, fmt.Errorf("failed to select rule parents: %w", err) + } + stmt = s.applyFilter(stmt, filter) stmt = stmt.Limit(database.Limit(filter.Size)) @@ -427,21 +424,6 @@ func (s *RuleStore) ListAllRepoRules(ctx context.Context, repoID int64) ([]types return s.mapToRuleInfos(result), nil } -func (*RuleStore) applyParentID( - stmt squirrel.SelectBuilder, - spaceID, repoID *int64, -) squirrel.SelectBuilder { - if spaceID != nil { - stmt = stmt.Where("rule_space_id = ?", *spaceID) - } - - if repoID != nil { - stmt = stmt.Where("rule_repo_id = ?", *repoID) - } - - return stmt -} - func (*RuleStore) applyFilter( stmt squirrel.SelectBuilder, filter *types.RuleFilter, @@ -544,3 +526,28 @@ func (s *RuleStore) mapToRuleInfos( } return res } + +func selectRuleParents( + parents []types.RuleParentInfo, + stmt *squirrel.SelectBuilder, +) error { + var parentSelector squirrel.Or + for _, parent := range parents { + switch parent.Type { + case enum.RuleParentRepo: + parentSelector = append(parentSelector, squirrel.Eq{ + "rule_repo_id": parent.ID, + }) + case enum.RuleParentSpace: + parentSelector = append(parentSelector, squirrel.Eq{ + "rule_space_id": parent.ID, + }) + default: + return fmt.Errorf("rule parent type '%s' is not supported", parent.Type) + } + } + + *stmt = stmt.Where(parentSelector) + + return nil +} diff --git a/app/store/database/webhook.go b/app/store/database/webhook.go index c7d71c701..683dbf98e 100644 --- a/app/store/database/webhook.go +++ b/app/store/database/webhook.go @@ -352,9 +352,9 @@ func (s *WebhookStore) Count( Select("count(*)"). From("webhooks") - err := selectParents(parents, &stmt) + err := selectWebhookParents(parents, &stmt) if err != nil { - return 0, fmt.Errorf("failed to select parents: %w", err) + return 0, fmt.Errorf("failed to select webhook parents: %w", err) } stmt = applyWebhookFilter(opts, stmt) @@ -384,9 +384,9 @@ func (s *WebhookStore) List( Select(webhookColumns). From("webhooks") - err := selectParents(parents, &stmt) + err := selectWebhookParents(parents, &stmt) if err != nil { - return nil, fmt.Errorf("failed to select parents: %w", err) + return nil, fmt.Errorf("failed to select webhook parents: %w", err) } stmt = applyWebhookFilter(opts, stmt) @@ -559,7 +559,7 @@ func applyWebhookFilter( return stmt } -func selectParents( +func selectWebhookParents( parents []types.WebhookParentInfo, stmt *squirrel.SelectBuilder, ) error { diff --git a/audit/audit.go b/audit/audit.go index 5971a459d..06abb410e 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -33,6 +33,7 @@ var ( const ( ResourceName = "resourceName" RepoName = "repoName" + SpaceName = "spaceName" BypassedResourceType = "bypassedResourceType" BypassedResourceName = "bypassedResourceName" RepoPath = "repoPath" diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index 1668f45ea..e2dcf8777 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -104,6 +104,7 @@ import ( "github.com/harness/gitness/app/services/publickey" pullreqservice "github.com/harness/gitness/app/services/pullreq" reposervice "github.com/harness/gitness/app/services/repo" + "github.com/harness/gitness/app/services/rules" secretservice "github.com/harness/gitness/app/services/secret" "github.com/harness/gitness/app/services/settings" systemsvc "github.com/harness/gitness/app/services/system" @@ -243,6 +244,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e gitspaceevent.WireSet, cliserver.ProvideKeywordSearchConfig, keywordsearch.WireSet, + rules.WireSet, controllerkeywordsearch.WireSet, settings.WireSet, systemsvc.WireSet, diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 54b8f3cff..ea649467d 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -95,6 +95,7 @@ import ( "github.com/harness/gitness/app/services/publickey" "github.com/harness/gitness/app/services/pullreq" repo2 "github.com/harness/gitness/app/services/repo" + "github.com/harness/gitness/app/services/rules" secret3 "github.com/harness/gitness/app/services/secret" "github.com/harness/gitness/app/services/settings" system2 "github.com/harness/gitness/app/services/system" @@ -251,7 +252,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro instrumentService := instrument.ProvideService() userGroupStore := database.ProvideUserGroupStore(db) searchService := usergroup.ProvideSearchService() - repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, searchService) + rulesService := rules.ProvideService(transactor, ruleStore, repoStore, spaceStore, protectionManager, auditService, instrumentService, principalInfoCache, userGroupStore, searchService) + repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, searchService, rulesService) reposettingsController := reposettings.ProvideController(authorizer, repoStore, settingsService, auditService) stageStore := database.ProvideStageStore(db) schedulerScheduler, err := scheduler.ProvideScheduler(stageStore, mutexManager) @@ -324,7 +326,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro resolverFactory := secret.ProvideResolverFactory(passwordResolver) orchestratorOrchestrator := orchestrator.ProvideOrchestrator(scmSCM, infraProviderResourceStore, infraProvisioner, containerOrchestrator, eventsReporter, orchestratorConfig, vsCode, vsCodeWeb, resolverFactory) gitspaceService := gitspace.ProvideGitspace(transactor, gitspaceConfigStore, gitspaceInstanceStore, eventsReporter, gitspaceEventStore, spaceStore, infraproviderService, orchestratorOrchestrator, scmSCM, config) - spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, listService, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceService, labelService, instrumentService, executionStore) + spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, listService, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceService, labelService, instrumentService, executionStore, rulesService) reporter3, err := events5.ProvideReporter(eventsSystem) if err != nil { return nil, err diff --git a/types/enum/rule.go b/types/enum/rule.go index 0c54def56..5c6af0c2a 100644 --- a/types/enum/rule.go +++ b/types/enum/rule.go @@ -83,3 +83,21 @@ func ParseRuleSortAttr(s string) RuleSort { return RuleSortIdentifier } + +// RuleParent defines different types of parents of a rule. +type RuleParent string + +func (RuleParent) Enum() []interface{} { return toInterfaceSlice(RuleParents) } + +const ( + // RuleParentRepo describes a repo as Rule owner. + RuleParentRepo RuleParent = "repo" + + // RuleParentSpace describes a space as Rule owner. + RuleParentSpace RuleParent = "space" +) + +var RuleParents = sortEnum([]RuleParent{ + RuleParentRepo, + RuleParentSpace, +}) diff --git a/types/rule.go b/types/rule.go index 9bd56e22d..547eac244 100644 --- a/types/rule.go +++ b/types/rule.go @@ -15,6 +15,7 @@ package types import ( + "bytes" "encoding/json" "fmt" @@ -124,6 +125,12 @@ func (r Rule) Clone() Rule { return r } +func (r *Rule) IsEqual(rule *Rule) bool { + return r.Identifier == rule.Identifier && r.State == rule.State && + r.Description == rule.Description && bytes.Equal(r.Pattern, rule.Pattern) && + bytes.Equal(r.Definition, rule.Definition) +} + type RuleType string type RuleFilter struct { @@ -211,3 +218,8 @@ type DryRunRulesOutput struct { DryRunRules bool `json:"dry_run_rules,omitempty"` RuleViolations []RuleViolations `json:"rule_violations,omitempty"` } + +type RuleParentInfo struct { + Type enum.RuleParent + ID int64 +}