mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 10:54:48 +00:00
fix(sources): honor auto-granted user access [BE-13225] (#3208)
This commit is contained in:
@@ -76,14 +76,17 @@ func userCanReadSource(source *portainer.Source, context UserContext) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if source.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
// An explicit user grant wins over AdministratorsOnly: sanitizeAccesses clears
|
||||
// UserAccesses whenever an admin marks a source admins-only, so an entry here
|
||||
// can only come from the deliberate auto-grant in FindOrCreateGitSource.
|
||||
if slices.Contains(source.UserAccesses, context.ID()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if source.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(userTeams) == 0 || len(source.TeamAccesses) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_UserCanReadSource_AdministratorsOnly(t *testing.T) {
|
||||
standardUser := NewUserContext(&portainer.User{ID: 2, Role: portainer.StandardUserRole}, []portainer.TeamMembership{})
|
||||
teamMember := NewUserContext(&portainer.User{ID: 3, Role: portainer.StandardUserRole}, []portainer.TeamMembership{{TeamID: 7}})
|
||||
|
||||
adminOnly := &portainer.Source{AdministratorsOnly: true}
|
||||
require.False(t, userCanReadSource(adminOnly, standardUser))
|
||||
|
||||
// An explicit UserAccesses entry (the FindOrCreateGitSource auto-grant) wins
|
||||
// over AdministratorsOnly.
|
||||
granted := &portainer.Source{AdministratorsOnly: true, UserAccesses: []portainer.UserID{2}}
|
||||
require.True(t, userCanReadSource(granted, standardUser))
|
||||
require.False(t, userCanReadSource(granted, teamMember))
|
||||
|
||||
// Team accesses do not override AdministratorsOnly — only user grants do.
|
||||
teamGranted := &portainer.Source{AdministratorsOnly: true, TeamAccesses: []portainer.TeamID{7}}
|
||||
require.False(t, userCanReadSource(teamGranted, teamMember))
|
||||
}
|
||||
@@ -9641,16 +9641,19 @@ paths:
|
||||
- helm
|
||||
"/templates/helm/{command}":
|
||||
get:
|
||||
description: "**Access policy**: authenticated"
|
||||
description: >-
|
||||
**Access policy**: authenticated
|
||||
|
||||
`repo` may be omitted when `chart` is a self-contained "oci://host/path" reference.
|
||||
operationId: HelmShow
|
||||
parameters:
|
||||
- description: Helm repository URL
|
||||
- description: Helm repository URL (required unless chart is a self-contained
|
||||
oci:// reference)
|
||||
in: query
|
||||
name: repo
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: Chart name
|
||||
- description: Chart name, or a self-contained oci:// chart reference
|
||||
in: query
|
||||
name: chart
|
||||
required: true
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
@@ -448,6 +449,59 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
||||
require.Len(t, sources, 2)
|
||||
}
|
||||
|
||||
func TestFindOrCreateGitSource_AutoGrantLetsStandardUserReadAdminOnlySource(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
standardUserContext := source.NewUserContext(&portainer.User{ID: 2, Role: portainer.StandardUserRole}, []portainer.TeamMembership{})
|
||||
|
||||
makeSource := func(ctx source.UserContext) func(tx dataservices.DataStoreTx) (*portainer.Source, error) {
|
||||
return func(tx dataservices.DataStoreTx) (*portainer.Source, error) {
|
||||
return FindOrCreateGitSource(tx, ctx, &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.GitSource{
|
||||
URL: "https://github.com/example/repo",
|
||||
Authentication: &gittypes.GitAuthentication{Username: "alice", Password: "secret"},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An admin creating a source without explicit accesses defaults it to
|
||||
// AdministratorsOnly.
|
||||
var sourceID portainer.SourceID
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
s, err := makeSource(adminUserContext)(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceID = s.ID
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
created, err := store.Source().Read(adminUserContext, sourceID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, created.AdministratorsOnly)
|
||||
|
||||
// A standard user supplying the same URL+auth is auto-granted access to the
|
||||
// existing source and must be able to read it afterwards (the stack builder
|
||||
// reads it back in the same transaction to persist sync status).
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
s, err := makeSource(standardUserContext)(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
require.Equal(t, sourceID, s.ID)
|
||||
|
||||
_, err = tx.Source().Read(standardUserContext, s.ID)
|
||||
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSaveWorkflowGitConfig_UpdatesFileAndSourceWhenURLUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
@@ -10032,6 +10032,7 @@ export const helmRepoSearch = <ThrowOnError extends boolean = true>(
|
||||
* Show Helm Chart Information
|
||||
*
|
||||
* **Access policy**: authenticated
|
||||
* `repo` may be omitted when `chart` is a self-contained "oci://host/path" reference.
|
||||
*/
|
||||
export const helmShow = <ThrowOnError extends boolean = true>(
|
||||
options: Options<HelmShowData, ThrowOnError>
|
||||
|
||||
@@ -18121,11 +18121,11 @@ export type HelmShowData = {
|
||||
};
|
||||
query: {
|
||||
/**
|
||||
* Helm repository URL
|
||||
* Helm repository URL (required unless chart is a self-contained oci:// reference)
|
||||
*/
|
||||
repo: string;
|
||||
repo?: string;
|
||||
/**
|
||||
* Chart name
|
||||
* Chart name, or a self-contained oci:// chart reference
|
||||
*/
|
||||
chart: string;
|
||||
/**
|
||||
|
||||
@@ -6176,7 +6176,7 @@ export const zHelmShowPath = z.object({
|
||||
});
|
||||
|
||||
export const zHelmShowQuery = z.object({
|
||||
repo: z.string(),
|
||||
repo: z.string().optional(),
|
||||
chart: z.string(),
|
||||
version: z.string().optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user