|
|
|
@@ -0,0 +1,346 @@
|
|
|
|
|
package svc
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"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"
|
|
|
|
|
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/qsfera/server/pkg/ldap"
|
|
|
|
|
"github.com/qsfera/server/pkg/log"
|
|
|
|
|
"github.com/qsfera/server/pkg/tracing"
|
|
|
|
|
"github.com/qsfera/server/services/idp/pkg/assets"
|
|
|
|
|
cs3BackendSupport "github.com/qsfera/server/services/idp/pkg/backends/cs3/bootstrap"
|
|
|
|
|
"github.com/qsfera/server/services/idp/pkg/config"
|
|
|
|
|
"github.com/qsfera/server/services/idp/pkg/middleware"
|
|
|
|
|
"github.com/riandyrn/otelchi"
|
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
|
"stash.kopano.io/kgol/rndm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Service defines the service 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.Commons.QsferaURL,
|
|
|
|
|
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()
|
|
|
|
|
libreGraphBackendSupport.MustRegister()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
idpSettings := bootstrap.Settings{
|
|
|
|
|
Iss: options.Config.IDP.Iss,
|
|
|
|
|
IdentityManager: options.Config.IDP.IdentityManager,
|
|
|
|
|
URIBasePath: options.Config.IDP.URIBasePath,
|
|
|
|
|
SignInURI: options.Config.IDP.SignInURI,
|
|
|
|
|
SignedOutURI: options.Config.IDP.SignedOutURI,
|
|
|
|
|
AuthorizationEndpointURI: options.Config.IDP.AuthorizationEndpointURI,
|
|
|
|
|
EndsessionEndpointURI: options.Config.IDP.EndsessionEndpointURI,
|
|
|
|
|
Insecure: options.Config.IDP.Insecure,
|
|
|
|
|
TrustedProxy: options.Config.IDP.TrustedProxy,
|
|
|
|
|
AllowScope: options.Config.IDP.AllowScope,
|
|
|
|
|
AllowClientGuests: options.Config.IDP.AllowClientGuests,
|
|
|
|
|
AllowDynamicClientRegistration: options.Config.IDP.AllowDynamicClientRegistration,
|
|
|
|
|
EncryptionSecretFile: options.Config.IDP.EncryptionSecretFile,
|
|
|
|
|
Listen: options.Config.IDP.Listen,
|
|
|
|
|
IdentifierClientDisabled: options.Config.IDP.IdentifierClientDisabled,
|
|
|
|
|
IdentifierClientPath: options.Config.IDP.IdentifierClientPath,
|
|
|
|
|
IdentifierRegistrationConf: options.Config.IDP.IdentifierRegistrationConf,
|
|
|
|
|
IdentifierScopesConf: options.Config.IDP.IdentifierScopesConf,
|
|
|
|
|
IdentifierDefaultBannerLogo: options.Config.IDP.IdentifierDefaultBannerLogo,
|
|
|
|
|
IdentifierDefaultSignInPageText: options.Config.IDP.IdentifierDefaultSignInPageText,
|
|
|
|
|
IdentifierDefaultLogoTargetURI: options.Config.IDP.IdentifierDefaultLogoTargetURI,
|
|
|
|
|
IdentifierDefaultUsernameHintText: options.Config.IDP.IdentifierDefaultUsernameHintText,
|
|
|
|
|
IdentifierUILocales: options.Config.IDP.IdentifierUILocales,
|
|
|
|
|
SigningKid: options.Config.IDP.SigningKid,
|
|
|
|
|
SigningMethod: options.Config.IDP.SigningMethod,
|
|
|
|
|
SigningPrivateKeyFiles: options.Config.IDP.SigningPrivateKeyFiles,
|
|
|
|
|
ValidationKeysPath: options.Config.IDP.ValidationKeysPath,
|
|
|
|
|
CookieBackendURI: options.Config.IDP.CookieBackendURI,
|
|
|
|
|
CookieNames: options.Config.IDP.CookieNames,
|
|
|
|
|
CookieSameSite: options.Config.IDP.CookieSameSite,
|
|
|
|
|
AccessTokenDurationSeconds: options.Config.IDP.AccessTokenDurationSeconds,
|
|
|
|
|
IDTokenDurationSeconds: options.Config.IDP.IDTokenDurationSeconds,
|
|
|
|
|
RefreshTokenDurationSeconds: options.Config.IDP.RefreshTokenDurationSeconds,
|
|
|
|
|
DyamicClientSecretDurationSeconds: options.Config.IDP.DynamicClientSecretDurationSeconds,
|
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
|
tp: options.TraceProvider,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
svc.initMux(ctx, routes, handlers, options)
|
|
|
|
|
|
|
|
|
|
return svc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type temporaryClientConfig struct {
|
|
|
|
|
Clients []config.Client `yaml:"clients"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createTemporaryClientsConfig(filePath, qsferaURL string, clients []config.Client) error {
|
|
|
|
|
folder := path.Dir(filePath)
|
|
|
|
|
if _, err := os.Stat(folder); os.IsNotExist(err) {
|
|
|
|
|
if err := os.MkdirAll(folder, 0o700); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, client := range clients {
|
|
|
|
|
|
|
|
|
|
for i, entry := range client.RedirectURIs {
|
|
|
|
|
client.RedirectURIs[i] = strings.ReplaceAll(entry, "{{OC_URL}}", strings.TrimRight(qsferaURL, "/"))
|
|
|
|
|
}
|
|
|
|
|
for i, entry := range client.Origins {
|
|
|
|
|
client.Origins[i] = strings.ReplaceAll(entry, "{{OC_URL}}", strings.TrimRight(qsferaURL, "/"))
|
|
|
|
|
}
|
|
|
|
|
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 = os.WriteFile(filePath, conf, 0o600)
|
|
|
|
|
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 {
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
var needsAnd bool
|
|
|
|
|
if ldap.Filter != "" {
|
|
|
|
|
filter += ldap.Filter
|
|
|
|
|
needsAnd = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ldap.UserEnabledAttribute != "" {
|
|
|
|
|
// Using a (!(enabled=FALSE)) filter here to allow user without
|
|
|
|
|
// any value for the enable flag to log in
|
|
|
|
|
filter += fmt.Sprintf("(!(%s=FALSE))", ldap.UserEnabledAttribute)
|
|
|
|
|
needsAnd = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if needsAnd {
|
|
|
|
|
filter = fmt.Sprintf("(&%s)", filter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_SUB_ATTRIBUTES": 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
|
|
|
|
|
tp trace.TracerProvider
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// initMux initializes the internal idp gorilla mux and mounts it in to an КуСфера 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),
|
|
|
|
|
),
|
|
|
|
|
idp.tp,
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
idp.mux.Use(
|
|
|
|
|
otelchi.Middleware(
|
|
|
|
|
"idp",
|
|
|
|
|
otelchi.WithChiRoutes(idp.mux),
|
|
|
|
|
otelchi.WithTracerProvider(idp.tp),
|
|
|
|
|
otelchi.WithPropagators(tracing.GetPropagator()),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
|
|
_ = chi.Walk(idp.mux, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
|
|
|
|
options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 templated variables.
|
|
|
|
|
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 := io.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)
|
|
|
|
|
|
|
|
|
|
background := idp.config.Asset.LoginBackgroundUrl
|
|
|
|
|
indexHTML = bytes.Replace(template, []byte("__BG_IMG_URL__"), []byte(background), 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")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|