Add 'glauth/' from commit '0735ec933777cb5fd1427c5311dcb6712def476d'

git-subtree-dir: glauth
git-subtree-mainline: d6733b47cc
git-subtree-split: 0735ec9337
This commit is contained in:
A.Unger
2020-09-18 12:45:18 +02:00
61 changed files with 5333 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
package command
import (
"fmt"
"net/http"
"github.com/micro/cli/v2"
"github.com/owncloud/ocis-glauth/pkg/config"
"github.com/owncloud/ocis-glauth/pkg/flagset"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "Check health status",
Flags: flagset.HealthWithConfig(cfg),
Action: func(c *cli.Context) error {
logger := NewLogger(cfg)
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 != 200 {
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
},
}
}
+108
View File
@@ -0,0 +1,108 @@
package command
import (
"os"
"strings"
"github.com/micro/cli/v2"
"github.com/owncloud/ocis-glauth/pkg/config"
"github.com/owncloud/ocis-glauth/pkg/flagset"
"github.com/owncloud/ocis-glauth/pkg/version"
"github.com/owncloud/ocis-pkg/v2/log"
"github.com/spf13/viper"
)
// Execute is the entry point for the ocis-glauth command.
func Execute() error {
cfg := config.New()
app := &cli.App{
Name: "ocis-glauth",
Version: version.String,
Usage: "Serve GLAuth API for oCIS",
Compiled: version.Compiled(),
Authors: []*cli.Author{
{
Name: "ownCloud GmbH",
Email: "support@owncloud.com",
},
},
Flags: flagset.RootWithConfig(cfg),
Before: func(c *cli.Context) error {
return ParseConfig(c, cfg)
},
Commands: []*cli.Command{
Server(cfg),
Health(cfg),
},
}
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
cli.VersionFlag = &cli.BoolFlag{
Name: "version,v",
Usage: "Print the version",
}
return app.Run(os.Args)
}
// NewLogger initializes a service-specific logger instance.
func NewLogger(cfg *config.Config) log.Logger {
return log.NewLogger(
log.Name("glauth"),
log.Level(cfg.Log.Level),
log.Pretty(cfg.Log.Pretty),
log.Color(cfg.Log.Color),
)
}
// ParseConfig loads glauth configuration from Viper known paths.
func ParseConfig(c *cli.Context, cfg *config.Config) error {
logger := NewLogger(cfg)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.SetEnvPrefix("GLAUTH")
viper.AutomaticEnv()
if c.IsSet("config-file") {
viper.SetConfigFile(c.String("config-file"))
} else {
viper.SetConfigName("glauth")
viper.AddConfigPath("/etc/ocis")
viper.AddConfigPath("$HOME/.ocis")
viper.AddConfigPath("./config")
}
if err := viper.ReadInConfig(); err != nil {
switch err.(type) {
case viper.ConfigFileNotFoundError:
logger.Info().
Msg("Continue without config")
case viper.UnsupportedConfigError:
logger.Fatal().
Err(err).
Msg("Unsupported config type")
default:
logger.Fatal().
Err(err).
Msg("Failed to read config")
}
}
if err := viper.Unmarshal(&cfg); err != nil {
logger.Fatal().
Err(err).
Msg("Failed to parse config")
}
return nil
}
+300
View File
@@ -0,0 +1,300 @@
package command
import (
"context"
"os"
"os/signal"
"strings"
"time"
"github.com/owncloud/ocis-glauth/pkg/crypto"
"contrib.go.opencensus.io/exporter/jaeger"
"contrib.go.opencensus.io/exporter/ocagent"
"contrib.go.opencensus.io/exporter/zipkin"
glauthcfg "github.com/glauth/glauth/pkg/config"
"github.com/micro/cli/v2"
"github.com/micro/go-micro/v2"
"github.com/micro/go-micro/v2/client"
"github.com/oklog/run"
openzipkin "github.com/openzipkin/zipkin-go"
zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"github.com/owncloud/ocis-glauth/pkg/config"
"github.com/owncloud/ocis-glauth/pkg/flagset"
"github.com/owncloud/ocis-glauth/pkg/server/debug"
"github.com/owncloud/ocis-glauth/pkg/server/glauth"
"go.opencensus.io/stats/view"
"go.opencensus.io/trace"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: "Start integrated server",
Flags: flagset.ServerWithConfig(cfg),
Before: func(c *cli.Context) error {
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
return ParseConfig(c, cfg)
},
Action: func(c *cli.Context) error {
logger := NewLogger(cfg)
if cfg.Tracing.Enabled {
switch t := cfg.Tracing.Type; t {
case "agent":
exporter, err := ocagent.NewExporter(
ocagent.WithReconnectionPeriod(5*time.Second),
ocagent.WithAddress(cfg.Tracing.Endpoint),
ocagent.WithServiceName(cfg.Tracing.Service),
)
if err != nil {
logger.Error().
Err(err).
Str("endpoint", cfg.Tracing.Endpoint).
Str("collector", cfg.Tracing.Collector).
Msg("Failed to create agent tracing")
return err
}
trace.RegisterExporter(exporter)
view.RegisterExporter(exporter)
case "jaeger":
exporter, err := jaeger.NewExporter(
jaeger.Options{
AgentEndpoint: cfg.Tracing.Endpoint,
CollectorEndpoint: cfg.Tracing.Collector,
ServiceName: cfg.Tracing.Service,
},
)
if err != nil {
logger.Error().
Err(err).
Str("endpoint", cfg.Tracing.Endpoint).
Str("collector", cfg.Tracing.Collector).
Msg("Failed to create jaeger tracing")
return err
}
trace.RegisterExporter(exporter)
case "zipkin":
endpoint, err := openzipkin.NewEndpoint(
cfg.Tracing.Service,
cfg.Tracing.Endpoint,
)
if err != nil {
logger.Error().
Err(err).
Str("endpoint", cfg.Tracing.Endpoint).
Str("collector", cfg.Tracing.Collector).
Msg("Failed to create zipkin tracing")
return err
}
exporter := zipkin.NewExporter(
zipkinhttp.NewReporter(
cfg.Tracing.Collector,
),
endpoint,
)
trace.RegisterExporter(exporter)
default:
logger.Warn().
Str("type", t).
Msg("Unknown tracing backend")
}
trace.ApplyConfig(
trace.Config{
DefaultSampler: trace.AlwaysSample(),
},
)
} else {
logger.Debug().
Msg("Tracing is not enabled")
}
var (
gr = run.Group{}
ctx, cancel = context.WithCancel(context.Background())
//metrics = metrics.New()
)
defer cancel()
{
cfg := glauthcfg.Config{
LDAP: glauthcfg.LDAP{
Enabled: cfg.Ldap.Enabled,
Listen: cfg.Ldap.Address,
},
LDAPS: glauthcfg.LDAPS{
Enabled: cfg.Ldaps.Enabled,
Listen: cfg.Ldaps.Address,
Cert: cfg.Ldaps.Cert,
Key: cfg.Ldaps.Key,
},
Backend: glauthcfg.Backend{
BaseDN: cfg.Backend.BaseDN,
Insecure: cfg.Backend.Insecure,
NameFormat: cfg.Backend.NameFormat,
GroupFormat: cfg.Backend.GroupFormat,
SSHKeyAttr: cfg.Backend.SSHKeyAttr,
},
}
if cfg.LDAPS.Enabled {
// GenCert has side effects as it writes 2 files to the binary running location
if err := crypto.GenCert("ldap.crt", "ldap.key", logger); err != nil {
logger.Fatal().Err(err).Msgf("Could not generate test-certificate")
}
}
as, gs, err := getAccountsServices()
if err != nil {
return err
}
server, err := glauth.Server(
glauth.AccountsService(as),
glauth.GroupsService(gs),
glauth.Logger(logger),
glauth.Config(&cfg),
)
if err != nil {
logger.Info().
Err(err).
Str("transport", "ldap").
Msg("Failed to initialize server")
return err
}
gr.Add(func() error {
err := make(chan error)
select {
case <-ctx.Done():
return nil
case err <- server.ListenAndServe():
return <-err
}
}, func(_ error) {
logger.Info().
Str("transport", "ldap").
Msg("Shutting down server")
server.Shutdown()
cancel()
})
gr.Add(func() error {
err := make(chan error)
select {
case <-ctx.Done():
return nil
case err <- server.ListenAndServeTLS():
return <-err
}
}, func(_ error) {
logger.Info().
Str("transport", "ldaps").
Msg("Shutting down server")
server.Shutdown()
cancel()
})
}
{
server, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().
Err(err).
Str("transport", "debug").
Msg("Failed to initialize server")
return err
}
gr.Add(func() error {
return server.ListenAndServe()
}, func(_ error) {
ctx, timeout := context.WithTimeout(ctx, 5*time.Second)
defer timeout()
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Info().
Err(err).
Str("transport", "debug").
Msg("Failed to shutdown server")
} else {
logger.Info().
Str("transport", "debug").
Msg("Shutting down server")
}
})
}
{
stop := make(chan os.Signal, 1)
gr.Add(func() error {
signal.Notify(stop, os.Interrupt)
<-stop
return nil
}, func(err error) {
close(stop)
cancel()
})
}
return gr.Run()
},
}
}
// getAccountsServices returns an ocis-accounts service
func getAccountsServices() (accounts.AccountsService, accounts.GroupsService, error) {
service := micro.NewService()
// parse command line flags
service.Init()
err := service.Client().Init(
client.ContentType("application/json"),
)
if err != nil {
return nil, nil, err
}
return accounts.NewAccountsService("com.owncloud.api.accounts", service.Client()),
accounts.NewGroupsService("com.owncloud.api.accounts", service.Client()),
nil
}
+71
View File
@@ -0,0 +1,71 @@
package config
// Log defines the available logging configuration.
type Log struct {
Level string
Pretty bool
Color bool
}
// Debug defines the available debug configuration.
type Debug struct {
Addr string
Token string
Pprof bool
Zpages bool
}
// HTTP defines the available http configuration.
type HTTP struct {
Addr string
Namespace string
Root string
}
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool
Type string
Endpoint string
Collector string
Service string
}
// Ldap defined the available LDAP configuration.
type Ldap struct {
Address string
Enabled bool
}
// Ldaps defined the available LDAPS configuration.
type Ldaps struct {
Ldap
Cert string
Key string
}
// Backend defined the available backend configuration.
type Backend struct {
BaseDN string
Insecure bool
NameFormat string
GroupFormat string
SSHKeyAttr string
}
// Config combines all available configuration parts.
type Config struct {
File string
Log Log
Debug Debug
HTTP HTTP
Tracing Tracing
Ldap Ldap
Ldaps Ldaps
Backend Backend
}
// New initializes a new configuration with or without defaults.
func New() *Config {
return &Config{}
}
+123
View File
@@ -0,0 +1,123 @@
package crypto
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"time"
"github.com/owncloud/ocis-pkg/v2/log"
)
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}
func pemBlockForKey(priv interface{}, l log.Logger) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
l.Fatal().Err(err).Msg("Unable to marshal ECDSA private key")
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
default:
return nil
}
}
// GenCert generates TLS-Certificates
func GenCert(certName string, keyName string, l log.Logger) error {
var priv interface{}
var err error
priv, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
l.Fatal().Err(err).Msg("Failed to generate private key")
}
notBefore := time.Now()
notAfter := notBefore.Add(24 * time.Hour * 365)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
l.Fatal().Err(err).Msg("Failed to generate serial number")
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Corp"},
CommonName: "OCIS",
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
hosts := []string{"127.0.0.1", "localhost"}
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}
//template.IsCA = true
//template.KeyUsage |= x509.KeyUsageCertSign
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
l.Fatal().Err(err).Msg("Failed to create certificate")
}
certOut, err := os.Create(certName)
if err != nil {
l.Fatal().Err(err).Msgf("Failed to open %v for writing", certName)
}
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
l.Fatal().Err(err).Msg("Failed to encode certificate")
}
err = certOut.Close()
if err != nil {
l.Fatal().Err(err).Msg("Failed to write cert")
}
l.Info().Msg("Written server.crt")
keyOut, err := os.OpenFile(keyName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
l.Fatal().Err(err).Msgf("Failed to open %v for writing", keyName)
}
err = pem.Encode(keyOut, pemBlockForKey(priv, l))
if err != nil {
l.Fatal().Err(err).Msg("Failed to encode key")
}
err = keyOut.Close()
if err != nil {
l.Fatal().Err(err).Msg("Failed to write key")
}
l.Info().Msgf("Written %v", keyName)
return nil
}
+199
View File
@@ -0,0 +1,199 @@
package flagset
import (
"github.com/micro/cli/v2"
"github.com/owncloud/ocis-glauth/pkg/config"
)
// RootWithConfig applies cfg to the root flagset
func RootWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "config-file",
Value: "",
Usage: "Path to config file",
EnvVars: []string{"GLAUTH_CONFIG_FILE"},
Destination: &cfg.File,
},
&cli.StringFlag{
Name: "log-level",
Value: "info",
Usage: "Set logging level",
EnvVars: []string{"GLAUTH_LOG_LEVEL"},
Destination: &cfg.Log.Level,
},
&cli.BoolFlag{
Value: true,
Name: "log-pretty",
Usage: "Enable pretty logging",
EnvVars: []string{"GLAUTH_LOG_PRETTY"},
Destination: &cfg.Log.Pretty,
},
&cli.BoolFlag{
Value: true,
Name: "log-color",
Usage: "Enable colored logging",
EnvVars: []string{"GLAUTH_LOG_COLOR"},
Destination: &cfg.Log.Color,
},
}
}
// HealthWithConfig applies cfg to the root flagset
func HealthWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "debug-addr",
Value: "0.0.0.0:9129",
Usage: "Address to debug endpoint",
EnvVars: []string{"GLAUTH_DEBUG_ADDR"},
Destination: &cfg.Debug.Addr,
},
}
}
// ServerWithConfig applies cfg to the root flagset
func ServerWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: "tracing-enabled",
Usage: "Enable sending traces",
EnvVars: []string{"GLAUTH_TRACING_ENABLED"},
Destination: &cfg.Tracing.Enabled,
},
&cli.StringFlag{
Name: "tracing-type",
Value: "jaeger",
Usage: "Tracing backend type",
EnvVars: []string{"GLAUTH_TRACING_TYPE"},
Destination: &cfg.Tracing.Type,
},
&cli.StringFlag{
Name: "tracing-endpoint",
Value: "",
Usage: "Endpoint for the agent",
EnvVars: []string{"GLAUTH_TRACING_ENDPOINT"},
Destination: &cfg.Tracing.Endpoint,
},
&cli.StringFlag{
Name: "tracing-collector",
Value: "",
Usage: "Endpoint for the collector",
EnvVars: []string{"GLAUTH_TRACING_COLLECTOR"},
Destination: &cfg.Tracing.Collector,
},
&cli.StringFlag{
Name: "tracing-service",
Value: "glauth",
Usage: "Service name for tracing",
EnvVars: []string{"GLAUTH_TRACING_SERVICE"},
Destination: &cfg.Tracing.Service,
},
&cli.StringFlag{
Name: "debug-addr",
Value: "0.0.0.0:9129",
Usage: "Address to bind debug server",
EnvVars: []string{"GLAUTH_DEBUG_ADDR"},
Destination: &cfg.Debug.Addr,
},
&cli.StringFlag{
Name: "debug-token",
Value: "",
Usage: "Token to grant metrics access",
EnvVars: []string{"GLAUTH_DEBUG_TOKEN"},
Destination: &cfg.Debug.Token,
},
&cli.BoolFlag{
Name: "debug-pprof",
Usage: "Enable pprof debugging",
EnvVars: []string{"GLAUTH_DEBUG_PPROF"},
Destination: &cfg.Debug.Pprof,
},
&cli.BoolFlag{
Name: "debug-zpages",
Usage: "Enable zpages debugging",
EnvVars: []string{"GLAUTH_DEBUG_ZPAGES"},
Destination: &cfg.Debug.Zpages,
},
&cli.StringFlag{
Name: "ldap-addr",
Value: "0.0.0.0:9125",
Usage: "Address to bind ldap server",
EnvVars: []string{"GLAUTH_LDAP_ADDR"},
Destination: &cfg.Ldap.Address,
},
&cli.BoolFlag{
Name: "ldap-enabled",
Value: true,
Usage: "Enable ldap server",
EnvVars: []string{"GLAUTH_LDAP_ENABLED"},
Destination: &cfg.Ldap.Enabled,
},
&cli.StringFlag{
Name: "ldaps-addr",
Value: "0.0.0.0:9126",
Usage: "Address to bind ldap server",
EnvVars: []string{"GLAUTH_LDAPS_ADDR"},
Destination: &cfg.Ldaps.Address,
},
&cli.BoolFlag{
Name: "ldaps-enabled",
Value: true,
Usage: "Enable ldap server",
EnvVars: []string{"GLAUTH_LDAPS_ENABLED"},
Destination: &cfg.Ldaps.Enabled,
},
&cli.StringFlag{
Name: "ldaps-cert",
Value: "./ldap.crt",
Usage: "path to ldaps certificate in PEM format",
EnvVars: []string{"GLAUTH_LDAPS_CERT"},
Destination: &cfg.Ldaps.Cert,
},
&cli.StringFlag{
Name: "ldaps-key",
Value: "./ldap.key",
Usage: "path to ldaps key in PEM format",
EnvVars: []string{"GLAUTH_LDAPS_KEY"},
Destination: &cfg.Ldaps.Key,
},
&cli.StringFlag{
Name: "backend-basedn",
Value: "dc=example,dc=org",
Usage: "base distinguished name to expose",
EnvVars: []string{"GLAUTH_BACKEND_BASEDN"},
Destination: &cfg.Backend.BaseDN,
},
&cli.BoolFlag{
Name: "backend-insecure",
Value: false,
Usage: "Allow insecure requests to the datastore",
EnvVars: []string{"GLAUTH_BACKEND_INSECURE"},
Destination: &cfg.Backend.Insecure,
},
&cli.StringFlag{
Name: "backend-name-format",
Value: "cn",
Usage: "name attribute for entries to expose. typically cn or uid",
EnvVars: []string{"GLAUTH_BACKEND_NAME_FORMAT"},
Destination: &cfg.Backend.NameFormat,
},
&cli.StringFlag{
Name: "backend-group-format",
Value: "ou",
Usage: "name attribute for entries to expose. typically ou, cn or dc",
EnvVars: []string{"GLAUTH_BACKEND_GROUP_FORMAT"},
Destination: &cfg.Backend.GroupFormat,
},
&cli.StringFlag{
Name: "backend-ssh-key-attr",
Value: "sshPublicKey",
Usage: "ssh key attribute for entries to expose",
EnvVars: []string{"GLAUTH_BACKEND_SSH_KEY_ATTR"},
Destination: &cfg.Backend.SSHKeyAttr,
},
}
}
+32
View File
@@ -0,0 +1,32 @@
package metrics
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "glauth"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
// Namespace: Namespace,
// Subsystem: Subsystem,
// Name: "greet_total",
// Help: "How many greeting requests processed",
// }, []string{}),
}
// prometheus.Register(
// m.Counter,
// )
return m
}
+134
View File
@@ -0,0 +1,134 @@
package mlogr
import (
"errors"
"github.com/go-logr/logr"
plog "github.com/owncloud/ocis-pkg/v2/log"
"github.com/rs/zerolog"
)
const debugVerbosity = 6
const traceVerbosity = 8
// New returns a logr.Logger which is implemented by the log.
func New(l *plog.Logger) logr.Logger {
return logger{
l: l,
verbosity: 0,
prefix: "glauth",
values: nil,
}
}
// logger is a logr.Logger that uses the ocis-pkg log.
type logger struct {
l *plog.Logger
verbosity int
prefix string
values []interface{}
}
func (l logger) clone() logger {
out := l
out.values = copySlice(l.values)
return out
}
func copySlice(in []interface{}) []interface{} {
out := make([]interface{}, len(in))
copy(out, in)
return out
}
// add converts a bunch of arbitrary key-value pairs into zerolog fields.
func add(e *zerolog.Event, keysAndVals []interface{}) {
// make sure we got an even number of arguments
if len(keysAndVals)%2 != 0 {
e.Interface("args", keysAndVals).
AnErr("zerologr-err", errors.New("odd number of arguments passed as key-value pairs for logging")).
Stack()
return
}
for i := 0; i < len(keysAndVals); {
// process a key-value pair,
// ensuring that the key is a string
key, val := keysAndVals[i], keysAndVals[i+1]
keyStr, isString := key.(string)
if !isString {
// if the key isn't a string, log additional error
e.Interface("invalid key", key).
AnErr("zerologr-err", errors.New("non-string key argument passed to logging, ignoring all later arguments")).
Stack()
return
}
e.Interface(keyStr, val)
i += 2
}
}
func (l logger) Info(msg string, keysAndVals ...interface{}) {
if l.Enabled() {
var e *zerolog.Event
if l.verbosity < debugVerbosity {
e = l.l.Info()
} else if l.verbosity < traceVerbosity {
e = l.l.Debug()
} else {
e = l.l.Trace()
}
e.Int("verbosity", l.verbosity)
if l.prefix != "" {
e.Str("name", l.prefix)
}
add(e, l.values)
add(e, keysAndVals)
e.Msg(msg)
}
}
func (l logger) Enabled() bool {
return true
}
func (l logger) Error(err error, msg string, keysAndVals ...interface{}) {
e := l.l.Error().Err(err)
if l.prefix != "" {
e.Str("name", l.prefix)
}
add(e, l.values)
add(e, keysAndVals)
e.Msg(msg)
}
func (l logger) V(verbosity int) logr.InfoLogger {
//new := l.clone()
//new.level = level
//return new
l.verbosity = verbosity
return l
}
// WithName returns a new logr.Logger with the specified name appended. zerologr
// uses '/' characters to separate name elements. Callers should not pass '/'
// in the provided name string, but this library does not actually enforce that.
func (l logger) WithName(name string) logr.Logger {
new := l.clone()
if len(l.prefix) > 0 {
new.prefix = l.prefix + "/"
}
new.prefix += name
return new
}
func (l logger) WithValues(kvList ...interface{}) logr.Logger {
new := l.clone()
new.values = append(new.values, kvList...)
return new
}
var _ logr.Logger = logger{}
var _ logr.InfoLogger = logger{}
+50
View File
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis-glauth/pkg/config"
"github.com/owncloud/ocis-pkg/v2/log"
)
// 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
}
}
+51
View File
@@ -0,0 +1,51 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis-glauth/pkg/config"
"github.com/owncloud/ocis-glauth/pkg/version"
"github.com/owncloud/ocis-pkg/v2/service/debug"
)
// 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("glauth"),
debug.Version(version.String),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(health(options.Config)),
debug.Ready(ready(options.Config)),
), nil
}
// health implements the health check.
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO(tboerger): check if services are up and running
io.WriteString(w, http.StatusText(http.StatusOK))
}
}
// ready implements the ready check.
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO(tboerger): check if services are up and running
io.WriteString(w, http.StatusText(http.StatusOK))
}
}
+462
View File
@@ -0,0 +1,462 @@
package glauth
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/glauth/glauth/pkg/config"
"github.com/glauth/glauth/pkg/handler"
"github.com/glauth/glauth/pkg/stats"
ber "github.com/nmcclain/asn1-ber"
"github.com/nmcclain/ldap"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"github.com/owncloud/ocis-pkg/v2/log"
)
type queryType string
const (
usersQuery queryType = "users"
groupsQuery queryType = "groups"
)
type ocisHandler struct {
as accounts.AccountsService
gs accounts.GroupsService
log log.Logger
cfg *config.Config
}
func (h ocisHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN)
h.log.Debug().
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Interface("src", conn.RemoteAddr()).
Msg("Bind request")
stats.Frontend.Add("bind_reqs", 1)
// parse the bindDN - ensure that the bindDN ends with the BaseDN
if !strings.HasSuffix(bindDN, baseDN) {
h.log.Error().
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Interface("src", conn.RemoteAddr()).
Msg("BindDN not part of our BaseDN")
return ldap.LDAPResultInvalidCredentials, nil
}
parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",")
if len(parts) > 2 {
h.log.Error().
Str("binddn", bindDN).
Int("numparts", len(parts)).
Interface("src", conn.RemoteAddr()).
Msg("BindDN should have only one or two parts")
return ldap.LDAPResultInvalidCredentials, nil
}
userName := strings.TrimPrefix(parts[0], "cn=")
// check password
res, err := h.as.ListAccounts(context.TODO(), &accounts.ListAccountsRequest{
//Query: fmt.Sprintf("username eq '%s'", username),
// TODO this allows lookung up users when you know the username using basic auth
// adding the password to the query is an option but sending the sover the wira a la scim seems ugly
// but to set passwords our accounts need it anyway
Query: fmt.Sprintf("login eq '%s' and password eq '%s'", userName, bindSimplePw),
})
if err != nil || len(res.Accounts) == 0 {
h.log.Error().
Str("username", userName).
Str("binddn", bindDN).
Interface("src", conn.RemoteAddr()).
Msg("Login failed")
return ldap.LDAPResultInvalidCredentials, nil
}
stats.Frontend.Add("bind_successes", 1)
h.log.Debug().
Str("binddn", bindDN).
Interface("src", conn.RemoteAddr()).
Msg("Bind success")
return ldap.LDAPResultSuccess, nil
}
func (h ocisHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN)
searchBaseDN := strings.ToLower(searchReq.BaseDN)
h.log.Debug().
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Str("filter", searchReq.Filter).
Interface("src", conn.RemoteAddr()).
Msg("Search request")
stats.Frontend.Add("search_reqs", 1)
// validate the user is authenticated and has appropriate access
if len(bindDN) < 1 {
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultInsufficientAccessRights,
}, fmt.Errorf("search error: Anonymous BindDN not allowed %s", bindDN)
}
if !strings.HasSuffix(bindDN, baseDN) {
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultInsufficientAccessRights,
}, fmt.Errorf("search error: BindDN %s not in our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)
}
if !strings.HasSuffix(searchBaseDN, h.cfg.Backend.BaseDN) {
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultInsufficientAccessRights,
}, fmt.Errorf("search error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.cfg.Backend.BaseDN)
}
var qtype queryType = ""
query := ""
var code ldap.LDAPResultCode
var err error
if searchReq.Filter == "(&)" { // see Absolute True and False Filters in https://tools.ietf.org/html/rfc4526#section-2
query = ""
} else {
var cf *ber.Packet
cf, err = ldap.CompileFilter(searchReq.Filter)
if err != nil {
h.log.Debug().
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Str("filter", searchReq.Filter).
Interface("src", conn.RemoteAddr()).
Msg("could not compile filter")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter)
}
qtype, query, code, err = parseFilter(cf)
if err != nil {
return ldap.ServerSearchResult{
ResultCode: code,
}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter)
}
// check if the searchBaseDN already has a username and add it to the query
parts := strings.Split(strings.TrimSuffix(searchBaseDN, baseDN), ",")
if len(parts) > 0 && strings.HasPrefix(parts[0], "cn=") {
if len(query) > 0 {
query += " AND "
}
query += fmt.Sprintf("on_premises_sam_account_name eq '%s'", escapeValue(strings.TrimPrefix(parts[0], "cn=")))
}
}
entries := []*ldap.Entry{}
h.log.Debug().
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Str("filter", searchReq.Filter).
Str("qtype", string(qtype)).
Str("query", query).
Msg("parsed query")
switch qtype {
case usersQuery:
accounts, err := h.as.ListAccounts(context.TODO(), &accounts.ListAccountsRequest{
Query: query,
})
if err != nil {
h.log.Error().
Err(err).
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Str("filter", searchReq.Filter).
Str("query", query).
Interface("src", conn.RemoteAddr()).
Msg("Could not list accounts")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, errors.New("search error: error listing users")
}
entries = append(entries, h.mapAccounts(accounts.Accounts)...)
case groupsQuery:
groups, err := h.gs.ListGroups(context.TODO(), &accounts.ListGroupsRequest{
Query: query,
})
if err != nil {
h.log.Error().
Err(err).
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Str("filter", searchReq.Filter).
Str("query", query).
Interface("src", conn.RemoteAddr()).
Msg("Could not list groups")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, errors.New("search error: error listing groups")
}
entries = append(entries, h.mapGroups(groups.Groups)...)
}
stats.Frontend.Add("search_successes", 1)
h.log.Debug().
Str("binddn", bindDN).
Str("basedn", h.cfg.Backend.BaseDN).
Str("filter", searchReq.Filter).
Interface("src", conn.RemoteAddr()).
Msg("AP: Search OK")
return ldap.ServerSearchResult{
Entries: entries,
Referrals: []string{},
Controls: []ldap.Control{},
ResultCode: ldap.LDAPResultSuccess,
}, nil
}
func attribute(name string, values ...string) *ldap.EntryAttribute {
return &ldap.EntryAttribute{
Name: name,
Values: values,
}
}
func (h ocisHandler) mapAccounts(accounts []*accounts.Account) []*ldap.Entry {
var entries []*ldap.Entry
for i := range accounts {
attrs := []*ldap.EntryAttribute{
attribute("objectClass", "posixAccount", "inetOrgPerson", "organizationalPerson", "Person", "top"),
attribute("cn", accounts[i].PreferredName),
attribute("uid", accounts[i].PreferredName),
attribute("sn", accounts[i].PreferredName),
attribute("homeDirectory", ""),
attribute("ownCloudUUID", accounts[i].Id), // see https://github.com/butonic/owncloud-ldap-schema/blob/master/owncloud.schema#L28-L34
}
if accounts[i].DisplayName != "" {
attrs = append(attrs, attribute("displayName", accounts[i].DisplayName))
}
if accounts[i].Mail != "" {
attrs = append(attrs, attribute("mail", accounts[i].Mail))
}
if accounts[i].UidNumber != 0 { // TODO no root?
attrs = append(attrs, attribute("uidnumber", strconv.FormatInt(accounts[i].UidNumber, 10)))
}
if accounts[i].GidNumber != 0 {
attrs = append(attrs, attribute("gidnumber", strconv.FormatInt(accounts[i].GidNumber, 10)))
}
if accounts[i].Description != "" {
attrs = append(attrs, attribute("description", accounts[i].Description))
}
dn := fmt.Sprintf("%s=%s,%s=%s,%s",
h.cfg.Backend.NameFormat,
accounts[i].PreferredName,
h.cfg.Backend.GroupFormat,
"users",
h.cfg.Backend.BaseDN,
)
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
}
return entries
}
func (h ocisHandler) mapGroups(groups []*accounts.Group) []*ldap.Entry {
var entries []*ldap.Entry
for i := range groups {
attrs := []*ldap.EntryAttribute{
attribute("objectClass", "posixGroup", "groupOfNames", "top"),
attribute("cn", groups[i].OnPremisesSamAccountName),
attribute("ownCloudUUID", groups[i].Id), // see https://github.com/butonic/owncloud-ldap-schema/blob/master/owncloud.schema#L28-L34
}
if groups[i].DisplayName != "" {
attrs = append(attrs, attribute("displayName", groups[i].DisplayName))
}
if groups[i].GidNumber != 0 {
attrs = append(attrs, attribute("gidnumber", strconv.FormatInt(groups[i].GidNumber, 10)))
}
if groups[i].Description != "" {
attrs = append(attrs, attribute("description", groups[i].Description))
}
dn := fmt.Sprintf("%s=%s,%s=%s,%s",
h.cfg.Backend.NameFormat,
groups[i].OnPremisesSamAccountName,
h.cfg.Backend.GroupFormat,
"groups",
h.cfg.Backend.BaseDN,
)
memberUids := make([]string, len(groups[i].Members))
for j := range groups[i].Members {
memberUids[j] = groups[i].Members[j].PreferredName
}
attrs = append(attrs, attribute("memberuid", memberUids...))
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
}
return entries
}
// LDAP filters might ask for groups and users at the same time, eg.
// (|
// (&(objectClass=posixaccount)(cn=einstein))
// (&(objectClass=posixgroup)(cn=users))
// )
// (&(objectClass=posixaccount)(objectClass=posixgroup))
// qtype is one of
// "" not determined
// "users"
// "groups"
func parseFilter(f *ber.Packet) (queryType, string, ldap.LDAPResultCode, error) {
var qtype queryType
var q string
var code ldap.LDAPResultCode
var err error
switch ldap.FilterMap[f.Tag] {
case "Equality Match":
if len(f.Children) != 2 {
return "", "", ldap.LDAPResultOperationsError, errors.New("equality match must have exactly two children")
}
attribute := strings.ToLower(f.Children[0].Value.(string))
value := f.Children[1].Value.(string)
// replace attributes
switch attribute {
case "objectclass":
switch strings.ToLower(value) {
case "posixaccount", "shadowaccount", "users", "person", "inetorgperson", "organizationalperson":
qtype = usersQuery
case "posixgroup", "groups":
qtype = groupsQuery
default:
qtype = ""
}
case "ownclouduuid":
q = fmt.Sprintf("id eq '%s'", escapeValue(value))
case "cn", "uid":
// on_premises_sam_account_name is indexed using the lowercase analyzer in ocis-accounts
// TODO use "tolower(on_premises_sam_account_name) eq '%s'" to be clear about the case insensitive comparison
q = fmt.Sprintf("on_premises_sam_account_name eq '%s'", escapeValue(value))
case "mail":
q = fmt.Sprintf("mail eq '%s'", escapeValue(value))
case "displayname":
q = fmt.Sprintf("display_name eq '%s'", escapeValue(value))
case "uidnumber":
if i, err := strconv.ParseUint(value, 10, 64); err != nil {
code = ldap.LDAPResultInvalidAttributeSyntax
} else {
q = fmt.Sprintf("uid_number eq %d", i)
}
case "gidnumber":
if i, err := strconv.ParseUint(value, 10, 64); err != nil {
code = ldap.LDAPResultInvalidAttributeSyntax
} else {
q = fmt.Sprintf("gid_number eq %d", i)
}
case "description":
q = fmt.Sprintf("description eq '%s'", escapeValue(value))
default:
code = ldap.LDAPResultUndefinedAttributeType
err = fmt.Errorf("unrecognized assertion type '%s' in filter item", attribute)
}
return qtype, q, code, err
case "Substrings":
if len(f.Children) != 2 {
return "", "", ldap.LDAPResultOperationsError, errors.New("substrings filter must have exactly two children")
}
attribute := strings.ToLower(f.Children[0].Value.(string))
if len(f.Children[1].Children) != 1 {
return "", "", ldap.LDAPResultUnwillingToPerform, fmt.Errorf("substrings filter only supports prefix match")
}
value := f.Children[1].Children[0].Value.(string)
// replace attributes
switch attribute {
case "objectclass":
switch strings.ToLower(value) {
case "posixaccount", "shadowaccount", "users", "person", "inetorgperson", "organizationalperson":
qtype = usersQuery
case "posixgroup", "groups":
qtype = groupsQuery
default:
qtype = ""
}
case "ownclouduuid":
q = fmt.Sprintf("startswith(id,'%s')", escapeValue(value))
case "cn", "uid":
// on_premises_sam_account_name is indexed using the lowercase analyzer in ocis-accounts
// TODO use "tolower(on_premises_sam_account_name) eq '%s'" to be clear about the case insensitive comparison
q = fmt.Sprintf("startswith(on_premises_sam_account_name,'%s')", escapeValue(value))
case "mail":
q = fmt.Sprintf("startswith(mail,'%s')", escapeValue(value))
case "displayname":
q = fmt.Sprintf("startswith(display_name,'%s')", escapeValue(value))
case "description":
q = fmt.Sprintf("startswith(description,'%s')", escapeValue(value))
default:
code = ldap.LDAPResultUndefinedAttributeType
err = fmt.Errorf("unrecognized assertion type '%s' in filter item", attribute)
}
return qtype, q, code, err
case "And", "Or":
subQueries := []string{}
for i := range f.Children {
var subQuery string
var qt queryType
qt, subQuery, code, err = parseFilter(f.Children[i])
if err != nil {
return "", "", code, err
}
if qtype == "" {
qtype = qt
} else if qt != "" && qt != qtype {
return "", "", ldap.LDAPResultUnwillingToPerform, fmt.Errorf("mixing user and group filters not supported")
}
if subQuery != "" {
subQueries = append(subQueries, subQuery)
}
}
return qtype, strings.Join(subQueries, " "+strings.ToLower(ldap.FilterMap[f.Tag])+" "), ldap.LDAPResultSuccess, nil
case "Not":
if len(f.Children) != 1 {
return "", "", ldap.LDAPResultOperationsError, errors.New("not filter match must have exactly one child")
}
qtype, subQuery, code, err := parseFilter(f.Children[0])
if err != nil {
return "", "", code, err
}
if subQuery != "" {
q = fmt.Sprintf("not %s", subQuery)
}
return qtype, q, code, nil
}
return qtype, q, ldap.LDAPResultUnwillingToPerform, fmt.Errorf("%s filter not implemented", ldap.FilterMap[f.Tag])
}
// escapeValue escapes all special characters in the value
func escapeValue(value string) string {
return strings.ReplaceAll(value, "'", "''")
}
func (h ocisHandler) Close(boundDN string, conn net.Conn) error {
stats.Frontend.Add("closes", 1)
return nil
}
// NewOCISHandler implements a glauth backend with ocis-accounts as tdhe datasource
func NewOCISHandler(opts ...Option) handler.Handler {
options := newOptions(opts...)
handler := ocisHandler{
log: options.Logger,
cfg: options.Config,
as: options.AccountsService,
gs: options.GroupsService,
}
return handler
}
+67
View File
@@ -0,0 +1,67 @@
package glauth
import (
"context"
"github.com/glauth/glauth/pkg/config"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"github.com/owncloud/ocis-pkg/v2/log"
)
// 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
AccountsService accounts.AccountsService
GroupsService accounts.GroupsService
}
// 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
}
}
// AccountsService provides an AccountsService client to set the AccountsService option.
func AccountsService(val accounts.AccountsService) Option {
return func(o *Options) {
o.AccountsService = val
}
}
// GroupsService provides an GroupsService client to set the GroupsService option.
func GroupsService(val accounts.GroupsService) Option {
return func(o *Options) {
o.GroupsService = val
}
}
+75
View File
@@ -0,0 +1,75 @@
package glauth
import (
"errors"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/pkg/config"
"github.com/go-logr/logr"
"github.com/nmcclain/ldap"
"github.com/owncloud/ocis-glauth/pkg/mlogr"
)
// LdapSvc holds the ldap server struct
type LdapSvc struct {
log logr.Logger
c *config.Config
yubiAuth *yubigo.YubiAuth
l *ldap.Server
}
// Server initializes the debug service and server.
func Server(opts ...Option) (*LdapSvc, error) {
options := newOptions(opts...)
s := LdapSvc{
log: mlogr.New(&options.Logger),
c: options.Config,
}
var err error
if len(s.c.YubikeyClientID) > 0 && len(s.c.YubikeySecret) > 0 {
s.yubiAuth, err = yubigo.NewYubiAuth(s.c.YubikeyClientID, s.c.YubikeySecret)
if err != nil {
return nil, errors.New("yubikey auth failed")
}
}
// configure the backend
s.l = ldap.NewServer()
s.l.EnforceLDAP = true
h := NewOCISHandler(
AccountsService(options.AccountsService),
GroupsService(options.GroupsService),
Logger(options.Logger),
Config(s.c),
)
s.l.BindFunc("", h)
s.l.SearchFunc("", h)
s.l.CloseFunc("", h)
return &s, nil
}
// ListenAndServe listens on the TCP network address s.c.LDAP.Listen
func (s *LdapSvc) ListenAndServe() error {
s.log.V(3).Info("LDAP server listening", "address", s.c.LDAP.Listen)
return s.l.ListenAndServe(s.c.LDAP.Listen)
}
// ListenAndServeTLS listens on the TCP network address s.c.LDAPS.Listen
func (s *LdapSvc) ListenAndServeTLS() error {
s.log.V(3).Info("LDAPS server listening", "address", s.c.LDAPS.Listen)
return s.l.ListenAndServeTLS(
s.c.LDAPS.Listen,
s.c.LDAPS.Cert,
s.c.LDAPS.Key,
)
}
// Shutdown ends listeners by sending true to the ldap serves quit channel
func (s *LdapSvc) Shutdown() {
s.l.Quit <- true
}
+19
View File
@@ -0,0 +1,19 @@
package version
import (
"time"
)
var (
// String gets defined by the build system.
String = "0.0.0"
// Date indicates the build date.
Date = "00000000"
)
// Compiled returns the compile time of this service.
func Compiled() time.Time {
t, _ := time.Parse("20060102", Date)
return t
}