mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 12:19:37 -06:00
use claims map instead of struct
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
@@ -33,6 +33,8 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl
|
||||
logger: logger,
|
||||
tokenManager: tokenManager,
|
||||
userProvider: options.UserProvider,
|
||||
userOIDCClaim: options.UserOIDCClaim,
|
||||
userCS3Claim: options.UserCS3Claim,
|
||||
autoProvisionAccounts: options.AutoprovisionAccounts,
|
||||
}
|
||||
}
|
||||
@@ -44,8 +46,11 @@ type accountResolver struct {
|
||||
tokenManager tokenPkg.Manager
|
||||
userProvider backend.UserBackend
|
||||
autoProvisionAccounts bool
|
||||
userOIDCClaim string
|
||||
userCS3Claim string
|
||||
}
|
||||
|
||||
// TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39
|
||||
func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
claims := oidc.FromContext(req.Context())
|
||||
u, ok := revauser.ContextGetUser(req.Context())
|
||||
@@ -56,30 +61,31 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if u == nil && claims != nil {
|
||||
var claim, value string
|
||||
switch {
|
||||
case claims.PreferredUsername != "":
|
||||
claim, value = "username", claims.PreferredUsername
|
||||
case claims.Email != "":
|
||||
claim, value = "mail", claims.Email
|
||||
case claims.OcisID != "":
|
||||
//claim, value = "id", claims.OcisID
|
||||
default:
|
||||
// TODO allow lookup by custom claim, eg an id ... or sub
|
||||
m.logger.Error().Msg("Could not lookup account, no mail or preferred_username claim set")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
var err error
|
||||
u, err = m.userProvider.GetUserByClaims(req.Context(), claim, value, true)
|
||||
var value string
|
||||
var ok bool
|
||||
if value, ok = claims[m.userOIDCClaim].(string); !ok || value == "" {
|
||||
m.logger.Error().Str("claim", m.userOIDCClaim).Interface("claims", claims).Msg("claim not set or empty")
|
||||
w.WriteHeader(http.StatusInternalServerError) // admin needs to make the idp send the right claim
|
||||
return
|
||||
}
|
||||
|
||||
if m.autoProvisionAccounts && err == backend.ErrAccountNotFound {
|
||||
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("User by claim not found... autoprovisioning.")
|
||||
u, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value, true)
|
||||
|
||||
if err == backend.ErrAccountNotFound {
|
||||
m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Msg("User by claim not found")
|
||||
if !m.autoProvisionAccounts {
|
||||
m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning disabled")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning user")
|
||||
u, err = m.userProvider.CreateUserFromClaims(req.Context(), claims)
|
||||
}
|
||||
|
||||
if err == backend.ErrAccountNotFound || err == backend.ErrAccountDisabled {
|
||||
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("Unautorized")
|
||||
if err == backend.ErrAccountDisabled {
|
||||
m.logger.Debug().Interface("claims", claims).Msg("Disabled")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -90,17 +96,17 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("associated claims with uuid")
|
||||
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msg("associated claims with user")
|
||||
}
|
||||
|
||||
s, err := scope.AddOwnerScope(nil)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msgf("could not get owner scope")
|
||||
m.logger.Error().Err(err).Msg("could not get owner scope")
|
||||
return
|
||||
}
|
||||
token, err := m.tokenManager.MintToken(req.Context(), u, s)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msgf("could not mint token")
|
||||
m.logger.Error().Err(err).Msg("could not mint token")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/cs3org/reva/pkg/token"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
@@ -10,20 +14,17 @@ import (
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTokenIsAddedWithMailClaim(t *testing.T) {
|
||||
sut := newMockAccountResolver(&userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
|
||||
Mail: "foo@example.com",
|
||||
}, nil)
|
||||
}, nil, oidc.Email, "mail")
|
||||
|
||||
req, rw := mockRequest(&oidc.StandardClaims{
|
||||
Iss: "https://idx.example.com",
|
||||
Email: "foo@example.com",
|
||||
req, rw := mockRequest(map[string]interface{}{
|
||||
oidc.Iss: "https://idx.example.com",
|
||||
oidc.Email: "foo@example.com",
|
||||
})
|
||||
|
||||
sut.ServeHTTP(rw, req)
|
||||
@@ -37,11 +38,11 @@ func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
|
||||
sut := newMockAccountResolver(&userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
|
||||
Mail: "foo@example.com",
|
||||
}, nil)
|
||||
}, nil, oidc.PreferredUsername, "username")
|
||||
|
||||
req, rw := mockRequest(&oidc.StandardClaims{
|
||||
Iss: "https://idx.example.com",
|
||||
PreferredUsername: "foo",
|
||||
req, rw := mockRequest(map[string]interface{}{
|
||||
oidc.Iss: "https://idx.example.com",
|
||||
oidc.PreferredUsername: "foo",
|
||||
})
|
||||
|
||||
sut.ServeHTTP(rw, req)
|
||||
@@ -53,7 +54,7 @@ func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNSkipOnNoClaims(t *testing.T) {
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled)
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.Email, "mail")
|
||||
req, rw := mockRequest(nil)
|
||||
|
||||
sut.ServeHTTP(rw, req)
|
||||
@@ -64,10 +65,10 @@ func TestNSkipOnNoClaims(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnauthorizedOnUserNotFound(t *testing.T) {
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound)
|
||||
req, rw := mockRequest(&oidc.StandardClaims{
|
||||
Iss: "https://idx.example.com",
|
||||
PreferredUsername: "foo",
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username")
|
||||
req, rw := mockRequest(map[string]interface{}{
|
||||
oidc.Iss: "https://idx.example.com",
|
||||
oidc.PreferredUsername: "foo",
|
||||
})
|
||||
|
||||
sut.ServeHTTP(rw, req)
|
||||
@@ -78,10 +79,10 @@ func TestUnauthorizedOnUserNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnauthorizedOnUserDisabled(t *testing.T) {
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled)
|
||||
req, rw := mockRequest(&oidc.StandardClaims{
|
||||
Iss: "https://idx.example.com",
|
||||
PreferredUsername: "foo",
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username")
|
||||
req, rw := mockRequest(map[string]interface{}{
|
||||
oidc.Iss: "https://idx.example.com",
|
||||
oidc.PreferredUsername: "foo",
|
||||
})
|
||||
|
||||
sut.ServeHTTP(rw, req)
|
||||
@@ -92,9 +93,9 @@ func TestUnauthorizedOnUserDisabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled)
|
||||
req, rw := mockRequest(&oidc.StandardClaims{
|
||||
Iss: "https://idx.example.com",
|
||||
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.Email, "mail")
|
||||
req, rw := mockRequest(map[string]interface{}{
|
||||
oidc.Iss: "https://idx.example.com",
|
||||
})
|
||||
|
||||
sut.ServeHTTP(rw, req)
|
||||
@@ -104,7 +105,7 @@ func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
|
||||
assert.Equal(t, http.StatusInternalServerError, rw.Code)
|
||||
}
|
||||
|
||||
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error) http.Handler {
|
||||
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string) http.Handler {
|
||||
mock := &test.UserBackendMock{
|
||||
GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, error) {
|
||||
return userBackendResult, userBackendErr
|
||||
@@ -115,11 +116,13 @@ func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr
|
||||
Logger(log.NewLogger()),
|
||||
UserProvider(mock),
|
||||
TokenManagerConfig(config.TokenManager{JWTSecret: "secret"}),
|
||||
UserOIDCClaim(oidcclaim),
|
||||
UserCS3Claim(cs3claim),
|
||||
AutoprovisionAccounts(false),
|
||||
)(mockHandler{})
|
||||
}
|
||||
|
||||
func mockRequest(claims *oidc.StandardClaims) (*http.Request, *httptest.ResponseRecorder) {
|
||||
func mockRequest(claims map[string]interface{}) (*http.Request, *httptest.ResponseRecorder) {
|
||||
if claims == nil {
|
||||
return httptest.NewRequest("GET", "http://example.com/foo", nil), httptest.NewRecorder()
|
||||
}
|
||||
|
||||
@@ -82,11 +82,12 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
claims := &oidc.StandardClaims{
|
||||
OcisID: user.Id.OpaqueId,
|
||||
Iss: user.Id.Idp,
|
||||
PreferredUsername: user.Username,
|
||||
Email: user.Mail,
|
||||
// fake oidc claims
|
||||
claims := map[string]interface{}{
|
||||
oidc.OwncloudUUID: user.Id.OpaqueId,
|
||||
oidc.Iss: user.Id.Idp,
|
||||
oidc.PreferredUsername: user.Username,
|
||||
oidc.Email: user.Mail,
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
|
||||
|
||||
@@ -59,11 +59,11 @@ func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// inject claims to the request context for the account_uuid middleware.
|
||||
req = req.WithContext(oidc.NewContext(req.Context(), &claims))
|
||||
req = req.WithContext(oidc.NewContext(req.Context(), claims))
|
||||
|
||||
// store claims in context
|
||||
// uses the original context, not the one with probably reduced security
|
||||
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), &claims)))
|
||||
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ type oidcAuth struct {
|
||||
tokenCacheTTL time.Duration
|
||||
}
|
||||
|
||||
func (m oidcAuth) getClaims(token string, req *http.Request) (claims oidc.StandardClaims, status int) {
|
||||
func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) {
|
||||
hit := m.tokenCache.Load(token)
|
||||
if hit == nil {
|
||||
// TODO cache userinfo for access token if we can determine the expiry (which works in case it is a jwt based access token)
|
||||
@@ -96,16 +96,12 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims oidc.Standa
|
||||
return
|
||||
}
|
||||
|
||||
// TODO allow extracting arbitrary claims ... or require idp to send a specific claim
|
||||
if err := userInfo.Claims(&claims); err != nil {
|
||||
m.logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims")
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
//TODO: This should be read from the token instead of config
|
||||
claims.Iss = m.oidcIss
|
||||
|
||||
expiration := m.extractExpiration(token)
|
||||
m.tokenCache.Store(token, claims, expiration)
|
||||
|
||||
@@ -114,7 +110,7 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims oidc.Standa
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if claims, ok = hit.V.(oidc.StandardClaims); !ok {
|
||||
if claims, ok = hit.V.(map[string]interface{}); !ok {
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ type Options struct {
|
||||
Store storepb.StoreService
|
||||
// PreSignedURLConfig to configure the middleware
|
||||
PreSignedURLConfig config.PreSignedURL
|
||||
// UserOIDCClaim to read from the oidc claims
|
||||
UserOIDCClaim string
|
||||
// UserCS3Claim to use when looking up a user in the CS3 API
|
||||
UserCS3Claim string
|
||||
// AutoprovisionAccounts when an accountResolver does not exist.
|
||||
AutoprovisionAccounts bool
|
||||
// EnableBasicAuth to allow basic auth
|
||||
@@ -141,6 +145,20 @@ func PreSignedURLConfig(cfg config.PreSignedURL) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// UserOIDCClaim provides a function to set the UserClaim config
|
||||
func UserOIDCClaim(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.UserOIDCClaim = val
|
||||
}
|
||||
}
|
||||
|
||||
// UserCS3Claim provides a function to set the UserClaimType config
|
||||
func UserCS3Claim(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.UserCS3Claim = val
|
||||
}
|
||||
}
|
||||
|
||||
// AutoprovisionAccounts provides a function to set the AutoprovisionAccounts config
|
||||
func AutoprovisionAccounts(val bool) Option {
|
||||
return func(o *Options) {
|
||||
|
||||
Reference in New Issue
Block a user