feat: [CODE-1805]: Add service account rule bypass list support (#3297)

* Make service account store code more consistent
* Merge remote-tracking branch 'origin/main' into dd/service-account-bypass
* Merge remote-tracking branch 'origin/main' into dd/service-account-bypass
* Merge remote-tracking branch 'origin/main' into dd/service-account-bypass
* Remove principal controller interface
* Address review comments
* Merge remote-tracking branch 'origin/main' into dd/service-account-bypass
* Merge remote-tracking branch 'origin/main' into dd/service-account-bypass
* Add pagination to repo/space list service account
* Revert to non embedded Principal to ServiceAccount
* Merge remote-tracking branch 'origin/main' into dd/service-account-bypass
* Add query param to service account list
* Add inherited param to principal filter
* Merge remote-tracking branch 'origin/main' into akp/CODE-1805
* Merge remote-tracking branch 'origin/main' into akp/CODE-1805
* Return service account info instead of vanilla service account
* Return principal info to return id instead o
pull/3616/head
Darko Draskovic 2025-01-31 17:46:24 +00:00 committed by Harness
parent 9224727874
commit 2f9d9583e6
16 changed files with 302 additions and 72 deletions

View File

@ -19,13 +19,13 @@ import (
"github.com/harness/gitness/app/store"
)
type controller struct {
type Controller struct {
principalStore store.PrincipalStore
authorizer authz.Authorizer
}
func newController(principalStore store.PrincipalStore, authorizer authz.Authorizer) *controller {
return &controller{
func newController(principalStore store.PrincipalStore, authorizer authz.Authorizer) Controller {
return Controller{
principalStore: principalStore,
authorizer: authorizer,
}

View File

@ -25,7 +25,7 @@ import (
"github.com/harness/gitness/types/enum"
)
func (c controller) Find(
func (c Controller) Find(
ctx context.Context,
session *auth.Session,
principalID int64,

View File

@ -26,7 +26,7 @@ import (
"github.com/harness/gitness/types/enum"
)
func (c controller) CheckExistenceByEmails(
func (c Controller) CheckExistenceByEmails(
ctx context.Context,
session *auth.Session,
in *CheckUsersInput,

View File

@ -1,31 +0,0 @@
// 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 principal
import (
"context"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types"
)
// Controller interface provides an abstraction that allows to have different implementations of
// principal related information.
type Controller interface {
// List lists the principals based on the provided filter.
List(ctx context.Context, session *auth.Session, opts *types.PrincipalFilter) ([]*types.PrincipalInfo, error)
Find(ctx context.Context, session *auth.Session, principalID int64) (*types.PrincipalInfo, error)
CheckExistenceByEmails(ctx context.Context, session *auth.Session, input *CheckUsersInput) (*CheckUsersOutput, error)
}

View File

@ -27,7 +27,7 @@ import (
"github.com/harness/gitness/types/enum"
)
func (c controller) List(
func (c Controller) List(
ctx context.Context,
session *auth.Session,
opts *types.PrincipalFilter,

View File

@ -26,6 +26,9 @@ var WireSet = wire.NewSet(
ProvideController,
)
func ProvideController(principalStore store.PrincipalStore, authorizer authz.Authorizer) Controller {
func ProvideController(
principalStore store.PrincipalStore,
authorizer authz.Authorizer,
) Controller {
return newController(principalStore, authorizer)
}

View File

@ -19,6 +19,7 @@ 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,7 +29,9 @@ func (c *Controller) ListServiceAccounts(
ctx context.Context,
session *auth.Session,
repoRef string,
) ([]*types.ServiceAccount, error) {
inherited bool,
opts *types.PrincipalFilter,
) ([]*types.ServiceAccountInfo, int64, error) {
repo, err := GetRepoCheckServiceAccountAccess(
ctx,
session,
@ -39,8 +42,61 @@ func (c *Controller) ListServiceAccounts(
c.repoStore,
c.spaceStore)
if err != nil {
return nil, fmt.Errorf("access check failed: %w", err)
return nil, 0, fmt.Errorf("access check failed: %w", err)
}
return c.principalStore.ListServiceAccounts(ctx, enum.ParentResourceTypeRepo, repo.ID)
repoParentInfo := &types.ServiceAccountParentInfo{
ID: repo.ID,
Type: enum.ParentResourceTypeRepo,
}
var parentInfos []*types.ServiceAccountParentInfo
if inherited {
ancestorIDs, err := c.spaceStore.GetAncestorIDs(ctx, repo.ParentID)
if err != nil {
return nil, 0, fmt.Errorf("failed to get parent space ids: %w", err)
}
parentInfos = make([]*types.ServiceAccountParentInfo, len(ancestorIDs)+1)
for i := range ancestorIDs {
parentInfos[i] = &types.ServiceAccountParentInfo{
Type: enum.ParentResourceTypeSpace,
ID: ancestorIDs[i],
}
}
parentInfos[len(parentInfos)-1] = repoParentInfo
} else {
parentInfos = make([]*types.ServiceAccountParentInfo, 1)
parentInfos[0] = repoParentInfo
}
var accounts []*types.ServiceAccount
var count int64
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
accounts, err = c.principalStore.ListServiceAccounts(ctx, parentInfos, opts)
if err != nil {
return fmt.Errorf("failed to list service accounts: %w", err)
}
if opts.Page == 1 && len(accounts) < opts.Size {
count = int64(len(accounts))
return nil
}
count, err = c.principalStore.CountServiceAccounts(ctx, parentInfos, opts)
if err != nil {
return fmt.Errorf("failed to count pull requests: %w", err)
}
return nil
}, dbtx.TxDefaultReadOnly)
if err != nil {
return nil, 0, err
}
infos := make([]*types.ServiceAccountInfo, len(accounts))
for i := range accounts {
infos[i] = accounts[i].ToServiceAccountInfo()
}
return infos, count, nil
}

View File

@ -80,8 +80,9 @@ func (c *Controller) CreateNoAuth(ctx context.Context,
Salt: uniuri.NewLen(uniuri.UUIDLen),
Created: time.Now().UnixMilli(),
Updated: time.Now().UnixMilli(),
ParentType: in.ParentType,
ParentID: in.ParentID,
ParentType: in.ParentType,
ParentID: in.ParentID,
}
err := c.principalStore.CreateServiceAccount(ctx, sa)

View File

@ -24,8 +24,11 @@ import (
)
// Find tries to find the provided service account.
func (c *Controller) Find(ctx context.Context, session *auth.Session,
saUID string) (*types.ServiceAccount, error) {
func (c *Controller) Find(
ctx context.Context,
session *auth.Session,
saUID string,
) (*types.ServiceAccount, error) {
sa, err := c.FindNoAuth(ctx, saUID)
if err != nil {
return nil, err

View File

@ -16,9 +16,11 @@ package space
import (
"context"
"fmt"
apiauth "github.com/harness/gitness/app/api/auth"
"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,10 +30,12 @@ func (c *Controller) ListServiceAccounts(
ctx context.Context,
session *auth.Session,
spaceRef string,
) ([]*types.ServiceAccount, error) {
inherited bool,
opts *types.PrincipalFilter,
) ([]*types.ServiceAccountInfo, int64, error) {
space, err := c.spaceCache.Get(ctx, spaceRef)
if err != nil {
return nil, err
return nil, 0, err
}
if err = apiauth.CheckServiceAccount(
@ -45,8 +49,56 @@ func (c *Controller) ListServiceAccounts(
"",
enum.PermissionServiceAccountView,
); err != nil {
return nil, err
return nil, 0, err
}
return c.principalStore.ListServiceAccounts(ctx, enum.ParentResourceTypeSpace, space.ID)
var parentInfos []*types.ServiceAccountParentInfo
if inherited {
ancestorIDs, err := c.spaceStore.GetAncestorIDs(ctx, space.ID)
if err != nil {
return nil, 0, fmt.Errorf("failed to get parent space ids: %w", err)
}
parentInfos = make([]*types.ServiceAccountParentInfo, len(ancestorIDs))
for i := range ancestorIDs {
parentInfos[i] = &types.ServiceAccountParentInfo{
Type: enum.ParentResourceTypeSpace,
ID: ancestorIDs[i],
}
}
} else {
parentInfos = make([]*types.ServiceAccountParentInfo, 1)
parentInfos[0] = &types.ServiceAccountParentInfo{
Type: enum.ParentResourceTypeSpace,
ID: space.ID,
}
}
var accounts []*types.ServiceAccount
var count int64
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
accounts, err = c.principalStore.ListServiceAccounts(ctx, parentInfos, opts)
if err != nil {
return fmt.Errorf("failed to list service accounts: %w", err)
}
if opts.Page == 1 && len(accounts) < opts.Size {
count = int64(len(accounts))
return nil
}
count, err = c.principalStore.CountServiceAccounts(ctx, parentInfos, opts)
if err != nil {
return fmt.Errorf("failed to count pull requests: %w", err)
}
return nil
}, dbtx.TxDefaultReadOnly)
infos := make([]*types.ServiceAccountInfo, len(accounts))
for i := range accounts {
infos[i] = accounts[i].ToServiceAccountInfo()
}
return infos, count, nil
}

View File

@ -27,7 +27,9 @@ func HandleList(principalCtrl principal.Controller) http.HandlerFunc {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
principalFilter := request.ParsePrincipalFilter(r)
principalInfos, err := principalCtrl.List(ctx, session, principalFilter)
if err != nil {
render.TranslatedUserError(ctx, w, err)

View File

@ -35,13 +35,23 @@ func HandleListServiceAccounts(repoCtrl *repo.Controller) http.HandlerFunc {
return
}
sas, err := repoCtrl.ListServiceAccounts(ctx, session, repoRef)
filter := request.ParsePrincipalFilter(r)
inherited, err := request.ParseInheritedFromQuery(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
// TODO: implement pagination - or should we block that many service accounts in the first place.
render.JSON(w, http.StatusOK, sas)
serviceAccountInfos, count, err := repoCtrl.ListServiceAccounts(
ctx, session, repoRef, inherited, filter,
)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.Pagination(r, w, filter.Page, filter.Size, int(count))
render.JSON(w, http.StatusOK, serviceAccountInfos)
}
}

View File

@ -33,13 +33,23 @@ func HandleListServiceAccounts(spaceCtrl *space.Controller) http.HandlerFunc {
return
}
sas, err := spaceCtrl.ListServiceAccounts(ctx, session, spaceRef)
filter := request.ParsePrincipalFilter(r)
inherited, err := request.ParseInheritedFromQuery(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
// TODO: do we need pagination? we should block that many service accounts in the first place.
render.JSON(w, http.StatusOK, sas)
serviceAccountInfos, count, err := spaceCtrl.ListServiceAccounts(
ctx, session, spaceRef, inherited, filter,
)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.Pagination(r, w, filter.Page, filter.Size, int(count))
render.JSON(w, http.StatusOK, serviceAccountInfos)
}
}

View File

@ -85,6 +85,8 @@ type (
// FindServiceAccountByUID finds the service account by uid.
FindServiceAccountByUID(ctx context.Context, uid string) (*types.ServiceAccount, error)
FindManyServiceAccountByUID(ctx context.Context, uid []string) ([]*types.ServiceAccount, error)
// CreateServiceAccount saves the service account.
CreateServiceAccount(ctx context.Context, sa *types.ServiceAccount) error
@ -97,13 +99,15 @@ type (
// ListServiceAccounts returns a list of service accounts for a specific parent.
ListServiceAccounts(
ctx context.Context,
parentType enum.ParentResourceType, parentID int64,
parentInfos []*types.ServiceAccountParentInfo,
opts *types.PrincipalFilter,
) ([]*types.ServiceAccount, error)
// CountServiceAccounts returns a count of service accounts for a specific parent.
CountServiceAccounts(
ctx context.Context,
parentType enum.ParentResourceType, parentID int64,
parentInfos []*types.ServiceAccountParentInfo,
opts *types.PrincipalFilter,
) (int64, error)
/*
@ -185,6 +189,7 @@ type (
GetRootSpace(ctx context.Context, spaceID int64) (*types.Space, error)
// GetAncestorIDs returns a list of all space IDs along the recursive path to the root space.
// NB: it returns also the spaceID itself in the []int64 slice.
GetAncestorIDs(ctx context.Context, spaceID int64) ([]int64, error)
// GetTreeLevel returns the level of a space in a space tree.

View File

@ -24,6 +24,7 @@ import (
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/Masterminds/squirrel"
"github.com/rs/zerolog/log"
)
@ -79,6 +80,40 @@ func (s *PrincipalStore) FindServiceAccountByUID(ctx context.Context, uid string
return s.mapDBServiceAccount(dst), nil
}
func (s *PrincipalStore) FindManyServiceAccountByUID(
ctx context.Context,
uids []string,
) ([]*types.ServiceAccount, error) {
uniqueUIDs := make([]string, len(uids))
var err error
for i, uid := range uids {
uniqueUIDs[i], err = s.uidTransformation(uid)
if err != nil {
log.Ctx(ctx).Debug().Msgf("failed to transform uid '%s': %s", uid, err.Error())
return nil, gitness_store.ErrResourceNotFound
}
}
stmt := database.Builder.
Select(serviceAccountColumns).
From("principals").
Where("principal_type = ?", enum.PrincipalTypeServiceAccount).
Where(squirrel.Eq{"principal_uid_unique": uniqueUIDs})
db := dbtx.GetAccessor(ctx, s.db)
sqlQuery, params, err := stmt.ToSql()
if err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "failed to generate find many service accounts query")
}
dst := []*serviceAccount{}
if err := db.SelectContext(ctx, &dst, sqlQuery, params...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "find many service accounts failed")
}
return s.mapDBServiceAccounts(dst), nil
}
// CreateServiceAccount saves the service account.
func (s *PrincipalStore) CreateServiceAccount(ctx context.Context, sa *types.ServiceAccount) error {
const sqlQuery = `
@ -178,17 +213,39 @@ func (s *PrincipalStore) DeleteServiceAccount(ctx context.Context, id int64) err
}
// ListServiceAccounts returns a list of service accounts for a specific parent.
func (s *PrincipalStore) ListServiceAccounts(ctx context.Context, parentType enum.ParentResourceType,
parentID int64) ([]*types.ServiceAccount, error) {
const sqlQuery = serviceAccountSelectBase + `
WHERE principal_type = 'serviceaccount' AND principal_sa_parent_type = $1 AND principal_sa_parent_id = $2
ORDER BY principal_uid ASC`
func (s *PrincipalStore) ListServiceAccounts(
ctx context.Context,
parentInfos []*types.ServiceAccountParentInfo,
opts *types.PrincipalFilter,
) ([]*types.ServiceAccount, error) {
stmt := database.Builder.
Select(serviceAccountColumns).
From("principals").
Where("principal_type = ?", enum.PrincipalTypeServiceAccount)
stmt, err := selectServiceAccountParents(parentInfos, stmt)
if err != nil {
return nil, fmt.Errorf("failed to select service account parents: %w", err)
}
stmt = stmt.Limit(database.Limit(opts.Size))
stmt = stmt.Offset(database.Offset(opts.Page, opts.Size))
if opts.Query != "" {
stmt = stmt.Where(PartialMatch("principal_display_name", opts.Query))
}
sqlQuery, params, err := stmt.ToSql()
if err != nil {
return nil, database.ProcessSQLErrorf(
ctx, err, "failed to generate list service accounts query",
)
}
db := dbtx.GetAccessor(ctx, s.db)
dst := []*serviceAccount{}
err := db.SelectContext(ctx, &dst, sqlQuery, parentType, parentID)
if err != nil {
if err := db.SelectContext(ctx, &dst, sqlQuery, params...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing default list query")
}
@ -196,18 +253,36 @@ func (s *PrincipalStore) ListServiceAccounts(ctx context.Context, parentType enu
}
// CountServiceAccounts returns a count of service accounts for a specific parent.
func (s *PrincipalStore) CountServiceAccounts(ctx context.Context,
parentType enum.ParentResourceType, parentID int64) (int64, error) {
const sqlQuery = `
SELECT count(*)
FROM principals
WHERE principal_type = 'serviceaccount' and principal_sa_parentType = $1 and principal_sa_parentId = $2`
func (s *PrincipalStore) CountServiceAccounts(
ctx context.Context,
parentInfos []*types.ServiceAccountParentInfo,
opts *types.PrincipalFilter,
) (int64, error) {
stmt := database.Builder.
Select("count(*)").
From("principals").
Where("principal_type = ?", enum.PrincipalTypeServiceAccount)
if opts.Query != "" {
stmt = stmt.Where(PartialMatch("principal_display_name", opts.Query))
}
stmt, err := selectServiceAccountParents(parentInfos, stmt)
if err != nil {
return 0, fmt.Errorf("failed to select service account parents: %w", err)
}
sqlQuery, params, err := stmt.ToSql()
if err != nil {
return 0, database.ProcessSQLErrorf(
ctx, err, "failed to generate count service accounts query",
)
}
db := dbtx.GetAccessor(ctx, s.db)
var count int64
err := db.QueryRowContext(ctx, sqlQuery, parentType, parentID).Scan(&count)
if err != nil {
if err = db.QueryRowContext(ctx, sqlQuery, params...).Scan(&count); err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "Failed executing count query")
}
@ -243,3 +318,28 @@ func (s *PrincipalStore) mapToDBserviceAccount(sa *types.ServiceAccount) (*servi
return dbSA, nil
}
func selectServiceAccountParents(
parents []*types.ServiceAccountParentInfo,
stmt squirrel.SelectBuilder,
) (squirrel.SelectBuilder, error) {
var typeSelector squirrel.Or
for _, parent := range parents {
switch parent.Type {
case enum.ParentResourceTypeRepo:
typeSelector = append(typeSelector, squirrel.Eq{
"principal_sa_parent_type": enum.ParentResourceTypeRepo,
"principal_sa_parent_id": parent.ID,
})
case enum.ParentResourceTypeSpace:
typeSelector = append(typeSelector, squirrel.Eq{
"principal_sa_parent_type": enum.ParentResourceTypeSpace,
"principal_sa_parent_id": parent.ID,
})
default:
return squirrel.SelectBuilder{}, fmt.Errorf("service account parent type '%s' is not supported", parent.Type)
}
}
return stmt.Where(typeSelector), nil
}

View File

@ -43,6 +43,12 @@ type (
ParentType *enum.ParentResourceType `json:"parent_type"`
ParentID *int64 `json:"parent_id"`
}
ServiceAccountInfo struct {
PrincipalInfo
ParentType enum.ParentResourceType `json:"parent_type"`
ParentID int64 `json:"parent_id"`
}
)
func (s *ServiceAccount) ToPrincipal() *Principal {
@ -63,3 +69,16 @@ func (s *ServiceAccount) ToPrincipal() *Principal {
func (s *ServiceAccount) ToPrincipalInfo() *PrincipalInfo {
return s.ToPrincipal().ToPrincipalInfo()
}
func (s *ServiceAccount) ToServiceAccountInfo() *ServiceAccountInfo {
return &ServiceAccountInfo{
PrincipalInfo: *s.ToPrincipalInfo(),
ParentType: s.ParentType,
ParentID: s.ParentID,
}
}
type ServiceAccountParentInfo struct {
Type enum.ParentResourceType `json:"type"`
ID int64 `json:"id"`
}