Parse role claims (#7713)
* extract and test role claim parsing Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add failing test Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * read segmented roles claim as array and string Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * reuse more code by extracting WalkSegments Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add TestSplitWithEscaping Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * docs and error for unhandled case Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add claims test Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add missing ReadStringClaim docs Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> --------- Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Iss = "iss"
|
||||
Sub = "sub"
|
||||
@@ -12,3 +17,60 @@ const (
|
||||
OwncloudUUID = "ownclouduuid"
|
||||
OcisRoutingPolicy = "ocis.routing.policy"
|
||||
)
|
||||
|
||||
// SplitWithEscaping splits s into segments using separator which can be escaped using the escape string
|
||||
// See https://codereview.stackexchange.com/a/280193
|
||||
func SplitWithEscaping(s string, separator string, escapeString string) []string {
|
||||
a := strings.Split(s, separator)
|
||||
|
||||
for i := len(a) - 2; i >= 0; i-- {
|
||||
if strings.HasSuffix(a[i], escapeString) {
|
||||
a[i] = a[i][:len(a[i])-len(escapeString)] + separator + a[i+1]
|
||||
a = append(a[:i+1], a[i+2:]...)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// WalkSegments uses the given array of segments to walk the claims and return whatever interface was found
|
||||
func WalkSegments(segments []string, claims map[string]interface{}) (interface{}, error) {
|
||||
i := 0
|
||||
for ; i < len(segments)-1; i++ {
|
||||
switch castedClaims := claims[segments[i]].(type) {
|
||||
case map[string]interface{}:
|
||||
claims = castedClaims
|
||||
case map[interface{}]interface{}:
|
||||
claims = make(map[string]interface{}, len(castedClaims))
|
||||
for k, v := range castedClaims {
|
||||
if s, ok := k.(string); ok {
|
||||
claims[s] = v
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not walk claims path, key '%v' is not a string", k)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type '%v'", castedClaims)
|
||||
}
|
||||
}
|
||||
return claims[segments[i]], nil
|
||||
}
|
||||
|
||||
// ReadStringClaim returns the string obtained by following the . seperated path in the claims
|
||||
func ReadStringClaim(path string, claims map[string]interface{}) (string, error) {
|
||||
// check the simple case first
|
||||
value, _ := claims[path].(string)
|
||||
if value != "" {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
claim, err := WalkSegments(SplitWithEscaping(path, ".", "\\"), claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if value, _ = claim.(string); value != "" {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return value, fmt.Errorf("claim path '%s' not set or empty", path)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
)
|
||||
|
||||
type splitWithEscapingTest struct {
|
||||
// Name of the subtest.
|
||||
name string
|
||||
|
||||
// string to split
|
||||
s string
|
||||
|
||||
// seperator to use
|
||||
seperator string
|
||||
|
||||
// escape character to use for escaping
|
||||
escape string
|
||||
|
||||
expectedParts []string
|
||||
}
|
||||
|
||||
func (swet splitWithEscapingTest) run(t *testing.T) {
|
||||
parts := oidc.SplitWithEscaping(swet.s, swet.seperator, swet.escape)
|
||||
if len(swet.expectedParts) != len(parts) {
|
||||
t.Errorf("mismatching length")
|
||||
}
|
||||
for i, v := range swet.expectedParts {
|
||||
if parts[i] != v {
|
||||
t.Errorf("expected part %d to be '%s', got '%s'", i, v, parts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitWithEscaping(t *testing.T) {
|
||||
tests := []splitWithEscapingTest{
|
||||
{
|
||||
name: "plain claim name",
|
||||
s: "roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with .",
|
||||
s: "my.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my", "roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with escaped .",
|
||||
s: "my\\.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my.roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with escaped . left",
|
||||
s: "my\\.other.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my.other", "roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with escaped . right",
|
||||
s: "my.other\\.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my", "other.roles"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
type walkSegmentsTest struct {
|
||||
// Name of the subtest.
|
||||
name string
|
||||
|
||||
// path segments to walk
|
||||
segments []string
|
||||
|
||||
// seperator to use
|
||||
claims map[string]interface{}
|
||||
|
||||
expected interface{}
|
||||
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
func (wst walkSegmentsTest) run(t *testing.T) {
|
||||
v, err := oidc.WalkSegments(wst.segments, wst.claims)
|
||||
if err != nil && !wst.wantErr {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if err == nil && wst.wantErr {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
if !reflect.DeepEqual(v, wst.expected) {
|
||||
t.Errorf("expected %v got %v", wst.expected, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkSegments(t *testing.T) {
|
||||
byt := []byte(`{"first":{"second":{"third":["value1","value2"]},"foo":"bar"},"fizz":"buzz"}`)
|
||||
var dat map[string]interface{}
|
||||
if err := json.Unmarshal(byt, &dat); err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
|
||||
tests := []walkSegmentsTest{
|
||||
{
|
||||
name: "one segment, single value",
|
||||
segments: []string{"first"},
|
||||
claims: map[string]interface{}{
|
||||
"first": "value",
|
||||
},
|
||||
expected: "value",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "one segment, array value",
|
||||
segments: []string{"first"},
|
||||
claims: map[string]interface{}{
|
||||
"first": []string{"value1", "value2"},
|
||||
},
|
||||
expected: []string{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "two segments, single value",
|
||||
segments: []string{"first", "second"},
|
||||
claims: map[string]interface{}{
|
||||
"first": map[string]interface{}{
|
||||
"second": "value",
|
||||
},
|
||||
},
|
||||
expected: "value",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "two segments, array value",
|
||||
segments: []string{"first", "second"},
|
||||
claims: map[string]interface{}{
|
||||
"first": map[string]interface{}{
|
||||
"second": []string{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
expected: []string{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "three segments, array value from json",
|
||||
segments: []string{"first", "second", "third"},
|
||||
claims: dat,
|
||||
expected: []interface{}{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "three segments, array value with interface key",
|
||||
segments: []string{"first", "second", "third"},
|
||||
claims: map[string]interface{}{
|
||||
"first": map[interface{}]interface{}{
|
||||
"second": map[interface{}]interface{}{
|
||||
"third": []string{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user