Skip to content

Commit

Permalink
Merge pull request #3825 from owncloud/lico-cs3-update
Browse files Browse the repository at this point in the history
IDP: allow using cs3 api to authenticate users
  • Loading branch information
butonic committed May 20, 2022
2 parents a0f5c7a + 9ac13e6 commit a7f9e14
Show file tree
Hide file tree
Showing 11 changed files with 558 additions and 19 deletions.
129 changes: 129 additions & 0 deletions extensions/idp/pkg/backends/cs3/bootstrap/cs3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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"
)

// Register adds the CS3 identity manager to the lico bootstrap
func Register() error {
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
}

// MustRegister adds the CS3 identity manager to the lico bootstrap or panics
func MustRegister() {
if err := Register(); err != nil {
panic(err)
}
}

// NewIdentityManager produces a CS3 backed identity manager instance for the idp
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
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,
// FIXME add a map[string]interface{} property to the lico config.Config so backends can pass custom config parameters through the bootstrap process
os.Getenv("CS3_GATEWAY"),
os.Getenv("CS3_MACHINE_AUTH_API_KEY"),
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
}
238 changes: 238 additions & 0 deletions extensions/idp/pkg/backends/cs3/identifier/cs3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
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,
}

// CS3 Backend holds the data for the CS3 identifier backend
type CS3Backend struct {
supportedScopes []string

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

sessions cmap.ConcurrentMap

gateway cs3gateway.GatewayAPIClient
}

// NewCS3Backend creates a new CS3 backend identifier backend
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 {
return false, nil, nil, nil, fmt.Errorf("cs3 backend basic authenticate rpc error: %v", err)
}
if res.Status.Code != cs3rpc.Code_CODE_OK {
return false, nil, nil, nil, fmt.Errorf("cs3 backend basic authenticate failed with code %s: %s", res.Status.Code.String(), res.Status.Message)
}

session := createSession(ctx, res.User)

user, err := newCS3User(res.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 {
return nil, fmt.Errorf("cs3 backend machine authenticate rpc error: %v", err)
}
if res.Status.Code != cs3rpc.Code_CODE_OK {
return nil, fmt.Errorf("cs3 backend machine authenticate failed with code %s: %s", res.Status.Code.String(), res.Status.Message)
}

user, err := newCS3User(res.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

0 comments on commit a7f9e14

Please sign in to comment.