Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,53 @@
/*
* Copyright 2017-2019 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 bsguest
import (
"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/managers"
)
// Identity managers.
const (
identityManagerName = "guest"
)
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) {
config := bs.Config()
logger := config.Config.Logger
identityManagerConfig := &identity.Config{
Logger: logger,
}
guestIdentityManager := managers.NewGuestIdentityManager(identityManagerConfig)
return guestIdentityManager, nil
}
@@ -0,0 +1,175 @@
/*
* Copyright 2017-2019 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 bsldap
import (
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identifier/backends/ldap"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/managers"
)
// Identity managers.
const (
identityManagerName = "ldap"
)
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) {
config := bs.Config()
logger := config.Config.Logger
if config.AuthorizationEndpointURI.String() != "" {
return nil, fmt.Errorf("ldap 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("ldap 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")
}
// Default LDAP attribute mappings.
attributeMapping := map[string]string{
ldap.AttributeLogin: os.Getenv("LDAP_LOGIN_ATTRIBUTE"),
ldap.AttributeEmail: os.Getenv("LDAP_EMAIL_ATTRIBUTE"),
ldap.AttributeName: os.Getenv("LDAP_NAME_ATTRIBUTE"),
ldap.AttributeFamilyName: os.Getenv("LDAP_FAMILY_NAME_ATTRIBUTE"),
ldap.AttributeGivenName: os.Getenv("LDAP_GIVEN_NAME_ATTRIBUTE"),
ldap.AttributeUUID: os.Getenv("LDAP_UUID_ATTRIBUTE"),
fmt.Sprintf("%s_type", ldap.AttributeUUID): os.Getenv("LDAP_UUID_ATTRIBUTE_TYPE"),
}
// Add optional LDAP attribute mappings.
if numericUIDAttribute := os.Getenv("LDAP_UIDNUMBER_ATTRIBUTE"); numericUIDAttribute != "" {
attributeMapping[ldap.AttributeNumericUID] = numericUIDAttribute
}
// Sub from LDAP attribute mappings.
var subMapping []string
if subMappingString := os.Getenv("LDAP_SUB_ATTRIBUTES"); subMappingString != "" {
subMapping = strings.Split(subMappingString, " ")
}
// Use a clone here to avoid changing the config of other possible users of the config.
tlsConfig := config.TLSClientConfig.Clone()
if caCertFile := os.Getenv("LDAP_TLS_CACERT"); caCertFile != "" {
if pemBytes, err := ioutil.ReadFile(caCertFile); err == nil {
rpool, _ := x509.SystemCertPool()
if rpool.AppendCertsFromPEM(pemBytes) {
tlsConfig.RootCAs = rpool
} else {
return nil, fmt.Errorf("failed to append CA certificate(s) from '%s' to pool", caCertFile)
}
} else {
return nil, fmt.Errorf("failed to read CA certificate(s) from '%s': %w", caCertFile, err)
}
}
identifierBackend, identifierErr := ldap.NewLDAPIdentifierBackend(
config.Config,
tlsConfig,
os.Getenv("LDAP_URI"),
os.Getenv("LDAP_BINDDN"),
os.Getenv("LDAP_BINDPW"),
os.Getenv("LDAP_BASEDN"),
os.Getenv("LDAP_SCOPE"),
os.Getenv("LDAP_FILTER"),
subMapping,
attributeMapping,
)
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,
ScopesConf: config.IdentifierScopesConf,
WebAppDisabled: config.IdentifierClientDisabled,
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
LogonCookieSameSite: config.CookieSameSite,
ConsentCookieSameSite: config.CookieSameSite,
StateCookieSameSite: config.CookieSameSite,
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
SignedOutEndpointURI: fullSignedOutEndpointURL,
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultSignInPageLogoURI: config.IdentifierDefaultLogoTargetURI,
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,158 @@
/*
* 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 bslibregraph
import (
"fmt"
"os"
"strings"
"github.com/cevaris/ordered_map"
"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identifier/backends/libregraph"
"github.com/libregraph/lico/identity"
identityClients "github.com/libregraph/lico/identity/clients"
"github.com/libregraph/lico/identity/managers"
)
// Identity managers.
const (
identityManagerName = "libregraph"
)
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) {
config := bs.Config()
logger := config.Config.Logger
if config.AuthorizationEndpointURI.String() != "" {
return nil, fmt.Errorf("libregraph 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("libregraph 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")
}
defaultURI := os.Getenv("LIBREGRAPH_URI")
var scopedURIs *ordered_map.OrderedMap
if scopedURIsString := os.Getenv("LIBREGRAPH_SCOPED_URIS"); scopedURIsString != "" {
scopedURIs = ordered_map.NewOrderedMap()
// Format is <scope>:<url>,<scope>:<url>,...
for _, v := range strings.Split(scopedURIsString, ",") {
parts := strings.SplitN(v, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("failed to parse scoped URIs, format invalid")
}
scopedURIs.Set(parts[0], parts[1])
}
}
var clients *identityClients.Registry
if clientsRecord, ok := bs.Managers().Get("clients"); ok {
clients = clientsRecord.(*identityClients.Registry)
} else {
return nil, fmt.Errorf("clients manager not found but is required")
}
identifierBackend, identifierErr := libregraph.NewLibreGraphIdentifierBackend(
config.Config,
config.TLSClientConfig,
defaultURI,
scopedURIs,
clients,
)
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,
ScopesConf: config.IdentifierScopesConf,
WebAppDisabled: config.IdentifierClientDisabled,
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
LogonCookieSameSite: config.CookieSameSite,
ConsentCookieSameSite: config.CookieSameSite,
StateCookieSameSite: config.CookieSameSite,
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
SignedOutEndpointURI: fullSignedOutEndpointURL,
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultSignInPageLogoURI: config.IdentifierDefaultLogoTargetURI,
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
}
+554
View File
@@ -0,0 +1,554 @@
/*
* Copyright 2017-2019 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 (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/encryption"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/managers"
oidcProvider "github.com/libregraph/lico/oidc/provider"
"github.com/libregraph/lico/utils"
)
// API types.
type APIType string
const (
APITypeKonnect APIType = "konnect"
APITypeSignin APIType = "signin"
)
// Defaults.
const (
DefaultSigningKeyID = "default"
DefaultSigningKeyBits = 2048
DefaultGuestIdentityManagerName = "guest"
DefaultCookieSameSite = http.SameSiteNoneMode
)
// Bootstrap is a data structure to hold configuration required to start
// konnectd.
type Bootstrap interface {
Config() *Config
Managers() *managers.Managers
MakeURIPath(api APIType, subpath string) string
}
// Implementation of the bootstrap interface.
type bootstrap struct {
config *Config
uriBasePath string
managers *managers.Managers
}
// Config returns the bootstap configuration.
func (bs *bootstrap) Config() *Config {
return bs.config
}
// Managers returns bootstrapped identity-managers.
func (bs *bootstrap) Managers() *managers.Managers {
return bs.managers
}
// Boot is the main entry point to bootstrap the service after validating the
// given configuration. The resulting Bootstrap struct can be used to retrieve
// configured identity-managers and their respective http-handlers and config.
//
// This function should be used by consumers which want to embed this project
// as a library.
func Boot(ctx context.Context, settings *Settings, cfg *config.Config) (Bootstrap, error) {
// NOTE(longsleep): Ensure to use same salt length as the hash size.
// See https://www.ietf.org/mail-archive/web/jose/current/msg02901.html for
// reference and https://github.com/golang-jwt/jwt/v4/issues/285 for
// the issue in upstream jwt-go.
for _, alg := range []string{jwt.SigningMethodPS256.Name, jwt.SigningMethodPS384.Name, jwt.SigningMethodPS512.Name} {
sm := jwt.GetSigningMethod(alg)
if signingMethodRSAPSS, ok := sm.(*jwt.SigningMethodRSAPSS); ok {
signingMethodRSAPSS.Options.SaltLength = rsa.PSSSaltLengthEqualsHash
}
}
bs := &bootstrap{
config: &Config{
Config: cfg,
Settings: settings,
},
}
err := bs.initialize(settings)
if err != nil {
return nil, err
}
err = bs.setup(ctx, settings)
if err != nil {
return nil, err
}
return bs, nil
}
// initialize, parsed parameters from commandline with validation and adds them
// to the associated Bootstrap data.
func (bs *bootstrap) initialize(settings *Settings) error {
logger := bs.config.Config.Logger
var err error
if settings.IdentityManager == "" {
return fmt.Errorf("identity-manager argument missing, use one of kc, ldap, cookie, dummy")
}
bs.config.IssuerIdentifierURI, err = url.Parse(settings.Iss)
if err != nil {
return fmt.Errorf("invalid iss value, iss is not a valid URL), %v", err)
} else if settings.Iss == "" {
return fmt.Errorf("missing iss value, did you provide the --iss parameter?")
} else if bs.config.IssuerIdentifierURI.Scheme != "https" {
return fmt.Errorf("invalid iss value, URL must start with https://")
} else if bs.config.IssuerIdentifierURI.Host == "" {
return fmt.Errorf("invalid iss value, URL must have a host")
}
bs.uriBasePath = settings.URIBasePath
bs.config.SignInFormURI, err = url.Parse(settings.SignInURI)
if err != nil {
return fmt.Errorf("invalid sign-in URI, %v", err)
}
bs.config.SignedOutURI, err = url.Parse(settings.SignedOutURI)
if err != nil {
return fmt.Errorf("invalid signed-out URI, %v", err)
}
bs.config.AuthorizationEndpointURI, err = url.Parse(settings.AuthorizationEndpointURI)
if err != nil {
return fmt.Errorf("invalid authorization-endpoint-uri, %v", err)
}
bs.config.EndSessionEndpointURI, err = url.Parse(settings.EndsessionEndpointURI)
if err != nil {
return fmt.Errorf("invalid endsession-endpoint-uri, %v", err)
}
if settings.Insecure {
// NOTE(longsleep): This disable http2 client support. See https://github.com/golang/go/issues/14275 for reasons.
bs.config.TLSClientConfig = utils.InsecureSkipVerifyTLSConfig()
logger.Warnln("insecure mode, TLS client connections are susceptible to man-in-the-middle attacks")
} else {
bs.config.TLSClientConfig = utils.DefaultTLSConfig()
}
for _, trustedProxy := range settings.TrustedProxy {
if ip := net.ParseIP(trustedProxy); ip != nil {
bs.config.Config.TrustedProxyIPs = append(bs.config.Config.TrustedProxyIPs, &ip)
continue
}
if _, ipNet, errParseCIDR := net.ParseCIDR(trustedProxy); errParseCIDR == nil {
bs.config.Config.TrustedProxyNets = append(bs.config.Config.TrustedProxyNets, ipNet)
continue
}
}
if len(bs.config.Config.TrustedProxyIPs) > 0 {
logger.Infoln("trusted proxy IPs", bs.config.Config.TrustedProxyIPs)
}
if len(bs.config.Config.TrustedProxyNets) > 0 {
logger.Infoln("trusted proxy networks", bs.config.Config.TrustedProxyNets)
}
if len(settings.AllowScope) > 0 {
bs.config.Config.AllowedScopes = settings.AllowScope
logger.Infoln("using custom allowed OAuth 2 scopes", bs.config.Config.AllowedScopes)
}
bs.config.Config.AllowClientGuests = settings.AllowClientGuests
if bs.config.Config.AllowClientGuests {
logger.Infoln("client controlled guests are enabled")
}
bs.config.Config.AllowDynamicClientRegistration = settings.AllowDynamicClientRegistration
if bs.config.Config.AllowDynamicClientRegistration {
logger.Infoln("dynamic client registration is enabled")
}
encryptionSecretFn := settings.EncryptionSecretFile
if encryptionSecretFn != "" {
logger.WithField("file", encryptionSecretFn).Infoln("loading encryption secret from file")
bs.config.EncryptionSecret, err = ioutil.ReadFile(encryptionSecretFn)
if err != nil {
return fmt.Errorf("failed to load encryption secret from file: %v", err)
}
if len(bs.config.EncryptionSecret) != encryption.KeySize {
return fmt.Errorf("invalid encryption secret size - must be %d bytes", encryption.KeySize)
}
} else {
logger.Warnf("missing --encryption-secret parameter, using random encyption secret with %d bytes", encryption.KeySize)
bs.config.EncryptionSecret = rndm.GenerateRandomBytes(encryption.KeySize)
}
bs.config.Config.ListenAddr = settings.Listen
bs.config.IdentifierClientDisabled = settings.IdentifierClientDisabled
bs.config.IdentifierClientPath = settings.IdentifierClientPath
bs.config.IdentifierRegistrationConf = settings.IdentifierRegistrationConf
if bs.config.IdentifierRegistrationConf != "" {
bs.config.IdentifierRegistrationConf, _ = filepath.Abs(bs.config.IdentifierRegistrationConf)
if _, errStat := os.Stat(bs.config.IdentifierRegistrationConf); errStat != nil {
return fmt.Errorf("identifier-registration-conf file not found or unable to access: %v", errStat)
}
bs.config.IdentifierAuthoritiesConf = bs.config.IdentifierRegistrationConf
}
bs.config.IdentifierScopesConf = settings.IdentifierScopesConf
if bs.config.IdentifierScopesConf != "" {
bs.config.IdentifierScopesConf, _ = filepath.Abs(bs.config.IdentifierScopesConf)
if _, errStat := os.Stat(bs.config.IdentifierScopesConf); errStat != nil {
return fmt.Errorf("identifier-scopes-conf file not found or unable to access: %v", errStat)
}
}
if settings.IdentifierDefaultBannerLogo != "" {
// Load from file.
b, errRead := ioutil.ReadFile(settings.IdentifierDefaultBannerLogo)
if errRead != nil {
return fmt.Errorf("identifier-default-banner-logo failed to open: %w", errRead)
}
bs.config.IdentifierDefaultBannerLogo = b
}
if settings.IdentifierDefaultSignInPageText != "" {
bs.config.IdentifierDefaultSignInPageText = &settings.IdentifierDefaultSignInPageText
}
if settings.IdentifierDefaultLogoTargetURI != "" {
bs.config.IdentifierDefaultLogoTargetURI = &settings.IdentifierDefaultLogoTargetURI
}
if settings.IdentifierDefaultUsernameHintText != "" {
bs.config.IdentifierDefaultUsernameHintText = &settings.IdentifierDefaultUsernameHintText
}
bs.config.IdentifierUILocales = settings.IdentifierUILocales
bs.config.SigningKeyID = settings.SigningKid
bs.config.Signers = make(map[string]crypto.Signer)
bs.config.Validators = make(map[string]crypto.PublicKey)
bs.config.Certificates = make(map[string][]*x509.Certificate)
signingMethodString := settings.SigningMethod
bs.config.SigningMethod = jwt.GetSigningMethod(signingMethodString)
if bs.config.SigningMethod == nil {
return fmt.Errorf("unknown signing method: %s", signingMethodString)
}
signingKeyFns := settings.SigningPrivateKeyFiles
if len(signingKeyFns) > 0 {
first := true
for _, signingKeyFn := range signingKeyFns {
logger.WithField("path", signingKeyFn).Infoln("loading signing key")
err = addSignerWithIDFromFile(signingKeyFn, "", bs)
if err != nil {
return err
}
if first {
// Also add key under the provided id.
first = false
err = addSignerWithIDFromFile(signingKeyFn, bs.config.SigningKeyID, bs)
if err != nil {
return err
}
}
}
} else {
//NOTE(longsleep): remove me - create keypair a random key pair.
sm := jwt.SigningMethodPS256
bs.config.SigningMethod = sm
logger.WithField("alg", sm.Name).Warnf("missing --signing-private-key parameter, using random %d bit signing key", DefaultSigningKeyBits)
signer, _ := rsa.GenerateKey(rand.Reader, DefaultSigningKeyBits)
bs.config.Signers[bs.config.SigningKeyID] = signer
}
// Ensure we have a signer for the things we need.
err = validateSigners(bs)
if err != nil {
return err
}
validationKeysPath := settings.ValidationKeysPath
if validationKeysPath != "" {
logger.WithField("path", validationKeysPath).Infoln("loading validation keys")
err = addValidatorsFromPath(validationKeysPath, bs)
if err != nil {
return err
}
}
bs.config.Config.HTTPTransport = utils.HTTPTransportWithTLSClientConfig(bs.config.TLSClientConfig)
bs.config.AccessTokenDurationSeconds = settings.AccessTokenDurationSeconds
if bs.config.AccessTokenDurationSeconds == 0 {
bs.config.AccessTokenDurationSeconds = 60 * 10 // 10 Minutes
}
bs.config.IDTokenDurationSeconds = settings.IDTokenDurationSeconds
if bs.config.IDTokenDurationSeconds == 0 {
bs.config.IDTokenDurationSeconds = 60 * 60 // 1 Hour
}
bs.config.RefreshTokenDurationSeconds = settings.RefreshTokenDurationSeconds
if bs.config.RefreshTokenDurationSeconds == 0 {
bs.config.RefreshTokenDurationSeconds = 60 * 60 * 24 * 365 * 3 // 3 Years
}
bs.config.DyamicClientSecretDurationSeconds = settings.DyamicClientSecretDurationSeconds
// add setting to allow setting the same site attribute of the cookies
bs.config.CookieSameSite = settings.CookieSameSite
if bs.config.CookieSameSite == 0 {
bs.config.CookieSameSite = DefaultCookieSameSite
}
return nil
}
// setup takes care of setting up the managers based on the associated
// Bootstrap's data.
func (bs *bootstrap) setup(ctx context.Context, settings *Settings) error {
managers, err := newManagers(ctx, bs)
if err != nil {
return err
}
bs.managers = managers
identityManager, err := bs.setupIdentity(ctx, settings)
if err != nil {
return err
}
managers.Set("identity", identityManager)
guestManager, err := bs.setupGuest(ctx, identityManager)
if err != nil {
return err
}
managers.Set("guest", guestManager)
oidcProvider, err := bs.setupOIDCProvider(ctx)
if err != nil {
return err
}
managers.Set("oidc", oidcProvider)
managers.Set("handler", oidcProvider) // Use OIDC provider as default HTTP handler.
err = managers.Apply()
if err != nil {
return fmt.Errorf("failed to apply managers: %v", err)
}
// Final steps
err = oidcProvider.InitializeMetadata()
if err != nil {
return fmt.Errorf("failed to initialize provider metadata: %v", err)
}
return nil
}
func (bs *bootstrap) MakeURIPath(api APIType, subpath string) string {
subpath = strings.TrimPrefix(subpath, "/")
uriPath := ""
switch api {
case APITypeKonnect:
uriPath = fmt.Sprintf("%s/konnect/v1/%s", strings.TrimSuffix(bs.uriBasePath, "/"), subpath)
case APITypeSignin:
uriPath = fmt.Sprintf("%s/signin/v1/%s", strings.TrimSuffix(bs.uriBasePath, "/"), subpath)
default:
panic("unknown api type")
}
if subpath == "" {
uriPath = strings.TrimSuffix(uriPath, "/")
}
return uriPath
}
func (bs *bootstrap) MakeURI(api APIType, subpath string) *url.URL {
uriPath := bs.MakeURIPath(api, subpath)
uri, _ := url.Parse(bs.config.IssuerIdentifierURI.String())
uri.Path = uriPath
return uri
}
func (bs *bootstrap) setupIdentity(ctx context.Context, settings *Settings) (identity.Manager, error) {
logger := bs.config.Config.Logger
if settings.IdentityManager == "" {
return nil, fmt.Errorf("identity-manager argument missing")
}
// Identity manager.
identityManagerName := settings.IdentityManager
identityManager, err := getIdentityManagerByName(identityManagerName, bs)
if err != nil {
return nil, err
}
logger.WithFields(logrus.Fields{
"name": identityManagerName,
"scopes": identityManager.ScopesSupported(nil),
"claims": identityManager.ClaimsSupported(nil),
}).Infoln("identity manager set up")
return identityManager, nil
}
func (bs *bootstrap) setupGuest(ctx context.Context, identityManager identity.Manager) (identity.Manager, error) {
if !bs.config.Config.AllowClientGuests {
return nil, nil
}
var err error
logger := bs.config.Config.Logger
guestManager, err := getIdentityManagerByName(DefaultGuestIdentityManagerName, bs)
if err != nil {
return nil, err
}
if guestManager != nil {
logger.Infoln("identity guest manager set up")
}
return guestManager, nil
}
func (bs *bootstrap) setupOIDCProvider(ctx context.Context) (*oidcProvider.Provider, error) {
var err error
logger := bs.config.Config.Logger
sessionCookiePath, err := getCommonURLPathPrefix(bs.config.AuthorizationEndpointURI.EscapedPath(), bs.config.EndSessionEndpointURI.EscapedPath())
if err != nil {
return nil, fmt.Errorf("failed to find common URL prefix for authorize and endsession: %v", err)
}
var registrationPath = ""
if bs.config.Config.AllowDynamicClientRegistration {
registrationPath = bs.MakeURIPath(APITypeKonnect, "/register")
}
provider, err := oidcProvider.NewProvider(&oidcProvider.Config{
Config: bs.config.Config,
IssuerIdentifier: bs.config.IssuerIdentifierURI.String(),
WellKnownPath: "/.well-known/openid-configuration",
JwksPath: bs.MakeURIPath(APITypeKonnect, "/jwks.json"),
AuthorizationPath: bs.config.AuthorizationEndpointURI.EscapedPath(),
TokenPath: bs.MakeURIPath(APITypeKonnect, "/token"),
UserInfoPath: bs.MakeURIPath(APITypeKonnect, "/userinfo"),
EndSessionPath: bs.config.EndSessionEndpointURI.EscapedPath(),
CheckSessionIframePath: bs.MakeURIPath(APITypeKonnect, "/session/check-session.html"),
RegistrationPath: registrationPath,
BrowserStateCookiePath: bs.MakeURIPath(APITypeKonnect, "/session/"),
BrowserStateCookieName: "__Secure-KKBS", // Kopano-Konnect-Browser-State
BrowserStateCookieSameSite: bs.config.CookieSameSite,
SessionCookiePath: sessionCookiePath,
SessionCookieName: "__Secure-KKCS", // Kopano-Konnect-Client-Session
SessionCookieSameSite: bs.config.CookieSameSite,
AccessTokenDuration: time.Duration(bs.config.AccessTokenDurationSeconds) * time.Second,
IDTokenDuration: time.Duration(bs.config.IDTokenDurationSeconds) * time.Second,
RefreshTokenDuration: time.Duration(bs.config.RefreshTokenDurationSeconds) * time.Second,
})
if err != nil {
return nil, fmt.Errorf("failed to create provider: %v", err)
}
if bs.config.SigningMethod != nil {
err = provider.SetSigningMethod(bs.config.SigningMethod)
if err != nil {
return nil, fmt.Errorf("failed to set provider signing method: %v", err)
}
}
// All add signers.
for id, signer := range bs.config.Signers {
if id == bs.config.SigningKeyID {
err = provider.SetSigningKey(id, signer)
// Always set default key.
if id != DefaultSigningKeyID {
provider.SetValidationKey(DefaultSigningKeyID, signer.Public())
}
} else {
// Set non default signers as well.
err = provider.SetSigningKey(id, signer)
}
if err != nil {
return nil, err
}
}
// Add all validators.
for id, publicKey := range bs.config.Validators {
err = provider.SetValidationKey(id, publicKey)
if err != nil {
return nil, err
}
}
// Add all certificates.
for id, certificate := range bs.config.Certificates {
err = provider.SetCertificate(id, certificate)
if err != nil {
return nil, err
}
}
sk, ok := provider.GetSigningKey(bs.config.SigningMethod)
if !ok {
return nil, fmt.Errorf("no signing key for selected signing method")
}
if bs.config.SigningKeyID == "" {
// Ensure that there is a default signing Key ID even if none was set.
provider.SetValidationKey(DefaultSigningKeyID, sk.PrivateKey.Public())
}
logger.WithFields(logrus.Fields{
"id": sk.ID,
"method": fmt.Sprintf("%T", sk.SigningMethod),
"alg": sk.SigningMethod.Alg(),
}).Infoln("oidc token signing default set up")
return provider, nil
}
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright 2017-2019 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 (
"crypto"
"crypto/tls"
"crypto/x509"
"net/http"
"net/url"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/lico/config"
)
// Config is a typed application config which represents the active
// bootstrap configuration.
type Config struct {
Config *config.Config
Settings *Settings
SignInFormURI *url.URL
SignedOutURI *url.URL
AuthorizationEndpointURI *url.URL
EndSessionEndpointURI *url.URL
TLSClientConfig *tls.Config
IssuerIdentifierURI *url.URL
IdentifierClientDisabled bool
IdentifierClientPath string
IdentifierRegistrationConf string
IdentifierAuthoritiesConf string
IdentifierScopesConf string
IdentifierDefaultBannerLogo []byte
IdentifierDefaultSignInPageText *string
IdentifierDefaultLogoTargetURI *string
IdentifierDefaultUsernameHintText *string
IdentifierUILocales []string
EncryptionSecret []byte
SigningMethod jwt.SigningMethod
SigningKeyID string
Signers map[string]crypto.Signer
Validators map[string]crypto.PublicKey
Certificates map[string][]*x509.Certificate
AccessTokenDurationSeconds uint64
IDTokenDurationSeconds uint64
RefreshTokenDurationSeconds uint64
DyamicClientSecretDurationSeconds uint64
CookieSameSite http.SameSite
}
+88
View File
@@ -0,0 +1,88 @@
/*
* Copyright 2017-2019 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 (
"context"
"fmt"
"time"
"github.com/libregraph/lico/identity"
identityAuthorities "github.com/libregraph/lico/identity/authorities"
identityClients "github.com/libregraph/lico/identity/clients"
identityManagers "github.com/libregraph/lico/identity/managers"
"github.com/libregraph/lico/managers"
codeManagers "github.com/libregraph/lico/oidc/code/managers"
)
type IdentityManagerFactory func(Bootstrap) (identity.Manager, error)
var identityManagerRegistry = make(map[string]IdentityManagerFactory)
func RegisterIdentityManager(name string, f IdentityManagerFactory) error {
identityManagerRegistry[name] = f
return nil
}
func getIdentityManagerByName(name string, bs Bootstrap) (identity.Manager, error) {
if f, found := identityManagerRegistry[name]; !found {
return nil, fmt.Errorf("no identity manager with name %s registered", name)
} else {
return f(bs)
}
}
func newManagers(ctx context.Context, bs *bootstrap) (*managers.Managers, error) {
logger := bs.config.Config.Logger
var err error
mgrs := managers.New()
// Encryption manager.
encryption, err := identityManagers.NewEncryptionManager(nil)
if err != nil {
return nil, fmt.Errorf("failed to create encryption manager: %v", err)
}
err = encryption.SetKey(bs.config.EncryptionSecret)
if err != nil {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for encryption: %v", err)
}
mgrs.Set("encryption", encryption)
logger.Infof("encryption set up with %d key size", encryption.GetKeySize())
// OIDC code manage.
code := codeManagers.NewMemoryMapManager(ctx)
mgrs.Set("code", code)
// Identifier client registry manager.
clients, err := identityClients.NewRegistry(ctx, bs.config.IssuerIdentifierURI, bs.config.IdentifierRegistrationConf, bs.config.Config.AllowDynamicClientRegistration, time.Duration(bs.config.DyamicClientSecretDurationSeconds)*time.Second, logger)
if err != nil {
return nil, fmt.Errorf("failed to create client registry: %v", err)
}
mgrs.Set("clients", clients)
// Identifier authorities registry manager.
authorities, err := identityAuthorities.NewRegistry(ctx, bs.MakeURI(APITypeSignin, ""), bs.config.IdentifierAuthoritiesConf, logger)
if err != nil {
return nil, fmt.Errorf("failed to create authorities registry: %v", err)
}
mgrs.Set("authorities", authorities)
return mgrs, nil
}
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright 2017-2019 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 (
"net/http"
)
// Settings is a typed application config which represents the user accessible
// boostrap settings params.
type Settings struct {
Iss string
IdentityManager string
URIBasePath string
SignInURI string
SignedOutURI string
AuthorizationEndpointURI string
EndsessionEndpointURI string
Insecure bool
TrustedProxy []string
AllowScope []string
AllowClientGuests bool
AllowDynamicClientRegistration bool
EncryptionSecretFile string
Listen string
IdentifierClientDisabled bool
IdentifierClientPath string
IdentifierRegistrationConf string
IdentifierScopesConf string
IdentifierDefaultBannerLogo string
IdentifierDefaultSignInPageText string
IdentifierDefaultLogoTargetURI string
IdentifierDefaultUsernameHintText string
IdentifierUILocales []string
SigningKid string
SigningMethod string
SigningPrivateKeyFiles []string
ValidationKeysPath string
CookieBackendURI string
CookieNames []string
CookieSameSite http.SameSite
AccessTokenDurationSeconds uint64
IDTokenDurationSeconds uint64
RefreshTokenDurationSeconds uint64
DyamicClientSecretDurationSeconds uint64
}
+408
View File
@@ -0,0 +1,408 @@
package bootstrap
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/go-jose/go-jose/v3"
"github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
)
func parseJSONWebKey(jsonBytes []byte) (*jose.JSONWebKey, error) {
k := &jose.JSONWebKey{}
if err := k.UnmarshalJSON(jsonBytes); err != nil {
return nil, err
}
return k, nil
}
// LoadSignerFromFile loads a private-key for signing
//
// Supports JSON (JWK/JWS) and PEM
func LoadSignerFromFile(fn string) (string, crypto.Signer, error) {
readBytes, errRead := ioutil.ReadFile(fn)
if errRead != nil {
return "", nil, fmt.Errorf("failed to parse key file: %v", errRead)
}
ext := filepath.Ext(fn)
switch ext {
case ".json":
k, err := parseJSONWebKey(readBytes)
if err != nil {
return "", nil, fmt.Errorf("failed to parse key file as JWK: %v", err)
}
if !k.Valid() {
return "", nil, fmt.Errorf("json file is not a valid JWK")
}
if k.IsPublic() {
return "", nil, fmt.Errorf("JWK is a public key, private key required to use as signer")
}
signer, ok := k.Key.(crypto.Signer)
if !ok {
return "", nil, fmt.Errorf("JWS key type %T is not a signer", k.Key)
}
return k.KeyID, signer, nil
case ".pem":
fallthrough
default:
// Try PEM if not otherwise detected.
signer, err := parsePEMSigner(readBytes)
return "", signer, err
}
}
func parsePEMSigner(pemBytes []byte) (crypto.Signer, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
var signer crypto.Signer
for {
pkcs1Key, errParse1 := x509.ParsePKCS1PrivateKey(block.Bytes)
if errParse1 == nil {
signer = pkcs1Key
break
}
pkcs8Key, errParse2 := x509.ParsePKCS8PrivateKey(block.Bytes)
if errParse2 == nil {
signerSigner, ok := pkcs8Key.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("failed to use key as crypto signer")
}
signer = signerSigner
break
}
ecKey, errParse3 := x509.ParseECPrivateKey(block.Bytes)
if errParse3 == nil {
signer = ecKey
break
}
return nil, fmt.Errorf("failed to parse signer key - valid PKCS#1, PKCS#8 ...? %v, %v, %v", errParse1, errParse2, errParse3)
}
return signer, nil
}
// LoadValidatorFromFile loads a public-key used for validation.
//
// Supported formats are JSON-JWK and PEM
func LoadValidatorFromFile(fn string) (string, crypto.PublicKey, error) {
kid, _, key, err := loadValidatorFromFile(fn)
return kid, key, err
}
// LoadCertificatesAndValidatorFromFile loads chain of certificates and a
// public-key used for validation.
//
// Supported formats are JSON-JWK and PEM
func LoadCertificatesAndValidatorFromFile(fn string) (string, []*x509.Certificate, crypto.PublicKey, error) {
return loadValidatorFromFile(fn)
}
func loadValidatorFromFile(fn string) (string, []*x509.Certificate, crypto.PublicKey, error) {
readBytes, errRead := ioutil.ReadFile(fn)
if errRead != nil {
return "", nil, nil, fmt.Errorf("failed to parse key file: %v", errRead)
}
ext := filepath.Ext(fn)
switch ext {
case ".json":
k, err := parseJSONWebKey(readBytes)
if err != nil {
return "", nil, nil, fmt.Errorf("failed to parse key file as JWK: %v", err)
}
if !k.Valid() {
return "", nil, nil, fmt.Errorf("json file is not a valid JWK")
}
if !k.IsPublic() {
public := k.Public()
k = &public
}
return k.KeyID, k.Certificates, k.Key, nil
case ".pem":
fallthrough
default:
// Try PEM if not otherwise detected.
certificates, validator, err := parsePEMValidator(readBytes)
return "", certificates, validator, err
}
}
func parsePEMValidator(pemBytes []byte) ([]*x509.Certificate, crypto.PublicKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, nil, fmt.Errorf("no PEM block found")
}
var certificates []*x509.Certificate
var validator crypto.PublicKey
for {
pkixPubKey, errParse0 := x509.ParsePKIXPublicKey(block.Bytes)
if errParse0 == nil {
validator = pkixPubKey
break
}
pkcs1PubKey, errParse1 := x509.ParsePKCS1PublicKey(block.Bytes)
if errParse1 == nil {
validator = pkcs1PubKey
break
}
pkcs1PrivKey, errParse2 := x509.ParsePKCS1PrivateKey(block.Bytes)
if errParse2 == nil {
validator = pkcs1PrivKey.Public()
break
}
pkcs8Key, errParse3 := x509.ParsePKCS8PrivateKey(block.Bytes)
if errParse3 == nil {
signerSigner, ok := pkcs8Key.(crypto.Signer)
if !ok {
return nil, nil, fmt.Errorf("failed to use key as crypto signer")
}
validator = signerSigner.Public()
break
}
ecKey, errParse4 := x509.ParseECPrivateKey(block.Bytes)
if errParse4 == nil {
validator = ecKey.Public()
break
}
certs, errParse5 := x509.ParseCertificates(block.Bytes)
if errParse5 == nil {
validator = certs[0].PublicKey
certificates = append(certificates, certs...)
break
}
return nil, nil, fmt.Errorf("failed to parse validator key - valid PKCS#1, PKCS#8 ...? %v, %v, %v, %v, %v, %v", errParse0, errParse1, errParse2, errParse3, errParse4, errParse5)
}
return certificates, validator, nil
}
func addSignerWithIDFromFile(fn string, kid string, bs *bootstrap) error {
fi, err := os.Lstat(fn)
if err != nil {
return fmt.Errorf("failed load load signer key: %v", err)
}
mode := fi.Mode()
switch {
case mode.IsDir():
return fmt.Errorf("signer key must be a file")
}
// Load file.
signerKid, signer, err := LoadSignerFromFile(fn)
if err != nil {
return err
}
if kid == "" {
kid = signerKid
}
if kid == "" {
// Get ID from file, following symbolic link.
var real string
if mode&os.ModeSymlink != 0 {
real, err = os.Readlink(fn)
if err != nil {
return err
}
_, real = filepath.Split(real)
} else {
real = fi.Name()
}
kid = getKeyIDFromFilename(real)
}
if _, ok := bs.config.Signers[kid]; ok {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": fn,
"kid": kid,
}).Warnln("skipped as signer with same kid already loaded")
return nil
} else {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": fn,
"kid": kid,
}).Debugln("loaded signer key")
}
bs.config.Signers[kid] = signer
return nil
}
func validateSigners(bs *bootstrap) error {
haveRSA := false
haveECDSA := false
haveEd25519 := false
for _, signer := range bs.config.Signers {
switch s := signer.(type) {
case *rsa.PrivateKey:
// Ensure the private key is not vulnerable with PKCS-1.5 signatures. See
// https://paragonie.com/blog/2018/04/protecting-rsa-based-protocols-against-adaptive-chosen-ciphertext-attacks#rsa-anti-bb98
// for details.
if s.PublicKey.E < 65537 {
return fmt.Errorf("RSA signing key with public exponent < 65537")
}
haveRSA = true
case *ecdsa.PrivateKey:
haveECDSA = true
case ed25519.PrivateKey:
haveEd25519 = true
default:
return fmt.Errorf("unsupported signer type: %v", s)
}
}
// Validate signing method
switch bs.config.SigningMethod.(type) {
case *jwt.SigningMethodRSA:
if !haveRSA {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
case *jwt.SigningMethodRSAPSS:
if !haveRSA {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
case *jwt.SigningMethodECDSA:
if !haveECDSA {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
case *jwt.SigningMethodEd25519:
if !haveEd25519 {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
default:
return fmt.Errorf("unsupported signing method: %s", bs.config.SigningMethod.Alg())
}
if !haveRSA {
bs.config.Config.Logger.Warnln("no RSA signing private key, some clients might not be compatible")
}
return nil
}
func addValidatorsFromPath(pn string, bs *bootstrap) error {
fi, err := os.Lstat(pn)
if err != nil {
return fmt.Errorf("failed load load validator keys: %v", err)
}
switch mode := fi.Mode(); {
case mode.IsDir():
// OK.
default:
return fmt.Errorf("validator path must be a directory")
}
// Load all files.
files := []string{}
if pemFiles, err := filepath.Glob(filepath.Join(pn, "*.pem")); err != nil {
return fmt.Errorf("validator path err: %v", err)
} else {
files = append(files, pemFiles...)
}
if jsonFiles, err := filepath.Glob(filepath.Join(pn, "*.json")); err != nil {
return fmt.Errorf("validator path err: %v", err)
} else {
files = append(files, jsonFiles...)
}
for _, file := range files {
kid, certificates, validator, err := loadValidatorFromFile(file)
if err != nil {
bs.config.Config.Logger.WithError(err).WithField("path", file).Warnln("failed to load validator key")
continue
}
// Get ID from file, without following symbolic links.
if kid == "" {
_, fn := filepath.Split(file)
kid = getKeyIDFromFilename(fn)
}
if _, ok := bs.config.Validators[kid]; ok {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": file,
"kid": kid,
}).Warnln("skipped as validator with same kid already loaded")
continue
} else {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": file,
"kid": kid,
}).Debugln("loaded validator key")
}
bs.config.Validators[kid] = validator
if certificates != nil {
bs.config.Certificates[kid] = certificates
}
}
return nil
}
func WithSchemeAndHost(u, base *url.URL) *url.URL {
if u.Host != "" && u.Scheme != "" {
return u
}
r, _ := url.Parse(u.String())
r.Scheme = base.Scheme
r.Host = base.Host
return r
}
func getKeyIDFromFilename(fn string) string {
ext := filepath.Ext(fn)
return strings.TrimSuffix(fn, ext)
}
func getCommonURLPathPrefix(p1, p2 string) (string, error) {
parts1 := strings.Split(p1, "/")
parts2 := strings.Split(p2, "/")
common := make([]string, 0)
for idx, p := range parts1 {
if idx >= len(parts2) {
break
}
if p != parts2[idx] {
break
}
common = append(common, p)
}
if len(common) == 0 {
return "", errors.New("no common path prefix")
}
return strings.Join(common, "/"), nil
}