fix(sources): honor auto-granted user access [BE-13225] (#3208)

This commit is contained in:
RHCowan
2026-07-17 13:51:19 +12:00
committed by GitHub
parent 42b9ce87a1
commit 89f89f8b04
7 changed files with 100 additions and 12 deletions
+7 -4
View File
@@ -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))
}
+7 -4
View File
@@ -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(),
});