Skip to content

Commit d307b3f

Browse files
2403905mmattel
andauthored
feat: [OCISDEV-226] Allow wildcards in role assignments (#11663)
* feat: [OCISDEV-226] Allow wildcards in role assignments * Update services/proxy/README.md Co-authored-by: Martin <[email protected]> --------- Co-authored-by: Martin <[email protected]>
1 parent d352f7b commit d307b3f

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

services/proxy/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,35 @@ role_assignment:
205205
This would assign the role `admin` to users with the value `myAdminRole` in the claim `ocisRoles`.
206206
The role `user` to users with the values `myUserRole` in the claims `ocisRoles` and so on.
207207

208+
#### Wildcard/Regex Claim Matching
209+
210+
The `claim_value` supports exact strings and regular expressions to map multiple claim values to a single role. Regexes are matched against the entire claim value (implicit start/end anchors).
211+
212+
Examples:
213+
214+
```yaml
215+
role_assignment:
216+
driver: oidc
217+
oidc_role_mapper:
218+
role_claim: ocisRoles
219+
role_mapping:
220+
# exact match
221+
- role_name: user
222+
claim_value: ocisUser
223+
224+
# regex: match any value starting with "ocis-user-"
225+
- role_name: user-light
226+
claim_value: ocis-user-.*
227+
228+
# regex: single alphanumeric suffix
229+
- role_name: guest
230+
claim_value: ocis-guest-[a-zA-Z0-9]
231+
```
232+
233+
Note: Regex patterns are treated as full matches. Typically you don't need `^` or `$`.
234+
If a `claim_value` is an invalid regex, it only matches claim values that are exactly equal; otherwise it's ignored.
235+
Ordering still applies, and the first matching mapping wins.
236+
208237
Claim values that are not mapped to a specific ownCloud Infinite Scale role will be ignored.
209238

210239
Note: An ownCloud Infinite Scale user can only have a single role assigned. If the configured

services/proxy/pkg/userroles/oidcroles.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package userroles
33
import (
44
"context"
55
"errors"
6+
"regexp"
67
"sync"
78
"time"
89

@@ -69,6 +70,28 @@ func extractRoles(rolesClaim string, claims map[string]interface{}) (map[string]
6970
return claimRoles, nil
7071
}
7172

73+
// matchesClaimMapping returns true if the provided mapping pattern matches at least
74+
// one of the values present in claimRoles. It supports:
75+
// - exact match when ClaimValue is a literal equal to a claim value
76+
// - regex match when ClaimValue is a regex pattern (e.g. "ocis-user-.*")
77+
// The regex is matched against the entire claim value, not a substring.
78+
func matchesClaimMapping(mappingValue string, claimRoles map[string]struct{}) bool {
79+
if _, ok := claimRoles[mappingValue]; ok {
80+
return true
81+
}
82+
83+
rx, err := regexp.Compile("^(?:" + mappingValue + ")$")
84+
if err != nil {
85+
return false
86+
}
87+
for cr := range claimRoles {
88+
if rx.MatchString(cr) {
89+
return true
90+
}
91+
}
92+
return false
93+
}
94+
7295
// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user
7396
// already has a different role assigned.
7497
func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) {
@@ -96,7 +119,7 @@ func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *c
96119
// pick the highest privileged role that matches a value from the claims
97120
roleIDFromClaim := ""
98121
for _, mapping := range ra.Options.roleMapping {
99-
if _, ok := claimRoles[mapping.ClaimValue]; ok {
122+
if matchesClaimMapping(mapping.ClaimValue, claimRoles) {
100123
logger.Debug().Str("ocisRole", mapping.RoleName).Str("role id", roleNamesToRoleIDs[mapping.RoleName]).Msg("first matching role")
101124
roleIDFromClaim = roleNamesToRoleIDs[mapping.RoleName]
102125
break

services/proxy/pkg/userroles/oidcroles_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,41 @@ func TestNoRoles(t *testing.T) {
118118
t.Fatal("length of roles mut be 0")
119119
}
120120
}
121+
122+
func TestMatchesClaimMappingExact(t *testing.T) {
123+
claimRoles := map[string]struct{}{
124+
"ocis-user": {},
125+
}
126+
if !matchesClaimMapping("ocis-user", claimRoles) {
127+
t.Fatal("expected exact match to succeed")
128+
}
129+
if matchesClaimMapping("admin", claimRoles) {
130+
t.Fatal("expected non-matching literal to fail")
131+
}
132+
}
133+
134+
func TestMatchesClaimMappingRegex(t *testing.T) {
135+
claimRoles := map[string]struct{}{
136+
"ocis-user-1": {},
137+
"ocis-user-42": {},
138+
"ocis-user-lth": {},
139+
"admin": {},
140+
}
141+
if !matchesClaimMapping("ocis-user-.*", claimRoles) {
142+
t.Fatal("expected regex match to succeed")
143+
}
144+
if !matchesClaimMapping("ocis-user-[a-zA-Z0-9]", claimRoles) {
145+
t.Fatal("expected regex match to succeed")
146+
}
147+
if matchesClaimMapping("admin-.*", claimRoles) {
148+
t.Fatal("expected regex match to fail for admin-.*")
149+
}
150+
}
151+
152+
func TestMatchesClaimMappingInvalidRegexFallsBackToExact(t *testing.T) {
153+
claimRoles := map[string]struct{}{"ocis-user": {}}
154+
// invalid regex pattern
155+
if matchesClaimMapping("ocis-user[", claimRoles) {
156+
t.Fatal("invalid regex should fall back to exact and not match")
157+
}
158+
}

0 commit comments

Comments
 (0)