feat(app/sources): UAC on sources (#2997)
Co-authored-by: Chaim Lev-Ari <chaim.lev-ari@portainer.io> Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com>
This commit is contained in:
@@ -7,8 +7,9 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/gitops/workflows"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -21,8 +22,16 @@ type GitAuthenticationPayload struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SourceAccessControlPayload struct {
|
||||
Public bool `json:"public" example:"true"`
|
||||
AdministratorsOnly bool `json:"administratorsOnly" example:"true"`
|
||||
UserAccesses []portainer.UserID `json:"userAccesses"`
|
||||
TeamAccesses []portainer.TeamID `json:"teamAccesses"`
|
||||
}
|
||||
|
||||
// GitSourceCreatePayload holds the parameters for creating a git-backed source
|
||||
type GitSourceCreatePayload struct {
|
||||
SourceAccessControlPayload
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url" validate:"required"`
|
||||
TLSSkipVerify bool `json:"tlsSkipVerify"`
|
||||
@@ -41,7 +50,7 @@ func (payload *GitSourceCreatePayload) Validate(_ *http.Request) error {
|
||||
// @id GitOpsSourcesCreateGit
|
||||
// @summary Create a Git source
|
||||
// @description Creates a new GitOps source backed by a Git repository.
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
@@ -61,26 +70,20 @@ func (h *Handler) gitSourceCreate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
src, err := BuildGitSource(payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
username, password := "", ""
|
||||
if payload.Authentication != nil {
|
||||
username = payload.Authentication.Username
|
||||
password = payload.Authentication.Password
|
||||
}
|
||||
|
||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
if isUnique, err := workflows.ValidateUniqueSource(tx, payload.URL, username, password, 0); err != nil {
|
||||
return err
|
||||
} else if !isUnique {
|
||||
return ErrDuplicateSource
|
||||
}
|
||||
|
||||
return tx.Source().Create(src)
|
||||
}); errors.Is(err, ErrDuplicateSource) {
|
||||
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||
return tx.Source().Create(userContext, src)
|
||||
}); errors.Is(err, source.ErrDuplicateSource) {
|
||||
return httperror.Conflict("A source with this URL and credentials already exists", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to create source", err)
|
||||
@@ -99,8 +102,7 @@ func BuildGitSource(payload GitSourceCreatePayload) (*portainer.Source, error) {
|
||||
return src, nil
|
||||
}
|
||||
|
||||
// BuildBaseGitSource constructs the source skeleton (name, URL, TLS) without
|
||||
// authentication.
|
||||
// BuildBaseGitSource constructs the source skeleton (name, URL, TLS, accesses) without authentication.
|
||||
func BuildBaseGitSource(payload GitSourceCreatePayload) *portainer.Source {
|
||||
name := payload.Name
|
||||
if strings.TrimSpace(name) == "" {
|
||||
@@ -114,6 +116,10 @@ func BuildBaseGitSource(payload GitSourceCreatePayload) *portainer.Source {
|
||||
URL: payload.URL,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
},
|
||||
UserAccesses: payload.UserAccesses,
|
||||
TeamAccesses: payload.TeamAccesses,
|
||||
Public: payload.Public,
|
||||
AdministratorsOnly: payload.AdministratorsOnly,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestGitSourceCreate_Success(t *testing.T) {
|
||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||
require.NotZero(t, src.ID)
|
||||
require.NotNil(t, src.Git)
|
||||
require.Equal(t, "https://github.com/org/repo.git", src.Git.URL)
|
||||
require.Equal(t, "https://github.com/org/repo", src.Git.URL)
|
||||
}
|
||||
|
||||
func TestGitSourceCreate_SanitizesCredentials(t *testing.T) {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -18,7 +20,7 @@ var ErrSourceInUse = errors.New("source is used by one or more workflows or cust
|
||||
// @id GitOpsSourcesDelete
|
||||
// @summary Delete a source
|
||||
// @description Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow or custom template.
|
||||
// @description **Access policy**: admin
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
@@ -36,8 +38,15 @@ func (h *Handler) sourceDelete(w http.ResponseWriter, r *http.Request) *httperro
|
||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
if exists, err := tx.Source().Exists(portainer.SourceID(sourceID)); err != nil {
|
||||
|
||||
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||
if exists, err := tx.Source().Exists(userContext, portainer.SourceID(sourceID)); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
return dserrors.ErrObjectNotFound
|
||||
@@ -71,11 +80,13 @@ func (h *Handler) sourceDelete(w http.ResponseWriter, r *http.Request) *httperro
|
||||
return ErrSourceInUse
|
||||
}
|
||||
|
||||
return tx.Source().Delete(portainer.SourceID(sourceID))
|
||||
return tx.Source().Delete(userContext, portainer.SourceID(sourceID))
|
||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||
} else if errors.Is(err, ErrSourceInUse) {
|
||||
return httperror.Conflict("Source is used by one or more workflows or custom templates", err)
|
||||
} else if errors.Is(err, source.ErrNotEnoughPermission) {
|
||||
return httperror.Forbidden("Not enough permissions to delete source", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to delete source", err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -19,8 +20,8 @@ func TestSourceDelete_Success(t *testing.T) {
|
||||
|
||||
var srcID portainer.SourceID
|
||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{Name: "to-delete", Type: portainer.SourceTypeGit}
|
||||
err := tx.Source().Create(src)
|
||||
src := &portainer.Source{Name: "to-delete", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -57,8 +58,8 @@ func TestSourceDelete_InUse(t *testing.T) {
|
||||
|
||||
var srcID portainer.SourceID
|
||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{Name: "in-use", Type: portainer.SourceTypeGit}
|
||||
err := tx.Source().Create(src)
|
||||
src := &portainer.Source{Name: "in-use", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -99,8 +100,8 @@ func TestSourceDelete_InUseByCustomTemplate(t *testing.T) {
|
||||
|
||||
var srcID portainer.SourceID
|
||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{Name: "in-use-by-template", Type: portainer.SourceTypeGit}
|
||||
err := tx.Source().Create(src)
|
||||
src := &portainer.Source{Name: "in-use-by-template", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package sources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
sourceDS "github.com/portainer/portainer/api/dataservices/source"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/gitops/workflows"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -26,12 +29,19 @@ type AutoUpdateInfo struct {
|
||||
FetchInterval string `json:"fetchInterval,omitempty"`
|
||||
}
|
||||
|
||||
type SourceAccess struct {
|
||||
Public bool `json:"public,omitempty"`
|
||||
Users []portainer.UserID `json:"users,omitempty"`
|
||||
Teams []portainer.TeamID `json:"teams,omitempty"`
|
||||
}
|
||||
|
||||
// SourceDetail extends Source with connection settings and linked workflows.
|
||||
type SourceDetail struct {
|
||||
Source
|
||||
Connection connectionInfo `json:"connection" validate:"required"`
|
||||
AutoUpdate *AutoUpdateInfo `json:"autoUpdate,omitempty"`
|
||||
Workflows []workflows.Workflow `json:"workflows"`
|
||||
Access SourceAccess `json:"access"`
|
||||
}
|
||||
|
||||
// @id GitOpsSourceGet
|
||||
@@ -55,6 +65,11 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H
|
||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
sourceID := portainer.SourceID(srcID)
|
||||
|
||||
var source *portainer.Source
|
||||
@@ -63,7 +78,8 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H
|
||||
|
||||
err = h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
source, err = tx.Source().Read(sourceID)
|
||||
userContext := sourceDS.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||
source, err = tx.Source().Read(userContext, sourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,15 +90,19 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H
|
||||
|
||||
if h.dataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Source not found", err)
|
||||
} else if errors.Is(err, sourceDS.ErrNotEnoughPermission) {
|
||||
return httperror.Forbidden("Not enough permissions to retrieve source", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve source", err)
|
||||
}
|
||||
|
||||
detail := BuildSourceDetail(h.buildSource(r.Context(), source, stats), source.Git, sourceWfs)
|
||||
access := BuildSourceAccess(source)
|
||||
|
||||
detail := BuildSourceDetail(h.buildSource(r.Context(), source, stats), source.Git, sourceWfs, access)
|
||||
return response.JSON(w, detail)
|
||||
}
|
||||
|
||||
func BuildSourceDetail(baseSource Source, cfg *gittypes.RepoConfig, sourceWfs []workflows.Workflow) SourceDetail {
|
||||
func BuildSourceDetail(baseSource Source, cfg *gittypes.RepoConfig, sourceWfs []workflows.Workflow, access SourceAccess) SourceDetail {
|
||||
var autoUpdate *AutoUpdateInfo
|
||||
if len(sourceWfs) > 0 {
|
||||
autoUpdate = BuildAutoUpdateInfo(sourceWfs[0].AutoUpdate)
|
||||
@@ -93,6 +113,29 @@ func BuildSourceDetail(baseSource Source, cfg *gittypes.RepoConfig, sourceWfs []
|
||||
Connection: buildConnectionInfo(cfg),
|
||||
AutoUpdate: autoUpdate,
|
||||
Workflows: redactWorkflowCredentials(sourceWfs),
|
||||
Access: access,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildSourceAccess(source *portainer.Source) SourceAccess {
|
||||
if source == nil {
|
||||
return SourceAccess{}
|
||||
}
|
||||
|
||||
if source.AdministratorsOnly {
|
||||
return SourceAccess{}
|
||||
}
|
||||
|
||||
if source.Public {
|
||||
return SourceAccess{
|
||||
Public: true,
|
||||
}
|
||||
}
|
||||
|
||||
return SourceAccess{
|
||||
Public: source.Public,
|
||||
Users: source.UserAccesses,
|
||||
Teams: source.TeamAccesses,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,13 +42,15 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
authenticatedRouter.Handle("", httperror.LoggerHandler(h.list)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/summary", httperror.LoggerHandler(h.summary)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.getSource)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/git", httperror.LoggerHandler(h.gitSourceCreate)).Methods(http.MethodPost)
|
||||
authenticatedRouter.Handle("/test", httperror.LoggerHandler(h.gitSourceTest)).Methods(http.MethodPost)
|
||||
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.gitSourceUpdate)).Methods(http.MethodPut)
|
||||
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.sourceDelete)).Methods(http.MethodDelete)
|
||||
authenticatedRouter.Handle("/{id}/test", httperror.LoggerHandler(h.sourceTestConnection)).Methods(http.MethodPost)
|
||||
|
||||
adminRouter := h.PathPrefix("/gitops/sources").Subrouter()
|
||||
adminRouter.Use(bouncer.AdminAccess)
|
||||
adminRouter.Handle("/git", httperror.LoggerHandler(h.gitSourceCreate)).Methods(http.MethodPost)
|
||||
adminRouter.Handle("/test", httperror.LoggerHandler(h.gitSourceTest)).Methods(http.MethodPost)
|
||||
adminRouter.Handle("/{id}", httperror.LoggerHandler(h.gitSourceUpdate)).Methods(http.MethodPut)
|
||||
adminRouter.Handle("/{id}", httperror.LoggerHandler(h.sourceDelete)).Methods(http.MethodDelete)
|
||||
adminRouter.Handle("/{id}/test", httperror.LoggerHandler(h.sourceTestConnection)).Methods(http.MethodPost)
|
||||
adminRouter.Handle("/{id}/access", httperror.LoggerHandler(h.gitSourceUpdateAccess)).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
@@ -17,6 +18,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var adminUserContext = source.InsecureNewAdminContext()
|
||||
|
||||
// createGitWorkflow creates a Source and Workflow for the given config and
|
||||
// wires them up by setting stack.WorkflowID before creating the stack.
|
||||
func createGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portainer.Stack, cfg *gittypes.RepoConfig) portainer.SourceID {
|
||||
@@ -31,7 +34,7 @@ func createGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portain
|
||||
TLSSkipVerify: cfg.TLSSkipVerify,
|
||||
},
|
||||
}
|
||||
require.NoError(t, tx.Source().Create(src))
|
||||
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.Artifact{{
|
||||
@@ -55,13 +58,19 @@ func newTestHandler(t *testing.T, store dataservices.DataStore) *Handler {
|
||||
return NewHandler(testhelpers.NewTestRequestBouncer(), store, nil, nil)
|
||||
}
|
||||
|
||||
func adminRestrictedContext(userID portainer.UserID) *security.RestrictedRequestContext {
|
||||
return &security.RestrictedRequestContext{
|
||||
UserID: userID,
|
||||
IsAdmin: true,
|
||||
User: &portainer.User{ID: userID, Role: portainer.AdministratorRole},
|
||||
}
|
||||
}
|
||||
|
||||
func buildListReq(t *testing.T, userID portainer.UserID, query string) *http.Request {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/gitops/sources?"+query, nil)
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -69,9 +78,7 @@ func buildGetReq(t *testing.T, userID portainer.UserID, id string) *http.Request
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/gitops/sources/"+id, nil)
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -104,9 +111,7 @@ func buildCreateReq(t *testing.T, userID portainer.UserID, body []byte) *http.Re
|
||||
req := httptest.NewRequest(http.MethodPost, "/gitops/sources/git", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -115,9 +120,7 @@ func buildUpdateReq(t *testing.T, userID portainer.UserID, id int, body []byte)
|
||||
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/gitops/sources/%d", id), bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -125,9 +128,7 @@ func buildDeleteReq(t *testing.T, userID portainer.UserID, id int) *http.Request
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/gitops/sources/%d", id), nil)
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -135,9 +136,7 @@ func buildSummaryReq(t *testing.T, userID portainer.UserID) *http.Request {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/gitops/sources/summary", nil)
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -146,9 +145,7 @@ func buildUpdateReqWithRawID(t *testing.T, userID portainer.UserID, id string, b
|
||||
req := httptest.NewRequest(http.MethodPut, "/gitops/sources/"+id, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -156,8 +153,6 @@ func buildDeleteReqWithRawID(t *testing.T, userID portainer.UserID, id string) *
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/gitops/sources/"+id, nil)
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||
UserID: userID, IsAdmin: true,
|
||||
}))
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
ceWorkflows "github.com/portainer/portainer/api/gitops/workflows"
|
||||
"github.com/portainer/portainer/api/gitops/workflows"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/http/utils/filters"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
@@ -56,7 +56,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request) *httperror.Handle
|
||||
}
|
||||
|
||||
if status, _ := request.RetrieveQueryParameter(r, "status", true); status != "" {
|
||||
s, err := ceWorkflows.ParseStatus(status)
|
||||
s, err := workflows.ParseStatus(status)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid status parameter", err)
|
||||
}
|
||||
@@ -111,11 +111,11 @@ func cacheKey(sc *security.RestrictedRequestContext) string {
|
||||
|
||||
func (h *Handler) fetchSources(ctx context.Context, sc *security.RestrictedRequestContext) ([]Source, error) {
|
||||
var allSrcs []portainer.Source
|
||||
var stats map[portainer.SourceID]ceWorkflows.SourceStats
|
||||
var stats map[portainer.SourceID]workflows.SourceStats
|
||||
|
||||
if err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
allSrcs, stats, err = ceWorkflows.FetchSourceStats(tx, h.k8sFactory, sc)
|
||||
allSrcs, stats, err = workflows.FetchSourceStats(tx, h.k8sFactory, sc)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -123,12 +123,12 @@ func (h *Handler) fetchSources(ctx context.Context, sc *security.RestrictedReque
|
||||
|
||||
result := make([]Source, 0, len(allSrcs))
|
||||
for _, src := range allSrcs {
|
||||
s, accessible := stats[src.ID]
|
||||
if !accessible && !sc.IsAdmin {
|
||||
continue
|
||||
stat, ok := stats[src.ID]
|
||||
if !ok {
|
||||
stat = workflows.SourceStats{}
|
||||
}
|
||||
|
||||
result = append(result, h.buildSource(ctx, &src, s))
|
||||
result = append(result, h.buildSource(ctx, &src, stat))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestSourcesList_GroupsByURLAndCredentials(t *testing.T) {
|
||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
cfg := gitCfg("https://github.com/org/repo")
|
||||
src := &portainer.Source{Name: "repo", Type: portainer.SourceTypeGit, Git: cfg}
|
||||
require.NoError(t, tx.Source().Create(src))
|
||||
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||
|
||||
wfA := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
||||
require.NoError(t, tx.Workflow().Create(wfA))
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -17,7 +19,7 @@ import (
|
||||
// @id GitOpsSourcesTestById
|
||||
// @summary Test the connection of a stored source
|
||||
// @description Tests connectivity for a GitOps source, applying optional overrides to the stored configuration.
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
@@ -37,6 +39,11 @@ func (h *Handler) sourceTestConnection(w http.ResponseWriter, r *http.Request) *
|
||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
var payload GitSourceUpdatePayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil && !errors.Is(err, io.EOF) {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
@@ -44,10 +51,13 @@ func (h *Handler) sourceTestConnection(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
var src *portainer.Source
|
||||
if err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
src, err = tx.Source().Read(portainer.SourceID(sourceID))
|
||||
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||
src, err = tx.Source().Read(userContext, portainer.SourceID(sourceID))
|
||||
return err
|
||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||
} else if errors.Is(err, source.ErrNotEnoughPermission) {
|
||||
return httperror.Forbidden("Not enough permissions to retrieve source", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find source", err)
|
||||
}
|
||||
@@ -75,7 +85,7 @@ type ConnectionTestResult struct {
|
||||
// @id GitOpsSourcesTest
|
||||
// @summary Test a Git source connection
|
||||
// @description Tests connectivity for Git connection details that have not been persisted yet.
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
ceWorkflows "github.com/portainer/portainer/api/gitops/workflows"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
@@ -38,10 +40,9 @@ func TestSourcesSummary_CountsByStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
// With nil gitService and nil GitConfig, all sources get StatusUnknown.
|
||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
for _, name := range []string{"source-a", "source-b", "source-c"} {
|
||||
err := tx.Source().Create(&portainer.Source{Name: name, Type: portainer.SourceTypeGit})
|
||||
for idx, name := range []string{"source-a", "source-b", "source-c"} {
|
||||
err := tx.Source().Create(adminUserContext, &portainer.Source{Name: name, Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: fmt.Sprintf("http://github.com/org/repo%d", idx)}})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package sources
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type SourceAccessUpdatePayload struct {
|
||||
Public bool `json:"public"`
|
||||
Users []portainer.UserID `json:"users,omitempty"`
|
||||
Teams []portainer.TeamID `json:"teams,omitempty"`
|
||||
}
|
||||
|
||||
// @id GitOpsSourcesUpdateAccess
|
||||
// @summary Update a GitOps source's access control
|
||||
// @description Updates the access control settings for an existing GitOps source.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Source identifier"
|
||||
// @param body body SourceAccessUpdatePayload true "Source access control"
|
||||
// @success 200 {object} portainer.Source
|
||||
// @failure 400 "Invalid request payload"
|
||||
// @failure 403 "Access denied"
|
||||
// @failure 404 "Source not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /gitops/sources/{id}/access [put]
|
||||
func (h *Handler) gitSourceUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||
}
|
||||
|
||||
var payload SourceAccessUpdatePayload
|
||||
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
sourceID := portainer.SourceID(id)
|
||||
|
||||
var src *portainer.Source
|
||||
|
||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
|
||||
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||
if src, err = tx.Source().Read(userContext, sourceID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ApplySourceAccessChanges(src, payload)
|
||||
|
||||
return tx.Source().Update(userContext, src.ID, src)
|
||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to update source access", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, src)
|
||||
}
|
||||
|
||||
// Validate implements the portainer.Validatable interface
|
||||
func (payload *SourceAccessUpdatePayload) Validate(_ *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplySourceAccessChanges applies the payload access changes to the source in place.
|
||||
func ApplySourceAccessChanges(src *portainer.Source, payload SourceAccessUpdatePayload) {
|
||||
src.Public = payload.Public
|
||||
|
||||
if payload.Public {
|
||||
src.AdministratorsOnly = false
|
||||
src.UserAccesses = []portainer.UserID{}
|
||||
src.TeamAccesses = []portainer.TeamID{}
|
||||
} else if len(payload.Users) == 0 && len(payload.Teams) == 0 {
|
||||
src.AdministratorsOnly = true
|
||||
src.UserAccesses = []portainer.UserID{}
|
||||
src.TeamAccesses = []portainer.TeamID{}
|
||||
} else {
|
||||
src.AdministratorsOnly = false
|
||||
src.UserAccesses = payload.Users
|
||||
src.TeamAccesses = payload.Teams
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/gitops/workflows"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -16,8 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotGitSource = errors.New("source is not a Git source")
|
||||
ErrDuplicateSource = errors.New("a source with this URL and credentials already exists")
|
||||
ErrNotGitSource = errors.New("source is not a Git source")
|
||||
)
|
||||
|
||||
// GitSourceUpdatePayload holds the parameters for creating a git-backed source
|
||||
@@ -46,7 +46,7 @@ func (payload *GitSourceUpdatePayload) Validate(_ *http.Request) error {
|
||||
// @id GitOpsSourcesUpdateGit
|
||||
// @summary Update a Git source
|
||||
// @description Updates an existing GitOps source backed by a Git repository.
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
@@ -73,6 +73,11 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
sourceID := portainer.SourceID(id)
|
||||
|
||||
var src *portainer.Source
|
||||
@@ -80,7 +85,8 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
|
||||
if src, err = tx.Source().Read(sourceID); err != nil {
|
||||
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||
if src, err = tx.Source().Read(userContext, sourceID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -88,24 +94,14 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return err
|
||||
}
|
||||
|
||||
username, password := "", ""
|
||||
if src.Git != nil && src.Git.Authentication != nil {
|
||||
username = src.Git.Authentication.Username
|
||||
password = src.Git.Authentication.Password
|
||||
}
|
||||
|
||||
if isUnique, err := workflows.ValidateUniqueSource(tx, src.Git.URL, username, password, sourceID); err != nil {
|
||||
return err
|
||||
} else if !isUnique {
|
||||
return ErrDuplicateSource
|
||||
}
|
||||
|
||||
return tx.Source().Update(src.ID, src)
|
||||
return tx.Source().Update(userContext, src.ID, src)
|
||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||
} else if errors.Is(err, ErrNotGitSource) {
|
||||
return httperror.BadRequest("Source is not a Git source", err)
|
||||
} else if errors.Is(err, ErrDuplicateSource) {
|
||||
} else if errors.Is(err, source.ErrNotEnoughPermission) {
|
||||
return httperror.Forbidden("Not enough permissions to update source", err)
|
||||
} else if errors.Is(err, source.ErrDuplicateSource) {
|
||||
return httperror.Conflict("A source with this URL and credentials already exists", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to update source", err)
|
||||
|
||||
@@ -20,8 +20,8 @@ func TestGitSourceUpdate_Success(t *testing.T) {
|
||||
|
||||
var srcID portainer.SourceID
|
||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{Name: "old-name", Type: portainer.SourceTypeGit}
|
||||
err := tx.Source().Create(src)
|
||||
src := &portainer.Source{Name: "old-name", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestGitSourceUpdate_Success(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new-name", src.Name)
|
||||
require.NotNil(t, src.Git)
|
||||
require.Equal(t, "https://github.com/org/new.git", src.Git.URL)
|
||||
require.Equal(t, "https://github.com/org/new", src.Git.URL)
|
||||
}
|
||||
|
||||
func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
||||
@@ -66,7 +66,7 @@ func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
||||
var stored *portainer.Source
|
||||
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
stored, err = tx.Source().Read(srcID)
|
||||
stored, err = tx.Source().Read(adminUserContext, srcID)
|
||||
return err
|
||||
}))
|
||||
require.NotNil(t, stored.Git)
|
||||
@@ -115,7 +115,7 @@ func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -138,7 +138,7 @@ func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) {
|
||||
var stored *portainer.Source
|
||||
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
stored, err = tx.Source().Read(srcID)
|
||||
stored, err = tx.Source().Read(adminUserContext, srcID)
|
||||
return err
|
||||
}))
|
||||
require.NotNil(t, stored.Git)
|
||||
@@ -162,7 +162,7 @@ func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -188,7 +188,7 @@ func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) {
|
||||
var stored *portainer.Source
|
||||
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
stored, err = tx.Source().Read(srcID)
|
||||
stored, err = tx.Source().Read(adminUserContext, srcID)
|
||||
return err
|
||||
}))
|
||||
require.NotNil(t, stored.Git)
|
||||
@@ -229,11 +229,11 @@ func TestGitSourceUpdate_ConflictOnDuplicateURL(t *testing.T) {
|
||||
URL: "https://github.com/org/existing.git",
|
||||
},
|
||||
}
|
||||
err := tx.Source().Create(existing)
|
||||
err := tx.Source().Create(adminUserContext, existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
src := &portainer.Source{Name: "other", Type: portainer.SourceTypeGit}
|
||||
err = tx.Source().Create(src)
|
||||
src := &portainer.Source{Name: "other", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||
err = tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -253,39 +253,14 @@ func TestGitSourceUpdate_ConflictOnDuplicateURL(t *testing.T) {
|
||||
require.Equal(t, http.StatusConflict, rr.Code)
|
||||
}
|
||||
|
||||
func TestGitSourceUpdate_NotGitSource(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 := &portainer.Source{Name: "helm-source", Type: portainer.SourceTypeHelm}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})
|
||||
}))
|
||||
|
||||
h := newTestHandler(t, store)
|
||||
|
||||
body, err := json.Marshal(GitSourceUpdatePayload{URL: new("https://github.com/org/repo.git")})
|
||||
require.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, buildUpdateReq(t, 1, int(srcID), body))
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestGitSourceUpdate_MalformedJSON(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 := &portainer.Source{Name: "src", Type: portainer.SourceTypeGit}
|
||||
err := tx.Source().Create(src)
|
||||
src := &portainer.Source{Name: "src", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||
err := tx.Source().Create(adminUserContext, src)
|
||||
require.NoError(t, err)
|
||||
srcID = src.ID
|
||||
|
||||
@@ -317,7 +292,7 @@ func TestGitSourceUpdate_ConflictWhenAuthChangesMatchAnotherSource(t *testing.T)
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := tx.Source().Create(existing); err != nil {
|
||||
if err := tx.Source().Create(adminUserContext, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -326,7 +301,7 @@ func TestGitSourceUpdate_ConflictWhenAuthChangesMatchAnotherSource(t *testing.T)
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo.git"},
|
||||
}
|
||||
if err := tx.Source().Create(other); err != nil {
|
||||
if err := tx.Source().Create(adminUserContext, other); err != nil {
|
||||
return err
|
||||
}
|
||||
srcID = other.ID
|
||||
|
||||
Reference in New Issue
Block a user