mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-25 06:18:42 -05:00
Make it possible to use CS3 as accounts backend instead of account-service
Configureable via: PROXY_ACCOUNT_BACKEND_TYPE=cs3 PROXY_ACCOUNT_BACKEND_TYPE=accounts (default) By using a backend which implements the CS3 user-api (currently provided by reva/storage) it is possible to bypass the ocis-accounts service and for example use ldap directly. Hides user and auth related communication behind a facade (user/backend) to minimize logic-duplication across middlewares. Allows to switich the account backend from accounts to cs3. Co-authored-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
12
changelog/unreleased/allow-ldap-user-backend.md
Normal file
12
changelog/unreleased/allow-ldap-user-backend.md
Normal file
@@ -0,0 +1,12 @@
|
||||
Change: CS3 can be used as accounts-backend
|
||||
|
||||
Tags: proxy
|
||||
|
||||
PROXY_ACCOUNT_BACKEND_TYPE=cs3
|
||||
PROXY_ACCOUNT_BACKEND_TYPE=accounts (default)
|
||||
|
||||
By using a backend which implements the CS3 user-api (currently provided by reva/storage) it is possible to bypass
|
||||
the ocis-accounts service and for example use ldap directly.
|
||||
|
||||
|
||||
https://github.com/owncloud/ocis/pull/1020
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -250,10 +251,24 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
}
|
||||
|
||||
func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alice.Chain {
|
||||
accountsClient := acc.NewAccountsService("com.owncloud.api.accounts", grpc.DefaultClient)
|
||||
rolesClient := settings.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient)
|
||||
storeClient := storepb.NewStoreService("com.owncloud.api.store", grpc.DefaultClient)
|
||||
revaClient, err := cs3.GetGatewayServiceClient(cfg.Reva.Address)
|
||||
var userProvider backend.UserBackend
|
||||
switch cfg.AccountBackend {
|
||||
case "accounts":
|
||||
userProvider = backend.NewAccountsServiceUserBackend(
|
||||
acc.NewAccountsService("com.owncloud.api.accounts", grpc.DefaultClient),
|
||||
rolesClient,
|
||||
cfg.OIDC.Issuer,
|
||||
l,
|
||||
)
|
||||
case "cs3":
|
||||
userProvider = backend.NewCS3UserBackend(revaClient, rolesClient, revaClient, l)
|
||||
default:
|
||||
l.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend)
|
||||
}
|
||||
|
||||
storeClient := storepb.NewStoreService("com.owncloud.api.store", grpc.DefaultClient)
|
||||
if err != nil {
|
||||
l.Error().Err(err).
|
||||
Str("gateway", cfg.Reva.Address).
|
||||
@@ -290,27 +305,24 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
|
||||
// basic Options
|
||||
middleware.Logger(l),
|
||||
middleware.EnableBasicAuth(cfg.EnableBasicAuth),
|
||||
middleware.AccountsClient(accountsClient),
|
||||
middleware.UserProvider(userProvider),
|
||||
middleware.OIDCIss(cfg.OIDC.Issuer),
|
||||
middleware.CredentialsByUserAgent(cfg.Reva.Middleware.Auth.CredentialsByUserAgent),
|
||||
),
|
||||
middleware.SignedURLAuth(
|
||||
middleware.Logger(l),
|
||||
middleware.PreSignedURLConfig(cfg.PreSignedURL),
|
||||
middleware.AccountsClient(accountsClient),
|
||||
middleware.UserProvider(userProvider),
|
||||
middleware.Store(storeClient),
|
||||
),
|
||||
middleware.AccountResolver(
|
||||
middleware.Logger(l),
|
||||
middleware.AccountsClient(accountsClient),
|
||||
middleware.OIDCIss(cfg.OIDC.Issuer),
|
||||
middleware.UserProvider(userProvider),
|
||||
middleware.TokenManagerConfig(cfg.TokenManager),
|
||||
middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts),
|
||||
middleware.SettingsRoleService(rolesClient),
|
||||
),
|
||||
middleware.CreateHome(
|
||||
middleware.Logger(l),
|
||||
middleware.AccountsClient(accountsClient),
|
||||
middleware.TokenManagerConfig(cfg.TokenManager),
|
||||
middleware.RevaGatewayClient(revaClient),
|
||||
),
|
||||
|
||||
@@ -115,6 +115,7 @@ type Config struct {
|
||||
PolicySelector *PolicySelector `mapstructure:"policy_selector"`
|
||||
Reva Reva
|
||||
PreSignedURL PreSignedURL
|
||||
AccountBackend string
|
||||
AutoprovisionAccounts bool
|
||||
EnableBasicAuth bool
|
||||
InsecureBackends bool
|
||||
|
||||
@@ -256,6 +256,14 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
|
||||
Destination: &cfg.EnableBasicAuth,
|
||||
},
|
||||
|
||||
&cli.StringFlag{
|
||||
Name: "account-backend-type",
|
||||
Value: "accounts",
|
||||
Usage: "account-backend-type",
|
||||
EnvVars: []string{"PROXY_ACCOUNT_BACKEND_TYPE"},
|
||||
Destination: &cfg.AccountBackend,
|
||||
},
|
||||
|
||||
// Reva Middlewares Config
|
||||
&cli.StringSliceFlag{
|
||||
Name: "proxy-user-agent-lock-in",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package middleware
|
||||
|
||||
/*
|
||||
|
||||
Temporarily disabled
|
||||
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -1,21 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
revaUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
tokenPkg "github.com/cs3org/reva/pkg/token"
|
||||
"github.com/cs3org/reva/pkg/token/manager/jwt"
|
||||
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
revauser "github.com/cs3org/reva/pkg/user"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
|
||||
)
|
||||
|
||||
// AccountResolver provides a middleware which mints a jwt and adds it to the proxied request based
|
||||
@@ -37,117 +30,68 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl
|
||||
next: next,
|
||||
logger: logger,
|
||||
tokenManager: tokenManager,
|
||||
accountsClient: options.AccountsClient,
|
||||
oidcIss: options.OIDCIss,
|
||||
autoprovisionAccounts: options.AutoprovisionAccounts,
|
||||
settingsRoleService: options.SettingsRoleService,
|
||||
userProvider: options.UserProvider,
|
||||
autoProvisionAccounts: options.AutoprovisionAccounts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type accountResolver struct {
|
||||
oidcIss string
|
||||
autoprovisionAccounts bool
|
||||
next http.Handler
|
||||
logger log.Logger
|
||||
tokenManager tokenPkg.Manager
|
||||
accountsClient accounts.AccountsService
|
||||
settingsRoleService settings.RoleService
|
||||
userProvider backend.UserBackend
|
||||
autoProvisionAccounts bool
|
||||
}
|
||||
|
||||
func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
var account *accounts.Account
|
||||
var status int
|
||||
|
||||
claims := oidc.FromContext(req.Context())
|
||||
u, ok := revauser.ContextGetUser(req.Context())
|
||||
|
||||
if claims == nil {
|
||||
if claims == nil && !ok {
|
||||
m.next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case claims.Email != "":
|
||||
account, status = getAccount(m.logger, m.accountsClient, fmt.Sprintf("mail eq '%s'", strings.ReplaceAll(claims.Email, "'", "''")))
|
||||
case claims.PreferredUsername != "":
|
||||
account, status = getAccount(m.logger, m.accountsClient, fmt.Sprintf("preferred_name eq '%s'", strings.ReplaceAll(claims.PreferredUsername, "'", "''")))
|
||||
case claims.OcisID != "":
|
||||
account, status = getAccount(m.logger, m.accountsClient, fmt.Sprintf("id eq '%s'", strings.ReplaceAll(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)
|
||||
}
|
||||
|
||||
if m.autoprovisionAccounts && status == http.StatusNotFound {
|
||||
account, status = createAccount(m.logger, claims, m.accountsClient)
|
||||
}
|
||||
|
||||
if status != 0 || account == nil {
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
if !account.AccountEnabled {
|
||||
m.logger.Debug().Interface("account", account).Msg("account is disabled")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
groups := make([]string, len(account.MemberOf))
|
||||
for i := range account.MemberOf {
|
||||
// reva needs the unix group name
|
||||
groups[i] = account.MemberOf[i].OnPremisesSamAccountName
|
||||
}
|
||||
|
||||
// fetch active roles from ocis-settings
|
||||
assignmentResponse, err := m.settingsRoleService.ListRoleAssignments(req.Context(), &settings.ListRoleAssignmentsRequest{AccountUuid: account.Id})
|
||||
roleIDs := make([]string, 0)
|
||||
if err != nil {
|
||||
m.logger.Err(err).Str("accountID", account.Id).Msg("failed to fetch role assignments")
|
||||
} else {
|
||||
for _, assignment := range assignmentResponse.Assignments {
|
||||
roleIDs = append(roleIDs, assignment.RoleId)
|
||||
if u == nil && claims != nil {
|
||||
var claim, value string
|
||||
switch {
|
||||
case claims.Email != "":
|
||||
claim, value = "mail", claims.Email
|
||||
case claims.PreferredUsername != "":
|
||||
claim, value = "username", claims.PreferredUsername
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Debug().Interface("claims", claims).Interface("account", account).Msgf("associated claims with uuid")
|
||||
var err error
|
||||
u, err = m.userProvider.GetUserByClaims(req.Context(), claim, value, true)
|
||||
|
||||
user := &revaUser.User{
|
||||
Id: &revaUser.UserId{
|
||||
OpaqueId: account.Id,
|
||||
Idp: claims.Iss,
|
||||
},
|
||||
Username: account.OnPremisesSamAccountName,
|
||||
DisplayName: account.DisplayName,
|
||||
Mail: account.Mail,
|
||||
MailVerified: account.ExternalUserState == "" || account.ExternalUserState == "Accepted",
|
||||
Groups: groups,
|
||||
Opaque: &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{},
|
||||
},
|
||||
}
|
||||
user.Opaque.Map["uid"] = &types.OpaqueEntry{
|
||||
Decoder: "plain",
|
||||
Value: []byte(strconv.FormatInt(account.UidNumber, 10)),
|
||||
}
|
||||
user.Opaque.Map["gid"] = &types.OpaqueEntry{
|
||||
Decoder: "plain",
|
||||
Value: []byte(strconv.FormatInt(account.GidNumber, 10)),
|
||||
}
|
||||
|
||||
// encode roleIDs as json string
|
||||
roleIDsJSON, jsonErr := json.Marshal(roleIDs)
|
||||
if jsonErr != nil {
|
||||
m.logger.Err(jsonErr).Str("accountID", account.Id).Msg("failed to marshal roleIDs into json")
|
||||
} else {
|
||||
user.Opaque.Map["roles"] = &types.OpaqueEntry{
|
||||
Decoder: "json",
|
||||
Value: roleIDsJSON,
|
||||
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.CreateUserFromClaims(req.Context(), claims)
|
||||
}
|
||||
|
||||
if err == backend.ErrAccountNotFound || err == backend.ErrAccountDisabled {
|
||||
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("Unautorized")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("Could not get user by claim")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("associated claims with uuid")
|
||||
}
|
||||
|
||||
token, err := m.tokenManager.MintToken(req.Context(), user)
|
||||
token, err := m.tokenManager.MintToken(req.Context(), u)
|
||||
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msgf("could not mint token")
|
||||
@@ -159,52 +103,3 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
m.next.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func getAccount(logger log.Logger, ac accounts.AccountsService, query string) (account *accounts.Account, status int) {
|
||||
resp, err := ac.ListAccounts(context.Background(), &accounts.ListAccountsRequest{
|
||||
Query: query,
|
||||
PageSize: 2,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("query", query).Msgf("error fetching from accounts-service")
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Accounts) <= 0 {
|
||||
logger.Error().Str("query", query).Msgf("account not found")
|
||||
status = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Accounts) > 1 {
|
||||
logger.Error().Str("query", query).Msgf("more than one account found, aborting")
|
||||
status = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
|
||||
account = resp.Accounts[0]
|
||||
return
|
||||
}
|
||||
|
||||
func createAccount(l log.Logger, claims *oidc.StandardClaims, ac accounts.AccountsService) (*accounts.Account, int) {
|
||||
// TODO check if fields are missing.
|
||||
req := &accounts.CreateAccountRequest{
|
||||
Account: &accounts.Account{
|
||||
DisplayName: claims.DisplayName,
|
||||
PreferredName: claims.PreferredUsername,
|
||||
OnPremisesSamAccountName: claims.PreferredUsername,
|
||||
Mail: claims.Email,
|
||||
CreationType: "LocalAccount",
|
||||
AccountEnabled: true,
|
||||
},
|
||||
}
|
||||
created, err := ac.CreateAccount(context.Background(), req)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Interface("account", req.Account).Msg("could not create account")
|
||||
return nil, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return created, 0
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ func newOIDCAuth(options Options) func(http.Handler) http.Handler {
|
||||
// newBasicAuth returns a configured basic middleware
|
||||
func newBasicAuth(options Options) func(http.Handler) http.Handler {
|
||||
return BasicAuth(
|
||||
UserProvider(options.UserProvider),
|
||||
Logger(options.Logger),
|
||||
EnableBasicAuth(options.EnableBasicAuth),
|
||||
AccountsClient(options.AccountsClient),
|
||||
|
||||
@@ -2,12 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const publicFilesEndpoint = "/remote.php/dav/public-files/"
|
||||
@@ -16,16 +15,15 @@ const publicFilesEndpoint = "/remote.php/dav/public-files/"
|
||||
func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
options := newOptions(optionSetters...)
|
||||
logger := options.Logger
|
||||
oidcIss := options.OIDCIss
|
||||
|
||||
if options.EnableBasicAuth {
|
||||
options.Logger.Warn().Msg("basic auth enabled, use only for testing or development")
|
||||
}
|
||||
|
||||
h := basicAuth{
|
||||
logger: logger,
|
||||
enabled: options.EnableBasicAuth,
|
||||
accountsClient: options.AccountsClient,
|
||||
logger: logger,
|
||||
enabled: options.EnableBasicAuth,
|
||||
userProvider: options.UserProvider,
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
@@ -40,14 +38,15 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
removeSuperfluousAuthenticate(w)
|
||||
account, ok := h.getAccount(req)
|
||||
login, password, _ := req.BasicAuth()
|
||||
user, err := h.userProvider.Authenticate(req.Context(), login, password)
|
||||
|
||||
// touch is a user agent locking guard, when touched changes to true it indicates the User-Agent on the
|
||||
// request is configured to support only one challenge, it it remains untouched, there are no considera-
|
||||
// tions and we should write all available authentication challenges to the response.
|
||||
touch := false
|
||||
|
||||
if !ok {
|
||||
if err != nil {
|
||||
for k, v := range options.CredentialsByUserAgent {
|
||||
if strings.Contains(k, req.UserAgent()) {
|
||||
removeSuperfluousAuthenticate(w)
|
||||
@@ -67,8 +66,10 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
claims := &oidc.StandardClaims{
|
||||
OcisID: account.Id,
|
||||
Iss: oidcIss,
|
||||
OcisID: user.Id.OpaqueId,
|
||||
Iss: user.Id.Idp,
|
||||
PreferredUsername: user.Username,
|
||||
Email: user.Mail,
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
|
||||
@@ -78,35 +79,17 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
type basicAuth struct {
|
||||
logger log.Logger
|
||||
enabled bool
|
||||
accountsClient accounts.AccountsService
|
||||
logger log.Logger
|
||||
enabled bool
|
||||
userProvider backend.UserBackend
|
||||
}
|
||||
|
||||
func (m basicAuth) isPublicLink(req *http.Request) bool {
|
||||
login, _, ok := req.BasicAuth()
|
||||
|
||||
return ok && login == "public" && strings.HasPrefix(req.URL.Path, publicFilesEndpoint)
|
||||
}
|
||||
|
||||
func (m basicAuth) isBasicAuth(req *http.Request) bool {
|
||||
login, password, ok := req.BasicAuth()
|
||||
|
||||
return m.enabled && ok && login != "" && password != ""
|
||||
}
|
||||
|
||||
func (m basicAuth) getAccount(req *http.Request) (*accounts.Account, bool) {
|
||||
login, password, _ := req.BasicAuth()
|
||||
|
||||
account, status := getAccount(
|
||||
m.logger,
|
||||
m.accountsClient,
|
||||
fmt.Sprintf(
|
||||
"login eq '%s' and password eq '%s'",
|
||||
strings.ReplaceAll(login, "'", "''"),
|
||||
strings.ReplaceAll(password, "'", "''"),
|
||||
),
|
||||
)
|
||||
|
||||
return account, status == 0
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/cs3org/reva/pkg/rgrpc/status"
|
||||
tokenPkg "github.com/cs3org/reva/pkg/token"
|
||||
"github.com/cs3org/reva/pkg/token/manager/jwt"
|
||||
microErrors "github.com/micro/go-micro/v2/errors"
|
||||
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
@@ -31,7 +29,6 @@ func CreateHome(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
return &createHome{
|
||||
next: next,
|
||||
logger: logger,
|
||||
accountsClient: options.AccountsClient,
|
||||
tokenManager: tokenManager,
|
||||
revaGatewayClient: options.RevaGatewayClient,
|
||||
}
|
||||
@@ -41,7 +38,6 @@ func CreateHome(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
type createHome struct {
|
||||
next http.Handler
|
||||
logger log.Logger
|
||||
accountsClient accounts.AccountsService
|
||||
tokenManager tokenPkg.Manager
|
||||
revaGatewayClient gateway.GatewayAPIClient
|
||||
}
|
||||
@@ -54,31 +50,6 @@ func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
token := req.Header.Get("x-access-token")
|
||||
|
||||
user, err := m.tokenManager.DismantleToken(req.Context(), token)
|
||||
if err != nil {
|
||||
m.logger.Logger.Err(err).Msg("error getting user from access token")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = m.accountsClient.GetAccount(req.Context(), &accounts.GetAccountRequest{
|
||||
Id: user.Id.OpaqueId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
e := microErrors.Parse(err.Error())
|
||||
|
||||
if e.Code == http.StatusNotFound {
|
||||
m.logger.Debug().Msgf("account with id %s not found", user.Id.OpaqueId)
|
||||
m.next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Err(err).Msgf("error getting user with id %s from accounts service", user.Id.OpaqueId)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// we need to pass the token to authenticate the CreateHome request.
|
||||
//ctx := tokenpkg.ContextSetToken(r.Context(), token)
|
||||
ctx := metadata.AppendToOutgoingContext(req.Context(), tokenPkg.TokenHeader, token)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -26,6 +27,8 @@ type Options struct {
|
||||
HTTPClient *http.Client
|
||||
// AccountsClient for resolving accounts
|
||||
AccountsClient acc.AccountsService
|
||||
// UP
|
||||
UserProvider backend.UserBackend
|
||||
// SettingsRoleService for the roles API in settings
|
||||
SettingsRoleService settings.RoleService
|
||||
// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
|
||||
@@ -165,3 +168,10 @@ func TokenCacheTTL(ttl time.Duration) Option {
|
||||
o.UserinfoCacheTTL = ttl
|
||||
}
|
||||
}
|
||||
|
||||
// UserProvider sets the accounts user provider
|
||||
func UserProvider(up backend.UserBackend) Option {
|
||||
return func(o *Options) {
|
||||
o.UserProvider = up
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
revauser "github.com/cs3org/reva/pkg/user"
|
||||
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
ocisoidc "github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
"github.com/owncloud/ocis/proxy/pkg/config"
|
||||
store "github.com/owncloud/ocis/store/pkg/proto/v0"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
@@ -29,8 +28,8 @@ func SignedURLAuth(optionSetters ...Option) func(next http.Handler) http.Handler
|
||||
next: next,
|
||||
logger: options.Logger,
|
||||
preSignedURLConfig: options.PreSignedURLConfig,
|
||||
accountsClient: options.AccountsClient,
|
||||
store: options.Store,
|
||||
userProvider: options.UserProvider,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +38,7 @@ type signedURLAuth struct {
|
||||
next http.Handler
|
||||
logger log.Logger
|
||||
preSignedURLConfig config.PreSignedURL
|
||||
accountsClient accounts.AccountsService
|
||||
userProvider backend.UserBackend
|
||||
store store.StoreService
|
||||
}
|
||||
|
||||
@@ -49,51 +48,22 @@ func (m signedURLAuth) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.userProvider.GetUserByClaims(req.Context(), "username", req.URL.Query().Get("OC-Credential"), true)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("Could not get user by claim")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
ctx := revauser.ContextSetUser(req.Context(), user)
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
if err := m.validate(req); err != nil {
|
||||
http.Error(w, "Invalid url signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := m.claims(req.URL.Query().Get("OC-Credential"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid url signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
m.next.ServeHTTP(w, req.WithContext(ocisoidc.NewContext(req.Context(), claims)))
|
||||
}
|
||||
|
||||
func (m signedURLAuth) claims(credential string) (*ocisoidc.StandardClaims, error) {
|
||||
// use openid claims to let the account_uuid middleware do a lookup by username
|
||||
claims := ocisoidc.StandardClaims{
|
||||
OcisID: credential,
|
||||
}
|
||||
|
||||
// OC10 username is handled as id, if we get a credantial that is not of type uuid we expect
|
||||
// that it is a PreferredUsername und we need to get the corresponding uuid
|
||||
if _, err := uuid.Parse(claims.OcisID); err != nil {
|
||||
// todo caching
|
||||
account, status := getAccount(
|
||||
m.logger,
|
||||
m.accountsClient,
|
||||
fmt.Sprintf(
|
||||
"preferred_name eq '%s'",
|
||||
strings.ReplaceAll(
|
||||
claims.OcisID,
|
||||
"'",
|
||||
"''",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if status != 0 || account == nil {
|
||||
return nil, fmt.Errorf("no oc-credential found for %v", claims.OcisID)
|
||||
}
|
||||
|
||||
claims.OcisID = account.Id
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
m.next.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (m signedURLAuth) shouldServe(req *http.Request) bool {
|
||||
@@ -194,7 +164,8 @@ func (m signedURLAuth) urlIsExpired(query url.Values, now func() time.Time) (exp
|
||||
}
|
||||
|
||||
func (m signedURLAuth) signatureIsValid(req *http.Request) (ok bool, err error) {
|
||||
signingKey, err := m.getSigningKey(req.Context(), req.URL.Query().Get("OC-Credential"))
|
||||
u := revauser.ContextMustGetUser(req.Context())
|
||||
signingKey, err := m.getSigningKey(req.Context(), u.Id.OpaqueId)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("could not retrieve signing key")
|
||||
return false, err
|
||||
@@ -225,18 +196,13 @@ func (m signedURLAuth) createSignature(url string, signingKey []byte) string {
|
||||
return hex.EncodeToString(hash)
|
||||
}
|
||||
|
||||
func (m signedURLAuth) getSigningKey(ctx context.Context, credential string) ([]byte, error) {
|
||||
claims, err := m.claims(credential)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
func (m signedURLAuth) getSigningKey(ctx context.Context, ocisID string) ([]byte, error) {
|
||||
res, err := m.store.Read(ctx, &store.ReadRequest{
|
||||
Options: &store.ReadOptions{
|
||||
Database: "proxy",
|
||||
Table: "signing-keys",
|
||||
},
|
||||
Key: claims.OcisID,
|
||||
Key: ocisID,
|
||||
})
|
||||
if err != nil || len(res.Records) < 1 {
|
||||
return []byte{}, err
|
||||
|
||||
218
proxy/pkg/user/backend/accounts.go
Normal file
218
proxy/pkg/user/backend/accounts.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewAccountsServiceUserBackend creates a user-provider which fetches users from the ocis accounts-service
|
||||
func NewAccountsServiceUserBackend(ac accounts.AccountsService, rs settings.RoleService, oidcISS string, logger log.Logger) UserBackend {
|
||||
return &accountsServiceBackend{
|
||||
accountsClient: ac,
|
||||
settingsRoleService: rs,
|
||||
OIDCIss: oidcISS,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type accountsServiceBackend struct {
|
||||
accountsClient accounts.AccountsService
|
||||
settingsRoleService settings.RoleService
|
||||
OIDCIss string
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (a accountsServiceBackend) GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, error) {
|
||||
var account *accounts.Account
|
||||
var status int
|
||||
var query string
|
||||
|
||||
switch claim {
|
||||
case "mail":
|
||||
query = fmt.Sprintf("mail eq '%s'", strings.ReplaceAll(value, "'", "''"))
|
||||
case "username":
|
||||
query = fmt.Sprintf("preferred_name eq '%s'", strings.ReplaceAll(value, "'", "''"))
|
||||
case "id":
|
||||
query = fmt.Sprintf("id eq '%s'", strings.ReplaceAll(value, "'", "''"))
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid user by claim lookup must be 'mail', 'username' or 'id")
|
||||
}
|
||||
|
||||
account, status = a.getAccount(ctx, query)
|
||||
if status == http.StatusNotFound {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
|
||||
if status != 0 || account == nil {
|
||||
return nil, fmt.Errorf("could not get account, got status: %d", status)
|
||||
}
|
||||
|
||||
if !account.AccountEnabled {
|
||||
return nil, ErrAccountDisabled
|
||||
}
|
||||
|
||||
user := a.accountToUser(account)
|
||||
|
||||
if !withRoles {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
if err := injectRoles(ctx, user, a.settingsRoleService); err != nil {
|
||||
a.logger.Warn().Err(err).Msgf("Could not load roles... continuing without")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
}
|
||||
|
||||
// Authenticate authenticates against the accounts services and returns the user on success
|
||||
func (a *accountsServiceBackend) Authenticate(ctx context.Context, username string, password string) (*cs3.User, error) {
|
||||
query := fmt.Sprintf(
|
||||
"login eq '%s' and password eq '%s'",
|
||||
strings.ReplaceAll(username, "'", "''"),
|
||||
strings.ReplaceAll(password, "'", "''"),
|
||||
)
|
||||
account, status := a.getAccount(ctx, query)
|
||||
|
||||
if status != 0 {
|
||||
return nil, fmt.Errorf("could not authenticate with username, password for user %s. Status: %d", username, status)
|
||||
}
|
||||
|
||||
user := a.accountToUser(account)
|
||||
|
||||
if err := injectRoles(ctx, user, a.settingsRoleService); err != nil {
|
||||
a.logger.Warn().Err(err).Msgf("Could not load roles... continuing without")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a accountsServiceBackend) CreateUserFromClaims(ctx context.Context, claims *oidc.StandardClaims) (*cs3.User, error) {
|
||||
// TODO check if fields are missing.
|
||||
req := &accounts.CreateAccountRequest{
|
||||
Account: &accounts.Account{
|
||||
DisplayName: claims.DisplayName,
|
||||
PreferredName: claims.PreferredUsername,
|
||||
OnPremisesSamAccountName: claims.PreferredUsername,
|
||||
Mail: claims.Email,
|
||||
CreationType: "LocalAccount",
|
||||
AccountEnabled: true,
|
||||
},
|
||||
}
|
||||
created, err := a.accountsClient.CreateAccount(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := a.accountToUser(created)
|
||||
|
||||
if err := injectRoles(ctx, user, a.settingsRoleService); err != nil {
|
||||
a.logger.Warn().Err(err).Msgf("Could not load roles... continuing without")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a accountsServiceBackend) GetUserGroups(ctx context.Context, userID string) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// accountToUser converts an owncloud account struct to a reva user struct. In the proxy
|
||||
// we work with the reva struct as a token can be minted from it.
|
||||
func (a *accountsServiceBackend) accountToUser(account *accounts.Account) *cs3.User {
|
||||
user := &cs3.User{
|
||||
Id: &cs3.UserId{
|
||||
OpaqueId: account.Id,
|
||||
Idp: a.OIDCIss,
|
||||
},
|
||||
Username: account.OnPremisesSamAccountName,
|
||||
DisplayName: account.DisplayName,
|
||||
Mail: account.Mail,
|
||||
MailVerified: account.ExternalUserState == "" || account.ExternalUserState == "Accepted",
|
||||
Groups: expandGroups(account),
|
||||
Opaque: &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{},
|
||||
},
|
||||
}
|
||||
|
||||
user.Opaque.Map["uid"] = &types.OpaqueEntry{
|
||||
Decoder: "plain",
|
||||
Value: []byte(strconv.FormatInt(account.UidNumber, 10)),
|
||||
}
|
||||
user.Opaque.Map["gid"] = &types.OpaqueEntry{
|
||||
Decoder: "plain",
|
||||
Value: []byte(strconv.FormatInt(account.GidNumber, 10)),
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (a *accountsServiceBackend) getAccount(ctx context.Context, query string) (account *accounts.Account, status int) {
|
||||
resp, err := a.accountsClient.ListAccounts(ctx, &accounts.ListAccountsRequest{
|
||||
Query: query,
|
||||
PageSize: 2,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error().Err(err).Str("query", query).Msgf("error fetching from accounts-service")
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Accounts) <= 0 {
|
||||
a.logger.Error().Str("query", query).Msgf("account not found")
|
||||
status = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Accounts) > 1 {
|
||||
a.logger.Error().Str("query", query).Msgf("more than one account found, aborting")
|
||||
status = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
|
||||
account = resp.Accounts[0]
|
||||
return
|
||||
}
|
||||
|
||||
func expandGroups(account *accounts.Account) []string {
|
||||
groups := make([]string, len(account.MemberOf))
|
||||
for i := range account.MemberOf {
|
||||
// reva needs the unix group name
|
||||
groups[i] = account.MemberOf[i].OnPremisesSamAccountName
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// injectRoles adds roles from the roles-service to the user-struct by mutating an existing struct
|
||||
func injectRoles(ctx context.Context, u *cs3.User, ss settings.RoleService) error {
|
||||
roleIDs, err := loadRolesIDs(ctx, u.Id.OpaqueId, ss)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(roleIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
enc, err := encodeRoleIDs(roleIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Opaque = &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"roles": enc,
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
proxy/pkg/user/backend/backend.go
Normal file
66
proxy/pkg/user/backend/backend.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAccountNotFound account not found
|
||||
ErrAccountNotFound = errors.New("user not found")
|
||||
// ErrAccountDisabled account disabled
|
||||
ErrAccountDisabled = errors.New("account disabled")
|
||||
// ErrNotSupported operation not supported by user-backend
|
||||
ErrNotSupported = errors.New("operation not supported")
|
||||
)
|
||||
|
||||
// UserBackend allows the proxy to retrieve users from different user-backends (accounts-service, CS3)
|
||||
type UserBackend interface {
|
||||
GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, error)
|
||||
Authenticate(ctx context.Context, username string, password string) (*cs3.User, error)
|
||||
CreateUserFromClaims(ctx context.Context, claims *oidc.StandardClaims) (*cs3.User, error)
|
||||
GetUserGroups(ctx context.Context, userID string)
|
||||
}
|
||||
|
||||
// RevaAuthenticator helper interface to mock auth-method from reva gateway-client.
|
||||
type RevaAuthenticator interface {
|
||||
Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error)
|
||||
}
|
||||
|
||||
// loadRolesIDs returns the role-ids assigned to an user
|
||||
func loadRolesIDs(ctx context.Context, opaqueUserID string, rs settings.RoleService) ([]string, error) {
|
||||
req := &settings.ListRoleAssignmentsRequest{AccountUuid: opaqueUserID}
|
||||
assignmentResponse, err := rs.ListRoleAssignments(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleIDs := make([]string, 0)
|
||||
|
||||
for _, assignment := range assignmentResponse.Assignments {
|
||||
roleIDs = append(roleIDs, assignment.RoleId)
|
||||
}
|
||||
|
||||
return roleIDs, nil
|
||||
}
|
||||
|
||||
// encodeRoleIDs encoded the given role id's in to reva-specific format to be able to mint a token from them
|
||||
func encodeRoleIDs(roleIDs []string) (*types.OpaqueEntry, error) {
|
||||
roleIDsJSON, err := json.Marshal(roleIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.OpaqueEntry{
|
||||
Decoder: "json",
|
||||
Value: roleIDsJSON,
|
||||
}, nil
|
||||
}
|
||||
100
proxy/pkg/user/backend/cs3.go
Normal file
100
proxy/pkg/user/backend/cs3.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
|
||||
)
|
||||
|
||||
type cs3backend struct {
|
||||
userProvider cs3.UserAPIClient
|
||||
settingsRoleService settings.RoleService
|
||||
authProvider RevaAuthenticator
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// NewCS3UserBackend creates a user-provider which fetches users from a CS3 UserBackend
|
||||
func NewCS3UserBackend(up cs3.UserAPIClient, rs settings.RoleService, ap RevaAuthenticator, logger log.Logger) UserBackend {
|
||||
return &cs3backend{
|
||||
userProvider: up,
|
||||
settingsRoleService: rs,
|
||||
authProvider: ap,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, error) {
|
||||
res, err := c.userProvider.GetUserByClaim(ctx, &cs3.GetUserByClaimRequest{
|
||||
Claim: claim,
|
||||
Value: value,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("could not get user by claim %v with value %v: %w", claim, value, err)
|
||||
case res.Status.Code != rpcv1beta1.Code_CODE_OK:
|
||||
if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("could not get user by claim %v with value %v : %w ", claim, value, err)
|
||||
}
|
||||
|
||||
user := res.User
|
||||
|
||||
if !withRoles {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, c.settingsRoleService)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("Could not load roles")
|
||||
}
|
||||
|
||||
if len(roleIDs) == 0 {
|
||||
return user, nil
|
||||
|
||||
}
|
||||
|
||||
enc, err := encodeRoleIDs(roleIDs)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("Could not encode loaded roles")
|
||||
}
|
||||
|
||||
user.Opaque = &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"roles": enc,
|
||||
},
|
||||
}
|
||||
|
||||
return res.User, nil
|
||||
}
|
||||
|
||||
func (c *cs3backend) Authenticate(ctx context.Context, username string, password string) (*cs3.User, error) {
|
||||
res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
|
||||
ClientId: username,
|
||||
ClientSecret: password,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("could not authenticate with username and password user: %s, %w", username, err)
|
||||
case res.Status.Code != rpcv1beta1.Code_CODE_OK:
|
||||
return nil, fmt.Errorf("could not authenticate with username and password user: %s, got code: %d", username, res.Status.Code)
|
||||
}
|
||||
|
||||
return res.User, nil
|
||||
}
|
||||
|
||||
func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims *oidc.StandardClaims) (*cs3.User, error) {
|
||||
return nil, fmt.Errorf("CS3 Backend does not support creating users from claims")
|
||||
}
|
||||
|
||||
func (c cs3backend) GetUserGroups(ctx context.Context, userID string) {
|
||||
panic("implement me")
|
||||
}
|
||||
Reference in New Issue
Block a user