Skip to content
Open
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ require (
github.com/jellydator/ttlcache/v2 v2.11.1
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/jinzhu/now v1.1.5
github.com/jmespath-community/go-jmespath v1.1.1
github.com/justinas/alice v1.2.0
github.com/kovidgoyal/imaging v1.8.20
github.com/leonelquinteros/gotext v1.7.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,8 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4=
github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
Expand Down
62 changes: 0 additions & 62 deletions pkg/oidc/claims.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package oidc

import (
"fmt"
"strings"
)

const (
Iss = "iss"
Sub = "sub"
Expand All @@ -17,60 +12,3 @@ const (
OpenCloudUUID = "openclouduuid"
OpenCloudRoutingPolicy = "opencloud.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)
}
182 changes: 0 additions & 182 deletions pkg/oidc/claims_test.go

This file was deleted.

22 changes: 15 additions & 7 deletions services/proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,13 @@ The name of an OIDC claim whose value should be used to maintain a user's group
membership. The claim value should contain a list of group names the user should
be a member of. Defaults to `groups`.
* `PROXY_USER_OIDC_CLAIM`\
When resolving and authenticated OIDC user, the value of this claims is used to
lookup the user in the users service. For auto provisioning setups this usually is the
same claims as set via `PROXY_AUTOPROVISION_CLAIM_USERNAME`.
When resolving an authenticated OIDC user, this JMESPath expression is evaluated
against the token claims to extract the user identifier used to look up the user
in the users service. Simple claim names such as `sub`, `email` or
`preferred_username` work as-is. Nested claims can be accessed with dot notation,
e.g. `identity.username`. Claim keys that contain a literal dot must use
JMESPath quoted identifier syntax, e.g. `"claim.name"`. For auto provisioning
setups this is usually the same claim as `PROXY_AUTOPROVISION_CLAIM_USERNAME`.
* `PROXY_USER_CS3_CLAIM`\
This is the name of the user attribute in OpenCloud that is used to lookup the user by the
value of the `PROXY_USER_OIDC_CLAIM`. For auto provisioning setups this usually
Expand Down Expand Up @@ -177,10 +181,14 @@ get the role 'user' assigned. (This is also the default behavior if `PROXY_ROLE_
is unset.

When `PROXY_ROLE_ASSIGNMENT_DRIVER` is set to `oidc` the role assignment for a user will happen
based on the values of an OpenID Connect Claim of that user. The name of the OpenID Connect Claim to
be used for the role assignment can be configured via the `PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM`
environment variable. It is also possible to define a mapping of claim values to role names defined
in OpenCloud via a `yaml` configuration. See the following `proxy.yaml` snippet for an example.
based on the values of an OIDC claim of that user. The claim to use is configured via
`PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM`, which accepts a JMESPath expression evaluated against
the token claims. Simple claim names such as `roles` work as-is. Nested claims can be
accessed with dot notation, e.g. `realm_access.roles`. Claim keys that contain a literal
dot must use JMESPath quoted identifier syntax, e.g. `"claim.name"`. The claim value may
be a single string or an array of strings. It is also possible to define a mapping of
claim values to role names defined in OpenCloud via a `yaml` configuration. See the
following `proxy.yaml` snippet for an example.

```yaml
role_assignment:
Expand Down
4 changes: 2 additions & 2 deletions services/proxy/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Config struct {
PolicySelector *PolicySelector `yaml:"policy_selector"`
PreSignedURL PreSignedURL `yaml:"pre_signed_url"`
AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here." introductionVersion:"1.0.0"`
UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_username' but you can also add your own claim." introductionVersion:"1.0.0"`
UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"A JMESPath expression to extract the user identifier from the OIDC token claims. Simple claim names such as 'email' or 'preferred_username' work as-is. Nested claims can be accessed with dot notation, e.g. 'identity.username'. Claim keys that contain a literal dot must use JMESPath quoted identifier syntax, e.g. '\"claim.name\"'. The extracted value must be unique, stable and non re-assignable for the lifetime of the user." introductionVersion:"1.0.0"`
UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'." introductionVersion:"1.0.0"`
TenantOIDCClaim string `yaml:"tenant_oidc_claim" env:"PROXY_TENANT_OIDC_CLAIM" desc:"JMESPath expression to extract the tenant ID from the OIDC token claims. When set, the extracted value is verified against the tenant ID returned by the user backend, rejecting requests where they do not match. Only relevant when multi-tenancy is enabled." introductionVersion:"%%NEXT%%"`
TenantIDMappingEnabled bool `yaml:"tenant_id_mapping_enabled" env:"PROXY_TENANT_ID_MAPPING_ENABLED" desc:"When set to 'true', the proxy will resolve the internal tenant ID from the external tenant ID provided in the OIDC claims by calling the TenantAPI before verifying the tenant. Use this when the external tenant ID in the OIDC token differs from the internal tenant ID stored on the user. Requires 'tenant_oidc_claim' to be set. Only relevant when multi-tenancy is enabled." introductionVersion:"%%NEXT%%"`
Expand Down Expand Up @@ -150,7 +150,7 @@ type RoleAssignment struct {

// OIDCRoleMapper contains the configuration for the "oidc" role assignment driver
type OIDCRoleMapper struct {
RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment." introductionVersion:"1.0.0"`
RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"A JMESPath expression to extract the role value(s) from the OIDC token claims. Simple claim names such as 'roles' work as-is. Nested claims can be accessed with dot notation, e.g. 'realm_access.roles'. Claim keys that contain a literal dot must use JMESPath quoted identifier syntax, e.g. '\"claim.name\"'. The extracted value may be a string or an array of strings." introductionVersion:"1.0.0"`
RolesMap []RoleMapping `yaml:"role_mapping" desc:"A list of mappings of OpenCloud role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."`
}

Expand Down
38 changes: 9 additions & 29 deletions services/proxy/pkg/middleware/account_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import (
"net/http"
"time"

"github.com/jellydator/ttlcache/v3"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
tenantpb "github.com/cs3org/go-cs3apis/cs3/identity/tenant/v1beta1"
rpcpb "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/jellydator/ttlcache/v3"
jmespath "github.com/jmespath-community/go-jmespath"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/router"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/userroles"
Expand Down Expand Up @@ -92,38 +93,17 @@ type accountResolver struct {
}

func readStringClaim(path string, claims map[string]interface{}) (string, error) {
// happy path
value, _ := claims[path].(string)
if value != "" {
return value, nil
result, err := jmespath.Search(path, claims)
if err != nil {
return "", err
}

// try splitting path at .
segments := oidc.SplitWithEscaping(path, ".", "\\")
subclaims := claims
lastSegment := len(segments) - 1
for i := range segments {
if i < lastSegment {
if castedClaims, ok := subclaims[segments[i]].(map[string]interface{}); ok {
subclaims = castedClaims
} else if castedClaims, ok := subclaims[segments[i]].(map[interface{}]interface{}); ok {
subclaims = make(map[string]interface{}, len(castedClaims))
for k, v := range castedClaims {
if s, ok := k.(string); ok {
subclaims[s] = v
} else {
return "", fmt.Errorf("could not walk claims path, key '%v' is not a string", k)
}
}
}
} else {
if value, _ = subclaims[segments[i]].(string); value != "" {
return value, nil
}
}
value, ok := result.(string)
if !ok || value == "" {
return "", fmt.Errorf("claim path '%s' not set or empty", path)
}

return value, fmt.Errorf("claim path '%s' not set or empty", path)
return value, nil
}

// TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39
Expand Down
Loading