diff --git a/go.mod b/go.mod index 7feb3e6..fad0379 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/mux v1.8.1 github.com/k8shell-io/api-server v0.15.0 - github.com/k8shell-io/common v0.29.4 + github.com/k8shell-io/common v0.30.7 github.com/pkg/sftp v1.13.10 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 @@ -29,6 +29,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index 1adba49..7bf2be2 100644 --- a/go.sum +++ b/go.sum @@ -45,10 +45,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/k8shell-io/api-server v0.15.0 h1:FyBgblQAE5FJfPLDf+0khW9zESxCf5WrwzMtPH8hoW8= github.com/k8shell-io/api-server v0.15.0/go.mod h1:OIUI93twcGf0qQVdWCRTK+rkDFYLhTnkC+SDxfa5AqU= -github.com/k8shell-io/common v0.21.0 h1:EOwaQOFnHQJsHcDLDVEAwNnJJe7uvQCqcOiI6BTu3GE= -github.com/k8shell-io/common v0.21.0/go.mod h1:E8dsb9ta4v3ne61AJgtRyTTbTkMMmKeCMAcXD+/9+cY= -github.com/k8shell-io/common v0.29.4 h1:patjuhCWs3g/JVNmx7SVa+wgvu46Z5RYAkD7YY6A4tY= -github.com/k8shell-io/common v0.29.4/go.mod h1:E8dsb9ta4v3ne61AJgtRyTTbTkMMmKeCMAcXD+/9+cY= +github.com/k8shell-io/common v0.30.7 h1:rraXjV+njEThfdzPRQ7fR7o0W53yZ3/KjMu2s5ldJNM= +github.com/k8shell-io/common v0.30.7/go.mod h1:40c5GkpS7Y0/aOFa37Lq8z/mLUn3k3GV/AHtFJFL28k= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/internal/models/user.go b/internal/models/user.go index 88e5fb6..5114560 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -36,9 +36,8 @@ type User struct { groups []Group // Mutable — replaced atomically on token renewal; requires mu. - claims *authz.UserClaims - userToken string - previousToken string + claims *authz.UserClaims + userToken string } // NewUser creates a User from a verified JWT claims set and the raw token string. @@ -86,12 +85,7 @@ func (u *User) Update(claims *authz.UserClaims, token string) (bool, error) { if claims.Source != u.claims.Source { return false, fmt.Errorf("cannot update user source from %s to %s", u.claims.Source, claims.Source) } - if claims.UID != u.uid || claims.GID != u.gid { - return false, fmt.Errorf("cannot update user UID/GID from %d/%d to %d/%d", u.uid, u.gid, claims.UID, claims.GID) - } - u.claims = claims - u.previousToken = u.userToken u.userToken = token return true, nil } @@ -99,24 +93,19 @@ func (u *User) Update(claims *authz.UserClaims, token string) (bool, error) { func (u *User) TokenEqual(token string) bool { u.mu.RLock() defer u.mu.RUnlock() - eq := token == u.userToken - if !eq { - // token was verified before calling TokenEqual - claims1, err1 := authz.ParseUnverifiedClaims(token, true) - if err1 != nil { - return false - } - claims2, err2 := authz.ParseUnverifiedClaims(u.previousToken, false) - if err2 != nil { - return false - } - // previous token might be expired, but if the claims match then we can consider it equal - eq = claims1.Subject == claims2.Subject && claims1.Source == claims2.Source && - claims1.UID == claims2.UID && claims1.GID == claims2.GID + if token == u.userToken { + return true } - return eq + // The caller's token was already verified (signature + expiry) by the interceptor. + // Accept any valid token whose Subject+Source match the workspace identity. + // Both fields are immutable: Subject is set in NewUser; Source is validated in Update. + claims, err := authz.ParseUnverifiedClaims(token, true) + if err != nil { + return false + } + return claims.Subject == u.username && claims.Source == u.claims.Source } // HasRole checks if the user has a specific role. diff --git a/internal/server/identity.go b/internal/server/identity.go index 8d8c971..768f240 100644 --- a/internal/server/identity.go +++ b/internal/server/identity.go @@ -158,6 +158,17 @@ func (s *Server) renewIdentityTokenIfNeeded(ctx context.Context) error { return fmt.Errorf("verify token: %w", err) } + if !s.uidGIDMismatchWarned { + snap := s.user.ClaimsSnapshot() + if claims.UID != s.user.GetUID() || claims.GID != s.user.GetGID() { + s.logger.Warn().Msgf( + "Refreshed token has different UID/GID (%d/%d → %d/%d); keeping existing OS identity, not changing workspace user", + snap.UID, snap.GID, claims.UID, claims.GID, + ) + s.uidGIDMismatchWarned = true + } + } + _, err = s.user.Update(claims, tokenStr) if err != nil { return fmt.Errorf("update user from refresh token: %w", err) diff --git a/internal/server/server.go b/internal/server/server.go index 6e1409a..e9ea8f6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -52,6 +52,8 @@ type Server struct { appManager *apps.AppManager jwtVerifier *authz.JWTVerifier initTracker *models.InitTracker + + uidGIDMismatchWarned bool } func NewServer(cfg *config.Config, restApiUnixSocketPath string, testMode bool) (*Server, error) {