refactor glauth

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-04-13 17:04:37 +02:00
parent 2089ac5f7b
commit d4442941a1
36 changed files with 32 additions and 32 deletions
+2
View File
@@ -0,0 +1,2 @@
*
!bin/
+1
View File
@@ -0,0 +1 @@
data/
+37
View File
@@ -0,0 +1,37 @@
SHELL := bash
NAME := glauth
include ../../.make/recursion.mk
############ tooling ############
ifneq (, $(shell which go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
############ go tooling ############
include ../../.make/go.mk
############ release ############
include ../../.make/release.mk
############ docs generate ############
include ../../.make/docs.mk
.PHONY: docs-generate
docs-generate: config-docs-generate
############ generate ############
include ../../.make/generate.mk
.PHONY: ci-go-generate
ci-go-generate: # CI runs ci-node-generate automatically before this target
.PHONY: ci-node-generate
ci-node-generate:
############ licenses ############
.PHONY: ci-node-check-licenses
ci-node-check-licenses:
.PHONY: ci-node-save-licenses
ci-node-save-licenses:
+14
View File
@@ -0,0 +1,14 @@
package main
import (
"os"
"github.com/owncloud/ocis/extensions/glauth/pkg/command"
"github.com/owncloud/ocis/extensions/glauth/pkg/config/defaults"
)
func main() {
if err := command.Execute(defaults.DefaultConfig()); err != nil {
os.Exit(1)
}
}
@@ -0,0 +1,19 @@
FROM amd64/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS GLAuth" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9120 9124
ENTRYPOINT ["/usr/bin/ocis-glauth"]
CMD ["server"]
COPY bin/ocis-glauth /usr/bin/ocis-glauth
@@ -0,0 +1,19 @@
FROM arm32v6/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS GLAuth" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9120 9124
ENTRYPOINT ["/usr/bin/ocis-glauth"]
CMD ["server"]
COPY bin/ocis-glauth /usr/bin/ocis-glauth
@@ -0,0 +1,19 @@
FROM arm64v8/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS GLAuth" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9120 9124
ENTRYPOINT ["/usr/bin/ocis-glauth"]
CMD ["server"]
COPY bin/ocis-glauth /usr/bin/ocis-glauth
+22
View File
@@ -0,0 +1,22 @@
image: owncloud/ocis-glauth:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: owncloud/ocis-glauth:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: owncloud/ocis-glauth:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
variant: v8
os: linux
- image: owncloud/ocis-glauth:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
variant: v6
os: linux
+53
View File
@@ -0,0 +1,53 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/extensions/glauth/pkg/config/parser"
"github.com/owncloud/ocis/extensions/glauth/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "check health status",
Category: "info",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
+64
View File
@@ -0,0 +1,64 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/clihelper"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/thejerf/suture/v4"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the ocis-glauth command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "ocis-glauth",
Usage: "Serve GLAuth API for oCIS",
Commands: GetCommands(cfg),
})
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
return app.Run(os.Args)
}
// SutureService allows for the glauth command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new glauth.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.GLAuth.Commons = cfg.Commons
return SutureService{
cfg: cfg.GLAuth,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}
+186
View File
@@ -0,0 +1,186 @@
package command
import (
"context"
"fmt"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
glauthcfg "github.com/glauth/glauth/v2/pkg/config"
"github.com/oklog/run"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/extensions/glauth/pkg/config/parser"
"github.com/owncloud/ocis/extensions/glauth/pkg/logging"
"github.com/owncloud/ocis/extensions/glauth/pkg/metrics"
"github.com/owncloud/ocis/extensions/glauth/pkg/server/debug"
"github.com/owncloud/ocis/extensions/glauth/pkg/server/glauth"
"github.com/owncloud/ocis/extensions/glauth/pkg/tracing"
pkgcrypto "github.com/owncloud/ocis/ocis-pkg/crypto"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/ocis-pkg/version"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
return err
}
gr := run.Group{}
ctx, cancel := func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
metrics := metrics.New()
defer cancel()
metrics.BuildInfo.WithLabelValues(version.String).Set(1)
{
lcfg := glauthcfg.LDAP{
Enabled: cfg.Ldap.Enabled,
Listen: cfg.Ldap.Addr,
}
lscfg := glauthcfg.LDAPS{
Enabled: cfg.Ldaps.Enabled,
Listen: cfg.Ldaps.Addr,
Cert: cfg.Ldaps.Cert,
Key: cfg.Ldaps.Key,
}
bcfg := glauthcfg.Config{
LDAP: lcfg, // TODO remove LDAP from the backend config upstream
LDAPS: lscfg, // TODO remove LDAP from the backend config upstream
Backend: glauthcfg.Backend{
Datastore: cfg.Backend.Datastore,
BaseDN: cfg.Backend.BaseDN,
Insecure: cfg.Backend.Insecure,
NameFormat: cfg.Backend.NameFormat,
GroupFormat: cfg.Backend.GroupFormat,
Servers: cfg.Backend.Servers,
SSHKeyAttr: cfg.Backend.SSHKeyAttr,
UseGraphAPI: cfg.Backend.UseGraphAPI,
},
}
fcfg := glauthcfg.Config{
LDAP: lcfg, // TODO remove LDAP from the backend config upstream
LDAPS: lscfg, // TODO remove LDAP from the backend config upstream
Backend: glauthcfg.Backend{
Datastore: cfg.Fallback.Datastore,
BaseDN: cfg.Fallback.BaseDN,
Insecure: cfg.Fallback.Insecure,
NameFormat: cfg.Fallback.NameFormat,
GroupFormat: cfg.Fallback.GroupFormat,
Servers: cfg.Fallback.Servers,
SSHKeyAttr: cfg.Fallback.SSHKeyAttr,
UseGraphAPI: cfg.Fallback.UseGraphAPI,
},
}
if lscfg.Enabled {
if err := pkgcrypto.GenCert(cfg.Ldaps.Cert, cfg.Ldaps.Key, logger); err != nil {
logger.Fatal().Err(err).Msgf("Could not generate test-certificate")
}
}
as, gs := getAccountsServices()
server, err := glauth.Server(
glauth.AccountsService(as),
glauth.GroupsService(gs),
glauth.Logger(logger),
glauth.LDAP(&lcfg),
glauth.LDAPS(&lscfg),
glauth.Backend(&bcfg),
glauth.Fallback(&fcfg),
glauth.RoleBundleUUID(cfg.RoleBundleUUID),
)
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(server.ListenAndServe, func(_ error) {
_ = server.Shutdown(ctx)
cancel()
})
}
return gr.Run()
},
}
}
// getAccountsServices returns an ocis-accounts service
func getAccountsServices() (accountssvc.AccountsService, accountssvc.GroupsService) {
return accountssvc.NewAccountsService("com.owncloud.api.accounts", grpc.DefaultClient),
accountssvc.NewGroupsService("com.owncloud.api.accounts", grpc.DefaultClient)
}
+50
View File
@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/ocis-pkg/registry"
"github.com/owncloud/ocis/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running extension instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.String)
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.Ldap.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
*shared.Commons `yaml:"-"`
Service Service `yaml:"-"`
Tracing *Tracing `yaml:"tracing"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
Ldap Ldap `yaml:"ldap"`
Ldaps Ldaps `yaml:"ldaps"`
Backend Backend `yaml:"backend"`
Fallback FallbackBackend `yaml:"fallback"`
RoleBundleUUID string `yaml:"role_bundle_uuid" env:"GLAUTH_ROLE_BUNDLE_ID"`
Context context.Context `yaml:"-"`
}
// Backend defined the available backend configuration.
type Backend struct {
Datastore string `yaml:"datastore"`
BaseDN string `yaml:"base_dn"`
Insecure bool `yaml:"insecure"`
NameFormat string `yaml:"name_format"`
GroupFormat string `yaml:"group_format"`
Servers []string `yaml:"servers"`
SSHKeyAttr string `yaml:"ssh_key_attr"`
UseGraphAPI bool `yaml:"use_graph_api"`
}
// FallbackBackend defined the available fallback backend configuration.
type FallbackBackend struct {
Datastore string `yaml:"datastore"`
BaseDN string `yaml:"base_dn"`
Insecure bool `yaml:"insecure"`
NameFormat string `yaml:"name_format"`
GroupFormat string `yaml:"group_format"`
Servers []string `yaml:"servers"`
SSHKeyAttr string `yaml:"ssh_key_attr"`
UseGraphAPI bool `yaml:"use_graph_api"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"GLAUTH_DEBUG_ADDR"`
Token string `yaml:"token" env:"GLAUTH_DEBUG_TOKEN"`
Pprof bool `yaml:"pprof" env:"GLAUTH_DEBUG_PPROF"`
Zpages bool `yaml:"zpages" env:"GLAUTH_DEBUG_ZPAGES"`
}
@@ -0,0 +1,90 @@
package defaults
import (
"path"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/config/defaults"
)
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9129",
},
Service: config.Service{
Name: "glauth",
},
Ldap: config.Ldap{
Enabled: true,
Addr: "127.0.0.1:9125",
Namespace: "com.owncloud.ldap",
},
Ldaps: config.Ldaps{
Enabled: true,
Addr: "127.0.0.1:9126",
Namespace: "com.owncloud.ldaps",
Cert: path.Join(defaults.BaseDataPath(), "ldap", "ldap.crt"),
Key: path.Join(defaults.BaseDataPath(), "ldap", "ldap.key"),
},
Backend: config.Backend{
Datastore: "accounts",
BaseDN: "dc=ocis,dc=test",
Insecure: false,
NameFormat: "cn",
GroupFormat: "ou",
Servers: nil,
SSHKeyAttr: "sshPublicKey",
UseGraphAPI: true,
},
Fallback: config.FallbackBackend{
Datastore: "",
BaseDN: "dc=ocis,dc=test",
Insecure: false,
NameFormat: "cn",
GroupFormat: "ou",
Servers: nil,
SSHKeyAttr: "sshPublicKey",
UseGraphAPI: true,
},
RoleBundleUUID: "71881883-1768-46bd-a24d-a356a2afdf7f", // BundleUUIDRoleAdmin
}
}
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
cfg.Tracing = &config.Tracing{
Enabled: cfg.Commons.Tracing.Enabled,
Type: cfg.Commons.Tracing.Type,
Endpoint: cfg.Commons.Tracing.Endpoint,
Collector: cfg.Commons.Tracing.Collector,
}
} else if cfg.Tracing == nil {
cfg.Tracing = &config.Tracing{}
}
}
func Sanitize(cfg *config.Config) {
// nothing to santizie here atm
}
+8
View File
@@ -0,0 +1,8 @@
package config
// Ldap defines the available LDAP configuration.
type Ldap struct {
Enabled bool `yaml:"enabled" env:"GLAUTH_LDAP_ENABLED"`
Addr string `yaml:"addr" env:"GLAUTH_LDAP_ADDR"`
Namespace string `yaml:"-"`
}
+10
View File
@@ -0,0 +1,10 @@
package config
// Ldaps defined the available LDAPS configuration.
type Ldaps struct {
Enabled bool `yaml:"enabled" env:"GLAUTH_LDAPS_ENABLED"`
Addr string `yaml:"addr" env:"GLAUTH_LDAPS_ADDR"`
Namespace string `yaml:"-"`
Cert string `yaml:"cert" env:"GLAUTH_LDAPS_CERT"`
Key string `yaml:"key" env:"GLAUTH_LDAPS_KEY"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;GLAUTH_LOG_LEVEL"`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;GLAUTH_LOG_PRETTY"`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;GLAUTH_LOG_COLOR"`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;GLAUTH_LOG_FILE"`
}
@@ -0,0 +1,32 @@
package parser
import (
"errors"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/extensions/glauth/pkg/config/defaults"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
)
// ParseConfig loads accounts configuration from known paths.
func ParseConfig(cfg *config.Config) error {
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
// sanitize config
defaults.Sanitize(cfg)
return nil
}
+6
View File
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;GLAUTH_TRACING_ENABLED"`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;GLAUTH_TRACING_TYPE"`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;GLAUTH_TRACING_ENDPOINT"`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;GLAUTH_TRACING_COLLECTOR"`
}
+17
View File
@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}
+41
View File
@@ -0,0 +1,41 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "glauth"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
BuildInfo *prometheus.GaugeVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
// Namespace: Namespace,
// Subsystem: Subsystem,
// Name: "greet_total",
// Help: "How many greeting requests processed",
// }, []string{}),
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build Information",
}, []string{"version"}),
}
// prometheus.Register(
// m.Counter,
// )
return m
}
+130
View File
@@ -0,0 +1,130 @@
package mlogr
import (
"errors"
"github.com/go-logr/logr"
plog "github.com/owncloud/ocis/ocis-pkg/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 {
sink := logSink{
l: l,
verbosity: 0,
prefix: "glauth",
values: nil,
}
return logr.New(sink)
}
func (l logSink) Init(info logr.RuntimeInfo) {
}
// logSink is a logr.LogSink that uses the ocis-pkg log.
type logSink struct {
l *plog.Logger
verbosity int
prefix string
values []interface{}
}
func (l logSink) clone() logSink {
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 logSink) Info(level int, msg string, kvList ...interface{}) {
if l.Enabled(level) {
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, kvList)
e.Msg(msg)
}
}
func (l logSink) Enabled(level int) bool {
return true
}
func (l logSink) 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)
}
// WithName returns a new logr.LogSink 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 logSink) WithName(name string) logr.LogSink {
nl := l.clone()
if len(l.prefix) > 0 {
nl.prefix = l.prefix + "/"
}
nl.prefix += name
return nl
}
func (l logSink) WithValues(kvList ...interface{}) logr.LogSink {
nl := l.clone()
nl.values = append(nl.values, kvList...)
return nl
}
var _ logr.LogSink = logSink{}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/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
}
}
@@ -0,0 +1,59 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/service/debug"
"github.com/owncloud/ocis/ocis-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.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: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
// 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: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
@@ -0,0 +1,121 @@
package glauth
import (
"net"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/handler"
"github.com/nmcclain/ldap"
"github.com/owncloud/ocis/ocis-pkg/log"
)
type chainHandler struct {
log log.Logger
b handler.Handler
f handler.Handler
}
func (h chainHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (res ldap.LDAPResultCode, err error) {
h.log.Debug().
Str("binddn", bindDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Bind request")
res, err = h.b.Bind(bindDN, bindSimplePw, conn)
switch {
case err != nil:
h.log.Error().
Err(err).
Str("binddn", bindDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Bind request")
return h.f.Bind(bindDN, bindSimplePw, conn)
case res == ldap.LDAPResultInvalidCredentials:
return h.f.Bind(bindDN, bindSimplePw, conn)
}
return
}
func (h chainHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (res ldap.ServerSearchResult, err error) {
h.log.Debug().
Str("binddn", bindDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Search request")
res, err = h.b.Search(bindDN, searchReq, conn)
switch {
case err != nil:
h.log.Error().
Err(err).
Str("binddn", bindDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Search request")
return h.f.Search(bindDN, searchReq, conn)
case len(res.Entries) == 0:
// yes, we only fall back if there are no results in the first backend
// this is not supposed to work for searching lots of users, only to look up a single user
// searching multiple users would require merging result sets. out of scope for now.
return h.f.Search(bindDN, searchReq, conn)
}
return
}
func (h chainHandler) Close(boundDN string, conn net.Conn) error {
h.log.Debug().
Str("boundDN", boundDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Close request")
if err := h.b.Close(boundDN, conn); err != nil {
h.log.Error().
Err(err).
Str("boundDN", boundDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Close request")
}
if err := h.f.Close(boundDN, conn); err != nil {
h.log.Error().
Err(err).
Str("boundDN", boundDN).
Interface("src", conn.RemoteAddr()).
Str("handler", "chain").
Msg("Close request")
}
return nil
}
// Add is not yet supported for the chain backend
func (h chainHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Modify is not yet supported for the chain backend
func (h chainHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Delete is not yet supported for the chain backend
func (h chainHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// FindUser with the given username. Called by the ldap backend to authenticate the bind. Optional
func (h chainHandler) FindUser(userName string, searchByUPN bool) (bool, config.User, error) {
return false, config.User{}, nil
}
// FindGroup is not yet supported for the chain backend
func (h chainHandler) FindGroup(groupName string) (bool, config.Group, error) {
return false, config.Group{}, nil
}
// NewChainHandler implements a chain backend with two backends
func NewChainHandler(log log.Logger, bh handler.Handler, fh handler.Handler) handler.Handler {
return chainHandler{
log: log,
b: bh,
f: fh,
}
}
+555
View File
@@ -0,0 +1,555 @@
package glauth
import (
"context"
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/handler"
"github.com/glauth/glauth/v2/pkg/stats"
ber "github.com/nmcclain/asn1-ber"
"github.com/nmcclain/ldap"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/middleware"
"go-micro.dev/v4/metadata"
)
type queryType string
const (
usersQuery queryType = "users"
groupsQuery queryType = "groups"
)
type ocisHandler struct {
as accountssvc.AccountsService
gs accountssvc.GroupsService
log log.Logger
basedn string
nameFormat string
groupFormat string
rbid string
}
func (h ocisHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + h.basedn)
h.log.Debug().
Str("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.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("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.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("handler", "ocis").
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=")
// TODO make glauth context aware
ctx := context.Background()
// use a session with the bound user?
roleIDs, err := json.Marshal([]string{h.rbid})
if err != nil {
h.log.Error().
Err(err).
Str("handler", "ocis").
Msg("could not marshal roleid json")
return ldap.LDAPResultOperationsError, nil
}
ctx = metadata.Set(ctx, middleware.RoleIDs, string(roleIDs))
// check password
res, err := h.as.ListAccounts(ctx, &accountssvc.ListAccountsRequest{
//Query: fmt.Sprintf("username eq '%s'", username),
// TODO this allows looking up users when you know the username using basic auth
// adding the password to the query is an option but sending this over the wire 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().
Err(err).
Str("handler", "ocis").
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("handler", "ocis").
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.basedn)
searchBaseDN := strings.ToLower(searchReq.BaseDN)
h.log.Debug().
Str("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.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.basedn)
}
if !strings.HasSuffix(searchBaseDN, h.basedn) {
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultInsufficientAccessRights,
}, fmt.Errorf("search error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.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.Error().
Err(err).
Str("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.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 compiling filter: %s, error: %s", searchReq.Filter, err.Error())
}
qtype, query, code, err = parseFilter(cf)
if err != nil {
return ldap.ServerSearchResult{
ResultCode: code,
}, fmt.Errorf("Search Error: error parsing filter: %s, error: %s", searchReq.Filter, err.Error())
}
// 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=")))
}
}
// TODO make glauth context aware
ctx := context.Background()
// use a session with the bound user?
roleIDs, err := json.Marshal([]string{h.rbid})
if err != nil {
h.log.Error().
Err(err).
Str("handler", "ocis").
Msg("could not marshal roleid json")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, nil
}
ctx = metadata.Set(ctx, middleware.RoleIDs, string(roleIDs))
entries := []*ldap.Entry{}
h.log.Debug().
Str("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.basedn).
Str("filter", searchReq.Filter).
Str("qtype", string(qtype)).
Str("query", query).
Msg("parsed query")
switch qtype {
case usersQuery:
accounts, err := h.as.ListAccounts(ctx, &accountssvc.ListAccountsRequest{
Query: query,
})
if err != nil {
h.log.Error().
Err(err).
Str("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.basedn).
Str("filter", searchReq.Filter).
Str("query", query).
Interface("src", conn.RemoteAddr()).
Msg("Could not list accounts")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, fmt.Errorf("search error: error listing users")
}
entries = append(entries, h.mapAccounts(accounts.Accounts)...)
case groupsQuery:
groups, err := h.gs.ListGroups(ctx, &accountssvc.ListGroupsRequest{
Query: query,
})
if err != nil {
h.log.Error().
Err(err).
Str("handler", "ocis").
Str("binddn", bindDN).
Str("basedn", h.basedn).
Str("filter", searchReq.Filter).
Str("query", query).
Interface("src", conn.RemoteAddr()).
Msg("Could not list groups")
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultOperationsError,
}, fmt.Errorf("search error: error listing groups")
}
entries = append(entries, h.mapGroups(groups.Groups)...)
}
stats.Frontend.Add("search_successes", 1)
h.log.Debug().
Str("handler", "ocis").
Int("num_entries", len(entries)).
Str("binddn", bindDN).
Str("basedn", h.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 []*accountsmsg.Account) []*ldap.Entry {
entries := make([]*ldap.Entry, 0, len(accounts))
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.nameFormat,
accounts[i].PreferredName,
h.groupFormat,
"users",
h.basedn,
)
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
}
return entries
}
func (h ocisHandler) mapGroups(groups []*accountsmsg.Group) []*ldap.Entry {
entries := make([]*ldap.Entry, 0, len(groups))
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.nameFormat,
groups[i].OnPremisesSamAccountName,
h.groupFormat,
"groups",
h.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 "Present":
if len(f.Children) != 0 {
return "", "", ldap.LDAPResultOperationsError, fmt.Errorf("equality match must have no children, got %+v", f)
}
attribute := strings.ToLower(f.Data.String())
if attribute == "objectclass" {
// TODO implement proper present odata query, for now fall back to listing users
return "users", q, code, err
}
return qtype, q, ldap.LDAPResultUnwillingToPerform, fmt.Errorf("%s filter match for %s not implemented", ldap.FilterMap[f.Tag], attribute)
case "Equality Match":
if len(f.Children) != 2 {
return "", "", ldap.LDAPResultOperationsError, fmt.Errorf("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
case "*":
// TODO not implemented yet
qtype = usersQuery
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, fmt.Errorf("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, fmt.Errorf("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
}
// Add is not yet supported for the ocis backend
func (h ocisHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Modify is not yet supported for the ocis backend
func (h ocisHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Delete is not yet supported for the ocis backend
func (h ocisHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// FindUser with the given username
func (h ocisHandler) FindUser(userName string, searchByUPN bool) (found bool, user config.User, err error) {
return false, config.User{}, nil
}
// FindGroup with the given groupname
func (h ocisHandler) FindGroup(groupName string) (found bool, user config.Group, err error) {
return false, config.Group{}, nil
}
// NewOCISHandler implements a glauth backend with ocis-accounts as the datasource
func NewOCISHandler(opts ...Option) handler.Handler {
options := newOptions(opts...)
handler := ocisHandler{
log: options.Logger,
as: options.AccountsService,
gs: options.GroupsService,
basedn: options.BaseDN,
nameFormat: options.NameFormat,
groupFormat: options.GroupFormat,
rbid: options.RoleBundleUUID,
}
return handler
}
@@ -0,0 +1,124 @@
package glauth
import (
"context"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/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
LDAP *config.LDAP
LDAPS *config.LDAPS
Backend *config.Config
Fallback *config.Config
BaseDN string
NameFormat string
GroupFormat string
RoleBundleUUID string
AccountsService accountssvc.AccountsService
GroupsService accountssvc.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
}
}
// LDAP provides a function to set the LDAP option.
func LDAP(val *config.LDAP) Option {
return func(o *Options) {
o.LDAP = val
}
}
// LDAPS provides a function to set the LDAPS option.
func LDAPS(val *config.LDAPS) Option {
return func(o *Options) {
o.LDAPS = val
}
}
// Backend provides a function to set the backend option.
func Backend(val *config.Config) Option {
return func(o *Options) {
o.Backend = val
}
}
// Fallback provides a string to set the fallback option.
func Fallback(val *config.Config) Option {
return func(o *Options) {
o.Fallback = val
}
}
// BaseDN provides a string to set the BaseDN option.
func BaseDN(val string) Option {
return func(o *Options) {
o.BaseDN = val
}
}
// NameFormat provides a string to set the NameFormat option.
func NameFormat(val string) Option {
return func(o *Options) {
o.NameFormat = val
}
}
// GroupFormat provides a string to set the GroupFormat option.
func GroupFormat(val string) Option {
return func(o *Options) {
o.GroupFormat = val
}
}
// AccountsService provides an AccountsService client to set the AccountsService option.
func AccountsService(val accountssvc.AccountsService) Option {
return func(o *Options) {
o.AccountsService = val
}
}
// GroupsService provides an GroupsService client to set the GroupsService option.
func GroupsService(val accountssvc.GroupsService) Option {
return func(o *Options) {
o.GroupsService = val
}
}
// RoleBundleUUID provides a role bundle UUID to make internal grpc requests.
func RoleBundleUUID(val string) Option {
return func(o *Options) {
o.RoleBundleUUID = val
}
}
@@ -0,0 +1,155 @@
package glauth
import (
"errors"
"fmt"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/handler"
"github.com/go-logr/logr"
"github.com/nmcclain/ldap"
"github.com/owncloud/ocis/extensions/glauth/pkg/mlogr"
)
// LdapSvc holds the ldap server struct
type LdapSvc struct {
log logr.Logger
ldap *config.LDAP
ldaps *config.LDAPS
backend *config.Config
fallback *config.Config
yubiAuth *yubigo.YubiAuth
l *ldap.Server
}
// Server initializes the ldap server.
// It is a fork github.com/glauth/pkg/server because it would introduce a go-micro dependency upstream.
func Server(opts ...Option) (*LdapSvc, error) {
options := newOptions(opts...)
s := LdapSvc{
log: mlogr.New(&options.Logger),
backend: options.Backend,
fallback: options.Fallback,
ldap: options.LDAP,
ldaps: options.LDAPS,
}
var err error
if len(s.backend.YubikeyClientID) > 0 && len(s.backend.YubikeySecret) > 0 {
s.yubiAuth, err = yubigo.NewYubiAuth(s.backend.YubikeyClientID, s.backend.YubikeySecret)
if err != nil {
return nil, errors.New("yubikey auth failed")
}
}
// configure the backend
s.l = ldap.NewServer()
s.l.EnforceLDAP = true
var bh handler.Handler
switch s.backend.Backend.Datastore {
/* TODO bring back file config
case "config":
bh = handler.NewConfigHandler(
handler.Logger(s.log),
handler.Config(s.c),
handler.YubiAuth(s.yubiAuth),
)
*/
case "ldap":
bh = handler.NewLdapHandler(
handler.Logger(s.log),
handler.Backend(s.backend.Backend),
)
case "owncloud":
bh = handler.NewOwnCloudHandler(
handler.Logger(s.log),
handler.Backend(s.backend.Backend),
)
case "accounts":
bh = NewOCISHandler(
AccountsService(options.AccountsService),
GroupsService(options.GroupsService),
Logger(options.Logger),
BaseDN(s.backend.Backend.BaseDN),
NameFormat(s.backend.Backend.NameFormat),
GroupFormat(s.backend.Backend.GroupFormat),
RoleBundleUUID(options.RoleBundleUUID),
)
default:
return nil, fmt.Errorf("unsupported backend %s - must be 'ldap', 'owncloud' or 'accounts'", s.backend.Backend.Datastore)
}
s.log.V(3).Info("Using backend", "backend", s.backend.Backend)
if s.fallback != nil && s.fallback.Backend.Datastore != "" {
var fh handler.Handler
switch s.fallback.Backend.Datastore {
/* TODO bring back file config
case "config":
fh = handler.NewConfigHandler(
handler.Logger(s.log),
handler.Config(s.c),
handler.YubiAuth(s.yubiAuth),
)
*/
case "ldap":
fh = handler.NewLdapHandler(
handler.Logger(s.log),
handler.Backend(s.fallback.Backend),
)
case "owncloud":
fh = handler.NewOwnCloudHandler(
handler.Logger(s.log),
handler.Backend(s.fallback.Backend),
)
case "accounts":
fh = NewOCISHandler(
AccountsService(options.AccountsService),
GroupsService(options.GroupsService),
Logger(options.Logger),
BaseDN(s.fallback.Backend.BaseDN),
NameFormat(s.fallback.Backend.NameFormat),
GroupFormat(s.fallback.Backend.GroupFormat),
RoleBundleUUID(options.RoleBundleUUID),
)
default:
return nil, fmt.Errorf("unsupported fallback %s - must be 'ldap', 'owncloud' or 'accounts'", s.fallback.Backend.Datastore)
}
s.log.V(3).Info("Using fallback", "backend", s.fallback.Backend)
bh = NewChainHandler(options.Logger, bh, fh)
}
s.l.BindFunc(s.backend.Backend.BaseDN, bh)
s.l.SearchFunc(s.backend.Backend.BaseDN, bh)
s.l.CloseFunc(s.backend.Backend.BaseDN, bh)
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.ldap.Listen)
return s.l.ListenAndServe(s.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.ldaps.Listen)
return s.l.ListenAndServeTLS(
s.ldaps.Listen,
s.ldaps.Cert,
s.ldaps.Key,
)
}
// Shutdown ends listeners by sending true to the ldap serves quit channel
func (s *LdapSvc) Shutdown() {
s.l.Quit <- true
}
+23
View File
@@ -0,0 +1,23 @@
package tracing
import (
"github.com/owncloud/ocis/extensions/glauth/pkg/config"
pkgtrace "github.com/owncloud/ocis/ocis-pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
var (
// TraceProvider is the global trace provider for the glauth service.
TraceProvider = trace.NewNoopTracerProvider()
)
func Configure(cfg *config.Config) error {
var err error
if cfg.Tracing.Enabled {
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
return err
}
}
return nil
}
+2
View File
@@ -0,0 +1,2 @@
# backend
-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-glauth-debug && bin/ocis-glauth-debug --log-level debug server --debug-pprof --debug-zpages'