mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
API: Refactor "GET /api/v1/config" endpoint for JWT sessions #5230
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -112,6 +112,18 @@ func authAnyJWT(c *gin.Context, clientIP, authToken string, resource acl.Resourc
|
||||
IssuedAt: issuedAt,
|
||||
NotBefore: notBefore,
|
||||
ExpiresAt: expiresAt,
|
||||
PreviewToken: func() string {
|
||||
if tokenScopes.Contains(acl.ResourceFiles.String()) {
|
||||
return conf.PreviewToken()
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
DownloadToken: func() string {
|
||||
if tokenScopes.Contains(acl.ResourceFiles.String()) {
|
||||
return conf.DownloadToken()
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,43 @@ func TestAuthAnyJWT(t *testing.T) {
|
||||
assert.True(t, strings.HasPrefix(session.AuthID, "jwt"))
|
||||
assert.Equal(t, session.AuthID, session.RefID)
|
||||
assert.True(t, rnd.IsRefID(session.RefID))
|
||||
assert.False(t, session.CreatedAt.IsZero())
|
||||
assert.False(t, session.UpdatedAt.IsZero())
|
||||
assert.NotZero(t, session.SessExpires)
|
||||
assert.Greater(t, session.SessExpires, session.CreatedAt.Unix())
|
||||
assert.True(t, session.SessExpires > session.CreatedAt.Unix())
|
||||
assert.GreaterOrEqual(t, session.LastActive, session.CreatedAt.Unix())
|
||||
assert.True(t, session.GetUser().IsUnknown())
|
||||
assert.Equal(t, acl.RolePortal, session.GetClientRole())
|
||||
assert.Empty(t, session.PreviewToken)
|
||||
assert.Empty(t, session.DownloadToken)
|
||||
})
|
||||
t.Run("FilesScopeTokens", func(t *testing.T) {
|
||||
fx := newPortalJWTFixture(t, "cluster-jwt-files")
|
||||
spec := fx.defaultClaimsSpec()
|
||||
spec.Scope = []string{"cluster", "files"}
|
||||
token := fx.issue(t, spec)
|
||||
|
||||
origScope := fx.nodeConf.Options().JWTScope
|
||||
fx.nodeConf.Options().JWTScope = "cluster vision metrics files"
|
||||
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/cluster/theme", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set(header.UserAgent, "PhotoPrism Portal/1.0")
|
||||
req.RemoteAddr = "192.0.2.50:4567"
|
||||
c.Request = req
|
||||
|
||||
session := authAnyJWT(c, "192.0.2.50", token, acl.ResourceFiles, acl.Permissions{acl.AccessLibrary})
|
||||
require.NotNil(t, session)
|
||||
assert.Equal(t, http.StatusOK, session.HttpStatus())
|
||||
assert.Equal(t, fx.preview, session.PreviewToken)
|
||||
assert.Equal(t, fx.download, session.DownloadToken)
|
||||
assert.True(t, session.SessExpires > session.CreatedAt.Unix())
|
||||
assert.True(t, session.LastActive >= session.CreatedAt.Unix())
|
||||
})
|
||||
t.Run("ClusterCIDRAllowed", func(t *testing.T) {
|
||||
fx := newPortalJWTFixture(t, "cluster-jwt-cidr-allow")
|
||||
|
||||
@@ -250,6 +250,8 @@ type portalJWTFixture struct {
|
||||
issuer *clusterjwt.Issuer
|
||||
clusterUUID string
|
||||
nodeUUID string
|
||||
preview string
|
||||
download string
|
||||
}
|
||||
|
||||
func newPortalJWTFixture(t *testing.T, suffix string) portalJWTFixture {
|
||||
@@ -294,6 +296,8 @@ func newPortalJWTFixture(t *testing.T, suffix string) portalJWTFixture {
|
||||
issuer: clusterjwt.NewIssuer(mgr),
|
||||
clusterUUID: clusterUUID,
|
||||
nodeUUID: nodeUUID,
|
||||
preview: nodeConf.PreviewToken(),
|
||||
download: nodeConf.DownloadToken(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
)
|
||||
@@ -27,16 +28,12 @@ func UpdateClientConfig() {
|
||||
// @Router /api/v1/config [get]
|
||||
func GetClientConfig(router *gin.RouterGroup) {
|
||||
router.GET("/config", func(c *gin.Context) {
|
||||
sess := Session(ClientIP(c), AuthToken(c))
|
||||
conf := get.Config()
|
||||
|
||||
// Check authentication.
|
||||
if sess != nil {
|
||||
// Return custom client config for authenticated user.
|
||||
c.JSON(http.StatusOK, conf.ClientSession(sess))
|
||||
if s := AuthAny(c, acl.ResourceConfig, acl.Permissions{acl.ActionView}); s.Valid() {
|
||||
c.JSON(http.StatusOK, conf.ClientSession(s))
|
||||
return
|
||||
} else if conf.DisableFrontend() {
|
||||
// Abort if not authenticated, and the web frontend is disabled.
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -931,12 +931,18 @@ func (m *Session) UpdateLastActive(save bool) *Session {
|
||||
|
||||
// Invalid checks if the session does not belong to a registered user or a visitor with shares.
|
||||
func (m *Session) Invalid() bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return !m.Valid()
|
||||
}
|
||||
|
||||
// Valid checks whether the session belongs to a registered user or a visitor with shares.
|
||||
func (m *Session) Valid() bool {
|
||||
if m.IsClient() {
|
||||
if m == nil {
|
||||
return false
|
||||
} else if m.IsClient() {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ type JWT struct {
|
||||
IssuedAt *time.Time
|
||||
NotBefore *time.Time
|
||||
ExpiresAt *time.Time
|
||||
PreviewToken string
|
||||
DownloadToken string
|
||||
}
|
||||
|
||||
// NewSessionFromJWT constructs an in-memory session based on verified
|
||||
@@ -58,6 +60,14 @@ func NewSessionFromJWT(c *gin.Context, jwt *JWT) *Session {
|
||||
sess.SetClientIP(header.ClientIP(c))
|
||||
sess.SetUserAgent(header.ClientUserAgent(c))
|
||||
|
||||
// Set media preview and download tokens, if specified.
|
||||
if jwt.PreviewToken != "" {
|
||||
sess.PreviewToken = jwt.PreviewToken
|
||||
}
|
||||
if jwt.DownloadToken != "" {
|
||||
sess.DownloadToken = jwt.DownloadToken
|
||||
}
|
||||
|
||||
// Derive timestamps from JWT claims when available.
|
||||
now := time.Now().UTC()
|
||||
issuedAt := now
|
||||
|
||||
Reference in New Issue
Block a user