Auth: Adjust JWT default scope and ACL, add tests #5230

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2025-10-29 14:28:26 +01:00
parent e1e673be7f
commit 6e43f14476
6 changed files with 66 additions and 2 deletions

View file

@ -91,6 +91,39 @@ func TestAuthAnyJWT(t *testing.T) {
assert.True(t, session.SessExpires > session.CreatedAt.Unix())
assert.True(t, session.LastActive >= session.CreatedAt.Unix())
})
t.Run("ConfigScopePortalRole", func(t *testing.T) {
fx := newPortalJWTFixture(t, "cluster-jwt-config")
spec := fx.defaultClaimsSpec()
spec.Scope = []string{"cluster", "config"}
token := fx.issue(t, spec)
origScope := fx.nodeConf.Options().JWTScope
fx.nodeConf.Options().JWTScope = "cluster config"
get.SetConfig(fx.nodeConf)
t.Cleanup(func() {
fx.nodeConf.Options().JWTScope = origScope
get.SetConfig(fx.nodeConf)
})
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/config", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set(header.UserAgent, "PhotoPrism Portal/1.0")
req.RemoteAddr = "192.0.2.51:1234"
c.Request = req
session := authAnyJWT(c, "192.0.2.51", token, acl.ResourceConfig, acl.Permissions{acl.ActionView})
require.NotNil(t, session)
assert.Equal(t, http.StatusOK, session.HttpStatus())
assert.Equal(t, acl.RolePortal, session.GetClientRole())
assert.True(t, session.Valid())
cfg := fx.nodeConf.ClientSession(session)
require.NotNil(t, cfg)
assert.Equal(t, string(config.ClientUser), cfg.Mode)
})
t.Run("ClusterCIDRAllowed", func(t *testing.T) {
fx := newPortalJWTFixture(t, "cluster-jwt-cidr-allow")
spec := fx.defaultClaimsSpec()

View file

@ -2,12 +2,15 @@ package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/http/header"
)
func TestGetClientConfig(t *testing.T) {
@ -39,4 +42,29 @@ func TestGetClientConfig(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, r.Code)
conf.Options().DisableFrontend = false
})
t.Run("PortalJWT", func(t *testing.T) {
fx := newPortalJWTFixture(t, "client-config-handler")
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
GetClientConfig(router)
spec := fx.defaultClaimsSpec()
spec.Scope = []string{acl.ResourceCluster.String(), acl.ResourceConfig.String()}
token := fx.issue(t, spec)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/config", nil)
req.RemoteAddr = "10.10.0.5:1234"
header.SetAuthorization(req, token)
req.Header.Set(header.UserAgent, "PhotoPrism Portal/1.0")
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "user", gjson.Get(w.Body.String(), "mode").String())
})
}

View file

@ -58,6 +58,7 @@ var Rules = ACL{
},
ResourceConfig: Roles{
RoleAdmin: GrantFullAccess,
RolePortal: GrantFullAccess,
RoleClient: GrantViewOwn,
RoleDefault: GrantViewOwn,
},

View file

@ -23,7 +23,7 @@ import (
// DefaultPortalUrl specifies the default portal URL with variable cluster domain.
var DefaultPortalUrl = "https://portal.${PHOTOPRISM_CLUSTER_DOMAIN}"
var DefaultNodeRole = cluster.RoleInstance
var DefaultJWTAllowedScopes = "cluster vision metrics"
var DefaultJWTAllowedScopes = "config cluster vision metrics"
// ClusterDomain returns the cluster DOMAIN (lowercase DNS name; 163 chars).
func (c *Config) ClusterDomain() string {

View file

@ -181,7 +181,7 @@ func TestConfig_Cluster(t *testing.T) {
c.options.JWTScope = "cluster vision"
assert.Equal(t, list.ParseAttr("cluster vision"), c.JWTAllowedScopes())
c.options.JWTScope = ""
assert.Equal(t, list.ParseAttr("cluster vision metrics"), c.JWTAllowedScopes())
assert.Equal(t, list.ParseAttr("config cluster vision metrics"), c.JWTAllowedScopes())
})
t.Run("Paths", func(t *testing.T) {
c := NewConfig(CliTestContext())

View file

@ -180,6 +180,8 @@ func NewTestOptionsForPath(dbName, dataPath string) *Options {
DatabaseDriver: driver,
DatabaseDSN: dsn,
AdminPassword: "photoprism",
ClusterCIDR: "",
JWTScope: DefaultJWTAllowedScopes,
OriginalsLimit: 66,
ResolutionLimit: 33,
VisionApi: true,