chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSource = errors.New("invalid source")
|
||||
ErrInvalidUserContext = errors.New("invalid user context")
|
||||
ErrNotEnoughPermission = errors.New("not enough permissions to perform this action")
|
||||
ErrDuplicateSource = errors.New("a source with this URL and credentials already exists")
|
||||
)
|
||||
|
||||
func validateUserContext(ctx UserContext) error {
|
||||
if ctx == nil {
|
||||
return ErrInvalidUserContext
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type actionType string
|
||||
|
||||
const (
|
||||
actionRead actionType = "read"
|
||||
actionWrite actionType = "write"
|
||||
)
|
||||
|
||||
func enforceUserPermissions(ctx UserContext, source *portainer.Source, action actionType) error {
|
||||
if action == actionRead && userCanReadSource(source, ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if action == actionWrite && userCanWriteSource(source, ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrNotEnoughPermission
|
||||
}
|
||||
|
||||
func userCanWriteSource(source *portainer.Source, context UserContext) bool {
|
||||
if source == nil || context == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if context.IsAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
if source.OwnerID != 0 && source.OwnerID == context.ID() && userCanReadSource(source, context) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterSources(sources []portainer.Source, context UserContext) []portainer.Source {
|
||||
return slicesx.Filter(sources, func(s portainer.Source) bool {
|
||||
return userCanReadSource(&s, context)
|
||||
})
|
||||
}
|
||||
|
||||
func userCanReadSource(source *portainer.Source, context UserContext) bool {
|
||||
if source == nil || context == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
userTeams := context.TeamMemberships()
|
||||
|
||||
if context.IsAdmin() || source.Public {
|
||||
return true
|
||||
}
|
||||
|
||||
if source.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
if slices.Contains(source.UserAccesses, context.ID()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(userTeams) == 0 || len(source.TeamAccesses) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
sTeams := set.ToSet(source.TeamAccesses)
|
||||
uTeams := set.ToSet(slicesx.Map(userTeams, func(u portainer.TeamMembership) portainer.TeamID { return u.TeamID }))
|
||||
|
||||
return set.Intersection(sTeams, uTeams).Len() != 0
|
||||
}
|
||||
|
||||
// enforceUniqueGitSource validates there are no other git sources with the same URL and credentials
|
||||
// It ignores itself
|
||||
func enforceUniqueGitSource(tx ServiceTx, src *portainer.Source) error {
|
||||
if src.Type != portainer.SourceTypeGit || src.Git == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
normalized, err := normalizeGitSource(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existing, err := tx.base.ReadAll(func(s portainer.Source) bool {
|
||||
if src.ID == s.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
n, err := normalizeGitSource(&s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return normalized.Equal(n)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(existing) > 0 {
|
||||
return ErrDuplicateSource
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
type normalizedGitSource struct {
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (a *normalizedGitSource) Equal(b *normalizedGitSource) bool {
|
||||
return a != nil && b != nil &&
|
||||
a.url == b.url &&
|
||||
a.username == b.username &&
|
||||
a.password == b.password
|
||||
}
|
||||
|
||||
// normalize git source to a lighter object used to compare sources together
|
||||
func normalizeGitSource(src *portainer.Source) (*normalizedGitSource, error) {
|
||||
if src == nil || src.Type != portainer.SourceTypeGit || src.Git == nil {
|
||||
return nil, ErrInvalidSource
|
||||
}
|
||||
|
||||
url, err := gittypes.NormalizeURL(gittypes.SanitizeURL(src.Git.URL))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
username, password := "", ""
|
||||
if src.Git.Authentication != nil {
|
||||
username = src.Git.Authentication.Username
|
||||
password = src.Git.Authentication.Password
|
||||
}
|
||||
|
||||
return &normalizedGitSource{
|
||||
url: url,
|
||||
username: username,
|
||||
password: password,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
// Sanitize the source URL and enforce fields values based on user context
|
||||
func sanitizeGitSource(source *portainer.Source) error {
|
||||
if source == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
if source.Type != portainer.SourceTypeGit {
|
||||
return nil
|
||||
}
|
||||
|
||||
if source.Git == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
source.Git.URL, err = gittypes.NormalizeURL(gittypes.SanitizeURL(source.Git.URL))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitizeAccesses(ctx UserContext, newValues *portainer.Source, previousValues *portainer.Source) error {
|
||||
if newValues == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
if ctx.IsAdmin() {
|
||||
if newValues.Public && newValues.AdministratorsOnly {
|
||||
newValues.Public = false
|
||||
}
|
||||
|
||||
if newValues.Public || newValues.AdministratorsOnly {
|
||||
newValues.UserAccesses = []portainer.UserID{}
|
||||
newValues.TeamAccesses = []portainer.TeamID{}
|
||||
}
|
||||
|
||||
if !newValues.Public && !newValues.AdministratorsOnly && len(newValues.UserAccesses) == 0 && len(newValues.TeamAccesses) == 0 {
|
||||
newValues.AdministratorsOnly = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update flow ; regular user is not allowed to change the UAC, visibility or ownership of the source
|
||||
if previousValues != nil {
|
||||
newValues.UserAccesses = previousValues.UserAccesses
|
||||
newValues.TeamAccesses = previousValues.TeamAccesses
|
||||
newValues.Public = previousValues.Public
|
||||
newValues.AdministratorsOnly = previousValues.AdministratorsOnly
|
||||
newValues.OwnerID = previousValues.OwnerID
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create flow
|
||||
userAccesses := []portainer.UserID{ctx.ID()}
|
||||
if newValues.Public {
|
||||
userAccesses = []portainer.UserID{}
|
||||
}
|
||||
newValues.UserAccesses = userAccesses
|
||||
newValues.TeamAccesses = []portainer.TeamID{}
|
||||
newValues.AdministratorsOnly = false
|
||||
newValues.OwnerID = ctx.ID()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type vFn func(new *portainer.Source, old *portainer.Source, err error)
|
||||
|
||||
func testUAC(
|
||||
t *testing.T,
|
||||
userContext UserContext,
|
||||
new *portainer.Source,
|
||||
old *portainer.Source,
|
||||
validationFuncs ...vFn,
|
||||
) {
|
||||
t.Helper()
|
||||
err := sanitizeAccesses(userContext, new, old)
|
||||
for _, validate := range validationFuncs {
|
||||
validate(new, old, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SanitizeAccesses_Admin(t *testing.T) {
|
||||
errInvalidSource := func(_, _ *portainer.Source, err error) {
|
||||
t.Helper()
|
||||
require.ErrorIs(t, err, ErrInvalidSource)
|
||||
}
|
||||
|
||||
noError := func(_, _ *portainer.Source, err error) { t.Helper(); require.NoError(t, err) }
|
||||
noOwner := func(new, _ *portainer.Source, _ error) { t.Helper(); require.Zero(t, new.OwnerID) }
|
||||
emptyUsers := func(new, _ *portainer.Source, _ error) { t.Helper(); require.Empty(t, new.UserAccesses) }
|
||||
emptyTeams := func(new, _ *portainer.Source, _ error) { t.Helper(); require.Empty(t, new.TeamAccesses) }
|
||||
public := func(v bool) func(new, _ *portainer.Source, _ error) {
|
||||
return func(new, _ *portainer.Source, _ error) { t.Helper(); require.Equal(t, v, new.Public) }
|
||||
}
|
||||
adminOnly := func(v bool) func(new, _ *portainer.Source, _ error) {
|
||||
return func(new, _ *portainer.Source, _ error) { t.Helper(); require.Equal(t, v, new.AdministratorsOnly) }
|
||||
}
|
||||
|
||||
adminUserContext := NewUserContext(&portainer.User{Role: portainer.AdministratorRole}, []portainer.TeamMembership{})
|
||||
|
||||
test := func(new *portainer.Source, old *portainer.Source, validationFuncs ...vFn) {
|
||||
t.Helper()
|
||||
testUAC(t, adminUserContext, new, old, validationFuncs...)
|
||||
}
|
||||
|
||||
test(nil, nil, errInvalidSource)
|
||||
test(&portainer.Source{}, nil, noError)
|
||||
test(&portainer.Source{Git: &gittypes.GitSource{}}, nil,
|
||||
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||
)
|
||||
test(&portainer.Source{Git: &gittypes.GitSource{}, Public: true}, nil,
|
||||
noError, emptyUsers, emptyTeams, adminOnly(false), noOwner, public(true),
|
||||
)
|
||||
test(&portainer.Source{Git: &gittypes.GitSource{}, AdministratorsOnly: true}, nil,
|
||||
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||
)
|
||||
test(&portainer.Source{Git: &gittypes.GitSource{}, AdministratorsOnly: true, Public: true}, nil,
|
||||
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||
)
|
||||
test(&portainer.Source{Git: &gittypes.GitSource{}, AdministratorsOnly: true, Public: true, UserAccesses: []portainer.UserID{1, 2}}, nil,
|
||||
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||
)
|
||||
}
|
||||
|
||||
// func Test_SanitizeAccesses_User(t *testing.T) {
|
||||
// user := NewUserContext(&portainer.User{Role: portainer.StandardUserRole}, []portainer.TeamMembership{})
|
||||
// }
|
||||
@@ -0,0 +1,106 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "sources"
|
||||
|
||||
// Service represents a service for managing GitOps source data.
|
||||
type Service struct {
|
||||
base dataservices.BaseDataService[portainer.Source, portainer.SourceID]
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
base: dataservices.BaseDataService[portainer.Source, portainer.SourceID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
base: dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.base.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new source.
|
||||
func (service *Service) Create(context UserContext, source *portainer.Source) error {
|
||||
return service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Create(context, source)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) Read(context UserContext, ID portainer.SourceID) (*portainer.Source, error) {
|
||||
var result *portainer.Source
|
||||
|
||||
err := service.base.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
result, err = service.Tx(tx).Read(context, ID)
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (service *Service) Exists(context UserContext, ID portainer.SourceID) (bool, error) {
|
||||
var result bool
|
||||
|
||||
err := service.base.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
result, err = service.Tx(tx).Exists(context, ID)
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (service *Service) ReadAll(context UserContext, predicates ...func(portainer.Source) bool) ([]portainer.Source, error) {
|
||||
var result []portainer.Source
|
||||
|
||||
err := service.base.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
result, err = service.Tx(tx).ReadAll(context, predicates...)
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (service *Service) Update(context UserContext, ID portainer.SourceID, source *portainer.Source) error {
|
||||
return service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Update(context, ID, source)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) Delete(context UserContext, ID portainer.SourceID) error {
|
||||
return service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Delete(context, ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) FindOrCreateGitSource(context UserContext, source *portainer.Source) (*portainer.Source, error) {
|
||||
var result *portainer.Source
|
||||
|
||||
err := service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
result, err = service.Tx(tx).FindOrCreateGitSource(context, source)
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
base dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID]
|
||||
}
|
||||
|
||||
// Create creates a new source.
|
||||
func (service ServiceTx) Create(context UserContext, source *portainer.Source) error {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if source == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
if err := sanitizeGitSource(source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sanitizeAccesses(context, source, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := enforceUniqueGitSource(service, source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.base.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, any) {
|
||||
source.ID = portainer.SourceID(id)
|
||||
return int(source.ID), source
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (service ServiceTx) Read(context UserContext, ID portainer.SourceID) (*portainer.Source, error) {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source, err := service.base.Read(ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := enforceUserPermissions(context, source, actionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return source, err
|
||||
}
|
||||
|
||||
// Access is not enforced on this to avoid the cost of deserialize
|
||||
// Any user can scan the DB IDs using this method, so be mindful with usage of this func.
|
||||
func (service ServiceTx) Exists(context UserContext, ID portainer.SourceID) (bool, error) {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return service.base.Exists(ID)
|
||||
}
|
||||
|
||||
// ReadAll fetches all sources the user can access, matching predicates
|
||||
func (service ServiceTx) ReadAll(context UserContext, predicates ...func(portainer.Source) bool) ([]portainer.Source, error) {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list, err := service.base.ReadAll(predicates...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return filterSources(list, context), nil
|
||||
}
|
||||
|
||||
// Update updates the source of id `ID` with the `source` content
|
||||
// It validates that the user has access to the source, and has enough permissions to perform the action
|
||||
func (service ServiceTx) Update(context UserContext, ID portainer.SourceID, source *portainer.Source) error {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originalSource, err := service.base.Read(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if source == nil || originalSource == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
if err := enforceUserPermissions(context, originalSource, actionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sanitizeGitSource(source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sanitizeAccesses(context, source, originalSource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := enforceUniqueGitSource(service, source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.base.Update(ID, source)
|
||||
}
|
||||
|
||||
// Delete deletes a source
|
||||
// It validates that the user has access to the source, and has enough permissions to perform the action
|
||||
func (service ServiceTx) Delete(context UserContext, ID portainer.SourceID) error {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := service.base.Read(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := enforceUserPermissions(context, source, actionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.base.Delete(ID)
|
||||
}
|
||||
|
||||
// FindOrCreateGitSource returns an existing Source whose URL and authentication match cfg,
|
||||
// or creates a new one. Only URL, authentication, and TLSSkipVerify are stored on the Source;
|
||||
// per-stack fields (ReferenceName, ConfigFilePath, ConfigHash) belong in the Artifact.
|
||||
// The function auto adds the user to an existing source if the user doesn't have access but provided a valid full
|
||||
// config (URL+Auth)
|
||||
func (service ServiceTx) FindOrCreateGitSource(context UserContext, src *portainer.Source) (*portainer.Source, error) {
|
||||
if err := validateUserContext(context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if src == nil || src.Git == nil {
|
||||
return nil, ErrInvalidSource
|
||||
}
|
||||
|
||||
normalized, err := normalizeGitSource(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := service.base.ReadAll(func(s portainer.Source) bool {
|
||||
n, err := normalizeGitSource(&s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return normalized.Equal(n)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(existing) > 0 {
|
||||
allowed := filterSources(existing, context)
|
||||
if len(allowed) > 0 {
|
||||
return &allowed[0], nil
|
||||
}
|
||||
|
||||
// give user access to the first source if he doesn't have access
|
||||
// to any of the sources that have the same url+auth
|
||||
existing[0].UserAccesses = append(existing[0].UserAccesses, context.ID())
|
||||
if err := service.base.Update(existing[0].ID, &existing[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &existing[0], nil
|
||||
}
|
||||
|
||||
toCreate := &portainer.Source{
|
||||
Name: src.Name,
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.GitSource{
|
||||
URL: src.Git.URL,
|
||||
Authentication: src.Git.Authentication,
|
||||
TLSSkipVerify: src.Git.TLSSkipVerify,
|
||||
},
|
||||
Public: src.Public,
|
||||
AdministratorsOnly: src.AdministratorsOnly,
|
||||
UserAccesses: src.UserAccesses,
|
||||
TeamAccesses: src.TeamAccesses,
|
||||
OwnerID: src.OwnerID,
|
||||
}
|
||||
|
||||
if err := service.Create(context, toCreate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toCreate, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type UserContext = dataservices.SourceServiceUserContext
|
||||
|
||||
type userContext struct {
|
||||
id portainer.UserID
|
||||
role portainer.UserRole
|
||||
memberships []portainer.TeamMembership
|
||||
}
|
||||
|
||||
func (uc userContext) ID() portainer.UserID {
|
||||
return uc.id
|
||||
}
|
||||
|
||||
func (uc userContext) TeamMemberships() []portainer.TeamMembership {
|
||||
return uc.memberships
|
||||
}
|
||||
|
||||
func (uc userContext) IsAdmin() bool {
|
||||
return uc.role == portainer.AdministratorRole
|
||||
}
|
||||
|
||||
// Create a new admin context
|
||||
//
|
||||
// # THIS FUNCTION MUST NOT BE USED IN A USER-AWARE FLOW, ONLY FOR MIGRATIONS AND TESTS
|
||||
//
|
||||
// The only flows outside of migrations/test allowed to use this func is the datastore.Import/Export for sources
|
||||
func InsecureNewAdminContext() UserContext {
|
||||
return NewUserContext(&portainer.User{Role: portainer.AdministratorRole}, []portainer.TeamMembership{})
|
||||
}
|
||||
|
||||
func NewUserContext(user *portainer.User, userMemberships []portainer.TeamMembership) UserContext {
|
||||
return userContext{id: user.ID, role: user.Role, memberships: userMemberships}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package source
|
||||
|
||||
// var adminUserContext = InsecureNewAdminContext()
|
||||
|
||||
// func newSourceWithAuth(url, username, password string) *portainer.Source {
|
||||
// return &portainer.Source{
|
||||
// Type: portainer.SourceTypeGit,
|
||||
// Git: &gittypes.RepoConfig{
|
||||
// URL: url,
|
||||
// Authentication: &gittypes.GitAuthentication{
|
||||
// Username: username,
|
||||
// Password: password,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// func newAuthlessSource(url string) *portainer.Source {
|
||||
// return &portainer.Source{
|
||||
// Type: portainer.SourceTypeGit,
|
||||
// Git: &gittypes.RepoConfig{URL: url},
|
||||
// }
|
||||
// }
|
||||
|
||||
// func validateUniqueSourceInStore(t *testing.T, tx ServiceTx, url, username, password string, sourceID portainer.SourceID) bool {
|
||||
// t.Helper()
|
||||
|
||||
// var isUnique bool
|
||||
// require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
// var err error
|
||||
// isUnique, err =// enforceUniqueGitSource(tx, url, username, password, sourceID)
|
||||
// return err
|
||||
// }))
|
||||
|
||||
// return isUnique
|
||||
// }
|
||||
|
||||
// func TestValidateUniqueSource_SameURLAndCreds_IsDuplicate(t *testing.T) {
|
||||
// t.Parallel()
|
||||
// _, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
// require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
// return tx.Source().Create(adminUserContext, newSourceWithAuth("https://github.com/org/repo.git", "alice", "secret"))
|
||||
// }))
|
||||
|
||||
// require.False(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "alice", "secret", 0))
|
||||
// }
|
||||
|
||||
// func TestValidateUniqueSource_SameURLDifferentCreds_IsUnique(t *testing.T) {
|
||||
// t.Parallel()
|
||||
// _, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
// require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
// return tx.Source().Create(adminUserContext, newSourceWithAuth("https://github.com/org/repo.git", "alice", "secret"))
|
||||
// }))
|
||||
|
||||
// require.True(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "bob", "other", 0))
|
||||
// }
|
||||
|
||||
// func TestValidateUniqueSource_TwoAuthlessSameURL_IsDuplicate(t *testing.T) {
|
||||
// t.Parallel()
|
||||
// _, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
// require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
// return tx.Source().Create(adminUserContext, newAuthlessSource("https://github.com/org/repo.git"))
|
||||
// }))
|
||||
|
||||
// require.False(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "", "", 0))
|
||||
// }
|
||||
|
||||
// func TestValidateUniqueSource_AuthlessVsAuthenticated_IsUnique(t *testing.T) {
|
||||
// t.Parallel()
|
||||
// _, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
// require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
// return tx.Source().Create(adminUserContext, newAuthlessSource("https://github.com/org/repo.git"))
|
||||
// }))
|
||||
|
||||
// require.True(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "alice", "secret", 0))
|
||||
// }
|
||||
|
||||
// func TestValidateUniqueSource_ExcludesSelf(t *testing.T) {
|
||||
// t.Parallel()
|
||||
// _, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
// var srcID portainer.SourceID
|
||||
// require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
// src := newSourceWithAuth("https://github.com/org/repo.git", "alice", "secret")
|
||||
// if err := tx.Source().Create(adminUserContext, src); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// srcID = src.ID
|
||||
// return nil
|
||||
// }))
|
||||
|
||||
// require.True(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "alice", "secret", srcID))
|
||||
// }
|
||||
Reference in New Issue
Block a user