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
1 change: 1 addition & 0 deletions oauth/model/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var ValidScopes = map[string]string{
"openid": "Authenticate you and issue an ID token",
"profile": "Read your basic profile (name, username, picture)",
"email": "Read your email address",
"offline_access": "Stay signed in without re-authenticating (refresh token)",
"user:read": "Read user and entity profile information",
"user:write": "Update user profile information",
"groups:read": "Read group memberships",
Expand Down
4 changes: 2 additions & 2 deletions oauth/service/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func BuildIDTokenClaims(entityID string, clientID string, scope string, nonce st
e, _ := fetchOIDCEntity(entityID)
claims := identityClaims(e, scope)
if GroupsClaimAllowed(scope) {
claims["groups"] = FilteredGroups(entityID, clientID)
SetGroupClaims(claims, FilteredGroups(entityID, clientID))
}
claims["auth_time"] = authTime
if nonce != "" {
Expand All @@ -104,7 +104,7 @@ func BuildUserInfoClaims(entityID string, clientID string, scope string) (map[st
}
claims := identityClaims(e, scope)
if GroupsClaimAllowed(scope) {
claims["groups"] = FilteredGroups(entityID, clientID)
SetGroupClaims(claims, FilteredGroups(entityID, clientID))
}
claims["sub"] = entityID
return claims, nil
Expand Down
48 changes: 35 additions & 13 deletions oauth/service/token_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ type idHolder struct {
}

type groupResponse struct {
ID string `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
}

// GroupRef is a group's stable ID and its human-readable name, carried
// together so token claims can expose both.
type GroupRef struct {
ID string
Name string
}

type applicationResponse struct {
Expand Down Expand Up @@ -67,26 +75,40 @@ func BuildTokenClaims(entityID string, clientID string, scope string) map[string
}

if GroupsClaimAllowed(scope) {
claims["groups"] = FilteredGroups(entityID, clientID)
SetGroupClaims(claims, FilteredGroups(entityID, clientID))
}

return claims
}

// SetGroupClaims writes the group claims onto a claim map: `groups` holds the
// human-readable names (what RBAC policies key on) and `group_ids` holds the
// stable ULIDs (for consumers that need rename-safe references).
func SetGroupClaims(claims map[string]interface{}, groups []GroupRef) {
names := make([]string, 0, len(groups))
ids := make([]string, 0, len(groups))
for _, g := range groups {
names = append(names, g.Name)
ids = append(ids, g.ID)
}
claims["groups"] = names
claims["group_ids"] = ids
}

// GroupsClaimAllowed reports whether a token carrying the given scope should
// include the groups claim — granted by the first-party sentinel:all scope or
// the explicit groups:read scope.
func GroupsClaimAllowed(scope string) bool {
return ScopesContain(scope, "sentinel:all") || ScopesContain(scope, "groups:read")
}

// FilteredGroups resolves the group IDs an entity should expose to a given
// FilteredGroups resolves the groups an entity should expose to a given
// client, applying the same per-client visibility rules described on
// BuildTokenClaims: the Sentinel client sees all of the user's groups; any
// other client sees the user's groups intersected with the union of the
// client's linked groups and Sentinel's linked groups (the global default).
func FilteredGroups(entityID string, clientID string) []string {
userGroups := getEntityGroupIDs(entityID)
func FilteredGroups(entityID string, clientID string) []GroupRef {
userGroups := getEntityGroups(entityID)

if isSentinelClient(clientID) {
return userGroups
Expand All @@ -99,9 +121,9 @@ func FilteredGroups(entityID string, clientID string) []string {
for _, link := range getSentinelGroupLinks() {
allowed[link.GroupID] = struct{}{}
}
filtered := make([]string, 0, len(userGroups))
filtered := make([]GroupRef, 0, len(userGroups))
for _, g := range userGroups {
if _, ok := allowed[g]; ok {
if _, ok := allowed[g.ID]; ok {
filtered = append(filtered, g)
}
}
Expand All @@ -123,10 +145,10 @@ func CheckAccessGate(entityID, clientID string) error {
if len(required) == 0 {
return nil
}
userGroups := getEntityGroupIDs(entityID)
userGroups := getEntityGroups(entityID)
user := make(map[string]struct{}, len(userGroups))
for _, g := range userGroups {
user[g] = struct{}{}
user[g.ID] = struct{}{}
}
for _, g := range required {
if _, ok := user[g]; ok {
Expand All @@ -140,17 +162,17 @@ func isSentinelClient(clientID string) bool {
return clientID == config.SentinelClientID
}

func getEntityGroupIDs(entityID string) []string {
func getEntityGroups(entityID string) []GroupRef {
var groups []groupResponse
if err := sentinel.Get("/core/entity/"+entityID+"/groups", &groups); err != nil {
logger.SugarLogger.Errorf("Failed to load groups for entity %s: %v", entityID, err)
return nil
}
ids := make([]string, 0, len(groups))
refs := make([]GroupRef, 0, len(groups))
for _, g := range groups {
ids = append(ids, g.ID)
refs = append(refs, GroupRef{ID: g.ID, Name: g.Name})
}
return ids
return refs
}

func getAppGroupLinks(clientID string) []applicationGroupLink {
Expand Down
6 changes: 6 additions & 0 deletions web/src/lib/scopes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AppWindow,
Clock,
IdCard,
KeyRound,
Mail,
Expand Down Expand Up @@ -32,6 +33,11 @@ const SCOPES: Record<string, ScopeMeta> = {
description: "See the email address on your account.",
icon: Mail,
},
offline_access: {
label: "Stay signed in",
description: "Keep you signed in without re-authenticating.",
icon: Clock,
},
"user:read": {
label: "Read your profile",
description: "See your name, email, entity ID, and basic account info.",
Expand Down
Loading