Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IDP: allow using cs3 api to authenticate users #3825

Merged
merged 5 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions changelog/unreleased/idp-directly-use-cs3-to-authenticate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Allow authenticating users via CS3

The IDP can now directly authenticates users using the CS3 API by setting `IDP_IDENTITY_MANAGER="cs3"`.
butonic marked this conversation as resolved.
Show resolved Hide resolved

https://github.com/owncloud/ocis/pull/3825
125 changes: 125 additions & 0 deletions extensions/idp/pkg/backends/cs3/bootstrap/cs3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2021 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package bootstrap

import (
"fmt"
"os"

"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/managers"
cs3 "github.com/owncloud/ocis/v2/extensions/idp/pkg/backends/cs3/identifier"
)

// Identity managers.
const (
identityManagerName = "cs3"
)

func Register() error {
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
}

func MustRegister() {
if err := Register(); err != nil {
panic(err)
}
}

func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
butonic marked this conversation as resolved.
Show resolved Hide resolved
config := bs.Config()

logger := config.Config.Logger

if config.AuthorizationEndpointURI.String() != "" {
return nil, fmt.Errorf("cs3 backend is incompatible with authorization-endpoint-uri parameter")
}
config.AuthorizationEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/authorize")

if config.EndSessionEndpointURI.String() != "" {
return nil, fmt.Errorf("cs3 backend is incompatible with endsession-endpoint-uri parameter")
}
config.EndSessionEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/endsession")

if config.SignInFormURI.EscapedPath() == "" {
config.SignInFormURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier")
}

if config.SignedOutURI.EscapedPath() == "" {
config.SignedOutURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/goodbye")
}

identifierBackend, identifierErr := cs3.NewCS3Backend(
config.Config,
config.TLSClientConfig,
os.Getenv("CS3_GATEWAY"), // FIXME how do we pass custom config to backends?
os.Getenv("CS3_MACHINE_AUTH_API_KEY"), // FIXME how do we pass custom config to backends?
kobergj marked this conversation as resolved.
Show resolved Hide resolved
config.Settings.Insecure,
)
if identifierErr != nil {
return nil, fmt.Errorf("failed to create identifier backend: %v", identifierErr)
}

fullAuthorizationEndpointURL := bootstrap.WithSchemeAndHost(config.AuthorizationEndpointURI, config.IssuerIdentifierURI)
fullSignInFormURL := bootstrap.WithSchemeAndHost(config.SignInFormURI, config.IssuerIdentifierURI)
fullSignedOutEndpointURL := bootstrap.WithSchemeAndHost(config.SignedOutURI, config.IssuerIdentifierURI)

activeIdentifier, err := identifier.NewIdentifier(&identifier.Config{
Config: config.Config,

BaseURI: config.IssuerIdentifierURI,
PathPrefix: bs.MakeURIPath(bootstrap.APITypeSignin, ""),
StaticFolder: config.IdentifierClientPath,
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
ScopesConf: config.IdentifierScopesConf,
WebAppDisabled: config.IdentifierClientDisabled,

AuthorizationEndpointURI: fullAuthorizationEndpointURL,
SignedOutEndpointURI: fullSignedOutEndpointURL,

DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
UILocales: config.IdentifierUILocales,

Backend: identifierBackend,
})
if err != nil {
return nil, fmt.Errorf("failed to create identifier: %v", err)
}
err = activeIdentifier.SetKey(config.EncryptionSecret)
if err != nil {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
}

identityManagerConfig := &identity.Config{
SignInFormURI: fullSignInFormURL,
SignedOutURI: fullSignedOutEndpointURL,

Logger: logger,

ScopesSupported: config.Config.AllowedScopes,
}

identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)
logger.Infoln("using identifier backed identity manager")

return identifierIdentityManager, nil
}
242 changes: 242 additions & 0 deletions extensions/idp/pkg/backends/cs3/identifier/cs3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package cs3

import (
"context"
"crypto/tls"
"fmt"

cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/libregraph/lico"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/identifier/backends"
"github.com/libregraph/lico/identifier/meta/scopes"
"github.com/libregraph/lico/identity"
cmap "github.com/orcaman/concurrent-map"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
ins "google.golang.org/grpc/credentials/insecure"
"stash.kopano.io/kgol/oidc-go"
)

const cs3BackendName = "identifier-cs3"

var cs3SpportedScopes = []string{
oidc.ScopeProfile,
oidc.ScopeEmail,
lico.ScopeUniqueUserID,
lico.ScopeRawSubject,
}

type CS3Backend struct {
kobergj marked this conversation as resolved.
Show resolved Hide resolved
supportedScopes []string

logger logrus.FieldLogger
tlsConfig *tls.Config
gatewayURI string
machineAuthAPIKey string
insecure bool

sessions cmap.ConcurrentMap

gateway cs3gateway.GatewayAPIClient
}

func NewCS3Backend(
c *config.Config,
tlsConfig *tls.Config,
gatewayURI string,
machineAuthAPIKey string,
insecure bool,
) (*CS3Backend, error) {

// Build supported scopes based on default scopes.
supportedScopes := make([]string, len(cs3SpportedScopes))
copy(supportedScopes, cs3SpportedScopes)

b := &CS3Backend{
supportedScopes: supportedScopes,

logger: c.Logger,
tlsConfig: tlsConfig,
gatewayURI: gatewayURI,
machineAuthAPIKey: machineAuthAPIKey,
insecure: insecure,

sessions: cmap.New(),
}

b.logger.Infoln("cs3 backend connection set up")

return b, nil
}

