Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
35 changes: 12 additions & 23 deletions internal/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -86,37 +85,27 @@ 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
}

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.
Expand Down
11 changes: 11 additions & 0 deletions internal/server/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading