mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-24 16:41:35 -04:00
enhancement: finalize backchannel logout
This commit is contained in:
committed by
Christian Richter
parent
2e36859816
commit
66d220ff44
@@ -11,6 +11,7 @@ import (
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/justinas/alice"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
"github.com/opencloud-eu/opencloud/pkg/generators"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
@@ -73,6 +74,7 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...),
|
||||
microstore.Database("proxy"),
|
||||
microstore.Table("signing-keys"),
|
||||
store.DisablePersistence(cfg.PreSignedURL.SigningKeys.DisablePersistence),
|
||||
store.Authentication(cfg.PreSignedURL.SigningKeys.AuthUsername, cfg.PreSignedURL.SigningKeys.AuthPassword),
|
||||
)
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ func DefaultConfig() *config.Config {
|
||||
AccessTokenVerifyMethod: config.AccessTokenVerificationJWT,
|
||||
SkipUserInfo: false,
|
||||
UserinfoCache: &config.Cache{
|
||||
// toDo:
|
||||
// check if "memory" is the right default or if "nats-js-kv" is a better match
|
||||
Store: "memory",
|
||||
Nodes: []string{"127.0.0.1:9233"},
|
||||
Database: "cache-userinfo",
|
||||
|
||||
@@ -3,20 +3,20 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
store "go-micro.dev/v4/store"
|
||||
"go-micro.dev/v4/store"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -115,28 +115,26 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
|
||||
m.Logger.Error().Err(err).Msg("failed to write to userinfo cache")
|
||||
}
|
||||
|
||||
if sid := aClaims.SessionID; sid != "" {
|
||||
// reuse user cache for session id lookup
|
||||
err = m.userInfoCache.Write(&store.Record{
|
||||
Key: sid,
|
||||
Value: []byte(encodedHash),
|
||||
Expiry: time.Until(expiration),
|
||||
})
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
|
||||
}
|
||||
subject, sessionId := aClaims.Subject, aClaims.SessionID
|
||||
// if no session id is present, we can't do a session lookup,
|
||||
// so we can skip the cache entry for that.
|
||||
if sessionId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// create an additional entry mapping subject to session id
|
||||
if sub := aClaims.Subject; sub != "" {
|
||||
err = m.userInfoCache.Write(&store.Record{
|
||||
Key: fmt.Sprintf("%s.%s", sub, sid),
|
||||
Value: []byte(sid),
|
||||
Expiry: time.Until(expiration),
|
||||
})
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write subject lookup cache")
|
||||
}
|
||||
}
|
||||
// if the claim has no subject, we can leave it empty,
|
||||
// it's important to keep the dot in the key to prevent
|
||||
// sufix and prefix exploration in the cache.
|
||||
//
|
||||
// ok: {key: ".sessionId"}
|
||||
// ok: {key: "subject.sessionId"}
|
||||
key := strings.Join([]string{subject, sessionId}, ".")
|
||||
if err := m.userInfoCache.Write(&store.Record{
|
||||
Key: key,
|
||||
Value: []byte(encodedHash),
|
||||
Expiry: time.Until(expiration),
|
||||
}); err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -7,15 +7,24 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
bcl "github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes/internal/backchannellogout"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
|
||||
// BackchannelLogout handles backchannel logout requests from the identity provider and invalidates the related sessions in the cache
|
||||
// spec: https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
|
||||
//
|
||||
// toDo:
|
||||
// - keyCloak "Sign out all active sessions" fails to log out, no incoming request
|
||||
// - if the keycloak setting "Backchannel logout session required" is disabled,
|
||||
// we resolve the session by the subject which can lead to multiple session records,
|
||||
// we then send a logout event to each connected client and delete our stored record (subject.session & claim).
|
||||
// but the session still exists in the identity provider.
|
||||
func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) {
|
||||
// parse the application/x-www-form-urlencoded POST request
|
||||
logger := s.Logger.SubloggerWithRequestID(r.Context())
|
||||
@@ -26,13 +35,6 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
if r.PostFormValue("logout_token") == "" {
|
||||
logger.Warn().Msg("logout_token is missing")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: "logout_token is missing"})
|
||||
return
|
||||
}
|
||||
|
||||
logoutToken, err := s.OidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token"))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("VerifyLogoutToken failed")
|
||||
@@ -41,70 +43,63 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
var cacheKeys map[string]bool
|
||||
|
||||
if strings.TrimSpace(logoutToken.SessionId) != "" {
|
||||
cacheKeys[logoutToken.SessionId] = true
|
||||
} else if strings.TrimSpace(logoutToken.Subject) != "" {
|
||||
records, err := s.UserInfoCache.Read(fmt.Sprintf("%s.*", logoutToken.Subject))
|
||||
if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error reading userinfo cache")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
for _, record := range records {
|
||||
cacheKeys[string(record.Value)] = true
|
||||
cacheKeys[record.Key] = false
|
||||
}
|
||||
} else {
|
||||
logger.Warn().Msg("invalid logout token")
|
||||
subject, session := strings.Join(strings.Fields(logoutToken.Subject), ""), strings.Join(strings.Fields(logoutToken.SessionId), "")
|
||||
if subject == "" && session == "" {
|
||||
jseErr := jse{Error: "invalid_request", ErrorDescription: "invalid logout token: subject and session id are empty"}
|
||||
logger.Warn().Msg(jseErr.ErrorDescription)
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: "invalid logout token"})
|
||||
render.JSON(w, r, jseErr)
|
||||
return
|
||||
}
|
||||
|
||||
for key, isSID := range cacheKeys {
|
||||
if isSID {
|
||||
records, err := s.UserInfoCache.Read(key)
|
||||
if err != nil && !errors.Is(err, microstore.ErrNotFound) {
|
||||
logger.Error().Err(err).Msg("Error reading userinfo cache")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
for _, record := range records {
|
||||
err = s.UserInfoCache.Delete(string(record.Value))
|
||||
if err != nil && !errors.Is(err, microstore.ErrNotFound) {
|
||||
logger.Error().Err(err).Msg("Error deleting userinfo cache")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
requestSubjectAndSession := bcl.SuSe{Session: session, Subject: subject}
|
||||
// find out which mode of backchannel logout we are in
|
||||
// by checking if the session or subject is present in the token
|
||||
logoutMode := bcl.GetLogoutMode(requestSubjectAndSession)
|
||||
lookupRecords, err := bcl.GetLogoutRecords(requestSubjectAndSession, logoutMode, s.UserInfoCache)
|
||||
if errors.Is(err, microstore.ErrNotFound) || len(lookupRecords) == 0 {
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error reading userinfo cache")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range lookupRecords {
|
||||
// the record key is in the format "subject.session" or ".session"
|
||||
// the record value is the key of the record that contains the claim in its value
|
||||
key, value := record.Key, string(record.Value)
|
||||
|
||||
subjectSession, ok := bcl.NewSuSe(key)
|
||||
if !ok {
|
||||
logger.Warn().Msgf("invalid logout record key: %s", key)
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.UserInfoCache.Delete(key)
|
||||
err := s.publishBackchannelLogoutEvent(r.Context(), subjectSession.Session, value)
|
||||
if err != nil {
|
||||
s.Logger.Warn().Err(err).Msg("could not publish backchannel logout event")
|
||||
}
|
||||
|
||||
err = s.UserInfoCache.Delete(value)
|
||||
if err != nil && !errors.Is(err, microstore.ErrNotFound) {
|
||||
// Spec requires us to return a 400 BadRequest when the session could not be destroyed
|
||||
logger.Err(err).Msg(fmt.Errorf("could not delete session from cache (%s)", key).Error())
|
||||
// We only return on requests that do only attempt to destroy a single session, not multiple
|
||||
// we have to return a 400 BadRequest when we fail to delete the session
|
||||
// https://openid.net/specs/openid-connect-backchannel-1_0.html#rfc.section.2.8
|
||||
logger.Err(err).Msg("could not delete user info from cache")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
if isSID {
|
||||
err := s.publishBackchannelLogoutEvent(r.Context(), key, logoutToken)
|
||||
if err != nil {
|
||||
s.Logger.Warn().Err(err).Msg("could not publish backchannel logout event")
|
||||
}
|
||||
|
||||
// we can ignore errors when deleting the lookup record
|
||||
err = s.UserInfoCache.Delete(key)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("Failed to cleanup sessionid lookup entry")
|
||||
}
|
||||
logger.Debug().Msg("Deleted userinfo from cache")
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
@@ -112,20 +107,20 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
// publishBackchannelLogoutEvent publishes a backchannel logout event when the callback revived from the identity provider
|
||||
func (s StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, cacheKey string, logoutToken *oidc.LogoutToken) error {
|
||||
func (s *StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, sessionId, claimKey string) error {
|
||||
if s.EventsPublisher == nil {
|
||||
return fmt.Errorf("the events publisher is not set")
|
||||
}
|
||||
urecords, err := s.UserInfoCache.Read(cacheKey)
|
||||
claimRecords, err := s.UserInfoCache.Read(claimKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading userinfo cache: %w", err)
|
||||
}
|
||||
if len(urecords) == 0 {
|
||||
if len(claimRecords) == 0 {
|
||||
return fmt.Errorf("userinfo not found")
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err = msgpack.Unmarshal(urecords[0].Value, &claims); err != nil {
|
||||
if err = msgpack.Unmarshal(claimRecords[0].Value, &claims); err != nil {
|
||||
return fmt.Errorf("could not unmarshal userinfo: %w", err)
|
||||
}
|
||||
|
||||
@@ -141,7 +136,7 @@ func (s StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, c
|
||||
|
||||
e := events.BackchannelLogout{
|
||||
Executant: user.GetId(),
|
||||
SessionId: logoutToken.SessionId,
|
||||
SessionId: sessionId,
|
||||
Timestamp: utils.TSNow(),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// package backchannellogout provides functions to classify and lookup
|
||||
// backchannel logout records from the cache store.
|
||||
|
||||
package backchannellogout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// SuSe 🦎 ;) is a struct that groups the subject and session together
|
||||
// to prevent mix-ups for ('session, subject' || 'subject, session')
|
||||
// return values.
|
||||
type SuSe struct {
|
||||
Subject string
|
||||
Session string
|
||||
}
|
||||
|
||||
// NewSuSe parses the subject and session id from the given key and returns a SuSe struct
|
||||
func NewSuSe(key string) (SuSe, bool) {
|
||||
var subject, session string
|
||||
switch keys := strings.Split(strings.Join(strings.Fields(key), ""), "."); {
|
||||
case len(keys) == 2 && keys[0] == "" && keys[1] != "":
|
||||
session = keys[1]
|
||||
case len(keys) == 2 && keys[0] != "" && keys[1] == "":
|
||||
subject = keys[0]
|
||||
case len(keys) == 2 && keys[0] != "" && keys[1] != "":
|
||||
subject = keys[0]
|
||||
session = keys[1]
|
||||
case len(keys) == 1 && keys[0] != "":
|
||||
session = keys[0]
|
||||
default:
|
||||
return SuSe{}, false
|
||||
}
|
||||
|
||||
return SuSe{Session: session, Subject: subject}, true
|
||||
}
|
||||
|
||||
// LogoutMode defines the mode of backchannel logout, either by session or by subject
|
||||
type LogoutMode int
|
||||
|
||||
const (
|
||||
// LogoutModeUnknown is used when the logout mode cannot be determined
|
||||
LogoutModeUnknown LogoutMode = iota
|
||||
// LogoutModeSession is used when the logout mode is determined by the session id
|
||||
LogoutModeSession
|
||||
// LogoutModeSubject is used when the logout mode is determined by the subject
|
||||
LogoutModeSubject
|
||||
)
|
||||
|
||||
// GetLogoutMode determines the backchannel logout mode based on the presence of subject and session in the SuSe struct
|
||||
func GetLogoutMode(suse SuSe) LogoutMode {
|
||||
switch {
|
||||
case suse.Session != "":
|
||||
return LogoutModeSession
|
||||
case suse.Subject != "":
|
||||
return LogoutModeSubject
|
||||
default:
|
||||
return LogoutModeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSuspiciousCacheResult is returned when the cache result is suspicious
|
||||
var ErrSuspiciousCacheResult = errors.New("suspicious cache result")
|
||||
|
||||
// GetLogoutRecords retrieves the records from the user info cache based on the backchannel
|
||||
// logout mode and the provided SuSe struct.
|
||||
// it uses a seperator to prevent sufix and prefix exploration in the cache and checks
|
||||
// if the retrieved records match the requested subject and or session id as well, to prevent false positives.
|
||||
func GetLogoutRecords(suse SuSe, mode LogoutMode, store microstore.Store) ([]*microstore.Record, error) {
|
||||
var key string
|
||||
var opts []microstore.ReadOption
|
||||
switch mode {
|
||||
case LogoutModeSession:
|
||||
// the dot at the beginning prevents sufix exploration in the cache,
|
||||
// so only keys that end with '*.session' will be returned, but not '*sion'.
|
||||
key = "." + suse.Session
|
||||
opts = append(opts, microstore.ReadSuffix())
|
||||
case LogoutModeSubject:
|
||||
// the dot at the end prevents prefix exploration in the cache,
|
||||
// so only keys that start with 'subject.*' will be returned, but not 'sub*'.
|
||||
key = suse.Subject + "."
|
||||
opts = append(opts, microstore.ReadPrefix())
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: cannot determine logout mode", ErrSuspiciousCacheResult)
|
||||
}
|
||||
|
||||
records, err := store.Read(key, append(opts, microstore.ReadLimit(1000))...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, microstore.ErrNotFound
|
||||
}
|
||||
|
||||
if mode == LogoutModeSession && len(records) > 1 {
|
||||
return nil, fmt.Errorf("%w: multiple session records found", ErrSuspiciousCacheResult)
|
||||
}
|
||||
|
||||
// double-check if the found records match the requested subject and or session id as well,
|
||||
// to prevent false positives.
|
||||
for _, record := range records {
|
||||
recordSuSe, ok := NewSuSe(record.Key)
|
||||
if !ok {
|
||||
return nil, microstore.ErrNotFound
|
||||
}
|
||||
|
||||
switch {
|
||||
// in session mode, the session id must match, but the subject can be different
|
||||
case mode == LogoutModeSession && suse.Session == recordSuSe.Session:
|
||||
continue
|
||||
// in subject mode, the subject must match, but the session id can be different
|
||||
case mode == LogoutModeSubject && suse.Subject == recordSuSe.Subject:
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: record key does not match the requested subject or session", ErrSuspiciousCacheResult)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package backchannellogout
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
func TestNewSuSe(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
wantsuSe SuSe
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: ".session",
|
||||
key: ".session",
|
||||
wantsuSe: SuSe{Session: "session", Subject: ""},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: ".session",
|
||||
key: ".session",
|
||||
wantsuSe: SuSe{Session: "session", Subject: ""},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "session",
|
||||
key: "session",
|
||||
wantsuSe: SuSe{Session: "session", Subject: ""},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "subject.",
|
||||
key: "subject.",
|
||||
wantsuSe: SuSe{Session: "", Subject: "subject"},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "subject.session",
|
||||
key: "subject.session",
|
||||
wantsuSe: SuSe{Session: "session", Subject: "subject"},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "dot",
|
||||
key: ".",
|
||||
wantsuSe: SuSe{Session: "", Subject: ""},
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
key: "",
|
||||
wantsuSe: SuSe{Session: "", Subject: ""},
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace . whitespace",
|
||||
key: " . ",
|
||||
wantsuSe: SuSe{Session: "", Subject: ""},
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace subject whitespace . whitespace",
|
||||
key: " subject . ",
|
||||
wantsuSe: SuSe{Session: "", Subject: "subject"},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "whitespace . whitespace session whitespace",
|
||||
key: " . session ",
|
||||
wantsuSe: SuSe{Session: "session", Subject: ""},
|
||||
wantOk: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
suSe, ok := NewSuSe(tt.key)
|
||||
require.Equal(t, tt.wantOk, ok)
|
||||
require.Equal(t, tt.wantsuSe, suSe)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLogoutMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
suSe SuSe
|
||||
want LogoutMode
|
||||
}{
|
||||
{
|
||||
name: ".session",
|
||||
suSe: SuSe{Session: "session", Subject: ""},
|
||||
want: LogoutModeSession,
|
||||
},
|
||||
{
|
||||
name: "subject.session",
|
||||
suSe: SuSe{Session: "session", Subject: "subject"},
|
||||
want: LogoutModeSession,
|
||||
},
|
||||
{
|
||||
name: "subject.",
|
||||
suSe: SuSe{Session: "", Subject: "subject"},
|
||||
want: LogoutModeSubject,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
suSe: SuSe{Session: "", Subject: ""},
|
||||
want: LogoutModeUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mode := GetLogoutMode(tt.suSe)
|
||||
require.Equal(t, tt.want, mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLogoutRecords(t *testing.T) {
|
||||
sessionStore := store.NewMemoryStore()
|
||||
|
||||
recordClaimA := &store.Record{Key: "claim-a", Value: []byte("claim-a-data")}
|
||||
recordClaimB := &store.Record{Key: "claim-b", Value: []byte("claim-b-data")}
|
||||
recordClaimC := &store.Record{Key: "claim-c", Value: []byte("claim-c-data")}
|
||||
recordClaimD := &store.Record{Key: "claim-d", Value: []byte("claim-d-data")}
|
||||
recordSessionA := &store.Record{Key: "session-a", Value: []byte(recordClaimA.Key)}
|
||||
recordSessionB := &store.Record{Key: "session-b", Value: []byte(recordClaimB.Key)}
|
||||
recordSubjectASessionC := &store.Record{Key: "subject-a.session-c", Value: []byte(recordSessionA.Key)}
|
||||
recordSubjectASessionD := &store.Record{Key: "subject-a.session-d", Value: []byte(recordSessionB.Key)}
|
||||
|
||||
for _, r := range []*store.Record{
|
||||
recordClaimA,
|
||||
recordClaimB,
|
||||
recordClaimC,
|
||||
recordClaimD,
|
||||
recordSessionA,
|
||||
recordSessionB,
|
||||
recordSubjectASessionC,
|
||||
recordSubjectASessionD,
|
||||
} {
|
||||
require.NoError(t, sessionStore.Write(r))
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
suSe SuSe
|
||||
mode LogoutMode
|
||||
store store.Store
|
||||
wantRecords []*store.Record
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "session-c",
|
||||
suSe: SuSe{Session: "session-c"},
|
||||
mode: LogoutModeSession,
|
||||
store: sessionStore,
|
||||
wantRecords: []*store.Record{recordSubjectASessionC},
|
||||
},
|
||||
{
|
||||
name: "ession-c",
|
||||
suSe: SuSe{Session: "ession-c"},
|
||||
mode: LogoutModeSession,
|
||||
store: sessionStore,
|
||||
wantError: store.ErrNotFound,
|
||||
wantRecords: []*store.Record{},
|
||||
},
|
||||
{
|
||||
name: "subject-a",
|
||||
suSe: SuSe{Subject: "subject-a"},
|
||||
mode: LogoutModeSubject,
|
||||
store: sessionStore,
|
||||
wantRecords: []*store.Record{recordSubjectASessionC, recordSubjectASessionD},
|
||||
},
|
||||
{
|
||||
name: "subject-",
|
||||
suSe: SuSe{Subject: "subject-"},
|
||||
mode: LogoutModeSubject,
|
||||
store: sessionStore,
|
||||
wantError: store.ErrNotFound,
|
||||
wantRecords: []*store.Record{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
records, err := GetLogoutRecords(tt.suSe, tt.mode, tt.store)
|
||||
require.ErrorIs(t, err, tt.wantError)
|
||||
require.Len(t, records, len(tt.wantRecords))
|
||||
|
||||
sortRecords := func(r []*store.Record) []*store.Record {
|
||||
slices.SortFunc(r, func(a, b *store.Record) int {
|
||||
return strings.Compare(a.Key, b.Key)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
records = sortRecords(records)
|
||||
for i, wantRecords := range sortRecords(tt.wantRecords) {
|
||||
require.True(t, len(records) >= i+1)
|
||||
require.Equal(t, wantRecords.Key, records[i].Key)
|
||||
require.Equal(t, wantRecords.Value, records[i].Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// StaticRouteHandler defines a Route Handler for static routes
|
||||
|
||||
Reference in New Issue
Block a user