// RunWithContext implements the Backend interface.
func (b *CS3Backend) RunWithContext(ctx context.Context) error {
return nil
}

// Logon implements the Backend interface, enabling Logon with user name and
// password as provided. Requests are bound to the provided context.
func (b *CS3Backend) Logon(ctx context.Context, audience, username, password string) (bool, *string, *string, backends.UserFromBackend, error) {

l, err := b.connect(ctx)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("cs3 backend logon connect error: %v", err)
}
defer l.Close()

client := cs3gateway.NewGatewayAPIClient(l)

res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{
Type: "basic",
ClientId: username,
ClientSecret: password,
})
if err != nil || res.Status.Code != cs3rpc.Code_CODE_OK {
return false, nil, nil, nil, nil
butonic marked this conversation as resolved.
Show resolved Hide resolved
}
res2, err := client.WhoAmI(ctx, &cs3gateway.WhoAmIRequest{
Token: res.Token,
})
butonic marked this conversation as resolved.
Show resolved Hide resolved
if err != nil || res2.Status.Code != cs3rpc.Code_CODE_OK {
return false, nil, nil, nil, nil
}

session, _ := createSession(ctx, res2.User)
butonic marked this conversation as resolved.
Show resolved Hide resolved

user, err := newCS3User(res2.User)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("cs3 backend resolve entry data error: %v", err)
}

// Use the users subject as user id.
userID := user.Subject()

sessionRef := identity.GetSessionRef(b.Name(), audience, userID)
b.sessions.Set(*sessionRef, session)
b.logger.WithFields(logrus.Fields{
"session": session,
"ref": *sessionRef,
"username": user.Username(),
"id": userID,
}).Debugln("cs3 backend logon")

return true, &userID, sessionRef, user, nil
}

// GetUser implements the Backend interface, providing user meta data retrieval
// for the user specified by the userID. Requests are bound to the provided
// context.
func (b *CS3Backend) GetUser(ctx context.Context, userEntryID string, sessionRef *string, requestedScopes map[string]bool) (backends.UserFromBackend, error) {

session, err := b.getSessionForUser(ctx, userEntryID, sessionRef, true, true, false)
if err != nil {
return nil, fmt.Errorf("cs3 backend resolve session error: %v", err)
}

user, err := newCS3User(session.User())
if err != nil {
return nil, fmt.Errorf("cs3 backend get user failed to process user: %v", err)
}
// TODO double check userEntryID matches session?

return user, nil
}

// ResolveUserByUsername implements the Backend interface, providing lookup for
// user by providing the username. Requests are bound to the provided context.
func (b *CS3Backend) ResolveUserByUsername(ctx context.Context, username string) (backends.UserFromBackend, error) {

l, err := b.connect(ctx)
if err != nil {
return nil, fmt.Errorf("cs3 backend resolve username connect error: %v", err)
}
defer l.Close()

client := cs3gateway.NewGatewayAPIClient(l)

res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{
Type: "machine",
ClientId: "username:" + username,
ClientSecret: b.machineAuthAPIKey,
})
if err != nil || res.Status.Code != cs3rpc.Code_CODE_OK {
return nil, nil
butonic marked this conversation as resolved.
Show resolved Hide resolved
}
res2, err := client.WhoAmI(ctx, &cs3gateway.WhoAmIRequest{
Token: res.Token,
})
butonic marked this conversation as resolved.
Show resolved Hide resolved
if err != nil || res2.Status.Code != cs3rpc.Code_CODE_OK {
return nil, nil
}

user, err := newCS3User(res2.User)
if err != nil {
return nil, fmt.Errorf("cs3 backend resolve username data error: %v", err)
}

return user, nil
}

// RefreshSession implements the Backend interface.
func (b *CS3Backend) RefreshSession(ctx context.Context, userID string, sessionRef *string, claims map[string]interface{}) error {
return nil
}

// DestroySession implements the Backend interface providing destroy CS3 session.
func (b *CS3Backend) DestroySession(ctx context.Context, sessionRef *string) error {
b.sessions.Remove(*sessionRef)
return nil
}

// UserClaims implements the Backend interface, providing user specific claims
// for the user specified by the userID.
func (b *CS3Backend) UserClaims(userID string, authorizedScopes map[string]bool) map[string]interface{} {
return nil
// TODO should we return the "ownclouduuid" as a claim? there is also "LibgreGraph.UUID" / lico.ScopeUniqueUserID
}

// ScopesSupported implements the Backend interface, providing supported scopes
// when running this backend.
func (b *CS3Backend) ScopesSupported() []string {
return b.supportedScopes
}

// ScopesMeta implements the Backend interface, providing meta data for
// supported scopes.
func (b *CS3Backend) ScopesMeta() *scopes.Scopes {
return nil
}

// Name implements the Backend interface.
func (b *CS3Backend) Name() string {
return cs3BackendName
}

func (b *CS3Backend) connect(ctx context.Context) (*grpc.ClientConn, error) {
if b.insecure {
return grpc.Dial(b.gatewayURI, grpc.WithTransportCredentials(ins.NewCredentials()))
}

creds := credentials.NewTLS(b.tlsConfig)
return grpc.Dial(b.gatewayURI, grpc.WithTransportCredentials(creds))
}

func (b *CS3Backend) getSessionForUser(ctx context.Context, userEntryID string, sessionRef *string, register bool, refresh bool, removeIfRegistered bool) (*cs3Session, error) {
if sessionRef == nil {
return nil, nil
}

var session *cs3Session
if s, ok := b.sessions.Get(*sessionRef); ok {
// Existing session.
session = s.(*cs3Session)
if session != nil {
return session, nil
}
}

return session, nil
}
Loading