diff --git a/admin/server/deployment.go b/admin/server/deployment.go index 0fb10195fea..b52da93d184 100644 --- a/admin/server/deployment.go +++ b/admin/server/deployment.go @@ -217,6 +217,7 @@ func (s *Server) GetDeployment(ctx context.Context, req *adminv1.GetDeploymentRe project: proj, deployment: depl, projectPermissions: permissions, + omitServiceSubject: true, externalUserID: req.ExternalUserId, ttl: ttlDuration, } @@ -558,6 +559,7 @@ func (s *Server) GetDeploymentCredentials(ctx context.Context, req *adminv1.GetD project: proj, deployment: prodDepl, projectPermissions: permissions, + omitServiceSubject: true, externalUserID: req.ExternalUserId, ttl: ttlDuration, } @@ -662,6 +664,7 @@ func (s *Server) GetIFrame(ctx context.Context, req *adminv1.GetIFrameRequest) ( project: proj, deployment: prodDepl, projectPermissions: permissions, + omitServiceSubject: true, externalUserID: req.ExternalUserId, extraAttributes: map[string]any{"embed": true}, overrideResources: overrideResources, diff --git a/admin/server/deployment_test.go b/admin/server/deployment_test.go index 6b4053fc780..a00e3fcc72f 100644 --- a/admin/server/deployment_test.go +++ b/admin/server/deployment_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/golang-jwt/jwt/v4" + "github.com/rilldata/rill/admin/database" "github.com/rilldata/rill/admin/testadmin" "github.com/rilldata/rill/cli/testcli" adminv1 "github.com/rilldata/rill/proto/gen/rill/admin/v1" @@ -131,6 +132,52 @@ func TestDeploymentJWTs(t *testing.T) { require.NoError(t, err) require.Equal(t, c.Subject, decodeJWT(t, resp2.AccessToken).Subject) }) + + t.Run("Service tokens without for", func(t *testing.T) { + svc, err := u1Client.CreateService(t.Context(), &adminv1.CreateServiceRequest{ + Name: "jwt-service", + Org: "jwt-test", + Project: "jwt-project", + ProjectRoleName: database.ProjectRoleNameAdmin, + }) + require.NoError(t, err) + tok, err := u1Client.IssueServiceAuthToken(t.Context(), &adminv1.IssueServiceAuthTokenRequest{ + Org: "jwt-test", + ServiceName: svc.Service.Name, + }) + require.NoError(t, err) + svcClient := adm.NewClient(t, tok.Token) + + // GetProject keeps the service as the subject. + proj, err := svcClient.GetProject(t.Context(), &adminv1.GetProjectRequest{ + Org: "jwt-test", + Project: "jwt-project", + }) + require.NoError(t, err) + require.Equal(t, svc.Service.Id, decodeJWT(t, proj.Jwt).Subject) + + // Endpoints commonly used to issue tokens for end users must not share the service subject across them. + depl, err := svcClient.GetDeployment(t.Context(), &adminv1.GetDeploymentRequest{ + DeploymentId: depl.ID, + }) + require.NoError(t, err) + require.Empty(t, decodeJWT(t, depl.AccessToken).Subject) + + creds, err := svcClient.GetDeploymentCredentials(t.Context(), &adminv1.GetDeploymentCredentialsRequest{ + Org: "jwt-test", + Project: "jwt-project", + }) + require.NoError(t, err) + require.Empty(t, decodeJWT(t, creds.AccessToken).Subject) + + iframe, err := svcClient.GetIFrame(t.Context(), &adminv1.GetIFrameRequest{ + Org: "jwt-test", + Project: "jwt-project", + Navigation: true, + }) + require.NoError(t, err) + require.Empty(t, decodeJWT(t, iframe.AccessToken).Subject) + }) } // jwtPayload mirrors the subset of the runtime JWT payload that the assertions touch. diff --git a/admin/server/runtime_jwt.go b/admin/server/runtime_jwt.go index 0c827262ada..02539f389fa 100644 --- a/admin/server/runtime_jwt.go +++ b/admin/server/runtime_jwt.go @@ -26,6 +26,10 @@ type issueRuntimeTokenOptions struct { projectPermissions *adminv1.ProjectPermissions // forOwner issues the token for the current claims owner (user/service/anon/etc). forOwner bool + // omitServiceSubject leaves the subject empty when forOwner is set and the owner is a service. + // Some users use service accounts to issue tokens for end users without specifying who they are; + // a shared subject across those users could leak per-user state, such as AI chats. + omitServiceSubject bool // forUserID issues the token for a specific Rill user. Mutually exclusive with the other for* fields. forUserID string // forUserEmail issues the token for a user by email. @@ -104,7 +108,9 @@ func (s *Server) issueRuntimeToken(ctx context.Context, opts *issueRuntimeTokenO } rules = append(rules, securityRulesFromResources(restrict, resources)...) case auth.OwnerTypeService: - subject = claims.OwnerID() + if !opts.omitServiceSubject { + subject = claims.OwnerID() + } a, err := s.jwtAttributesForService(ctx, claims.OwnerID(), opts.projectPermissions) if err != nil { return "", err