// Copyright 2023 Harness, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package database import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/harness/gitness/app/store" gitness_store "github.com/harness/gitness/store" "github.com/harness/gitness/store/database" "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/Masterminds/squirrel" "github.com/guregu/null" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" ) var _ store.RuleStore = (*RuleStore)(nil) // NewRuleStore returns a new RuleStore. func NewRuleStore( db *sqlx.DB, pCache store.PrincipalInfoCache, ) *RuleStore { return &RuleStore{ pCache: pCache, db: db, } } // RuleStore implements a store.RuleStore backed by a relational database. type RuleStore struct { db *sqlx.DB pCache store.PrincipalInfoCache } type rule struct { ID int64 `db:"rule_id"` Version int64 `db:"rule_version"` CreatedBy int64 `db:"rule_created_by"` Created int64 `db:"rule_created"` Updated int64 `db:"rule_updated"` SpaceID null.Int `db:"rule_space_id"` RepoID null.Int `db:"rule_repo_id"` Identifier string `db:"rule_uid"` Description string `db:"rule_description"` Type types.RuleType `db:"rule_type"` State enum.RuleState `db:"rule_state"` Pattern string `db:"rule_pattern"` Definition string `db:"rule_definition"` } const ( ruleColumns = ` rule_id ,rule_version ,rule_created_by ,rule_created ,rule_updated ,rule_space_id ,rule_repo_id ,rule_uid ,rule_description ,rule_type ,rule_state ,rule_pattern ,rule_definition` ruleSelectBase = ` SELECT` + ruleColumns + ` FROM rules` ) // Find finds the rule by id. func (s *RuleStore) Find(ctx context.Context, id int64) (*types.Rule, error) { const sqlQuery = ruleSelectBase + ` WHERE rule_id = $1` db := dbtx.GetAccessor(ctx, s.db) dst := &rule{} if err := db.GetContext(ctx, dst, sqlQuery, id); err != nil { return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find rule") } r := s.mapToRule(ctx, dst) return &r, nil } func (s *RuleStore) FindByIdentifier( ctx context.Context, spaceID *int64, repoID *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) sql, args, err := stmt.ToSql() if err != nil { return nil, fmt.Errorf("failed to convert find rule by Identifier to sql: %w", err) } db := dbtx.GetAccessor(ctx, s.db) dst := &rule{} if err = db.GetContext(ctx, dst, sql, args...); err != nil { return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing find rule by identifier query") } r := s.mapToRule(ctx, dst) return &r, nil } // Create creates a new protection rule. func (s *RuleStore) Create(ctx context.Context, rule *types.Rule) error { const sqlQuery = ` INSERT INTO rules ( rule_version ,rule_created_by ,rule_created ,rule_updated ,rule_space_id ,rule_repo_id ,rule_uid ,rule_description ,rule_type ,rule_state ,rule_pattern ,rule_definition ) values ( :rule_version ,:rule_created_by ,:rule_created ,:rule_updated ,:rule_space_id ,:rule_repo_id ,:rule_uid ,:rule_description ,:rule_type ,:rule_state ,:rule_pattern ,:rule_definition ) RETURNING rule_id` db := dbtx.GetAccessor(ctx, s.db) dbRule := mapToInternalRule(rule) query, arg, err := db.BindNamed(sqlQuery, &dbRule) if err != nil { return database.ProcessSQLErrorf(ctx, err, "Failed to bind rule object") } if err = db.QueryRowContext(ctx, query, arg...).Scan(&dbRule.ID); err != nil { return database.ProcessSQLErrorf(ctx, err, "Insert rule query failed") } r := s.mapToRule(ctx, &dbRule) *rule = r return nil } // Update updates the protection rule details. func (s *RuleStore) Update(ctx context.Context, rule *types.Rule) error { const sqlQuery = ` UPDATE rules SET rule_version = :rule_version ,rule_updated = :rule_updated ,rule_uid = :rule_uid ,rule_description = :rule_description ,rule_state = :rule_state ,rule_pattern = :rule_pattern ,rule_definition = :rule_definition WHERE rule_id = :rule_id AND rule_version = :rule_version - 1` dbRule := mapToInternalRule(rule) dbRule.Version++ dbRule.Updated = time.Now().UnixMilli() db := dbtx.GetAccessor(ctx, s.db) query, arg, err := db.BindNamed(sqlQuery, dbRule) if err != nil { return database.ProcessSQLErrorf(ctx, err, "Failed to bind rule object") } result, err := db.ExecContext(ctx, query, arg...) if err != nil { return database.ProcessSQLErrorf(ctx, err, "Failed to update rule") } count, err := result.RowsAffected() if err != nil { return database.ProcessSQLErrorf(ctx, err, "Failed to get number of updated rule rows") } if count == 0 { return gitness_store.ErrVersionConflict } rule.Version = dbRule.Version rule.Updated = dbRule.Updated return nil } // Delete the protection rule. func (s *RuleStore) Delete(ctx context.Context, id int64) error { const sqlQuery = ` DELETE FROM rules WHERE rule_id = $1` db := dbtx.GetAccessor(ctx, s.db) if _, err := db.ExecContext(ctx, sqlQuery, id); err != nil { return database.ProcessSQLErrorf(ctx, err, "the delete rule query failed") } 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) { stmt := database.Builder. Select("count(*)"). From("rules") stmt = s.applyParentID(stmt, spaceID, repoID) stmt = s.applyFilter(stmt, filter) sql, args, err := stmt.ToSql() if err != nil { return 0, fmt.Errorf("failed to convert count rules query to sql: %w", err) } db := dbtx.GetAccessor(ctx, s.db) var count int64 err = db.QueryRowContext(ctx, sql, args...).Scan(&count) if err != nil { return 0, database.ProcessSQLErrorf(ctx, err, "Failed executing count rules query") } return count, nil } // 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) { stmt := database.Builder. Select(ruleColumns). From("rules") stmt = s.applyParentID(stmt, spaceID, repoID) stmt = s.applyFilter(stmt, filter) stmt = stmt.Limit(database.Limit(filter.Size)) stmt = stmt.Offset(database.Offset(filter.Page, filter.Size)) order := filter.Order if order == enum.OrderDefault { order = enum.OrderAsc } switch filter.Sort { case enum.RuleSortCreated: stmt = stmt.OrderBy("rule_created " + order.String()) case enum.RuleSortUpdated: stmt = stmt.OrderBy("rule_updated " + order.String()) // TODO [CODE-1363]: remove after identifier migration. case enum.RuleSortUID, enum.RuleSortIdentifier: stmt = stmt.OrderBy("LOWER(rule_uid) " + order.String()) } sql, args, err := stmt.ToSql() if err != nil { return nil, fmt.Errorf("failed to convert query to sql: %w", err) } db := dbtx.GetAccessor(ctx, s.db) dst := make([]rule, 0) if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") } return s.mapToRules(ctx, dst), nil } type ruleInfo struct { SpacePath string `db:"space_path"` RepoPath string `db:"repo_path"` ID int64 `db:"rule_id"` Identifier string `db:"rule_uid"` Type types.RuleType `db:"rule_type"` State enum.RuleState `db:"rule_state"` Pattern string `db:"rule_pattern"` Definition string `db:"rule_definition"` } // ListAllRepoRules returns a list of all protection rules that can be applied on a repository. // This includes the rules defined directly on the repository and all those defined on the parent spaces. func (s *RuleStore) ListAllRepoRules(ctx context.Context, repoID int64) ([]types.RuleInfoInternal, error) { const query = ` WITH RECURSIVE repo_info(repo_id, repo_uid, repo_space_id) AS ( SELECT repo_id, repo_uid, repo_parent_id FROM repositories WHERE repo_id = $1 ), space_parents(space_id, space_uid, space_parent_id) AS ( SELECT space_id, space_uid, space_parent_id FROM spaces INNER JOIN repo_info ON repo_info.repo_space_id = spaces.space_id UNION ALL SELECT spaces.space_id, spaces.space_uid, spaces.space_parent_id FROM spaces INNER JOIN space_parents ON space_parents.space_parent_id = spaces.space_id ), spaces_with_path(space_id, space_parent_id, space_uid, space_full_path) AS ( SELECT space_id, space_parent_id, space_uid, space_uid FROM space_parents WHERE space_parent_id IS NULL UNION ALL SELECT space_parents.space_id, space_parents.space_parent_id, space_parents.space_uid, spaces_with_path.space_full_path || '/' || space_parents.space_uid FROM space_parents INNER JOIN spaces_with_path ON spaces_with_path.space_id = space_parents.space_parent_id ) SELECT space_full_path AS "space_path" ,'' as "repo_path" ,rule_id ,rule_uid ,rule_type ,rule_state ,rule_pattern ,rule_definition FROM spaces_with_path INNER JOIN rules ON rules.rule_space_id = spaces_with_path.space_id WHERE rule_state IN ('active', 'monitor') UNION ALL SELECT '' as "space_path" ,space_full_path || '/' || repo_info.repo_uid AS "repo_path" ,rule_id ,rule_uid ,rule_type ,rule_state ,rule_pattern ,rule_definition FROM rules INNER JOIN repo_info ON repo_info.repo_id = rules.rule_repo_id INNER JOIN spaces_with_path ON spaces_with_path.space_id = repo_info.repo_space_id WHERE rule_state IN ('active', 'monitor')` db := dbtx.GetAccessor(ctx, s.db) result := make([]ruleInfo, 0) if err := db.SelectContext(ctx, &result, query, repoID); err != nil { return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") } 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, ) squirrel.SelectBuilder { if len(filter.States) == 1 { stmt = stmt.Where("rule_state = ?", filter.States[0]) } else if len(filter.States) > 1 { stmt = stmt.Where(squirrel.Eq{"rule_state": filter.States}) } if filter.Query != "" { stmt = stmt.Where("LOWER(rule_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(filter.Query))) } return stmt } func (s *RuleStore) mapToRule( ctx context.Context, in *rule, ) types.Rule { r := types.Rule{ ID: in.ID, Version: in.Version, CreatedBy: in.CreatedBy, Created: in.Created, Updated: in.Updated, SpaceID: in.SpaceID.Ptr(), RepoID: in.RepoID.Ptr(), Identifier: in.Identifier, Description: in.Description, Type: in.Type, State: in.State, Pattern: json.RawMessage(in.Pattern), Definition: json.RawMessage(in.Definition), } createdBy, err := s.pCache.Get(ctx, in.CreatedBy) if err != nil { log.Ctx(ctx).Err(err).Msg("failed to load rule creator") } if createdBy != nil { r.CreatedByInfo = *createdBy } return r } func (s *RuleStore) mapToRules( ctx context.Context, rules []rule, ) []types.Rule { res := make([]types.Rule, len(rules)) for i := 0; i < len(rules); i++ { res[i] = s.mapToRule(ctx, &rules[i]) } return res } func mapToInternalRule(in *types.Rule) rule { return rule{ ID: in.ID, Version: in.Version, CreatedBy: in.CreatedBy, Created: in.Created, Updated: in.Updated, SpaceID: null.IntFromPtr(in.SpaceID), RepoID: null.IntFromPtr(in.RepoID), Identifier: in.Identifier, Description: in.Description, Type: in.Type, State: in.State, Pattern: string(in.Pattern), Definition: string(in.Definition), } } func (*RuleStore) mapToRuleInfo(in *ruleInfo) types.RuleInfoInternal { return types.RuleInfoInternal{ RuleInfo: types.RuleInfo{ SpacePath: in.SpacePath, RepoPath: in.RepoPath, ID: in.ID, Identifier: in.Identifier, Type: in.Type, State: in.State, }, Pattern: json.RawMessage(in.Pattern), Definition: json.RawMessage(in.Definition), } } func (s *RuleStore) mapToRuleInfos( ruleInfos []ruleInfo, ) []types.RuleInfoInternal { res := make([]types.RuleInfoInternal, len(ruleInfos)) for i := 0; i < len(ruleInfos); i++ { res[i] = s.mapToRuleInfo(&ruleInfos[i]) } return res }