Skip to content

Commit e1e673b

Browse files
committed
API: Refactor "GET /api/v1/config" endpoint for JWT sessions photoprism#5230
Signed-off-by: Michael Mayer <michael@photoprism.app>
1 parent 99cf432 commit e1e673b

File tree

6 files changed

+79
-20
lines changed

6 files changed

+79
-20
lines changed

internal/api/api_auth_jwt.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ func authAnyJWT(c *gin.Context, clientIP, authToken string, resource acl.Resourc
112112
IssuedAt: issuedAt,
113113
NotBefore: notBefore,
114114
ExpiresAt: expiresAt,
115+
PreviewToken: func() string {
116+
if tokenScopes.Contains(acl.ResourceFiles.String()) {
117+
return conf.PreviewToken()
118+
}
119+
return ""
120+
}(),
121+
DownloadToken: func() string {
122+
if tokenScopes.Contains(acl.ResourceFiles.String()) {
123+
return conf.DownloadToken()
124+
}
125+
return ""
126+
}(),
115127
})
116128
}
117129

internal/api/api_auth_jwt_test.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,43 @@ func TestAuthAnyJWT(t *testing.T) {
5353
assert.True(t, strings.HasPrefix(session.AuthID, "jwt"))
5454
assert.Equal(t, session.AuthID, session.RefID)
5555
assert.True(t, rnd.IsRefID(session.RefID))
56-
assert.False(t, session.CreatedAt.IsZero())
57-
assert.False(t, session.UpdatedAt.IsZero())
58-
assert.NotZero(t, session.SessExpires)
59-
assert.Greater(t, session.SessExpires, session.CreatedAt.Unix())
56+
assert.True(t, session.SessExpires > session.CreatedAt.Unix())
6057
assert.GreaterOrEqual(t, session.LastActive, session.CreatedAt.Unix())
6158
assert.True(t, session.GetUser().IsUnknown())
6259
assert.Equal(t, acl.RolePortal, session.GetClientRole())
60+
assert.Empty(t, session.PreviewToken)
61+
assert.Empty(t, session.DownloadToken)
62+
})
63+
t.Run("FilesScopeTokens", func(t *testing.T) {
64+
fx := newPortalJWTFixture(t, "cluster-jwt-files")
65+
spec := fx.defaultClaimsSpec()
66+
spec.Scope = []string{"cluster", "files"}
67+
token := fx.issue(t, spec)
68+
69+
origScope := fx.nodeConf.Options().JWTScope
70+
fx.nodeConf.Options().JWTScope = "cluster vision metrics files"
71+
get.SetConfig(fx.nodeConf)
72+
t.Cleanup(func() {
73+
fx.nodeConf.Options().JWTScope = origScope
74+
get.SetConfig(fx.nodeConf)
75+
})
76+
77+
gin.SetMode(gin.TestMode)
78+
w := httptest.NewRecorder()
79+
c, _ := gin.CreateTestContext(w)
80+
req, _ := http.NewRequest(http.MethodGet, "/api/v1/cluster/theme", nil)
81+
req.Header.Set("Authorization", "Bearer "+token)
82+
req.Header.Set(header.UserAgent, "PhotoPrism Portal/1.0")
83+
req.RemoteAddr = "192.0.2.50:4567"
84+
c.Request = req
85+
86+
session := authAnyJWT(c, "192.0.2.50", token, acl.ResourceFiles, acl.Permissions{acl.AccessLibrary})
87+
require.NotNil(t, session)
88+
assert.Equal(t, http.StatusOK, session.HttpStatus())
89+
assert.Equal(t, fx.preview, session.PreviewToken)
90+
assert.Equal(t, fx.download, session.DownloadToken)
91+
assert.True(t, session.SessExpires > session.CreatedAt.Unix())
92+
assert.True(t, session.LastActive >= session.CreatedAt.Unix())
6393
})
6494
t.Run("ClusterCIDRAllowed", func(t *testing.T) {
6595
fx := newPortalJWTFixture(t, "cluster-jwt-cidr-allow")

internal/api/api_auth_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ type portalJWTFixture struct {
250250
issuer *clusterjwt.Issuer
251251
clusterUUID string
252252
nodeUUID string
253+
preview string
254+
download string
253255
}
254256

255257
func newPortalJWTFixture(t *testing.T, suffix string) portalJWTFixture {
@@ -294,6 +296,8 @@ func newPortalJWTFixture(t *testing.T, suffix string) portalJWTFixture {
294296
issuer: clusterjwt.NewIssuer(mgr),
295297
clusterUUID: clusterUUID,
296298
nodeUUID: nodeUUID,
299+
preview: nodeConf.PreviewToken(),
300+
download: nodeConf.DownloadToken(),
297301
}
298302
}
299303

internal/api/api_client_config.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/gin-gonic/gin"
77

8+
"github.com/photoprism/photoprism/internal/auth/acl"
89
"github.com/photoprism/photoprism/internal/event"
910
"github.com/photoprism/photoprism/internal/photoprism/get"
1011
)
@@ -27,16 +28,12 @@ func UpdateClientConfig() {
2728
// @Router /api/v1/config [get]
2829
func GetClientConfig(router *gin.RouterGroup) {
2930
router.GET("/config", func(c *gin.Context) {
30-
sess := Session(ClientIP(c), AuthToken(c))
3131
conf := get.Config()
3232

33-
// Check authentication.
34-
if sess != nil {
35-
// Return custom client config for authenticated user.
36-
c.JSON(http.StatusOK, conf.ClientSession(sess))
33+
if s := AuthAny(c, acl.ResourceConfig, acl.Permissions{acl.ActionView}); s.Valid() {
34+
c.JSON(http.StatusOK, conf.ClientSession(s))
3735
return
3836
} else if conf.DisableFrontend() {
39-
// Abort if not authenticated, and the web frontend is disabled.
4037
AbortUnauthorized(c)
4138
return
4239
}

internal/entity/auth_session.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,12 +931,18 @@ func (m *Session) UpdateLastActive(save bool) *Session {
931931

932932
// Invalid checks if the session does not belong to a registered user or a visitor with shares.
933933
func (m *Session) Invalid() bool {
934+
if m == nil {
935+
return true
936+
}
937+
934938
return !m.Valid()
935939
}
936940

937941
// Valid checks whether the session belongs to a registered user or a visitor with shares.
938942
func (m *Session) Valid() bool {
939-
if m.IsClient() {
943+
if m == nil {
944+
return false
945+
} else if m.IsClient() {
940946
return true
941947
}
942948

internal/entity/auth_session_jwt.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ import (
1313
// JWT captures the subset of JWT fields needed to construct
1414
// an in-memory session for portal-to-node authentication flows.
1515
type JWT struct {
16-
Token string
17-
ID string
18-
Issuer string
19-
Subject string
20-
Scope string
21-
Audience []string
22-
IssuedAt *time.Time
23-
NotBefore *time.Time
24-
ExpiresAt *time.Time
16+
Token string
17+
ID string
18+
Issuer string
19+
Subject string
20+
Scope string
21+
Audience []string
22+
IssuedAt *time.Time
23+
NotBefore *time.Time
24+
ExpiresAt *time.Time
25+
PreviewToken string
26+
DownloadToken string
2527
}
2628

2729
// NewSessionFromJWT constructs an in-memory session based on verified
@@ -58,6 +60,14 @@ func NewSessionFromJWT(c *gin.Context, jwt *JWT) *Session {
5860
sess.SetClientIP(header.ClientIP(c))
5961
sess.SetUserAgent(header.ClientUserAgent(c))
6062

63+
// Set media preview and download tokens, if specified.
64+
if jwt.PreviewToken != "" {
65+
sess.PreviewToken = jwt.PreviewToken
66+
}
67+
if jwt.DownloadToken != "" {
68+
sess.DownloadToken = jwt.DownloadToken
69+
}
70+
6171
// Derive timestamps from JWT claims when available.
6272
now := time.Now().UTC()
6373
issuedAt := now

0 commit comments

Comments
 (0)