mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 04:10:29 +00:00
fix(customtemplates): add resource controls BE-13019 (#2897)
This commit is contained in:
@@ -51,6 +51,7 @@ func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.InternalServerError("Unable to retrieve a resource control associated to the custom template", err)
|
||||
}
|
||||
|
||||
customTemplate.ResourceControl = resourceControl
|
||||
access := userCanEditTemplate(customTemplate, securityContext)
|
||||
if !access {
|
||||
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCustomTemplateDelete_NotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, _, _ := newTestHandler(t)
|
||||
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/99", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "99"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusNotFound, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_Forbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// User 2 did not create this template and is not an admin
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_CreatorDeniedWhenAdminOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
AdministratorsOnly: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// User 2 created the template but an admin later changed it to admins-only
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_CreatorDeniedWithoutResourceControl(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// User 2 created this template but there is no resource control
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
UserAccesses: []portainer.UserResourceAccess{{UserID: 2}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.Nil(t, herr)
|
||||
require.Equal(t, http.StatusNoContent, rr.Code)
|
||||
|
||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
_, err := tx.CustomTemplate().Read(1)
|
||||
require.True(t, tx.IsErrObjectNotFound(err))
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_AdminCanDeleteAdminOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
AdministratorsOnly: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.Nil(t, herr)
|
||||
require.Equal(t, http.StatusNoContent, rr.Code)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_PublicTemplateAllowsAnyUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
Public: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// User 2 is not the creator but the template is public
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.Nil(t, herr)
|
||||
require.Equal(t, http.StatusNoContent, rr.Code)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_NonCreatorForbiddenWithPrivateRC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
UserAccesses: []portainer.UserResourceAccess{{UserID: 1}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// User 2 is not the creator and the template has a private resource control
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateDelete_CreatorDeniedWithoutAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// RC exists but only grants access to user 3, not the creator (user 2)
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
UserAccesses: []portainer.UserResourceAccess{{UserID: 3}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r := httptest.NewRequest(http.MethodDelete, "/custom_templates/1", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "1"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateDelete(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
@@ -59,12 +59,11 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques
|
||||
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||
}
|
||||
|
||||
customTemplate.ResourceControl = resourceControl
|
||||
canEdit := userCanEditTemplate(customTemplate, securityContext)
|
||||
hasAccess := false
|
||||
|
||||
if resourceControl != nil {
|
||||
customTemplate.ResourceControl = resourceControl
|
||||
|
||||
teamIDs := slicesx.Map(securityContext.UserMemberships, func(m portainer.TeamMembership) portainer.TeamID {
|
||||
return m.TeamID
|
||||
})
|
||||
|
||||
@@ -101,6 +101,45 @@ func TestCustomTemplateFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCustomTemplateFile_CreatorDeniedWhenAdminOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, fs := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
path, err := fs.StoreCustomTemplateFileFromBytes("5", "entrypoint", []byte("content"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 5,
|
||||
EntryPoint: "entrypoint",
|
||||
ProjectPath: path,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 5,
|
||||
ResourceID: "5",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
AdministratorsOnly: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/custom_templates/5/file", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "5"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateFile(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateFile_GitTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -54,18 +54,16 @@ func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Req
|
||||
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||
}
|
||||
|
||||
customTemplate.ResourceControl = resourceControl
|
||||
canEdit := userCanEditTemplate(customTemplate, securityContext)
|
||||
hasAccess := false
|
||||
|
||||
if resourceControl != nil {
|
||||
customTemplate.ResourceControl = resourceControl
|
||||
|
||||
teamIDs := slicesx.Map(securityContext.UserMemberships, func(m portainer.TeamMembership) portainer.TeamID {
|
||||
return m.TeamID
|
||||
})
|
||||
|
||||
hasAccess = authorization.UserCanAccessResource(securityContext.UserID, teamIDs, resourceControl)
|
||||
|
||||
}
|
||||
|
||||
if !canEdit && !hasAccess {
|
||||
|
||||
@@ -126,6 +126,40 @@ func TestInspectHandler(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestInspectHandler_CreatorDeniedWhenAdminOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 5,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 5,
|
||||
ResourceID: "5",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
AdministratorsOnly: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/custom_templates/5", nil)
|
||||
r = mux.SetURLVars(r, map[string]string{"id": "5"})
|
||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 2}))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateInspect(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestInspectHandler_GitConfigPopulatedFromSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -142,6 +176,7 @@ func TestInspectHandler_GitConfigPopulatedFromSource(t *testing.T) {
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
srcID = src.ID
|
||||
|
||||
return tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
|
||||
@@ -129,15 +129,15 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
customTemplates, err := handler.DataStore.CustomTemplate().ReadAll()
|
||||
duplicates, err := handler.DataStore.CustomTemplate().ReadAll(func(t portainer.CustomTemplate) bool {
|
||||
return t.ID != portainer.CustomTemplateID(customTemplateID) && t.Title == payload.Title
|
||||
})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve custom templates from the database", err)
|
||||
}
|
||||
|
||||
for _, existingTemplate := range customTemplates {
|
||||
if existingTemplate.ID != portainer.CustomTemplateID(customTemplateID) && existingTemplate.Title == payload.Title {
|
||||
return httperror.InternalServerError("Template name must be unique", errors.New("Template name must be unique"))
|
||||
}
|
||||
if len(duplicates) > 0 {
|
||||
return httperror.InternalServerError("Template name must be unique", errors.New("Template name must be unique"))
|
||||
}
|
||||
|
||||
customTemplate, err := handler.DataStore.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID))
|
||||
@@ -152,8 +152,13 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
access := userCanEditTemplate(customTemplate, securityContext)
|
||||
if !access {
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(strconv.Itoa(customTemplateID), portainer.CustomTemplateResourceControl)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve a resource control associated to the custom template", err)
|
||||
}
|
||||
|
||||
customTemplate.ResourceControl = resourceControl
|
||||
if !userCanEditTemplate(customTemplate, securityContext) {
|
||||
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
|
||||
}
|
||||
|
||||
|
||||
@@ -162,43 +162,6 @@ func TestCustomTemplateUpdate_Success_FileContent(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCustomTemplateUpdate_OwnerCanUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, ds, _ := newTestHandler(t)
|
||||
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
Title: "User Template",
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Type: portainer.DockerComposeStack,
|
||||
Platform: portainer.CustomTemplatePlatformLinux,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
}))
|
||||
|
||||
payload := customTemplateUpdatePayload{
|
||||
Title: "User Template Updated",
|
||||
Description: "Updated by owner",
|
||||
FileContent: "version: '3'",
|
||||
Type: portainer.DockerComposeStack,
|
||||
Platform: portainer.CustomTemplatePlatformLinux,
|
||||
}
|
||||
|
||||
// User 2 is the creator, not an admin
|
||||
r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 2})
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateUpdate(rr, r)
|
||||
require.Nil(t, herr)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var tmpl portainer.CustomTemplate
|
||||
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
||||
require.Equal(t, "User Template Updated", tmpl.Title)
|
||||
}
|
||||
|
||||
func TestCustomTemplateUpdate_SameTitleAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -441,6 +404,94 @@ func TestCustomTemplateUpdate_ClearsArtifact(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCustomTemplateUpdate_CreatorDeniedWhenAdminOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
Title: "User Template",
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Type: portainer.DockerComposeStack,
|
||||
Platform: portainer.CustomTemplatePlatformLinux,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
AdministratorsOnly: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := customTemplateUpdatePayload{
|
||||
Title: "User Template Updated",
|
||||
Description: "Attempted update by creator after adminonly change",
|
||||
FileContent: "version: '3'",
|
||||
Type: portainer.DockerComposeStack,
|
||||
Platform: portainer.CustomTemplatePlatformLinux,
|
||||
}
|
||||
|
||||
r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 2})
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateUpdate(rr, r)
|
||||
require.NotNil(t, herr)
|
||||
require.Equal(t, http.StatusForbidden, herr.StatusCode)
|
||||
}
|
||||
|
||||
func TestCustomTemplateUpdate_AdminCanUpdateAdminOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler, store, _ := newTestHandler(t)
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
Title: "User Template",
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Type: portainer.DockerComposeStack,
|
||||
Platform: portainer.CustomTemplatePlatformLinux,
|
||||
CreatedByUserID: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.ResourceControl().Create(&portainer.ResourceControl{
|
||||
ID: 1,
|
||||
ResourceID: "1",
|
||||
Type: portainer.CustomTemplateResourceControl,
|
||||
AdministratorsOnly: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := customTemplateUpdatePayload{
|
||||
Title: "Updated by Admin",
|
||||
Description: "Admin update of adminonly template",
|
||||
FileContent: "version: '3'",
|
||||
Type: portainer.DockerComposeStack,
|
||||
Platform: portainer.CustomTemplatePlatformLinux,
|
||||
}
|
||||
|
||||
r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
herr := handler.customTemplateUpdate(rr, r)
|
||||
require.Nil(t, herr)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestCustomTemplateUpdate_GitRepository_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
||||
@@ -48,5 +51,27 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
}
|
||||
|
||||
func userCanEditTemplate(customTemplate *portainer.CustomTemplate, securityContext *security.RestrictedRequestContext) bool {
|
||||
return securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID
|
||||
resourceControl := customTemplate.ResourceControl
|
||||
|
||||
if securityContext.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
if resourceControl == nil || resourceControl.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
if resourceControl.Public {
|
||||
return true
|
||||
}
|
||||
|
||||
if customTemplate.CreatedByUserID != securityContext.UserID {
|
||||
return false
|
||||
}
|
||||
|
||||
teamIDs := slicesx.Map(securityContext.UserMemberships, func(m portainer.TeamMembership) portainer.TeamID {
|
||||
return m.TeamID
|
||||
})
|
||||
|
||||
return authorization.UserCanAccessResource(securityContext.UserID, teamIDs, resourceControl)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user