Initial QSfera import
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
SHELL := bash
|
||||
NAME := idm
|
||||
TAGS := disable_crypt
|
||||
|
||||
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
|
||||
include ../../.bingo/Variables.mk
|
||||
endif
|
||||
|
||||
include ../../.make/default.mk
|
||||
include ../../.make/go.mk
|
||||
include ../../.make/release.mk
|
||||
include ../../.make/docs.mk
|
||||
@@ -0,0 +1,21 @@
|
||||
# IDM
|
||||
|
||||
The IDM service provides a minimal LDAP Service, based on [Libregraph idm](https://github.com/libregraph/idm), for КуСфера. It is started as part of the default configuration and serves as a central place for storing user and group information.
|
||||
|
||||
It is mainly targeted at small КуСфера installations. For larger setups it is recommended to replace IDM with a “real” LDAP server or to switch to an external identity management solution.
|
||||
|
||||
IDM listens on port 9235 by default. In the default configuration it only accepts TLS-protected connections (LDAPS). The BaseDN of the LDAP tree is `o=libregraph-idm`. IDM gives LDAP write permissions to a single user (DN: `uid=libregraph,ou=sysusers,o=libregraph-idm`). Any other authenticated user has read-only access. IDM stores its data in a boltdb file `idm/idm.boltdb` inside the КуСфера base data directory.
|
||||
|
||||
The internal LDAP certificate and key are stored as `ldap.crt` and `ldap.key` in the IDM data directory. By default, these certificates expire after 12 months. When the certificate has expired, IDM can no longer establish valid TLS connections and requests that depend on LDAP may fail with `500 Internal Server Error`.
|
||||
|
||||
To renew the internal LDAP certificate, stop or restart the КуСфера container after deleting the expired certificate and key:
|
||||
|
||||
```bash
|
||||
cd .qsfera/idm
|
||||
rm ldap.crt ldap.key
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
The certificate and key are automatically regenerated when the container starts again. For more details, see [Internal LibreIDM cert expires](https://docs.qsfera.eu/docs/admin/resources/common-issues/#internal-libreidm-cert-expires).
|
||||
|
||||
Note: IDM is limited in its functionality. It only supports a subset of the LDAP operations (namely `BIND`, `SEARCH`, `ADD`, `MODIFY`, `DELETE`). Also, IDM currently does not do any schema verification (like. structural vs. auxiliary object classes, require and option attributes, syntax checks, …). Therefore it is not meant as a general purpose LDAP server.
|
||||
@@ -0,0 +1,15 @@
|
||||
package idm
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
// BaseLDIF is a template for base LDAP entries
|
||||
//
|
||||
//go:embed ldif/base.ldif.tmpl
|
||||
var BaseLDIF string
|
||||
|
||||
// DemoUsersLDIF is a template for demo users
|
||||
//
|
||||
//go:embed ldif/demousers.ldif.tmpl
|
||||
var DemoUsersLDIF string
|
||||
@@ -0,0 +1,45 @@
|
||||
dn: o=libregraph-idm
|
||||
o: libregraph-idm
|
||||
objectClass: organization
|
||||
|
||||
dn: ou=users,o=libregraph-idm
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
|
||||
dn: ou=sysusers,o=libregraph-idm
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
|
||||
dn: ou=groups,o=libregraph-idm
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
{{ range . -}}
|
||||
{{ if eq .Name "admin" -}}
|
||||
dn: uid=admin,ou=users,o=libregraph-idm
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: qsferaUser
|
||||
objectClass: qsferaObject
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
uid: admin
|
||||
givenName: Admin
|
||||
sn: Admin
|
||||
cn: admin
|
||||
displayName: Admin
|
||||
description: An admin for this КуСфера instance.
|
||||
mail: admin@example.org
|
||||
qsferaUUID: {{ .ID }}
|
||||
qsferaExternalIdentity: $ {{ .Issuer }} $ {{ .ID }}
|
||||
{{ else -}}
|
||||
dn: uid={{ .Name }},ou=sysusers,o=libregraph-idm
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
uid: {{ .Name }}
|
||||
{{ end -}}
|
||||
userPassword:: {{ .Password }}
|
||||
|
||||
{{ end -}}
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
dn: uid=alan,ou=users,o=libregraph-idm
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: qsferaUser
|
||||
objectClass: qsferaObject
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
uid: alan
|
||||
givenName: Alan
|
||||
sn: Turing
|
||||
cn: alan
|
||||
displayName: Alan Turing
|
||||
description: An English mathematician, computer scientist, logician, cryptanalyst, philosopher and theoretical biologist. He was highly influential in the development of theoretical computer science, providing a formalisation of the concepts of algorithm and computation with the Turing machine.
|
||||
mail: alan@example.org
|
||||
qsferaUserEnabled: TRUE
|
||||
qsferaUUID: b1f74ec4-dd7e-11ef-a543-03775734d0f7
|
||||
qsferaExternalIdentity: $ {{.}} $ b1f74ec4-dd7e-11ef-a543-03775734d0f7
|
||||
userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTE2JGg1NUxqckhWVjdEdXVzTkxjbXRoa0EkMzZ3aGZSMjdyTDFOYXQxa0xTajdrVGFubTBnb3VKRGZ0ck9DTStuRHo5cw==
|
||||
|
||||
dn: uid=lynn,ou=users,o=libregraph-idm
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: qsferaUser
|
||||
objectClass: qsferaObject
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
uid: lynn
|
||||
givenName: Lynn
|
||||
sn: Conway
|
||||
cn: lynn
|
||||
displayName: Lynn Conway
|
||||
description: An American computer scientist, electrical engineer, and transgender activist.
|
||||
mail: lynn@example.org
|
||||
qsferaUserEnabled: TRUE
|
||||
qsferaUUID: 60708dda-e897-11ef-919f-bbb7437d6ec2
|
||||
qsferaExternalIdentity: $ {{.}} $ 60708dda-e897-11ef-919f-bbb7437d6ec2
|
||||
userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTE2JGg1NUxqckhWVjdEdXVzTkxjbXRoa0EkMzZ3aGZSMjdyTDFOYXQxa0xTajdrVGFubTBnb3VKRGZ0ck9DTStuRHo5cw==
|
||||
|
||||
dn: uid=mary,ou=users,o=libregraph-idm
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: qsferaUser
|
||||
objectClass: qsferaObject
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
uid: mary
|
||||
givenName: Mary
|
||||
sn: Kenneth Keller
|
||||
cn: mary
|
||||
displayName: Mary Kenneth Keller
|
||||
description: Mary Kenneth Keller of the Sisters of Charity of the Blessed Virgin Mary was a pioneer in computer science.
|
||||
mail: mary@example.org
|
||||
qsferaUserEnabled: TRUE
|
||||
qsferaUUID: 056fc874-dd7f-11ef-ba84-af6fca4b7289
|
||||
qsferaExternalIdentity: $ {{.}} $ 056fc874-dd7f-11ef-ba84-af6fca4b7289
|
||||
userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTE2JGg1NUxqckhWVjdEdXVzTkxjbXRoa0EkMzZ3aGZSMjdyTDFOYXQxa0xTajdrVGFubTBnb3VKRGZ0ck9DTStuRHo5cw==
|
||||
|
||||
dn: uid=margaret,ou=users,o=libregraph-idm
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: qsferaUser
|
||||
objectClass: qsferaObject
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
uid: margaret
|
||||
givenName: Margaret
|
||||
sn: Hamilton
|
||||
cn: margaret
|
||||
displayName: Margaret Hamilton
|
||||
description: A director of the Software Engineering Division of the MIT Instrumentation Laboratory, which developed on-board flight software for NASA's Apollo program.
|
||||
mail: margaret@example.org
|
||||
qsferaUserEnabled: TRUE
|
||||
qsferaUUID: 801abee4-dd7f-11ef-a324-83f55a754b62
|
||||
qsferaExternalIdentity: $ {{.}} $ 801abee4-dd7f-11ef-a324-83f55a754b62
|
||||
userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTE2JGg1NUxqckhWVjdEdXVzTkxjbXRoa0EkMzZ3aGZSMjdyTDFOYXQxa0xTajdrVGFubTBnb3VKRGZ0ck9DTStuRHo5cw==
|
||||
|
||||
dn: uid=dennis,ou=users,o=libregraph-idm
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: qsferaUser
|
||||
objectClass: qsferaObject
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
uid: dennis
|
||||
givenName: Dennis
|
||||
sn: Ritchie
|
||||
cn: dennis
|
||||
displayName: Dennis Ritchie
|
||||
description: American computer scientist. He created the C programming language and the Unix operating system and B language with long-time colleague Ken Thompson.
|
||||
mail: dennis@example.org
|
||||
qsferaUserEnabled: TRUE
|
||||
qsferaUUID: cd88bf9a-dd7f-11ef-a609-7f78deb2345f
|
||||
qsferaExternalIdentity: $ {{.}} $ cd88bf9a-dd7f-11ef-a609-7f78deb2345f
|
||||
userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTE2JGg1NUxqckhWVjdEdXVzTkxjbXRoa0EkMzZ3aGZSMjdyTDFOYXQxa0xTajdrVGFubTBnb3VKRGZ0ck9DTStuRHo5cw==
|
||||
|
||||
dn: cn=users,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: users
|
||||
description: Users
|
||||
qsferaUUID: 509a9dcd-bb37-4f4f-a01a-19dca27d9cfa
|
||||
member: uid=alan,ou=users,o=libregraph-idm
|
||||
member: uid=mary,ou=users,o=libregraph-idm
|
||||
member: uid=margaret,ou=users,o=libregraph-idm
|
||||
member: uid=dennis,ou=users,o=libregraph-idm
|
||||
member: uid=lynn,ou=users,o=libregraph-idm
|
||||
member: uid=admin,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=chess-lovers,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: chess-lovers
|
||||
description: Chess lovers
|
||||
qsferaUUID: 9d31ec04-dd80-11ef-ac47-a38ba68cc36d
|
||||
member: uid=alan,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=machine-lovers,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: machine-lovers
|
||||
description: Machine Lovers
|
||||
qsferaUUID: d901562a-dd80-11ef-a510-fba1ed43fb21
|
||||
member: uid=alan,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=bible-readers,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: bible-readers
|
||||
description: Bible readers
|
||||
qsferaUUID: 2fc6ba22-dd81-11ef-89e6-e3eff494a998
|
||||
member: uid=mary,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=apollos,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: apollos
|
||||
description: Contributors to the Appollo mission
|
||||
qsferaUUID: 6f9bab36-dd94-11ef-a252-dbbdd20299dd
|
||||
member: uid=margaret,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=unix-lovers,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: unix-lovers
|
||||
description: Unix lovers
|
||||
qsferaUUID: 75bc3882-dd94-11ef-ad60-335f3df6cef3
|
||||
member: uid=dennis,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=basic-haters,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: basic-haters
|
||||
description: Haters of the Basic programming language
|
||||
qsferaUUID: a4eb2c12-dd94-11ef-9ebe-eb96f938d517
|
||||
member: uid=dennis,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=vlsi-lovers,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: vlsi-lovers
|
||||
description: Lovers of VLSI microchip design
|
||||
qsferaUUID: 914ce3de-e899-11ef-9a4b-732fbb2acc42
|
||||
member: uid=lynn,ou=users,o=libregraph-idm
|
||||
|
||||
dn: cn=programmers,ou=groups,o=libregraph-idm
|
||||
objectClass: groupOfNames
|
||||
objectClass: qsferaObject
|
||||
objectClass: top
|
||||
cn: programmers
|
||||
description: Computer Programmers
|
||||
qsferaUUID: ce4aa240-dd94-11ef-82b8-4f4828849072
|
||||
member: uid=alan,ou=users,o=libregraph-idm
|
||||
member: uid=margaret,ou=users,o=libregraph-idm
|
||||
member: uid=dennis,ou=users,o=libregraph-idm
|
||||
member: uid=lynn,ou=users,o=libregraph-idm
|
||||
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/configlog"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
"github.com/qsfera/server/services/idm/pkg/config/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "check health status",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return configlog.ReturnError(parser.ParseConfig(cfg))
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
|
||||
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,117 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/configlog"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
"github.com/qsfera/server/services/idm/pkg/config/parser"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/libregraph/idm/pkg/ldbbolt"
|
||||
"github.com/libregraph/idm/server"
|
||||
"github.com/spf13/cobra"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// ResetPassword is the entrypoint for the resetpassword command
|
||||
func ResetPassword(cfg *config.Config) *cobra.Command {
|
||||
resetPasswordCmd := &cobra.Command{
|
||||
Use: "resetpassword",
|
||||
Short: "Reset user password",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
|
||||
defer cancel()
|
||||
userNameFlag, _ := cmd.Flags().GetString("user-name")
|
||||
return resetPassword(ctx, logger, cfg, userNameFlag)
|
||||
},
|
||||
}
|
||||
resetPasswordCmd.Flags().StringP(
|
||||
"user-name",
|
||||
"u",
|
||||
"admin",
|
||||
"User name",
|
||||
)
|
||||
|
||||
return resetPasswordCmd
|
||||
}
|
||||
|
||||
func resetPassword(_ context.Context, logger log.Logger, cfg *config.Config, userName string) error {
|
||||
servercfg := server.Config{
|
||||
Logger: log.LogrusWrap(logger.Logger),
|
||||
LDAPHandler: "boltdb",
|
||||
LDAPBaseDN: "o=libregraph-idm",
|
||||
|
||||
BoltDBFile: cfg.IDM.DatabasePath,
|
||||
}
|
||||
|
||||
userDN := fmt.Sprintf("uid=%s,ou=users,%s", userName, servercfg.LDAPBaseDN)
|
||||
fmt.Printf("Resetting password for user '%s'.\n", userDN)
|
||||
if _, err := os.Stat(servercfg.BoltDBFile); errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Fprintf(os.Stderr, "IDM database does not exist.\n")
|
||||
return err
|
||||
}
|
||||
|
||||
newPw, err := getPassword()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading password: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
bdb := &ldbbolt.LdbBolt{}
|
||||
|
||||
opts := bolt.Options{
|
||||
Timeout: 1 * time.Millisecond,
|
||||
}
|
||||
if err := bdb.Configure(servercfg.Logger, servercfg.LDAPBaseDN, servercfg.BoltDBFile, &opts); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open database: '%s'. Please stop any running КуСфера idm instance, as this tool requires exclusive access to the database.\n", err)
|
||||
return err
|
||||
}
|
||||
defer bdb.Close()
|
||||
|
||||
if err := bdb.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pwRequest := ldap.NewPasswordModifyRequest(userDN, "", newPw)
|
||||
if err := bdb.UpdatePassword(pwRequest); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to update user password: %v\n", err)
|
||||
}
|
||||
fmt.Printf("Password for user '%s' updated.\n", userDN)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPassword() (string, error) {
|
||||
fmt.Print("Enter new password: ")
|
||||
bytePassword, err := term.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println("")
|
||||
fmt.Print("Re-enter new password: ")
|
||||
bytePasswordVerify, err := term.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println("")
|
||||
|
||||
password := string(bytePassword)
|
||||
passwordVerify := string(bytePasswordVerify)
|
||||
|
||||
if password != passwordVerify {
|
||||
return "", errors.New("Passwords do not match")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/qsfera/server/pkg/clihelper"
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) []*cobra.Command {
|
||||
return []*cobra.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
ResetPassword(cfg),
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the qsfera idm command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cobra.Command{
|
||||
Use: "idm",
|
||||
Short: "Embedded LDAP service for КуСфера",
|
||||
})
|
||||
app.AddCommand(GetCommands(cfg)...)
|
||||
app.SetArgs(os.Args[1:])
|
||||
|
||||
return app.ExecuteContext(cfg.Context)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/configlog"
|
||||
pkgcrypto "github.com/qsfera/server/pkg/crypto"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/runner"
|
||||
"github.com/qsfera/server/services/idm"
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
"github.com/qsfera/server/services/idm/pkg/config/parser"
|
||||
"github.com/qsfera/server/services/idm/pkg/server/debug"
|
||||
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
"github.com/libregraph/idm/pkg/ldbbolt"
|
||||
"github.com/libregraph/idm/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "server",
|
||||
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var cancel context.CancelFunc
|
||||
if cfg.Context == nil {
|
||||
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
|
||||
defer cancel()
|
||||
}
|
||||
ctx := cfg.Context
|
||||
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
|
||||
gr := runner.NewGroup()
|
||||
{
|
||||
servercfg := server.Config{
|
||||
Logger: log.LogrusWrap(logger.Logger),
|
||||
LDAPHandler: "boltdb",
|
||||
LDAPSListenAddr: cfg.IDM.LDAPSAddr,
|
||||
TLSCertFile: cfg.IDM.Cert,
|
||||
TLSKeyFile: cfg.IDM.Key,
|
||||
LDAPBaseDN: "o=libregraph-idm",
|
||||
LDAPAdminDN: "uid=libregraph,ou=sysusers,o=libregraph-idm",
|
||||
|
||||
BoltDBFile: cfg.IDM.DatabasePath,
|
||||
}
|
||||
|
||||
if cfg.IDM.LDAPSAddr != "" {
|
||||
// Generate a self-signing cert if no certificate is present
|
||||
if err := pkgcrypto.GenCert(cfg.IDM.Cert, cfg.IDM.Key, logger); err != nil {
|
||||
logger.Fatal().Err(err).Msgf("Could not generate test-certificate")
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(servercfg.BoltDBFile); errors.Is(err, os.ErrNotExist) {
|
||||
logger.Debug().Msg("Bootstrapping IDM database")
|
||||
if err = bootstrap(logger, cfg, servercfg); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to bootstrap idm database")
|
||||
}
|
||||
}
|
||||
|
||||
svc, err := server.NewServer(&servercfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we need an additional context for the idm server in order to
|
||||
// cancel it anytime
|
||||
svcCtx, svcCancel := context.WithCancel(ctx)
|
||||
defer svcCancel()
|
||||
|
||||
gr.Add(runner.New(cfg.Service.Name+".svc", func() error {
|
||||
return svc.Serve(svcCtx)
|
||||
}, func() {
|
||||
svcCancel()
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
debugServer, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
|
||||
}
|
||||
|
||||
grResults := gr.Run(ctx)
|
||||
|
||||
// return the first non-nil error found in the results
|
||||
for _, grResult := range grResults {
|
||||
if grResult.RunnerError != nil {
|
||||
return grResult.RunnerError
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrap(logger log.Logger, cfg *config.Config, srvcfg server.Config) error {
|
||||
// Hash password if the config does not supply a hash already
|
||||
var err error
|
||||
|
||||
type svcUser struct {
|
||||
Name string
|
||||
Password string
|
||||
ID string
|
||||
Issuer string
|
||||
}
|
||||
|
||||
serviceUsers := []svcUser{
|
||||
{
|
||||
Name: "libregraph",
|
||||
Password: cfg.ServiceUserPasswords.Idm,
|
||||
},
|
||||
{
|
||||
Name: "idp",
|
||||
Password: cfg.ServiceUserPasswords.Idp,
|
||||
},
|
||||
{
|
||||
Name: "reva",
|
||||
Password: cfg.ServiceUserPasswords.Reva,
|
||||
},
|
||||
}
|
||||
|
||||
if cfg.AdminUserID != "" {
|
||||
serviceUsers = append(serviceUsers, svcUser{
|
||||
Name: "admin",
|
||||
Password: cfg.ServiceUserPasswords.OCAdmin,
|
||||
ID: cfg.AdminUserID,
|
||||
Issuer: cfg.DemoUsersIssuerUrl,
|
||||
})
|
||||
}
|
||||
|
||||
bdb := &ldbbolt.LdbBolt{}
|
||||
|
||||
if err := bdb.Configure(srvcfg.Logger, srvcfg.LDAPBaseDN, srvcfg.BoltDBFile, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
defer bdb.Close()
|
||||
|
||||
if err := bdb.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare the initial Data from template. To be able to set the
|
||||
// supplied service user passwords
|
||||
tmpl, err := template.New("baseldif").Parse(idm.BaseLDIF)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range serviceUsers {
|
||||
if strings.HasPrefix(serviceUsers[i].Password, "$argon2id$") {
|
||||
// password is alread hashed
|
||||
serviceUsers[i].Password = "{ARGON2}" + serviceUsers[i].Password
|
||||
} else {
|
||||
if serviceUsers[i].Password, err = ldappassword.Hash(serviceUsers[i].Password, "{ARGON2}"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// We need to treat the hash as binary in the LDIF template to avoid
|
||||
// go-ldap/ldif to do any fancy escaping
|
||||
serviceUsers[i].Password = base64.StdEncoding.EncodeToString([]byte(serviceUsers[i].Password))
|
||||
}
|
||||
var tmplWriter strings.Builder
|
||||
err = tmpl.Execute(&tmplWriter, serviceUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bootstrapData := tmplWriter.String()
|
||||
if cfg.CreateDemoUsers {
|
||||
demoUsersTmpl, err := template.New("demousers").Parse(idm.DemoUsersLDIF)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var demoUsersWriter strings.Builder
|
||||
err = demoUsersTmpl.Execute(&demoUsersWriter, cfg.DemoUsersIssuerUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bootstrapData = bootstrapData + "\n" + demoUsersWriter.String()
|
||||
}
|
||||
|
||||
s := strings.NewReader(bootstrapData)
|
||||
lf := &ldif.LDIF{}
|
||||
err = ldif.Unmarshal(s, lf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range lf.AllEntries() {
|
||||
logger.Debug().Str("dn", entry.DN).Msg("Adding entry")
|
||||
if err := bdb.EntryPut(entry); err != nil {
|
||||
return fmt.Errorf("error adding Entry '%s': %w", entry.DN, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "print the version of this binary and the running service instances",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// not implemented
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/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:"-"`
|
||||
|
||||
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;IDM_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
IDM Settings `yaml:"idm"`
|
||||
CreateDemoUsers bool `yaml:"create_demo_users" env:"IDM_CREATE_DEMO_USERS" desc:"Flag to enable or disable the creation of the demo users." introductionVersion:"1.0.0"`
|
||||
DemoUsersIssuerUrl string `yaml:"demo_users_issuer_url" env:"OC_URL;OC_OIDC_ISSUER" desc:"The OIDC issuer URL to assign to the demo users." introductionVersion:"1.0.0"`
|
||||
|
||||
ServiceUserPasswords ServiceUserPasswords `yaml:"service_user_passwords"`
|
||||
AdminUserID string `yaml:"admin_user_id" env:"OC_ADMIN_USER_ID;IDM_ADMIN_USER_ID" desc:"ID of the user that should receive admin privileges. Consider that the UUID can be encoded in some LDAP deployment configurations like in .ldif files. These need to be decoded beforehand." introductionVersion:"1.0.0"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
LDAPSAddr string `yaml:"ldaps_addr" env:"IDM_LDAPS_ADDR" desc:"Listen address for the LDAPS listener (ip-addr:port)." introductionVersion:"1.0.0"`
|
||||
Cert string `yaml:"cert" env:"IDM_LDAPS_CERT" desc:"File name of the TLS server certificate for the LDAPS listener. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
|
||||
Key string `yaml:"key" env:"IDM_LDAPS_KEY" desc:"File name for the TLS certificate key for the server certificate. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
|
||||
DatabasePath string `yaml:"database" env:"IDM_DATABASE_PATH" desc:"Full path to the IDM backend database. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
|
||||
}
|
||||
|
||||
type ServiceUserPasswords struct {
|
||||
OCAdmin string `yaml:"admin_password" env:"IDM_ADMIN_PASSWORD" desc:"Password to set for the КуСфера 'admin' user. Either cleartext or an argon2id hash." introductionVersion:"1.0.0"`
|
||||
Idm string `yaml:"idm_password" env:"IDM_SVC_PASSWORD" desc:"Password to set for the 'idm' service user. Either cleartext or an argon2id hash." introductionVersion:"1.0.0"`
|
||||
Reva string `yaml:"reva_password" env:"IDM_REVASVC_PASSWORD" desc:"Password to set for the 'reva' service user. Either cleartext or an argon2id hash." introductionVersion:"1.0.0"`
|
||||
Idp string `yaml:"idp_password" env:"IDM_IDPSVC_PASSWORD" desc:"Password to set for the 'idp' service user. Either cleartext or an argon2id hash." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"IDM_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
|
||||
Token string `yaml:"token" env:"IDM_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
|
||||
Pprof bool `yaml:"pprof" env:"IDM_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
|
||||
Zpages bool `yaml:"zpages" env:"IDM_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/defaults"
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
)
|
||||
|
||||
// FullDefaultConfig returns a fully initialized default configuration
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DefaultConfig returns a basic default configuration
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:9239",
|
||||
Token: "",
|
||||
Pprof: false,
|
||||
Zpages: false,
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "idm",
|
||||
},
|
||||
CreateDemoUsers: false,
|
||||
DemoUsersIssuerUrl: "https://localhost:9200",
|
||||
IDM: config.Settings{
|
||||
LDAPSAddr: "127.0.0.1:9235",
|
||||
Cert: path.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
|
||||
Key: path.Join(defaults.BaseDataPath(), "idm", "ldap.key"),
|
||||
DatabasePath: path.Join(defaults.BaseDataPath(), "idm", "idm.boltdb"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureDefaults adds default values to the configuration if they are not set yet
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
if cfg.LogLevel == "" {
|
||||
cfg.LogLevel = "error"
|
||||
}
|
||||
|
||||
if cfg.AdminUserID == "" && cfg.Commons != nil {
|
||||
cfg.AdminUserID = cfg.Commons.AdminUserID
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize sanitizes the configuration
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// nothing to sanitize here
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
occfg "github.com/qsfera/server/pkg/config"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
"github.com/qsfera/server/services/idm/pkg/config"
|
||||
"github.com/qsfera/server/services/idm/pkg/config/defaults"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
err := occfg.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 {
|
||||
if cfg.CreateDemoUsers && cfg.AdminUserID == "" {
|
||||
return shared.MissingAdminUserID(cfg.Service.Name)
|
||||
}
|
||||
|
||||
if cfg.ServiceUserPasswords.Idm == "" {
|
||||
return shared.MissingServiceUserPassword(cfg.Service.Name, "IDM")
|
||||
}
|
||||
|
||||
if cfg.AdminUserID != "" && cfg.ServiceUserPasswords.OCAdmin == "" {
|
||||
return shared.MissingServiceUserPassword(cfg.Service.Name, "admin")
|
||||
}
|
||||
|
||||
if cfg.ServiceUserPasswords.Idp == "" {
|
||||
return shared.MissingServiceUserPassword(cfg.Service.Name, "IDP")
|
||||
}
|
||||
|
||||
if cfg.ServiceUserPasswords.Reva == "" {
|
||||
return shared.MissingServiceUserPassword(cfg.Service.Name, "REVA")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idm/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,23 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/service/debug"
|
||||
"github.com/qsfera/server/pkg/version"
|
||||
)
|
||||
|
||||
// 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),
|
||||
), nil
|
||||
}
|
||||
Reference in New Issue
Block a user