Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Barz
9767e1e1b4 test: add test and assert given claim on GetUserByClaim 2025-11-18 18:59:26 +01:00
Jörn Friedrich Dreyer
11a9050709 autodecode base64 encoded uuid userid
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-11-18 17:45:06 +01:00
2 changed files with 75 additions and 20 deletions

View File

@@ -1,11 +1,13 @@
package middleware
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/jellydator/ttlcache/v3"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/router"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
@@ -124,6 +126,14 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
// keycloak automatically base64 encodes binary uuids like objectGUID, try to decode it
if decoded, decodeErr := base64.URLEncoding.DecodeString(value); decodeErr == nil {
if parsed, parseErr := uuid.FromBytes(decoded); parseErr == nil {
m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Str("parsed", parsed.String()).Msg("Detected base64 encoded binary uuid")
value = parsed.String()
}
}
user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value)
if errors.Is(err, backend.ErrAccountNotFound) {

View File

@@ -21,10 +21,10 @@ import (
)
func TestTokenIsAddedWithMailClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, oidc.Email, "mail", false)
}, nil, oidc.Email, "mail", false, "foo@example.com")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
@@ -39,10 +39,10 @@ func TestTokenIsAddedWithMailClaim(t *testing.T) {
}
func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, oidc.PreferredUsername, "username", false)
}, nil, oidc.PreferredUsername, "username", false, "foo")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
@@ -58,10 +58,10 @@ func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
}
func TestTokenIsAddedWithDotUsernamePathClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, "li.un", "username", false)
}, nil, "li.un", "username", false, "foo")
// This is how lico adds the username to the access token
req, rw := mockRequest(map[string]interface{}{
@@ -80,10 +80,10 @@ func TestTokenIsAddedWithDotUsernamePathClaim(t *testing.T) {
}
func TestTokenIsAddedWithDotEscapedUsernameClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, "li\\.un", "username", false)
}, nil, "li\\.un", "username", false, "foo")
// This tests the . escaping of the readUserIDClaim
req, rw := mockRequest(map[string]interface{}{
@@ -100,10 +100,10 @@ func TestTokenIsAddedWithDotEscapedUsernameClaim(t *testing.T) {
}
func TestTokenIsAddedWithDottedUsernameClaimFallback(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, "li.un", "username", false)
}, nil, "li.un", "username", false, "foo")
// This tests the . escaping fallback of the readUserIDClaim
req, rw := mockRequest(map[string]interface{}{
@@ -120,7 +120,7 @@ func TestTokenIsAddedWithDottedUsernameClaimFallback(t *testing.T) {
}
func TestNSkipOnNoClaims(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.Email, "mail", false)
sut := newMockAccountResolver(t, nil, backend.ErrAccountDisabled, oidc.Email, "mail", false, "")
req, rw := mockRequest(nil)
sut.ServeHTTP(rw, req)
@@ -131,7 +131,7 @@ func TestNSkipOnNoClaims(t *testing.T) {
}
func TestUnauthorizedOnUserNotFound(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username", false)
sut := newMockAccountResolver(t, nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username", false, "foo")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -145,7 +145,7 @@ func TestUnauthorizedOnUserNotFound(t *testing.T) {
}
func TestUnauthorizedOnUserDisabled(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username", false)
sut := newMockAccountResolver(t, nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username", false, "foo")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -159,7 +159,7 @@ func TestUnauthorizedOnUserDisabled(t *testing.T) {
}
func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.Email, "mail", false)
sut := newMockAccountResolver(t, nil, backend.ErrAccountNotFound, oidc.Email, "mail", false, "")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
})
@@ -173,11 +173,11 @@ func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
func TestUnauthorizedOnMissingTenantId(t *testing.T) {
sut := newMockAccountResolver(
&userv1beta1.User{
t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Username: "foo",
},
nil, oidc.PreferredUsername, "username", true)
nil, oidc.PreferredUsername, "username", true, "foo")
req, rw := mockRequest(map[string]any{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -192,7 +192,7 @@ func TestUnauthorizedOnMissingTenantId(t *testing.T) {
func TestTokenIsAddedWhenUserHasTenantId(t *testing.T) {
sut := newMockAccountResolver(
&userv1beta1.User{
t, &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "https://idx.example.com",
OpaqueId: "123",
@@ -200,7 +200,7 @@ func TestTokenIsAddedWhenUserHasTenantId(t *testing.T) {
},
Username: "foo",
},
nil, oidc.PreferredUsername, "username", true)
nil, oidc.PreferredUsername, "username", true, "foo")
req, rw := mockRequest(map[string]any{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -213,7 +213,46 @@ func TestTokenIsAddedWhenUserHasTenantId(t *testing.T) {
assert.Contains(t, token, "eyJ")
}
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string, multiTenant bool) http.Handler {
func TestTokenIsAddedWithBinaryBase64UserIDClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "b4963c7c-72b3-44b4-af98-eee1429dd8c2"},
Mail: "foo@example.com",
}, nil, "uuid", "username", false, "b4963c7c-72b3-44b4-af98-eee1429dd8c2")
// This tests the base64 decoding of binary user id claims
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
"uuid": "tJY8fHKzRLSvmO7hQp3Ywg==", // base64 encoded bytes value of b4963c7c-72b3-44b4-af98-eee1429dd8c2
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.NotEmpty(t, token)
assert.Contains(t, token, "eyJ")
}
func TestTokenIsAddedWithInvalidBinaryBase64UserIDClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "b4963c7c-72b3-44b4-af98-eee1429dd8c2"},
Mail: "foo@example.com",
}, nil, "uuid", "username", false, "aGVsbG8gd29ybGQ=")
// This tests the base64 decoding of binary user id claims
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
"uuid": "aGVsbG8gd29ybGQ=", // base64 encoded bytes value of invalid uuid
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.NotEmpty(t, token)
assert.Contains(t, token, "eyJ")
}
func newMockAccountResolver(t *testing.T, userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string, multiTenant bool, expectedClaim string) http.Handler {
tokenManager, _ := jwt.New(map[string]interface{}{
"secret": "change-me",
"expires": int64(60),
@@ -226,7 +265,13 @@ func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr
}
ub := mocks.UserBackend{}
ub.On("GetUserByClaims", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(userBackendResult, token, userBackendErr)
ub.On("GetUserByClaims", mock.Anything, mock.Anything,
mock.MatchedBy(
func(claim string) bool {
assert.Equal(t, expectedClaim, claim)
return true
})).
Return(userBackendResult, token, userBackendErr)
ub.On("GetUserRoles", mock.Anything, mock.Anything).Return(userBackendResult, nil)
ra := userRoleMocks.UserRoleAssigner{}