mirror of https://github.com/harness/drone.git
490 lines
13 KiB
Go
490 lines
13 KiB
Go
// 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"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/app/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/jmoiron/sqlx"
|
|
)
|
|
|
|
var _ store.MembershipStore = (*MembershipStore)(nil)
|
|
|
|
// NewMembershipStore returns a new MembershipStore.
|
|
func NewMembershipStore(
|
|
db *sqlx.DB,
|
|
pCache store.PrincipalInfoCache,
|
|
spacePathStore store.SpacePathStore,
|
|
spaceStore store.SpaceStore,
|
|
) *MembershipStore {
|
|
return &MembershipStore{
|
|
db: db,
|
|
pCache: pCache,
|
|
spacePathStore: spacePathStore,
|
|
spaceStore: spaceStore,
|
|
}
|
|
}
|
|
|
|
// MembershipStore implements store.MembershipStore backed by a relational database.
|
|
type MembershipStore struct {
|
|
db *sqlx.DB
|
|
pCache store.PrincipalInfoCache
|
|
spacePathStore store.SpacePathStore
|
|
spaceStore store.SpaceStore
|
|
}
|
|
|
|
type membership struct {
|
|
SpaceID int64 `db:"membership_space_id"`
|
|
PrincipalID int64 `db:"membership_principal_id"`
|
|
|
|
CreatedBy int64 `db:"membership_created_by"`
|
|
Created int64 `db:"membership_created"`
|
|
Updated int64 `db:"membership_updated"`
|
|
|
|
Role enum.MembershipRole `db:"membership_role"`
|
|
}
|
|
|
|
type membershipPrincipal struct {
|
|
membership
|
|
principalInfo
|
|
}
|
|
|
|
type membershipSpace struct {
|
|
membership
|
|
space
|
|
}
|
|
|
|
const (
|
|
membershipColumns = `
|
|
membership_space_id
|
|
,membership_principal_id
|
|
,membership_created_by
|
|
,membership_created
|
|
,membership_updated
|
|
,membership_role`
|
|
|
|
membershipSelectBase = `
|
|
SELECT` + membershipColumns + `
|
|
FROM memberships`
|
|
)
|
|
|
|
// Find finds the membership by space id and principal id.
|
|
func (s *MembershipStore) Find(ctx context.Context, key types.MembershipKey) (*types.Membership, error) {
|
|
const sqlQuery = membershipSelectBase + `
|
|
WHERE membership_space_id = $1 AND membership_principal_id = $2`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := &membership{}
|
|
if err := db.GetContext(ctx, dst, sqlQuery, key.SpaceID, key.PrincipalID); err != nil {
|
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find membership")
|
|
}
|
|
|
|
result := mapToMembership(dst)
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *MembershipStore) FindUser(ctx context.Context, key types.MembershipKey) (*types.MembershipUser, error) {
|
|
m, err := s.Find(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := s.addPrincipalInfos(ctx, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// Create creates a new membership.
|
|
func (s *MembershipStore) Create(ctx context.Context, membership *types.Membership) error {
|
|
const sqlQuery = `
|
|
INSERT INTO memberships (
|
|
membership_space_id
|
|
,membership_principal_id
|
|
,membership_created_by
|
|
,membership_created
|
|
,membership_updated
|
|
,membership_role
|
|
) values (
|
|
:membership_space_id
|
|
,:membership_principal_id
|
|
,:membership_created_by
|
|
,:membership_created
|
|
,:membership_updated
|
|
,:membership_role
|
|
)`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
query, arg, err := db.BindNamed(sqlQuery, mapToInternalMembership(membership))
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to bind membership object")
|
|
}
|
|
|
|
if _, err = db.ExecContext(ctx, query, arg...); err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to insert membership")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Update updates the role of a member of a space.
|
|
func (s *MembershipStore) Update(ctx context.Context, membership *types.Membership) error {
|
|
const sqlQuery = `
|
|
UPDATE memberships
|
|
SET
|
|
membership_updated = :membership_updated
|
|
,membership_role = :membership_role
|
|
WHERE membership_space_id = :membership_space_id AND
|
|
membership_principal_id = :membership_principal_id`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dbMembership := mapToInternalMembership(membership)
|
|
dbMembership.Updated = time.Now().UnixMilli()
|
|
|
|
query, arg, err := db.BindNamed(sqlQuery, dbMembership)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to bind membership object")
|
|
}
|
|
|
|
_, err = db.ExecContext(ctx, query, arg...)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to update membership role")
|
|
}
|
|
|
|
membership.Updated = dbMembership.Updated
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes the membership.
|
|
func (s *MembershipStore) Delete(ctx context.Context, key types.MembershipKey) error {
|
|
const sqlQuery = `
|
|
DELETE from memberships
|
|
WHERE membership_space_id = $1 AND
|
|
membership_principal_id = $2`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
if _, err := db.ExecContext(ctx, sqlQuery, key.SpaceID, key.PrincipalID); err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "delete membership query failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CountUsers returns a number of users memberships that matches the provided filter.
|
|
func (s *MembershipStore) CountUsers(ctx context.Context,
|
|
spaceID int64,
|
|
filter types.MembershipUserFilter,
|
|
) (int64, error) {
|
|
stmt := database.Builder.
|
|
Select("count(*)").
|
|
From("memberships").
|
|
InnerJoin("principals ON membership_principal_id = principal_id").
|
|
Where("membership_space_id = ?", spaceID)
|
|
|
|
stmt = applyMembershipUserFilter(stmt, filter)
|
|
|
|
sql, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to convert membership users count 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 membership users count query")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// ListUsers returns a list of memberships for a space or a user.
|
|
func (s *MembershipStore) ListUsers(ctx context.Context,
|
|
spaceID int64,
|
|
filter types.MembershipUserFilter,
|
|
) ([]types.MembershipUser, error) {
|
|
const columns = membershipColumns + "," + principalInfoCommonColumns
|
|
stmt := database.Builder.
|
|
Select(columns).
|
|
From("memberships").
|
|
InnerJoin("principals ON membership_principal_id = principal_id").
|
|
Where("membership_space_id = ?", spaceID)
|
|
|
|
stmt = applyMembershipUserFilter(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.MembershipUserSortName:
|
|
stmt = stmt.OrderBy("principal_display_name " + order.String())
|
|
case enum.MembershipUserSortCreated:
|
|
stmt = stmt.OrderBy("membership_created " + order.String())
|
|
}
|
|
|
|
sql, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert membership users list query to sql: %w", err)
|
|
}
|
|
|
|
dst := make([]*membershipPrincipal, 0)
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing membership users list query")
|
|
}
|
|
|
|
result, err := s.mapToMembershipUsers(ctx, dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map memberships users to external type: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func applyMembershipUserFilter(
|
|
stmt squirrel.SelectBuilder,
|
|
opts types.MembershipUserFilter,
|
|
) squirrel.SelectBuilder {
|
|
if opts.Query != "" {
|
|
searchTerm := "%%" + strings.ToLower(opts.Query) + "%%"
|
|
stmt = stmt.Where("LOWER(principal_display_name) LIKE ?", searchTerm)
|
|
}
|
|
|
|
return stmt
|
|
}
|
|
|
|
func (s *MembershipStore) CountSpaces(ctx context.Context,
|
|
userID int64,
|
|
filter types.MembershipSpaceFilter,
|
|
) (int64, error) {
|
|
stmt := database.Builder.
|
|
Select("count(*)").
|
|
From("memberships").
|
|
InnerJoin("spaces ON spaces.space_id = membership_space_id").
|
|
Where("membership_principal_id = ?", userID)
|
|
|
|
stmt = applyMembershipSpaceFilter(stmt, filter)
|
|
|
|
sql, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to convert membership spaces count 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 membership spaces count query")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// ListSpaces returns a list of spaces in which the provided user is a member.
|
|
func (s *MembershipStore) ListSpaces(ctx context.Context,
|
|
userID int64,
|
|
filter types.MembershipSpaceFilter,
|
|
) ([]types.MembershipSpace, error) {
|
|
const columns = membershipColumns + "," + spaceColumns
|
|
stmt := database.Builder.
|
|
Select(columns).
|
|
From("memberships").
|
|
InnerJoin("spaces ON spaces.space_id = membership_space_id").
|
|
Where("membership_principal_id = ?", userID)
|
|
|
|
stmt = applyMembershipSpaceFilter(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 {
|
|
// TODO [CODE-1363]: remove after identifier migration.
|
|
case enum.MembershipSpaceSortUID, enum.MembershipSpaceSortIdentifier:
|
|
stmt = stmt.OrderBy("space_uid " + order.String())
|
|
case enum.MembershipSpaceSortCreated:
|
|
stmt = stmt.OrderBy("membership_created " + order.String())
|
|
}
|
|
|
|
sql, args, err := stmt.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert membership spaces list query to sql: %w", err)
|
|
}
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := make([]*membershipSpace, 0)
|
|
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
|
|
}
|
|
|
|
result, err := s.mapToMembershipSpaces(ctx, dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map memberships spaces to external type: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func applyMembershipSpaceFilter(
|
|
stmt squirrel.SelectBuilder,
|
|
opts types.MembershipSpaceFilter,
|
|
) squirrel.SelectBuilder {
|
|
if opts.Query != "" {
|
|
searchTerm := "%%" + strings.ToLower(opts.Query) + "%%"
|
|
stmt = stmt.Where("LOWER(space_uid) LIKE ?", searchTerm)
|
|
}
|
|
|
|
return stmt
|
|
}
|
|
|
|
func mapToMembership(m *membership) types.Membership {
|
|
return types.Membership{
|
|
MembershipKey: types.MembershipKey{
|
|
SpaceID: m.SpaceID,
|
|
PrincipalID: m.PrincipalID,
|
|
},
|
|
CreatedBy: m.CreatedBy,
|
|
Created: m.Created,
|
|
Updated: m.Updated,
|
|
Role: m.Role,
|
|
}
|
|
}
|
|
|
|
func mapToInternalMembership(m *types.Membership) membership {
|
|
return membership{
|
|
SpaceID: m.SpaceID,
|
|
PrincipalID: m.PrincipalID,
|
|
CreatedBy: m.CreatedBy,
|
|
Created: m.Created,
|
|
Updated: m.Updated,
|
|
Role: m.Role,
|
|
}
|
|
}
|
|
|
|
func (s *MembershipStore) addPrincipalInfos(ctx context.Context, m *types.Membership) (types.MembershipUser, error) {
|
|
var result types.MembershipUser
|
|
|
|
// pull principal infos from cache
|
|
infoMap, err := s.pCache.Map(ctx, []int64{m.CreatedBy, m.PrincipalID})
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to load membership principal infos: %w", err)
|
|
}
|
|
|
|
if user, ok := infoMap[m.PrincipalID]; ok {
|
|
result.Principal = *user
|
|
} else {
|
|
return result, fmt.Errorf("failed to find membership principal info: %w", err)
|
|
}
|
|
|
|
if addedBy, ok := infoMap[m.CreatedBy]; ok {
|
|
result.AddedBy = *addedBy
|
|
}
|
|
|
|
result.Membership = *m
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *MembershipStore) mapToMembershipUsers(ctx context.Context,
|
|
ms []*membershipPrincipal,
|
|
) ([]types.MembershipUser, error) {
|
|
// collect all principal IDs
|
|
ids := make([]int64, 0, len(ms))
|
|
for _, m := range ms {
|
|
ids = append(ids, m.membership.CreatedBy)
|
|
}
|
|
|
|
// pull principal infos from cache
|
|
infoMap, err := s.pCache.Map(ctx, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load membership principal infos: %w", err)
|
|
}
|
|
|
|
// attach the principal infos back to the slice items
|
|
res := make([]types.MembershipUser, len(ms))
|
|
for i := range ms {
|
|
m := ms[i]
|
|
res[i].Membership = mapToMembership(&m.membership)
|
|
res[i].Principal = mapToPrincipalInfo(&m.principalInfo)
|
|
if addedBy, ok := infoMap[m.membership.CreatedBy]; ok {
|
|
res[i].AddedBy = *addedBy
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *MembershipStore) mapToMembershipSpaces(ctx context.Context,
|
|
ms []*membershipSpace,
|
|
) ([]types.MembershipSpace, error) {
|
|
// collect all principal IDs
|
|
ids := make([]int64, 0, len(ms))
|
|
for _, m := range ms {
|
|
ids = append(ids, m.membership.CreatedBy)
|
|
}
|
|
|
|
// pull principal infos from cache
|
|
infoMap, err := s.pCache.Map(ctx, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load membership principal infos: %w", err)
|
|
}
|
|
|
|
// attach the principal infos back to the slice items
|
|
res := make([]types.MembershipSpace, len(ms))
|
|
for i := range ms {
|
|
m := ms[i]
|
|
res[i].Membership = mapToMembership(&m.membership)
|
|
space, err := mapToSpace(ctx, s.db, s.spacePathStore, &m.space)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("faild to map space %d: %w", m.space.ID, err)
|
|
}
|
|
res[i].Space = *space
|
|
if addedBy, ok := infoMap[m.membership.CreatedBy]; ok {
|
|
res[i].AddedBy = *addedBy
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|