Merge branch 'master' into config-doc-descriptions
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/assetsfs"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/idp"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// New returns a new http filesystem to serve assets.
|
||||
func New(opts ...Option) http.FileSystem {
|
||||
options := newOptions(opts...)
|
||||
return assetsfs.New(idp.Assets, options.Config.Asset.Path, options.Logger)
|
||||
}
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -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/services/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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cs3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
)
|
||||
|
||||
// createSession creates a new Session without the server using the provided
|
||||
// data.
|
||||
func createSession(ctx context.Context, u *cs3user.User) *cs3Session {
|
||||
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
sessionCtx, cancel := context.WithCancel(ctx)
|
||||
s := &cs3Session{
|
||||
ctx: sessionCtx,
|
||||
u: u,
|
||||
ctxCancel: cancel,
|
||||
}
|
||||
|
||||
s.when = time.Now()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type cs3Session struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
u *cs3user.User
|
||||
when time.Time
|
||||
}
|
||||
|
||||
// User returns the cs3 user of the session
|
||||
func (s *cs3Session) User() *cs3user.User {
|
||||
return s.u
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cs3
|
||||
|
||||
import (
|
||||
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
konnect "github.com/libregraph/lico"
|
||||
)
|
||||
|
||||
type cs3User struct {
|
||||
u *cs3user.User
|
||||
}
|
||||
|
||||
func newCS3User(u *cs3user.User) (*cs3User, error) {
|
||||
return &cs3User{
|
||||
u: u,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Subject returns the cs3 users opaque id as sub
|
||||
func (u *cs3User) Subject() string {
|
||||
return u.u.GetId().GetOpaqueId()
|
||||
}
|
||||
|
||||
// Email returns the cs3 users email
|
||||
func (u *cs3User) Email() string {
|
||||
return u.u.GetMail()
|
||||
}
|
||||
|
||||
// EmailVerified returns the cs3 users email verified flag
|
||||
func (u *cs3User) EmailVerified() bool {
|
||||
return u.u.GetMailVerified()
|
||||
}
|
||||
|
||||
// Name returns the cs3 users displayname
|
||||
func (u *cs3User) Name() string {
|
||||
return u.u.GetDisplayName()
|
||||
}
|
||||
|
||||
// FamilyName always returns "" to fulfill the UserWithProfile interface
|
||||
func (u *cs3User) FamilyName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GivenName always returns "" to fulfill the UserWithProfile interface
|
||||
func (u *cs3User) GivenName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Username returns the cs3 users username
|
||||
func (u *cs3User) Username() string {
|
||||
return u.u.GetUsername()
|
||||
}
|
||||
|
||||
// UniqueID returns the cs3 users opaque id
|
||||
func (u *cs3User) UniqueID() string {
|
||||
return u.u.GetId().GetOpaqueId()
|
||||
}
|
||||
|
||||
// BackendClaims returns additional claims the cs3 users provides
|
||||
func (u *cs3User) BackendClaims() map[string]interface{} {
|
||||
claims := make(map[string]interface{})
|
||||
claims[konnect.IdentifiedUserIDClaim] = u.u.GetId().GetOpaqueId()
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
// BackendScopes returns nil to fulfill the UserFromBackend interface
|
||||
func (u *cs3User) BackendScopes() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredScopes returns nil to fulfill the UserFromBackend interface
|
||||
func (u *cs3User) RequiredScopes() []string {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "health",
|
||||
Usage: "check health status",
|
||||
Category: "info",
|
||||
Before: func(c *cli.Context) error {
|
||||
err := parser.ParseConfig(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
}
|
||||
return err
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
|
||||
resp, err := http.Get(
|
||||
fmt.Sprintf(
|
||||
"http://%s/healthz",
|
||||
cfg.Debug.Addr,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to request health check")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Fatal().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health seems to be in bad state")
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health got a good state")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) cli.Commands {
|
||||
return []*cli.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the ocis-idp command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cli.App{
|
||||
Name: "idp",
|
||||
Usage: "Serve IDP API for oCIS",
|
||||
Commands: GetCommands(cfg),
|
||||
})
|
||||
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "help,h",
|
||||
Usage: "Show the help",
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
// SutureService allows for the idp command to be embedded and supervised by a suture supervisor tree.
|
||||
type SutureService struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSutureService creates a new idp.SutureService
|
||||
func NewSutureService(cfg *ociscfg.Config) suture.Service {
|
||||
cfg.IDP.Commons = cfg.Commons
|
||||
return SutureService{
|
||||
cfg: cfg.IDP,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SutureService) Serve(ctx context.Context) error {
|
||||
s.cfg.Context = ctx
|
||||
if err := Execute(s.cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/logging"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/metrics"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/server/debug"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/server/http"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/tracing"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const _rsaKeySize = 4096
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "server",
|
||||
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
Category: "server",
|
||||
Before: func(c *cli.Context) error {
|
||||
err := parser.ParseConfig(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if cfg.IDP.EncryptionSecretFile != "" {
|
||||
if err := ensureEncryptionSecretExists(cfg.IDP.EncryptionSecretFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureSigningPrivateKeyExists(cfg.IDP.SigningPrivateKeyFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
err := tracing.Configure(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
gr = run.Group{}
|
||||
ctx, cancel = func() (context.Context, context.CancelFunc) {
|
||||
if cfg.Context == nil {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
return context.WithCancel(cfg.Context)
|
||||
}()
|
||||
metrics = metrics.New()
|
||||
)
|
||||
|
||||
defer cancel()
|
||||
|
||||
metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
||||
|
||||
{
|
||||
server, err := http.Server(
|
||||
http.Logger(logger),
|
||||
http.Context(ctx),
|
||||
http.Config(cfg),
|
||||
http.Metrics(metrics),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Str("transport", "http").
|
||||
Msg("Failed to initialize server")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(func() error {
|
||||
return server.Run()
|
||||
}, func(_ error) {
|
||||
logger.Info().
|
||||
Str("transport", "http").
|
||||
Msg("Shutting down server")
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
server, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(server.ListenAndServe, func(_ error) {
|
||||
_ = server.Shutdown(ctx)
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureEncryptionSecretExists(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
// If the file exists we can just return
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
secret := make([]byte, 32)
|
||||
_, err = rand.Read(secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, bytes.NewReader(secret))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureSigningPrivateKeyExists(paths []string) error {
|
||||
for _, path := range paths {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
// If the file exists we can just return
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pk, err := rsa.GenerateKey(rand.Reader, _rsaKeySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pb := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(pk),
|
||||
}
|
||||
if err := pem.Encode(f, pb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "print the version of this binary and the running extension instances",
|
||||
Category: "info",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Version: " + version.GetString())
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
fmt.Println("")
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No running " + cfg.Service.Name + " service found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
Tracing *Tracing `yaml:"tracing"`
|
||||
Log *Log `yaml:"log"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
|
||||
Reva *Reva `yaml:"reva"`
|
||||
|
||||
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;IDP_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used for accessing the 'auth-machine' service to impersonate users when looking up their userinfo via the 'cs3' backend."`
|
||||
|
||||
Asset Asset `yaml:"asset"`
|
||||
IDP Settings `yaml:"idp"`
|
||||
Clients []Client `yaml:"clients"`
|
||||
Ldap Ldap `yaml:"ldap"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Ldap defines the available LDAP configuration.
|
||||
type Ldap struct {
|
||||
URI string `yaml:"uri" env:"LDAP_URI;IDP_LDAP_URI" desc:"Url of the LDAP service to use as idp."`
|
||||
TLSCACert string `yaml:"cacert" env:"LDAP_CACERT;IDP_LDAP_TLS_CACERT" desc:"Path to the tls cert for the ldap service."`
|
||||
|
||||
BindDN string `yaml:"bind_dn" env:"LDAP_BIND_DN;IDP_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server."`
|
||||
BindPassword string `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;IDP_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'."`
|
||||
|
||||
BaseDN string `yaml:"base_dn" env:"LDAP_USER_BASE_DN;IDP_LDAP_BASE_DN" desc:"Search base DN for looking up LDAP users."`
|
||||
Scope string `yaml:"scope" env:"LDAP_USER_SCOPE;IDP_LDAP_SCOPE" desc:"LDAP search scope to use when looking up users ('base', 'one', 'sub')."`
|
||||
|
||||
LoginAttribute string `yaml:"login_attribute" env:"IDP_LDAP_LOGIN_ATTRIBUTE" desc:"LDAP User attribute to use for login (e.g. uid)."`
|
||||
EmailAttribute string `yaml:"email_attribute" env:"LDAP_USER_SCHEMA_MAIL;IDP_LDAP_EMAIL_ATTRIBUTE" desc:"LDAP User email attribute (e.g. mail)."`
|
||||
NameAttribute string `yaml:"name_attribute" env:"LDAP_USER_SCHEMA_USERNAME;IDP_LDAP_NAME_ATTRIBUTE" desc:"LDAP User name attribute (e.g. displayName)."`
|
||||
UUIDAttribute string `yaml:"uuid_attribute" env:"LDAP_USER_SCHEMA_ID;IDP_LDAP_UUID_ATTRIBUTE" desc:"LDAP User uuid attribute (e.g. uid)."`
|
||||
UUIDAttributeType string `yaml:"uuid_attribute_type" env:"IDP_LDAP_UUID_ATTRIBUTE_TYPE" desc:"LDAP User uuid attribute type (e.g. text)."`
|
||||
|
||||
Filter string `yaml:"filter" env:"LDAP_USER_FILTER;IDP_LDAP_FILTER" desc:"LDAP filter to add to the default filters for user search (e.g. '(objectclass=ownCloud)')."`
|
||||
ObjectClass string `yaml:"objectclass" env:"LDAP_USER_OBJECTCLASS;IDP_LDAP_OBJECTCLASS" desc:"LDAP User ObjectClass (e.g. inetOrgPerson)."`
|
||||
}
|
||||
|
||||
// Asset defines the available asset configuration.
|
||||
type Asset struct {
|
||||
Path string `yaml:"asset" env:"IDP_ASSET_PATH" desc:"Serve IDP assets from a path on the filesystem instead of the builtin assets."`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Trusted bool `yaml:"trusted"`
|
||||
Secret string `yaml:"secret"`
|
||||
RedirectURIs []string `yaml:"redirect_uris"`
|
||||
Origins []string `yaml:"origins"`
|
||||
ApplicationType string `yaml:"application_type"`
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
// don't change the order of elements in this struct
|
||||
// it needs to match github.com/libregraph/lico/bootstrap.Settings
|
||||
|
||||
Iss string `yaml:"iss" env:"OCIS_URL;OCIS_OIDC_ISSUER;IDP_ISS" desc:"The OIDC issuer URL to use."`
|
||||
|
||||
IdentityManager string `yaml:"identity_manager" env:"IDP_IDENTITY_MANAGER" desc:"The identity manager implementation to use, defaults to 'ldap', can be changed to 'cs3', 'kc', 'libregraph', 'cookie' or 'guest'."`
|
||||
|
||||
URIBasePath string `yaml:"uri_base_path" env:"IDP_URI_BASE_PATH" desc:"Idp uri base path (defaults to \"\")."`
|
||||
|
||||
SignInURI string `yaml:"sign_in_uri" env:"IDP_SIGN_IN_URI" desc:"Idp sign-in url."`
|
||||
SignedOutURI string `yaml:"signed_out_uri" env:"IDP_SIGN_OUT_URI" desc:"Idp sign-out url."`
|
||||
|
||||
AuthorizationEndpointURI string `yaml:"authorization_endpoint_uri" env:"IDP_ENDPOINT_URI" desc:"Url of IDP endpoint."`
|
||||
EndsessionEndpointURI string `yaml:"-"` // unused, not supported by lico-idp
|
||||
|
||||
Insecure bool `yaml:"insecure" env:"LDAP_INSECURE;IDP_INSECURE" desc:"Allow insecure connections to the user backend (eg. LDAP, CS3 api, ...)."`
|
||||
|
||||
TrustedProxy []string `yaml:"trusted_proxy"` //TODO: how to configure this via env?
|
||||
|
||||
AllowScope []string `yaml:"allow_scope"` // TODO: is this even needed?
|
||||
AllowClientGuests bool `yaml:"allow_client_guests" env:"IDP_ALLOW_CLIENT_GUESTS" desc:"Allow guest clients to access ocis."`
|
||||
AllowDynamicClientRegistration bool `yaml:"allow_dynamic_client_registration" env:"IDP_ALLOW_DYNAMIC_CLIENT_REGISTRATION" desc:"Allow dynamic client registration."`
|
||||
|
||||
EncryptionSecretFile string `yaml:"encrypt_secret_file" env:"IDP_ENCRYPTION_SECRET_FILE" desc:"Path to the encryption secret file, if unset, a new certificate will be autogenerated upon each restart, thus invalidating all existing sessions."`
|
||||
|
||||
Listen string
|
||||
|
||||
IdentifierClientDisabled bool `yaml:"-"` // unused
|
||||
IdentifierClientPath string `yaml:"-"`
|
||||
IdentifierRegistrationConf string `yaml:"-"`
|
||||
IdentifierScopesConf string `yaml:"-"` // unused
|
||||
IdentifierDefaultBannerLogo string
|
||||
IdentifierDefaultSignInPageText string
|
||||
IdentifierDefaultUsernameHintText string
|
||||
IdentifierUILocales []string
|
||||
|
||||
SigningKid string `yaml:"signing_kid" env:"IDP_SIGNING_KID" desc:"Value of the KID (Key ID) field which is used in created tokens to uniquely identify the signing-private-key."`
|
||||
SigningMethod string `yaml:"signing_method" env:"IDP_SIGNING_METHOD" desc:"Signing method of idp requests (e.g. PS256)"`
|
||||
SigningPrivateKeyFiles []string `yaml:"signing_private_key_files" env:"IDP_SIGNING_PRIVATE_KEY_FILES" desc:"Private key files for signing idp requests."`
|
||||
ValidationKeysPath string `yaml:"validation_keys_path" env:"IDP_VALIDATION_KEYS_PATH" desc:"Path to validation keys for idp requests."`
|
||||
|
||||
CookieBackendURI string
|
||||
CookieNames []string
|
||||
|
||||
AccessTokenDurationSeconds uint64 `yaml:"access_token_duration_seconds" env:"IDP_ACCESS_TOKEN_EXPIRATION" desc:"Expiration time for idp access token (in seconds)."`
|
||||
IDTokenDurationSeconds uint64 `yaml:"id_token_duration_seconds" env:"IDP_ID_TOKEN_EXPIRATION" desc:"Expiration time for idp id tokens (in seconds)."`
|
||||
RefreshTokenDurationSeconds uint64 `yaml:"refresh_token_duration_seconds" env:"IDP_REFRESH_TOKEN_EXPIRATION" desc:"Expiration time for refresh tokens (in seconds)."`
|
||||
DyamicClientSecretDurationSeconds uint64 `yaml:"dynamic_client_secret_duration_seconds" env:"IDP_DYNAMIC_CLIENT_SECRET_DURATION" desc:"Expiration time for dynamic clients (in seconds)."`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"IDP_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."`
|
||||
Token string `yaml:"token" env:"IDP_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint"`
|
||||
Pprof bool `yaml:"pprof" env:"IDP_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling"`
|
||||
Zpages bool `yaml:"zpages" env:"IDP_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."`
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:9134",
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:9130",
|
||||
Root: "/",
|
||||
Namespace: "com.owncloud.web",
|
||||
TLSCert: filepath.Join(defaults.BaseDataPath(), "idp", "server.crt"),
|
||||
TLSKey: filepath.Join(defaults.BaseDataPath(), "idp", "server.key"),
|
||||
TLS: false,
|
||||
},
|
||||
Reva: &config.Reva{
|
||||
Address: "127.0.0.1:9142",
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "idp",
|
||||
},
|
||||
IDP: config.Settings{
|
||||
Iss: "https://localhost:9200",
|
||||
IdentityManager: "ldap",
|
||||
URIBasePath: "",
|
||||
SignInURI: "",
|
||||
SignedOutURI: "",
|
||||
AuthorizationEndpointURI: "",
|
||||
EndsessionEndpointURI: "",
|
||||
Insecure: false,
|
||||
TrustedProxy: nil,
|
||||
AllowScope: nil,
|
||||
AllowClientGuests: false,
|
||||
AllowDynamicClientRegistration: false,
|
||||
EncryptionSecretFile: filepath.Join(defaults.BaseDataPath(), "idp", "encryption.key"),
|
||||
Listen: "",
|
||||
IdentifierClientDisabled: true,
|
||||
IdentifierClientPath: filepath.Join(defaults.BaseDataPath(), "idp"),
|
||||
IdentifierRegistrationConf: filepath.Join(defaults.BaseDataPath(), "idp", "tmp", "identifier-registration.yaml"),
|
||||
IdentifierScopesConf: "",
|
||||
IdentifierDefaultBannerLogo: "",
|
||||
IdentifierDefaultSignInPageText: "",
|
||||
IdentifierDefaultUsernameHintText: "",
|
||||
SigningKid: "private-key",
|
||||
SigningMethod: "PS256",
|
||||
SigningPrivateKeyFiles: []string{filepath.Join(defaults.BaseDataPath(), "idp", "private-key.pem")},
|
||||
ValidationKeysPath: "",
|
||||
CookieBackendURI: "",
|
||||
CookieNames: nil,
|
||||
AccessTokenDurationSeconds: 60 * 60 * 24, // 1 day
|
||||
IDTokenDurationSeconds: 60 * 60, // 1 hour
|
||||
RefreshTokenDurationSeconds: 60 * 60 * 24 * 365 * 3, // 1 year
|
||||
DyamicClientSecretDurationSeconds: 0,
|
||||
},
|
||||
Clients: []config.Client{
|
||||
{
|
||||
ID: "web",
|
||||
Name: "ownCloud Web app",
|
||||
Trusted: true,
|
||||
RedirectURIs: []string{
|
||||
"{{OCIS_URL}}/",
|
||||
"{{OCIS_URL}}/oidc-callback.html",
|
||||
"{{OCIS_URL}}/oidc-silent-redirect.html",
|
||||
},
|
||||
Origins: []string{
|
||||
"{{OCIS_URL}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "ocis-explorer.js",
|
||||
Name: "oCIS Graph Explorer",
|
||||
Trusted: true,
|
||||
RedirectURIs: []string{
|
||||
"{{OCIS_URL}}/graph-explorer/",
|
||||
},
|
||||
Origins: []string{
|
||||
"{{OCIS_URL}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69",
|
||||
Secret: "UBntmLjC2yYCeHwsyj73Uwo9TAaecAetRwMw0xYcvNL9yRdLSUi0hUAHfvCHFeFh",
|
||||
Name: "ownCloud desktop app",
|
||||
ApplicationType: "native",
|
||||
RedirectURIs: []string{
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "e4rAsNUSIUs0lF4nbv9FmCeUkTlV9GdgTLDH1b5uie7syb90SzEVrbN7HIpmWJeD",
|
||||
Secret: "dInFYGV33xKzhbRmpqQltYNdfLdJIfJ9L5ISoKhNoT9qZftpdWSP71VrpGR9pmoD",
|
||||
Name: "ownCloud Android app",
|
||||
ApplicationType: "native",
|
||||
RedirectURIs: []string{
|
||||
"oc://android.owncloud.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "mxd5OQDk6es5LzOzRvidJNfXLUZS2oN3oUFeXPP8LpPrhx3UroJFduGEYIBOxkY1",
|
||||
Secret: "KFeFWWEZO9TkisIQzR3fo7hfiMXlOpaqP8CFuTbSHzV1TUuGECglPxpiVKJfOXIx",
|
||||
Name: "ownCloud iOS app",
|
||||
ApplicationType: "native",
|
||||
RedirectURIs: []string{
|
||||
"oc://ios.owncloud.com",
|
||||
"oc.ios://ios.owncloud.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
Ldap: config.Ldap{
|
||||
URI: "ldaps://localhost:9235",
|
||||
TLSCACert: filepath.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
|
||||
BindDN: "uid=idp,ou=sysusers,o=libregraph-idm",
|
||||
BaseDN: "ou=users,o=libregraph-idm",
|
||||
Scope: "sub",
|
||||
LoginAttribute: "uid",
|
||||
EmailAttribute: "mail",
|
||||
NameAttribute: "displayName",
|
||||
UUIDAttribute: "uid",
|
||||
UUIDAttributeType: "text",
|
||||
Filter: "",
|
||||
ObjectClass: "inetOrgPerson",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
|
||||
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
|
||||
cfg.Log = &config.Log{
|
||||
Level: cfg.Commons.Log.Level,
|
||||
Pretty: cfg.Commons.Log.Pretty,
|
||||
Color: cfg.Commons.Log.Color,
|
||||
File: cfg.Commons.Log.File,
|
||||
}
|
||||
} else if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
Enabled: cfg.Commons.Tracing.Enabled,
|
||||
Type: cfg.Commons.Tracing.Type,
|
||||
Endpoint: cfg.Commons.Tracing.Endpoint,
|
||||
Collector: cfg.Commons.Tracing.Collector,
|
||||
}
|
||||
} else if cfg.Tracing == nil {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
|
||||
if cfg.Reva == nil && cfg.Commons != nil && cfg.Commons.Reva != nil {
|
||||
cfg.Reva = &config.Reva{
|
||||
Address: cfg.Commons.Reva.Address,
|
||||
}
|
||||
} else if cfg.Reva == nil {
|
||||
cfg.Reva = &config.Reva{}
|
||||
}
|
||||
|
||||
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
|
||||
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
|
||||
}
|
||||
}
|
||||
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
if cfg.HTTP.Root != "/" {
|
||||
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"IDP_HTTP_ADDR" desc:"The bind address of the HTTP service."`
|
||||
Root string `yaml:"root" env:"IDP_HTTP_ROOT" desc:"The root path of the HTTP service."`
|
||||
Namespace string `yaml:"-"`
|
||||
TLSCert string `yaml:"tls_cert" env:"IDP_TRANSPORT_TLS_CERT"`
|
||||
TLSKey string `yaml:"tls_key" env:"IDP_TRANSPORT_TLS_KEY"`
|
||||
TLS bool `yaml:"tls" env:"IDP_TLS"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `yaml:"level" env:"OCIS_LOG_LEVEL;IDP_LOG_LEVEL" desc:"The log level. Valid values are: \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"."`
|
||||
Pretty bool `yaml:"pretty" env:"OCIS_LOG_PRETTY;IDP_LOG_PRETTY" desc:"Activates pretty log output."`
|
||||
Color bool `yaml:"color" env:"OCIS_LOG_COLOR;IDP_LOG_COLOR" desc:"Activates colorized log output."`
|
||||
File string `yaml:"file" env:"OCIS_LOG_FILE;IDP_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config/defaults"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return Validate(cfg)
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
switch cfg.IDP.IdentityManager {
|
||||
case "cs3":
|
||||
if cfg.MachineAuthAPIKey == "" {
|
||||
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
|
||||
}
|
||||
case "ldap":
|
||||
if cfg.Ldap.BindPassword == "" {
|
||||
return shared.MissingLDAPBindPassword(cfg.Service.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Reva defines all available REVA configuration.
|
||||
type Reva struct {
|
||||
Address string `yaml:"address" env:"REVA_GATEWAY" desc:"CS3 gateway used to authenticate and look up users"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
PasswordResetURI string `yaml:"password_reset_uri" env:"IDP_PASSWORD_RESET_URI" desc:"The URI where a user can reset their password."`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Tracing defines the available tracing configuration.
|
||||
type Tracing struct {
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;IDP_TRACING_ENABLED" desc:"Activates tracing."`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;IDP_TRACING_TYPE" desc:"The type of tracing. Defaults to \"\", which is the same as \"jaeger\". Allowed tracing types are \"jaeger\" and \"\" as of now."`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;IDP_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;IDP_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// LoggerFromConfig initializes a service-specific logger instance.
|
||||
func Configure(name string, cfg *config.Log) log.Logger {
|
||||
return log.NewLogger(
|
||||
log.Name(name),
|
||||
log.Level(cfg.Level),
|
||||
log.Pretty(cfg.Pretty),
|
||||
log.Color(cfg.Color),
|
||||
log.File(cfg.File),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Namespace defines the namespace for the defines metrics.
|
||||
Namespace = "ocis"
|
||||
|
||||
// Subsystem defines the subsystem for the defines metrics.
|
||||
Subsystem = "idp"
|
||||
)
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
// Counter *prometheus.CounterVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New() *Metrics {
|
||||
m := &Metrics{
|
||||
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
// Namespace: Namespace,
|
||||
// Subsystem: Subsystem,
|
||||
// Name: "greet_total",
|
||||
// Help: "How many greeting requests processed",
|
||||
// }, []string{}),
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "build_info",
|
||||
Help: "Build Information",
|
||||
}, []string{"version"}),
|
||||
}
|
||||
|
||||
// prometheus.Register(
|
||||
// m.Counter,
|
||||
// )
|
||||
|
||||
_ = prometheus.Register(
|
||||
m.BuildInfo,
|
||||
)
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
idpTracing "github.com/owncloud/ocis/v2/services/idp/pkg/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
// Static is a middleware that serves static assets.
|
||||
func Static(root string, fs http.FileSystem) func(http.Handler) http.Handler {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root = root + "/"
|
||||
}
|
||||
|
||||
static := http.StripPrefix(
|
||||
root,
|
||||
http.FileServer(
|
||||
fs,
|
||||
),
|
||||
)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := idpTracing.TraceProvider.Tracer("idp").Start(r.Context(), "serve static asset")
|
||||
defer span.End()
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// serve the static assets for the identifier web app
|
||||
if strings.HasPrefix(r.URL.Path, "/signin/v1/static/") {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// but no listing of folders
|
||||
span.SetAttributes(attribute.KeyValue{Key: "path", Value: attribute.StringValue(r.URL.Path)})
|
||||
span.SetStatus(codes.Error, "asset not found")
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, "/signin/v1/static/", "/signin/v1/identifier/static/", 1)
|
||||
span.SetAttributes(attribute.KeyValue{Key: "server", Value: attribute.StringValue(r.URL.Path)})
|
||||
static.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
span.SetAttributes(attribute.KeyValue{Key: "path", Value: attribute.StringValue(r.URL.Path)})
|
||||
span.SetStatus(codes.Ok, "ok")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// Server initializes the debug service and server.
|
||||
func Server(opts ...Option) (*http.Server, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return debug.NewService(
|
||||
debug.Logger(options.Logger),
|
||||
debug.Name(options.Config.Service.Name),
|
||||
debug.Version(version.GetString()),
|
||||
debug.Address(options.Config.Debug.Addr),
|
||||
debug.Token(options.Config.Debug.Token),
|
||||
debug.Pprof(options.Config.Debug.Pprof),
|
||||
debug.Zpages(options.Config.Debug.Zpages),
|
||||
debug.Health(health(options.Config, options.Logger)),
|
||||
debug.Ready(ready(options.Config)),
|
||||
), nil
|
||||
}
|
||||
|
||||
// health implements the health check.
|
||||
func health(cfg *config.Config, l log.Logger) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
targetHost, err := url.Parse(cfg.Ldap.URI)
|
||||
if err != nil {
|
||||
l.Fatal().Err(err).Str("uri", cfg.Ldap.URI).Msg("invalid LDAP URI")
|
||||
}
|
||||
err = shared.RunChecklist(shared.TCPConnect(targetHost.Host))
|
||||
retVal := http.StatusOK
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Healtcheck failed")
|
||||
retVal = http.StatusInternalServerError
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(retVal)
|
||||
|
||||
_, err = io.WriteString(w, http.StatusText(retVal))
|
||||
if err != nil {
|
||||
l.Fatal().Err(err).Msg("Could not write health check body")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ready implements the ready check.
|
||||
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// if we can call this function, a http(200) is a valid response as
|
||||
// there is nothing we can check at this point for IDP
|
||||
// if there is a mishap when initializing, there is a minimal (talking ms or ns window)
|
||||
// timeframe where this code is callable
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/metrics"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Namespace string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []cli.Flag
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = val
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(val []cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, val...)
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the namespace option.
|
||||
func Namespace(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"os"
|
||||
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
pkgcrypto "github.com/owncloud/ocis/v2/ocis-pkg/crypto"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
svc "github.com/owncloud/ocis/v2/services/idp/pkg/service/v0"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if options.Config.HTTP.TLS {
|
||||
_, certErr := os.Stat(options.Config.HTTP.TLSCert)
|
||||
_, keyErr := os.Stat(options.Config.HTTP.TLSKey)
|
||||
|
||||
if os.IsNotExist(certErr) || os.IsNotExist(keyErr) {
|
||||
options.Logger.Info().Msgf("Generating certs")
|
||||
if err := pkgcrypto.GenCert(options.Config.HTTP.TLSCert, options.Config.HTTP.TLSKey, options.Logger); err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("Could not setup TLS")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
cer, err := tls.LoadX509KeyPair(options.Config.HTTP.TLSCert, options.Config.HTTP.TLSKey)
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("Could not setup TLS")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cer}}
|
||||
}
|
||||
|
||||
service := http.NewService(
|
||||
http.Logger(options.Logger),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Name(options.Config.Service.Name),
|
||||
http.Version(version.GetString()),
|
||||
http.Address(options.Config.HTTP.Addr),
|
||||
http.Context(options.Context),
|
||||
http.Flags(options.Flags...),
|
||||
http.TLSConfig(tlsConfig),
|
||||
)
|
||||
|
||||
handle := svc.NewService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(
|
||||
chimiddleware.RealIP,
|
||||
chimiddleware.RequestID,
|
||||
middleware.TraceContext,
|
||||
middleware.NoCache,
|
||||
middleware.Secure,
|
||||
middleware.Version(
|
||||
options.Config.Service.Name,
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
{
|
||||
handle = svc.NewInstrument(handle, options.Metrics)
|
||||
handle = svc.NewLoggingHandler(handle, options.Logger)
|
||||
}
|
||||
|
||||
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/metrics"
|
||||
)
|
||||
|
||||
// NewInstrument returns a service that instruments metrics.
|
||||
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
|
||||
return instrument{
|
||||
next: next,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
type instrument struct {
|
||||
next Service
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// NewLogging returns a service that logs messages.
|
||||
func NewLoggingHandler(next Service, logger log.Logger) Service {
|
||||
return loggingHandler{
|
||||
next: next,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type loggingHandler struct {
|
||||
next Service
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (l loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware provides a function to set the middleware option.
|
||||
func Middleware(val ...func(http.Handler) http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Middleware = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/libregraph/lico/bootstrap"
|
||||
guestBackendSupport "github.com/libregraph/lico/bootstrap/backends/guest"
|
||||
kcBackendSupport "github.com/libregraph/lico/bootstrap/backends/kc"
|
||||
ldapBackendSupport "github.com/libregraph/lico/bootstrap/backends/ldap"
|
||||
libreGraphBackendSupport "github.com/libregraph/lico/bootstrap/backends/libregraph"
|
||||
licoconfig "github.com/libregraph/lico/config"
|
||||
"github.com/libregraph/lico/server"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ldap"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/assets"
|
||||
cs3BackendSupport "github.com/owncloud/ocis/v2/services/idp/pkg/backends/cs3/bootstrap"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/middleware"
|
||||
"gopkg.in/yaml.v2"
|
||||
"stash.kopano.io/kgol/rndm"
|
||||
)
|
||||
|
||||
// Service defines the extension handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// NewService returns a service implementation for Service.
|
||||
func NewService(opts ...Option) Service {
|
||||
ctx := context.Background()
|
||||
options := newOptions(opts...)
|
||||
logger := options.Logger.Logger
|
||||
assetVFS := assets.New(
|
||||
assets.Logger(options.Logger),
|
||||
assets.Config(options.Config),
|
||||
)
|
||||
|
||||
if err := createTemporaryClientsConfig(
|
||||
options.Config.IDP.IdentifierRegistrationConf,
|
||||
options.Config.IDP.Iss,
|
||||
options.Config.Clients,
|
||||
); err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not create default config")
|
||||
}
|
||||
|
||||
switch options.Config.IDP.IdentityManager {
|
||||
case "cs3":
|
||||
cs3BackendSupport.MustRegister()
|
||||
if err := initCS3EnvVars(options.Config.Reva.Address, options.Config.MachineAuthAPIKey); err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not initialize cs3 backend env vars")
|
||||
}
|
||||
case "ldap":
|
||||
|
||||
if err := ldap.WaitForCA(options.Logger, options.Config.IDP.Insecure, options.Config.Ldap.TLSCACert); err != nil {
|
||||
logger.Fatal().Err(err).Msg("The configured LDAP CA cert does not exist")
|
||||
}
|
||||
if options.Config.IDP.Insecure {
|
||||
// force CACert to be empty to avoid lico try to load it
|
||||
options.Config.Ldap.TLSCACert = ""
|
||||
}
|
||||
|
||||
ldapBackendSupport.MustRegister()
|
||||
if err := initLicoInternalLDAPEnvVars(&options.Config.Ldap); err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not initialize ldap env vars")
|
||||
}
|
||||
default:
|
||||
guestBackendSupport.MustRegister()
|
||||
kcBackendSupport.MustRegister()
|
||||
libreGraphBackendSupport.MustRegister()
|
||||
}
|
||||
|
||||
// https://play.golang.org/p/Mh8AVJCd593
|
||||
idpSettings := bootstrap.Settings(options.Config.IDP)
|
||||
bs, err := bootstrap.Boot(ctx, &idpSettings, &licoconfig.Config{
|
||||
Logger: log.LogrusWrap(logger),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not bootstrap idp")
|
||||
}
|
||||
|
||||
managers := bs.Managers()
|
||||
routes := []server.WithRoutes{managers.Must("identity").(server.WithRoutes)}
|
||||
handlers := managers.Must("handler").(http.Handler)
|
||||
|
||||
svc := IDP{
|
||||
logger: options.Logger,
|
||||
config: options.Config,
|
||||
assets: assetVFS,
|
||||
}
|
||||
|
||||
svc.initMux(ctx, routes, handlers, options)
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
type temporaryClientConfig struct {
|
||||
Clients []config.Client `yaml:"clients"`
|
||||
}
|
||||
|
||||
func createTemporaryClientsConfig(filePath, ocisURL string, clients []config.Client) error {
|
||||
|
||||
folder := path.Dir(filePath)
|
||||
if _, err := os.Stat(folder); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(folder, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, client := range clients {
|
||||
|
||||
for i, entry := range client.RedirectURIs {
|
||||
client.RedirectURIs[i] = strings.ReplaceAll(entry, "{{OCIS_URL}}", strings.TrimRight(ocisURL, "/"))
|
||||
}
|
||||
for i, entry := range client.Origins {
|
||||
client.Origins[i] = strings.ReplaceAll(entry, "{{OCIS_URL}}", strings.TrimRight(ocisURL, "/"))
|
||||
}
|
||||
clients[i] = client
|
||||
}
|
||||
|
||||
c := temporaryClientConfig{
|
||||
Clients: clients,
|
||||
}
|
||||
|
||||
conf, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confOnDisk, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer confOnDisk.Close()
|
||||
|
||||
err = ioutil.WriteFile(filePath, conf, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Init cs3 backend vars which are currently not accessible via idp api
|
||||
func initCS3EnvVars(cs3Addr, machineAuthAPIKey string) error {
|
||||
var defaults = map[string]string{
|
||||
"CS3_GATEWAY": cs3Addr,
|
||||
"CS3_MACHINE_AUTH_API_KEY": machineAuthAPIKey,
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("could not set cs3 env var %s=%s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init ldap backend vars which are currently not accessible via idp api
|
||||
func initLicoInternalLDAPEnvVars(ldap *config.Ldap) error {
|
||||
filter := fmt.Sprintf("(objectclass=%s)", ldap.ObjectClass)
|
||||
if ldap.Filter != "" {
|
||||
filter = fmt.Sprintf("(&%s%s)", ldap.Filter, filter)
|
||||
}
|
||||
var defaults = map[string]string{
|
||||
"LDAP_URI": ldap.URI,
|
||||
"LDAP_BINDDN": ldap.BindDN,
|
||||
"LDAP_BINDPW": ldap.BindPassword,
|
||||
"LDAP_BASEDN": ldap.BaseDN,
|
||||
"LDAP_SCOPE": ldap.Scope,
|
||||
"LDAP_LOGIN_ATTRIBUTE": ldap.LoginAttribute,
|
||||
"LDAP_EMAIL_ATTRIBUTE": ldap.EmailAttribute,
|
||||
"LDAP_NAME_ATTRIBUTE": ldap.NameAttribute,
|
||||
"LDAP_UUID_ATTRIBUTE": ldap.UUIDAttribute,
|
||||
"LDAP_UUID_ATTRIBUTE_TYPE": ldap.UUIDAttributeType,
|
||||
"LDAP_FILTER": filter,
|
||||
}
|
||||
|
||||
if ldap.TLSCACert != "" {
|
||||
defaults["LDAP_TLS_CACERT"] = ldap.TLSCACert
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("could not set ldap env var %s=%s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDP defines implements the business logic for Service.
|
||||
type IDP struct {
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
mux *chi.Mux
|
||||
assets http.FileSystem
|
||||
}
|
||||
|
||||
// initMux initializes the internal idp gorilla mux and mounts it in to a ocis chi-router
|
||||
func (idp *IDP) initMux(ctx context.Context, r []server.WithRoutes, h http.Handler, options Options) {
|
||||
gm := mux.NewRouter()
|
||||
for _, route := range r {
|
||||
route.AddRoutes(ctx, gm)
|
||||
}
|
||||
|
||||
// Delegate rest to provider which is also a handler.
|
||||
if h != nil {
|
||||
gm.NotFoundHandler = h
|
||||
}
|
||||
|
||||
idp.mux = chi.NewMux()
|
||||
idp.mux.Use(options.Middleware...)
|
||||
|
||||
idp.mux.Use(middleware.Static(
|
||||
"/signin/v1/",
|
||||
assets.New(
|
||||
assets.Logger(options.Logger),
|
||||
assets.Config(options.Config),
|
||||
),
|
||||
))
|
||||
|
||||
// handle / | index.html with a template that needs to have the BASE_PREFIX replaced
|
||||
idp.mux.Get("/signin/v1/identifier", idp.Index())
|
||||
idp.mux.Get("/signin/v1/identifier/", idp.Index())
|
||||
idp.mux.Get("/signin/v1/identifier/index.html", idp.Index())
|
||||
|
||||
idp.mux.Mount("/", gm)
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (idp IDP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
idp.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Index renders the static html with the
|
||||
func (idp IDP) Index() http.HandlerFunc {
|
||||
|
||||
f, err := idp.assets.Open("/identifier/index.html")
|
||||
if err != nil {
|
||||
idp.logger.Fatal().Err(err).Msg("Could not open index template")
|
||||
}
|
||||
|
||||
template, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
idp.logger.Fatal().Err(err).Msg("Could not read index template")
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
idp.logger.Fatal().Err(err).Msg("Could not close body")
|
||||
}
|
||||
|
||||
// TODO add environment variable to make the path prefix configurable
|
||||
pp := "/signin/v1"
|
||||
indexHTML := bytes.Replace(template, []byte("__PATH_PREFIX__"), []byte(pp), 1)
|
||||
|
||||
nonce := rndm.GenerateRandomString(32)
|
||||
indexHTML = bytes.Replace(indexHTML, []byte("__CSP_NONCE__"), []byte(nonce), 1)
|
||||
|
||||
indexHTML = bytes.Replace(indexHTML, []byte("__PASSWORD_RESET_LINK__"), []byte(idp.config.Service.PasswordResetURI), 1)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(indexHTML); err != nil {
|
||||
idp.logger.Error().Err(err).Msg("could not write to response writer")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
)
|
||||
|
||||
// NewTracing returns a service that instruments traces.
|
||||
func NewTracing(next Service) Service {
|
||||
return tracing{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
type tracing struct {
|
||||
next Service
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.TraceContext(t.next).ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// TraceProvider is the global trace provider for the idp service.
|
||||
TraceProvider = trace.NewNoopTracerProvider()
|
||||
)
|
||||
|
||||
func Configure(cfg *config.Config) error {
|
||||
var err error
|
||||
if cfg.Tracing.Enabled {
|
||||
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user