package main import ( "net/http" "path/filepath" "strings" "github.com/gin-gonic/gin" ) // requireSession is the standard auth shim every mutating handler wears. // We don't store a shared service credential — the caller's X-Auth-Token // is the only authority, and we probe PhotoPrism with it before doing any // destructive work. The handler reads the validated token off the context // via ctxToken so it can keep forwarding it to PhotoPrism for the actual // operation. The resolved username is available via ctxUserName. func requireSession(pp *ppClient) gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("X-Auth-Token") if token == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "no token"}) return } user := pp.resolveSession(c.Request.Context(), token) if user == nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) return } c.Set("token", token) c.Set("userName", user.UserName) c.Set("userUID", user.UserUID) c.Set("basePath", user.BasePath) c.Next() } } // ctxToken returns the validated X-Auth-Token a previous requireSession // middleware stored on the request. Handlers MUST run behind that // middleware; otherwise this returns the empty string. func ctxToken(c *gin.Context) string { v, ok := c.Get("token") if !ok { return "" } s, ok := v.(string) if !ok { return "" } return s } // ctxUserName returns the PhotoPrism username resolved by requireSession. func ctxUserName(c *gin.Context) string { v, ok := c.Get("userName") if !ok { return "" } s, ok := v.(string) if !ok { return "" } return s } // ctxUserUID returns the PhotoPrism user UID resolved by requireSession. func ctxUserUID(c *gin.Context) string { v, ok := c.Get("userUID") if !ok { return "" } s, ok := v.(string) if !ok { return "" } return s } // ctxBasePath returns the PhotoPrism user BasePath resolved by requireSession. func ctxBasePath(c *gin.Context) string { v, ok := c.Get("basePath") if !ok { return "" } s, ok := v.(string) if !ok { return "" } return s } // userScopeRoot returns the absolute directory the session may mutate: // ORIGINALS_ROOT/ for scoped users, the whole originals root for // admins (empty BasePath). Lexical join only — callers compare it against // paths built the same way from cfg.OriginalsRoot. func userScopeRoot(c *gin.Context, cfg *Config) string { base := strings.Trim(ctxBasePath(c), "/") if base == "" { return cfg.OriginalsRoot } return filepath.Join(cfg.OriginalsRoot, base) } // requireUserScope guards an already-root-resolved absolute path against // the caller's BasePath. PhotoPrism scopes what a session can *see* by // BasePath, but the sidecar's filesystem endpoints accept raw paths, so // every mutation must re-check that boundary here. `strict` additionally // rejects the scope root itself — renaming/deleting/moving the user's own // base folder would detach their library from auth_users.base_path. // Writes the 403 response and returns false when out of bounds. func requireUserScope(c *gin.Context, cfg *Config, abs string, strict bool) bool { scope := userScopeRoot(c, cfg) if !sameOrUnder(abs, scope) { c.JSON(http.StatusForbidden, gin.H{"error": "path outside your library"}) return false } if strict && abs == scope { c.JSON(http.StatusForbidden, gin.H{"error": "cannot modify your library root"}) return false } return true }