mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-13 06:09:21 -04:00
chore(graph): disable HTTP or eventhandlers by configuration
In the scope of the broader issue #1312, this PR deals with performing those changes for the `graph` service, namely to add the ability to disable the HTTP API or to disable the events API handler by configuration. It also adds metrics for the events processing, and tests for the events processing. The previous implementation was combining the HTTP server service and the events consumption, which is why this PR refactors the composition of those services: * the event consumption has been moved into its own service * the identity.Backend is created beforehand, and then injected as a collaborator in both the HTTP service as well as the event consumer service It also adds metrics, mainly for the event processing. To encourage re-use in latter implementations and changes, it also introduces two top-level package changes: * internal/eventstest/events_test_helpers: contains a TestBus implementation to unit-test event consumers without NATS * internal/metricstest/metrics_test_helpers: contains assertion functions to test Prometheus metrics
This commit is contained in:
1 parent
d2bcd3cc9e
commit
e8cf677353
17 files changed
+739
-216
No files matched your search
@@ -0,0 +1,44 @@
|
||||
package eventstest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
rev "github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
microevents "go-micro.dev/v4/events"
|
||||
)
|
||||
|
||||
func NewTestBus() TestBus {
|
||||
return TestBus(make(chan rev.Event))
|
||||
}
|
||||
|
||||
type TestBus chan rev.Event
|
||||
|
||||
func (tb TestBus) Consume(_ string, _ ...microevents.ConsumeOption) (<-chan microevents.Event, error) {
|
||||
ch := make(chan microevents.Event)
|
||||
go func() {
|
||||
for ev := range tb {
|
||||
b, _ := json.Marshal(ev.Event)
|
||||
ch <- microevents.Event{
|
||||
Payload: b,
|
||||
Metadata: map[string]string{
|
||||
rev.MetadatakeyEventID: ev.ID,
|
||||
rev.MetadatakeyEventType: ev.Type,
|
||||
},
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (tb TestBus) Publish(e any) string {
|
||||
ev := rev.Event{
|
||||
ID: uuid.New().String(),
|
||||
Type: reflect.TypeOf(e).String(),
|
||||
Event: e,
|
||||
}
|
||||
tb <- ev
|
||||
return ev.ID
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package metricstest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// copied and adapted from Prometheus testutil.ToFloat64(), since we don't import that package
|
||||
func collect(c prometheus.Collector) []prometheus.Metric {
|
||||
result := []prometheus.Metric{}
|
||||
ch := make(chan prometheus.Metric)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for m := range ch {
|
||||
result = append(result, m)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
c.Collect(ch)
|
||||
close(ch)
|
||||
<-done
|
||||
return result
|
||||
}
|
||||
|
||||
func RequireIsNotSet(t require.TestingT, c prometheus.Collector, msgAndArgs ...any) {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
if !IsNotSet(t, c, msgAndArgs) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func IsNotSet(t assert.TestingT, c prometheus.Collector, msgAndArgs ...any) bool {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
m := collect(c)
|
||||
if len(m) > 0 {
|
||||
return assert.Fail(t, "Metric exists while expected to not exist", msgAndArgs)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func RequireEqual(t require.TestingT, expected float64, c prometheus.Collector, msgAndArgs ...any) {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
if !Equal(t, expected, c, msgAndArgs) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// copied and adapted from Prometheus testutil.ToFloat64(), since we don't import that package
|
||||
func Equal(t assert.TestingT, expected float64, c prometheus.Collector, msgAndArgs ...any) bool {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
m := collect(c)
|
||||
if !assert.Len(t, m, 1, msgAndArgs...) {
|
||||
return false
|
||||
}
|
||||
pb := &dto.Metric{}
|
||||
err := m[0].Write(pb)
|
||||
if !assert.NoError(t, err, msgAndArgs...) {
|
||||
return false
|
||||
}
|
||||
if pb.Gauge != nil {
|
||||
return assert.Equal(t, expected, pb.Gauge.GetValue(), msgAndArgs...)
|
||||
} else if pb.Counter != nil {
|
||||
return assert.Equal(t, expected, pb.Counter.GetValue(), msgAndArgs...)
|
||||
} else if pb.Untyped != nil {
|
||||
return assert.Equal(t, expected, pb.Untyped.GetValue(), msgAndArgs...)
|
||||
} else {
|
||||
return assert.Fail(t, fmt.Sprintf("collected a non-gauge/counter/untyped metric: %s", pb), msgAndArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
func RequireEqualWithLabels(t require.TestingT, expectedValue float64, expectedLabels map[string]string, c prometheus.Collector, msgAndArgs ...any) {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
if !EqualWithLabels(t, expectedValue, expectedLabels, c, msgAndArgs) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func EqualWithLabels(t assert.TestingT, expectedValue float64, expectedLabels map[string]string, c prometheus.Collector, msgAndArgs ...any) bool {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
m := collect(c)
|
||||
if !assert.Len(t, m, 1, "collected %d metrics instead of exactly 1", len(m)) {
|
||||
return false
|
||||
}
|
||||
pb := &dto.Metric{}
|
||||
err := m[0].Write(pb)
|
||||
if !assert.NoError(t, err) {
|
||||
return false
|
||||
}
|
||||
if pb.Gauge != nil {
|
||||
if !assert.Equal(t, expectedValue, pb.Gauge.GetValue()) {
|
||||
return false
|
||||
}
|
||||
} else if pb.Counter != nil {
|
||||
if !assert.Equal(t, expectedValue, pb.Counter.GetValue()) {
|
||||
return false
|
||||
}
|
||||
} else if pb.Untyped != nil {
|
||||
if !assert.Equal(t, expectedValue, pb.Untyped.GetValue()) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return assert.Fail(t, "collected a non-gauge/counter/untyped metric: %s", pb)
|
||||
}
|
||||
|
||||
if !assert.NotNil(t, pb.Label) {
|
||||
return false
|
||||
}
|
||||
actualLabels := map[string]string{}
|
||||
for _, label := range pb.Label {
|
||||
if !assert.NotNil(t, label) {
|
||||
return false
|
||||
}
|
||||
if !assert.NotNil(t, label.Name) {
|
||||
return false
|
||||
}
|
||||
if !assert.NotNil(t, label.Value) {
|
||||
return false
|
||||
}
|
||||
actualLabels[*label.Name] = *label.Value
|
||||
}
|
||||
return assert.Equal(t, expectedLabels, actualLabels, msgAndArgs)
|
||||
}
|
||||
@@ -168,7 +168,7 @@ The output of this command includes the following information for each role:
|
||||
* `Condition`
|
||||
* `Allowed resource actions`
|
||||
|
||||
**Example output (shortned)**
|
||||
**Example output (shortened)**
|
||||
|
||||
```bash
|
||||
+--------------------------------------+----------+--------------------------------+--------------------------------+------------------------------------------+
|
||||
@@ -184,3 +184,22 @@ The output of this command includes the following information for each role:
|
||||
+--------------------------------------+----------+--------------------------------+--------------------------------+------------------------------------------+
|
||||
```
|
||||
|
||||
## API Handlers
|
||||
|
||||
To specialize `graph` service instances in order to scale them independently, it is possible to disable its API handlers:
|
||||
|
||||
* `GRAPH_HTTP_DISABLE`: when set to `true`, the service does not listen on HTTP and only consumes events (defaults to `false`)
|
||||
* `GRAPH_EVENTS_DISABLE_CONSUMER`: when set to `true`, the service does not consome events and only listens on HTTP (defaults to `false`)
|
||||
|
||||
## Metrics
|
||||
|
||||
The `graph` service provides the following metrics:
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| `opencloud_graph_build_info{version=...}` | Contains a label `version` that is set to the current version of the service, and always has a value of `1` |
|
||||
| `opencloud_graph_events_enabled` | Is set to `1` if the Events API handler is enabled, or `0` if not |
|
||||
| `opencloud_graph_http_enabled` | Is set to `1` if the HTTP API handler is enabled, or `0` if not |
|
||||
| `opencloud_graph_events{event=...,result=...}` | Counts the number of events that have been consumed, with a `event` label that contains the name of the event, and a `result` label that is set to `success` or `failure` |
|
||||
| `opencloud_graph_events_invalid` | Counts the number of invalid events that are malformed or are missing required data |
|
||||
| `opencloud_graph_events_unsupported` | Counts the numbef of consumed events that cannot be processes by this service, should always be `0` |
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
"github.com/opencloud-eu/opencloud/pkg/generators"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
natspkg "github.com/opencloud-eu/opencloud/pkg/nats"
|
||||
"github.com/opencloud-eu/opencloud/pkg/runner"
|
||||
@@ -14,9 +15,14 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/parser"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/metrics"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/server/debug"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/server/http"
|
||||
evc "github.com/opencloud-eu/opencloud/services/graph/pkg/service/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
@@ -46,7 +52,7 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
}
|
||||
ctx := cfg.Context
|
||||
|
||||
mtrcs := metrics.New()
|
||||
mtrcs := metrics.New(prometheus.DefaultRegisterer)
|
||||
mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
||||
|
||||
var kv jetstream.KeyValue
|
||||
@@ -78,9 +84,37 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
identityBackend, eduBackend, err := identity.CreateIdentityBackends(
|
||||
cfg.Identity.Backend,
|
||||
cfg,
|
||||
&logger,
|
||||
traceProvider,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error initializing the identity backend")
|
||||
return fmt.Errorf("could not initialize identity backend: %w", err)
|
||||
}
|
||||
|
||||
var eventsStream events.Stream
|
||||
if cfg.Events.Endpoint != "" {
|
||||
var err error
|
||||
connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus)
|
||||
eventsStream, err = stream.NatsFromConfig(connName, false, cfg.Events.ToNatsConfig())
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error initializing events publisher")
|
||||
return fmt.Errorf("could not initialize events publisher: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
gr := runner.NewGroup()
|
||||
{
|
||||
|
||||
if !cfg.HTTP.Disabled {
|
||||
mtrcs.HttpEnabled.Set(1)
|
||||
|
||||
server, err := http.Server(
|
||||
identityBackend,
|
||||
eduBackend,
|
||||
eventsStream,
|
||||
http.Logger(logger),
|
||||
http.Context(ctx),
|
||||
http.Config(cfg),
|
||||
@@ -92,8 +126,37 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
logger.Error().Err(err).Str("transport", "http").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", server))
|
||||
} else {
|
||||
mtrcs.HttpEnabled.Set(0)
|
||||
logger.Info().Str("transport", "http").Msg("HTTP server is disabled")
|
||||
}
|
||||
|
||||
if !cfg.Events.DisabledConsumer {
|
||||
mtrcs.EventsEnabled.Set(1)
|
||||
|
||||
// even if events are enabled, we still need to differentiate between whether this process
|
||||
// show be consuming events or not (and even when that is disabled, we still need to be
|
||||
// able to produce events), which is why this is a separate setting;
|
||||
// for context, see https://github.com/opencloud-eu/opencloud/issues/1312
|
||||
|
||||
logger := &log.Logger{Logger: logger.With().Str("transport", "events").Logger()}
|
||||
eventConsumer, err := evc.NewService(cfg.Context, eventsStream, identityBackend, mtrcs, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize events consumer: %w", err)
|
||||
}
|
||||
|
||||
gr.Add(runner.New(cfg.Service.Name+".svc", func() error {
|
||||
return eventConsumer.Start()
|
||||
}, func() {
|
||||
err := eventConsumer.Close()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to stop event consumer")
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
mtrcs.EventsEnabled.Set(0)
|
||||
logger.Info().Str("transport", "events").Msg("event consumer is disabled")
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
@@ -129,6 +130,7 @@ type API struct {
|
||||
|
||||
// Events combines the configuration options for the event bus.
|
||||
type Events struct {
|
||||
DisabledConsumer bool `yaml:"disabled_consumer" env:"GRAPH_EVENTS_DISABLE_CONSUMER" desc:"Disables consuming events. Set this to true if the service should only handle HTTP requests." introductionVersion:"%NEXT%"`
|
||||
Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT;GRAPH_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Set to a empty string to disable emitting events." introductionVersion:"1.0.0"`
|
||||
Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER;GRAPH_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"`
|
||||
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE;GRAPH_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"1.0.0"`
|
||||
@@ -138,6 +140,18 @@ type Events struct {
|
||||
AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD;GRAPH_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"`
|
||||
}
|
||||
|
||||
func (e Events) ToNatsConfig() stream.NatsConfig {
|
||||
return stream.NatsConfig{
|
||||
Endpoint: e.Endpoint,
|
||||
Cluster: e.Cluster,
|
||||
TLSInsecure: e.TLSInsecure,
|
||||
TLSRootCACertificate: e.TLSRootCACertificate,
|
||||
EnableTLS: e.EnableTLS,
|
||||
AuthUsername: e.AuthUsername,
|
||||
AuthPassword: e.AuthPassword,
|
||||
}
|
||||
}
|
||||
|
||||
// CORS defines the available cors configuration.
|
||||
type CORS struct {
|
||||
AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GRAPH_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
|
||||
|
||||
@@ -43,6 +43,7 @@ func DefaultConfig() *config.Config {
|
||||
Token: "",
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Disabled: false,
|
||||
Addr: "127.0.0.1:9120",
|
||||
Namespace: "eu.opencloud.web",
|
||||
Root: "/graph",
|
||||
@@ -118,9 +119,10 @@ func DefaultConfig() *config.Config {
|
||||
TTL: time.Hour * 24,
|
||||
},
|
||||
Events: config.Events{
|
||||
Endpoint: "127.0.0.1:9233",
|
||||
Cluster: "opencloud-cluster",
|
||||
EnableTLS: false,
|
||||
DisabledConsumer: false,
|
||||
Endpoint: "127.0.0.1:9233",
|
||||
Cluster: "opencloud-cluster",
|
||||
EnableTLS: false,
|
||||
},
|
||||
MaxConcurrency: 20,
|
||||
UnifiedRoles: config.UnifiedRoles{
|
||||
|
||||
@@ -4,6 +4,7 @@ import "github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Disabled bool `yaml:"disabled" env:"GRAPH_HTTP_DISABLE" desc:"Disables the HTTP service. Set this to true if the service should only consume events." introductionVersion:"%NEXT%"`
|
||||
Addr string `yaml:"addr" env:"GRAPH_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"GRAPH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"`
|
||||
|
||||
@@ -39,6 +39,14 @@ func ParseConfig(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.HTTP.Disabled && cfg.Events.DisabledConsumer {
|
||||
// might be debatable, but this situation should be treated as an error,
|
||||
// as the process wouldn't be able to serve either API and would thus be
|
||||
// completely useless -- in that case, just don't start this service
|
||||
// in the first place (especially since it's optional)
|
||||
return errors.New("both HTTP and events consumption APIs are disabled by configuration; at least one must be enabled")
|
||||
}
|
||||
|
||||
if cfg.TokenManager.JWTSecret == "" {
|
||||
return shared.MissingJWTTokenError(cfg.Service.Name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
ldapv3 "github.com/go-ldap/ldap/v3"
|
||||
ocldap "github.com/opencloud-eu/opencloud/pkg/ldap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func CreateIdentityBackends(name string, cfg *config.Config, logger *log.Logger, traceProvider trace.TracerProvider) (Backend, EducationBackend, error) {
|
||||
switch name {
|
||||
case "cs3":
|
||||
gatewaySelector, err := pool.GatewaySelector(
|
||||
cfg.Reva.Address,
|
||||
append(
|
||||
cfg.Reva.GetRevaOptions(),
|
||||
pool.WithRegistry(registry.GetRegistry()),
|
||||
pool.WithTracerProvider(traceProvider),
|
||||
)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &CS3{
|
||||
Config: cfg.Reva,
|
||||
Logger: logger,
|
||||
GatewaySelector: gatewaySelector,
|
||||
}, nil, nil
|
||||
case "ldap":
|
||||
var err error
|
||||
|
||||
var tlsConf *tls.Config
|
||||
if cfg.Identity.LDAP.Insecure {
|
||||
// When insecure is set to true then we don't need a certificate.
|
||||
cfg.Identity.LDAP.CACert = ""
|
||||
tlsConf = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
|
||||
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
|
||||
InsecureSkipVerify: cfg.Identity.LDAP.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Identity.LDAP.CACert != "" {
|
||||
if err := ocldap.WaitForCA(*logger,
|
||||
cfg.Identity.LDAP.Insecure,
|
||||
cfg.Identity.LDAP.CACert); err != nil {
|
||||
logger.Fatal().Err(err).Msg("The configured LDAP CA cert does not exist")
|
||||
}
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
certs := x509.NewCertPool()
|
||||
pemData, err := os.ReadFile(cfg.Identity.LDAP.CACert)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error initializing LDAP Backend")
|
||||
return nil, nil, err
|
||||
}
|
||||
if !certs.AppendCertsFromPEM(pemData) {
|
||||
logger.Error().Msg("Error initializing LDAP Backend. Adding CA cert failed")
|
||||
return nil, nil, err
|
||||
}
|
||||
tlsConf.RootCAs = certs
|
||||
}
|
||||
|
||||
conn := ldap.NewLDAPWithReconnect(
|
||||
ldap.Config{
|
||||
URI: cfg.Identity.LDAP.URI,
|
||||
BindDN: cfg.Identity.LDAP.BindDN,
|
||||
BindPassword: cfg.Identity.LDAP.BindPassword,
|
||||
TLSConfig: tlsConf,
|
||||
},
|
||||
)
|
||||
conn.SetLogger(&logger.Logger)
|
||||
lb, err := NewLDAPBackend(conn, cfg.Identity.LDAP, logger)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error initializing LDAP Backend")
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
identityBackend := lb
|
||||
var eduBackend EducationBackend = lb
|
||||
|
||||
if !cfg.Identity.LDAP.EducationResourcesEnabled {
|
||||
eduBackend = &ErrEducationBackend{}
|
||||
}
|
||||
|
||||
disableMechanismType, err := ParseDisableMechanismType(cfg.Identity.LDAP.DisableUserMechanism)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error initializing LDAP Backend")
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if disableMechanismType == DisableMechanismGroup {
|
||||
logger.Info().Msg("LocalUserDisable is true, will create group if not exists")
|
||||
err := lb.CreateLDAPGroupByDN(cfg.Identity.LDAP.LdapDisabledUsersGroupDN)
|
||||
if err != nil {
|
||||
isAnError := false
|
||||
var lerr *ldapv3.Error
|
||||
if errors.As(err, &lerr) {
|
||||
if lerr.ResultCode != ldapv3.LDAPResultEntryAlreadyExists {
|
||||
isAnError = true
|
||||
}
|
||||
} else {
|
||||
isAnError = true
|
||||
}
|
||||
|
||||
if isAnError {
|
||||
msg := "error adding group for disabling users"
|
||||
logger.Error().Err(err).Str("local_user_disable", cfg.Identity.LDAP.LdapDisabledUsersGroupDN).Msg(msg)
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return identityBackend, eduBackend, nil
|
||||
|
||||
default:
|
||||
err := fmt.Errorf("unknown identity backend: '%s'", name)
|
||||
logger.Err(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
@@ -703,7 +703,7 @@ func (i *LDAP) usersFromLDAPEntries(entries []*ldap.Entry, exp []string) ([]*lib
|
||||
func (i *LDAP) UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error {
|
||||
if !i.writeEnabled {
|
||||
i.logger.Debug().Str("backend", "ldap").Msg("The LDAP Server is readonly. Skipping update of last sign in date")
|
||||
return nil
|
||||
return nil // TODO: do we really want to just silently do nothing here, rather than returning an error?
|
||||
}
|
||||
e, err := i.getLDAPUserByID(userID)
|
||||
switch {
|
||||
|
||||
@@ -12,12 +12,21 @@ var (
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
// Counter *prometheus.CounterVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
EventsEnabled prometheus.Gauge
|
||||
HttpEnabled prometheus.Gauge
|
||||
EventsProcessed *prometheus.CounterVec
|
||||
InvalidEvents prometheus.Counter
|
||||
UnsupportedEvents prometheus.Counter
|
||||
}
|
||||
|
||||
const (
|
||||
ResultSuccess = "success"
|
||||
ResultFailure = "failure"
|
||||
)
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New() *Metrics {
|
||||
func New(registerer prometheus.Registerer) *Metrics {
|
||||
m := &Metrics{
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
@@ -25,9 +34,44 @@ func New() *Metrics {
|
||||
Name: "build_info",
|
||||
Help: "Build information",
|
||||
}, []string{"version"}),
|
||||
EventsEnabled: prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "events_enabled",
|
||||
Help: "Whether this instance consumes events (1) or not (0)",
|
||||
}),
|
||||
HttpEnabled: prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "http_enabled",
|
||||
Help: "Whether this instance processes HTTP API calls (1) or not (0)",
|
||||
}),
|
||||
EventsProcessed: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "events",
|
||||
Help: "Number of consumed events",
|
||||
}, []string{"event", "result"}),
|
||||
InvalidEvents: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "events_invalid",
|
||||
Help: "Number of supported events with invalid data",
|
||||
}),
|
||||
UnsupportedEvents: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "events_unsupported",
|
||||
Help: "Number of unsupported events that were consumed and ignored",
|
||||
}),
|
||||
}
|
||||
|
||||
_ = prometheus.Register(m.BuildInfo)
|
||||
_ = prometheus.Register(m.EventsEnabled)
|
||||
_ = prometheus.Register(m.HttpEnabled)
|
||||
_ = prometheus.Register(m.EventsProcessed)
|
||||
_ = prometheus.Register(m.UnsupportedEvents)
|
||||
_ = prometheus.Register(m.InvalidEvents)
|
||||
// TODO: implement metrics
|
||||
return m
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
|
||||
"go-micro.dev/v4"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/account"
|
||||
"github.com/opencloud-eu/opencloud/pkg/cors"
|
||||
"github.com/opencloud-eu/opencloud/pkg/generators"
|
||||
"github.com/opencloud-eu/opencloud/pkg/keycloak"
|
||||
"github.com/opencloud-eu/opencloud/pkg/middleware"
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
@@ -27,12 +25,13 @@ import (
|
||||
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
|
||||
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
|
||||
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
|
||||
graphMiddleware "github.com/opencloud-eu/opencloud/services/graph/pkg/middleware"
|
||||
svc "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
func Server(identityBackend identity.Backend, eduBackend identity.EducationBackend, eventsStream events.Stream, opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
service, err := http.NewService(
|
||||
@@ -53,20 +52,6 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
|
||||
}
|
||||
|
||||
var eventsStream events.Stream
|
||||
|
||||
if options.Config.Events.Endpoint != "" {
|
||||
var err error
|
||||
connName := generators.GenerateConnectionName(options.Config.Service.Name, generators.NTypeBus)
|
||||
eventsStream, err = stream.NatsFromConfig(connName, false, stream.NatsConfig(options.Config.Events))
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing events publisher")
|
||||
return http.Service{}, fmt.Errorf("could not initialize events publisher: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
|
||||
middleware.TraceContext,
|
||||
chimiddleware.RequestID,
|
||||
@@ -168,8 +153,7 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(middlewares...),
|
||||
svc.EventsPublisher(eventsStream),
|
||||
svc.EventsConsumer(eventsStream),
|
||||
svc.EventsPublisher(eventsStream), // is required even when event consumption is disabled
|
||||
svc.WithRoleService(roleService),
|
||||
svc.WithValueService(valueService),
|
||||
svc.WithRequireAdminMiddleware(requireAdminMiddleware),
|
||||
@@ -179,6 +163,8 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
svc.EventHistoryClient(hClient),
|
||||
svc.TraceProvider(options.TraceProvider),
|
||||
svc.WithNatsKeyValue(options.NatsKeyValue),
|
||||
svc.WithIdentityBackend(identityBackend),
|
||||
svc.WithIdentityEducationBackend(eduBackend),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/metrics"
|
||||
)
|
||||
|
||||
func processEvents(ctx context.Context, consumer events.Consumer, stop *atomic.Bool, stopCh chan struct{},
|
||||
backend identity.Backend, m *metrics.Metrics, logger *log.Logger) error {
|
||||
var _registeredEvents = []events.Unmarshaller{
|
||||
events.UserSignedIn{},
|
||||
}
|
||||
evChannel, err := events.Consume(consumer, "graph", _registeredEvents...)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("cannot consume from nats")
|
||||
return err
|
||||
}
|
||||
logger.Debug().Msg("listening for events")
|
||||
for loop := true; loop; {
|
||||
select {
|
||||
case e := <-evChannel:
|
||||
switch ev := e.Event.(type) {
|
||||
default:
|
||||
// this branch is currently impossible to test and run into because we pick which events we're interested in
|
||||
// through the _registeredEvents above, and the stream won't hand us events we didn't register for
|
||||
m.UnsupportedEvents.Inc()
|
||||
logger.Error().Interface("event", e).Msg("unhandled event")
|
||||
case events.UserSignedIn:
|
||||
name := "UserSignedIn"
|
||||
userId := ""
|
||||
if ev.Executant != nil && ev.Executant.OpaqueId != "" {
|
||||
userId = ev.Executant.OpaqueId
|
||||
} else {
|
||||
m.InvalidEvents.Inc()
|
||||
logger.Error().Err(err).Interface("event", ev).Msg("Received invalid event: executant.opaqueId not set")
|
||||
continue
|
||||
}
|
||||
if err := backend.UpdateLastSignInDate(ctx, userId, utils.TSToTime(ev.Timestamp)); err != nil {
|
||||
m.EventsProcessed.WithLabelValues(name, metrics.ResultFailure).Inc()
|
||||
logger.Error().Err(err).Str("userid", userId).Str("event", name).Msg("Error updating last sign in date")
|
||||
} else {
|
||||
// TODO: UpdateLastSignInDate() currently returns nil instead of an error when the LDAP server is read-only, so those will be accounted for as a success
|
||||
m.EventsProcessed.WithLabelValues(name, metrics.ResultSuccess).Inc()
|
||||
logger.Debug().Str("userid", userId).Str("event", name).Msg("Successfully updated last sign in date")
|
||||
}
|
||||
}
|
||||
if stop.Load() {
|
||||
loop = false
|
||||
}
|
||||
case <-stopCh:
|
||||
logger.Info().Msg("instructed to stop")
|
||||
loop = false
|
||||
case <-ctx.Done():
|
||||
logger.Info().Msg("context cancelled")
|
||||
loop = false
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GraphEventConsumer interface {
|
||||
Start() error
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type GraphEventConsumerImpl struct {
|
||||
ctx context.Context
|
||||
consumer events.Consumer
|
||||
backend identity.Backend
|
||||
metrics *metrics.Metrics
|
||||
logger *log.Logger
|
||||
stopped atomic.Bool
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
var _ GraphEventConsumer = &GraphEventConsumerImpl{}
|
||||
|
||||
func (g *GraphEventConsumerImpl) Start() error {
|
||||
return processEvents(g.ctx, g.consumer, &g.stopped, g.stopCh, g.backend, g.metrics, g.logger)
|
||||
}
|
||||
|
||||
func (g *GraphEventConsumerImpl) Close() error {
|
||||
if g.stopped.CompareAndSwap(false, true) {
|
||||
close(g.stopCh)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullGraphEventConsumer struct {
|
||||
}
|
||||
|
||||
var _ GraphEventConsumer = &NullGraphEventConsumer{}
|
||||
|
||||
func (n *NullGraphEventConsumer) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NullGraphEventConsumer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, consumer events.Consumer, backend identity.Backend, metrics *metrics.Metrics, logger *log.Logger) (GraphEventConsumer, error) {
|
||||
if consumer == nil {
|
||||
return &NullGraphEventConsumer{}, nil
|
||||
} else {
|
||||
stopCh := make(chan struct{}, 1)
|
||||
return &GraphEventConsumerImpl{
|
||||
ctx: ctx,
|
||||
consumer: consumer,
|
||||
backend: backend,
|
||||
metrics: metrics,
|
||||
logger: logger,
|
||||
stopCh: stopCh,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/test-go/testify/mock"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/internal/eventstest"
|
||||
"github.com/opencloud-eu/opencloud/internal/metricstest"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/metrics"
|
||||
g "github.com/opencloud-eu/opencloud/services/graph/pkg/service/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSuccessfulCall(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
bus := eventstest.NewTestBus()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
userId := fmt.Sprintf("user%d", 1000+rand.IntN(10000))
|
||||
|
||||
backend := mocks.NewBackend(t)
|
||||
backend.EXPECT().UpdateLastSignInDate(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ string, _ time.Time) error {
|
||||
defer wg.Done()
|
||||
return nil
|
||||
})
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
m := metrics.New(reg)
|
||||
|
||||
logger := log.NewLogger()
|
||||
|
||||
svc, err := g.NewService(ctx, bus, backend, m, &logger)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() { svc.Close() })
|
||||
t.Cleanup(cancel)
|
||||
go func() {
|
||||
require.NoError(svc.Start())
|
||||
}()
|
||||
|
||||
metricstest.RequireEqual(t, 0, m.UnsupportedEvents)
|
||||
metricstest.RequireIsNotSet(t, m.EventsProcessed)
|
||||
|
||||
_ = bus.Publish(events.UserSignedIn{
|
||||
Timestamp: nil,
|
||||
Executant: &userv1beta1.UserId{
|
||||
OpaqueId: userId,
|
||||
},
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
require.Len(backend.Mock.Calls, 1)
|
||||
require.Len(backend.Mock.Calls[0].Arguments, 3)
|
||||
require.Equal(userId, backend.Mock.Calls[0].Arguments[1])
|
||||
|
||||
metricstest.RequireEqual(t, 0, m.UnsupportedEvents)
|
||||
metricstest.RequireEqualWithLabels(t, 1, map[string]string{"event": "UserSignedIn", "result": "success"}, m.EventsProcessed)
|
||||
}
|
||||
|
||||
func TestBackendReturningAnError(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
bus := eventstest.NewTestBus()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
userId := fmt.Sprintf("user%d", 1000+rand.IntN(10000))
|
||||
|
||||
backend := mocks.NewBackend(t)
|
||||
backend.EXPECT().UpdateLastSignInDate(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ string, _ time.Time) error {
|
||||
defer wg.Done()
|
||||
return errors.New("test")
|
||||
})
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
m := metrics.New(reg)
|
||||
|
||||
logger := log.NewLogger()
|
||||
|
||||
svc, err := g.NewService(ctx, bus, backend, m, &logger)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() { svc.Close() })
|
||||
t.Cleanup(cancel)
|
||||
go func() {
|
||||
require.NoError(svc.Start())
|
||||
}()
|
||||
|
||||
metricstest.RequireEqual(t, 0, m.UnsupportedEvents)
|
||||
metricstest.RequireIsNotSet(t, m.EventsProcessed)
|
||||
|
||||
_ = bus.Publish(events.UserSignedIn{
|
||||
Timestamp: nil,
|
||||
Executant: &userv1beta1.UserId{
|
||||
OpaqueId: userId,
|
||||
},
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
require.Len(backend.Mock.Calls, 1)
|
||||
require.Len(backend.Mock.Calls[0].Arguments, 3)
|
||||
require.Equal(userId, backend.Mock.Calls[0].Arguments[1])
|
||||
|
||||
metricstest.RequireEqual(t, 0, m.UnsupportedEvents)
|
||||
metricstest.RequireEqualWithLabels(t, 1, map[string]string{"event": "UserSignedIn", "result": "failure"}, m.EventsProcessed)
|
||||
}
|
||||
@@ -63,7 +63,6 @@ type Graph struct {
|
||||
valueService settingssvc.ValueService
|
||||
specialDriveItemsCache *ttlcache.Cache[string, any]
|
||||
eventsPublisher events.Publisher
|
||||
eventsConsumer events.Consumer
|
||||
searchService searchsvc.SearchProviderService
|
||||
keycloakClient keycloak.Client
|
||||
historyClient ehsvc.EventHistoryService
|
||||
|
||||
@@ -39,7 +39,6 @@ type Options struct {
|
||||
ValueService settingssvc.ValueService
|
||||
RoleManager *roles.Manager
|
||||
EventsPublisher events.Publisher
|
||||
EventsConsumer events.Consumer
|
||||
SearchService searchsvc.SearchProviderService
|
||||
KeycloakClient keycloak.Client
|
||||
EventHistoryClient ehsvc.EventHistoryService
|
||||
@@ -163,13 +162,6 @@ func EventsPublisher(val events.Publisher) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// EventsConsumer provides a function to set the EventsConsumer option.
|
||||
func EventsConsumer(val events.Consumer) Option {
|
||||
return func(o *Options) {
|
||||
o.EventsConsumer = val
|
||||
}
|
||||
}
|
||||
|
||||
// KeycloakClient provides a function to set the KeycloakCient option.
|
||||
func KeycloakClient(val keycloak.Client) Option {
|
||||
return func(o *Options) {
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
ldapv3 "github.com/go-ldap/ldap/v3"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/cache"
|
||||
"github.com/riandyrn/otelchi"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/store"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
|
||||
ocldap "github.com/opencloud-eu/opencloud/pkg/ldap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/roles"
|
||||
"github.com/opencloud-eu/opencloud/pkg/service/grpc"
|
||||
"github.com/opencloud-eu/opencloud/pkg/tracing"
|
||||
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
|
||||
graphm "github.com/opencloud-eu/opencloud/services/graph/pkg/middleware"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole"
|
||||
)
|
||||
@@ -199,8 +185,8 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
mux: m,
|
||||
specialDriveItemsCache: spacePropertiesCache,
|
||||
eventsPublisher: options.EventsPublisher,
|
||||
eventsConsumer: options.EventsConsumer,
|
||||
searchService: options.SearchService,
|
||||
identityBackend: options.IdentityBackend,
|
||||
identityEducationBackend: options.IdentityEducationBackend,
|
||||
keycloakClient: options.KeycloakClient,
|
||||
historyClient: options.EventHistoryClient,
|
||||
@@ -209,10 +195,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
natskv: options.NatsKeyValue,
|
||||
}
|
||||
|
||||
if err := setIdentityBackends(options, &svc); err != nil {
|
||||
return svc, err
|
||||
}
|
||||
|
||||
if options.PermissionService == nil {
|
||||
grpcClient, err := grpc.NewClient(append(grpc.GetClientOptions(options.Config.GRPCClientTLS), grpc.WithTraceProvider(options.TraceProvider))...)
|
||||
if err != nil {
|
||||
@@ -450,164 +432,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func setIdentityBackends(options Options, svc *Graph) error {
|
||||
if options.IdentityBackend == nil {
|
||||
switch options.Config.Identity.Backend {
|
||||
case "cs3":
|
||||
gatewaySelector, err := pool.GatewaySelector(
|
||||
options.Config.Reva.Address,
|
||||
append(
|
||||
options.Config.Reva.GetRevaOptions(),
|
||||
pool.WithRegistry(registry.GetRegistry()),
|
||||
pool.WithTracerProvider(options.TraceProvider),
|
||||
)...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.identityBackend = &identity.CS3{
|
||||
Config: options.Config.Reva,
|
||||
Logger: &options.Logger,
|
||||
GatewaySelector: gatewaySelector,
|
||||
}
|
||||
case "ldap":
|
||||
var err error
|
||||
|
||||
var tlsConf *tls.Config
|
||||
if options.Config.Identity.LDAP.Insecure {
|
||||
|
||||
// When insecure is set to true then we don't need a certificate.
|
||||
options.Config.Identity.LDAP.CACert = ""
|
||||
tlsConf = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
|
||||
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
|
||||
InsecureSkipVerify: options.Config.Identity.LDAP.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
if options.Config.Identity.LDAP.CACert != "" {
|
||||
if err := ocldap.WaitForCA(options.Logger,
|
||||
options.Config.Identity.LDAP.Insecure,
|
||||
options.Config.Identity.LDAP.CACert); err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("The configured LDAP CA cert does not exist")
|
||||
}
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
certs := x509.NewCertPool()
|
||||
pemData, err := os.ReadFile(options.Config.Identity.LDAP.CACert)
|
||||
if err != nil {
|
||||
options.Logger.Error().Err(err).Msg("Error initializing LDAP Backend")
|
||||
return err
|
||||
}
|
||||
if !certs.AppendCertsFromPEM(pemData) {
|
||||
options.Logger.Error().Msg("Error initializing LDAP Backend. Adding CA cert failed")
|
||||
return err
|
||||
}
|
||||
tlsConf.RootCAs = certs
|
||||
}
|
||||
|
||||
conn := ldap.NewLDAPWithReconnect(
|
||||
ldap.Config{
|
||||
URI: options.Config.Identity.LDAP.URI,
|
||||
BindDN: options.Config.Identity.LDAP.BindDN,
|
||||
BindPassword: options.Config.Identity.LDAP.BindPassword,
|
||||
TLSConfig: tlsConf,
|
||||
},
|
||||
)
|
||||
conn.SetLogger(&options.Logger.Logger)
|
||||
lb, err := identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger)
|
||||
if err != nil {
|
||||
options.Logger.Error().Err(err).Msg("Error initializing LDAP Backend")
|
||||
return err
|
||||
}
|
||||
svc.identityBackend = lb
|
||||
if options.IdentityEducationBackend == nil {
|
||||
if options.Config.Identity.LDAP.EducationResourcesEnabled {
|
||||
svc.identityEducationBackend = lb
|
||||
} else {
|
||||
errEduBackend := &identity.ErrEducationBackend{}
|
||||
svc.identityEducationBackend = errEduBackend
|
||||
}
|
||||
}
|
||||
|
||||
disableMechanismType, err := identity.ParseDisableMechanismType(options.Config.Identity.LDAP.DisableUserMechanism)
|
||||
if err != nil {
|
||||
options.Logger.Error().Err(err).Msg("Error initializing LDAP Backend")
|
||||
return err
|
||||
}
|
||||
|
||||
if disableMechanismType == identity.DisableMechanismGroup {
|
||||
options.Logger.Info().Msg("LocalUserDisable is true, will create group if not exists")
|
||||
err := lb.CreateLDAPGroupByDN(options.Config.Identity.LDAP.LdapDisabledUsersGroupDN)
|
||||
if err != nil {
|
||||
isAnError := false
|
||||
var lerr *ldapv3.Error
|
||||
if errors.As(err, &lerr) {
|
||||
if lerr.ResultCode != ldapv3.LDAPResultEntryAlreadyExists {
|
||||
isAnError = true
|
||||
}
|
||||
} else {
|
||||
isAnError = true
|
||||
}
|
||||
|
||||
if isAnError {
|
||||
msg := "error adding group for disabling users"
|
||||
options.Logger.Error().Err(err).Str("local_user_disable", options.Config.Identity.LDAP.LdapDisabledUsersGroupDN).Msg(msg)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
err := fmt.Errorf("unknown identity backend: '%s'", options.Config.Identity.Backend)
|
||||
options.Logger.Err(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
svc.identityBackend = options.IdentityBackend
|
||||
}
|
||||
|
||||
return svc.StartListenForLogonEvents(options.Context, options.Logger)
|
||||
}
|
||||
|
||||
func (g *Graph) StartListenForLogonEvents(ctx context.Context, l log.Logger) error {
|
||||
if g.eventsConsumer == nil {
|
||||
return nil
|
||||
}
|
||||
var _registeredEvents = []events.Unmarshaller{
|
||||
events.UserSignedIn{},
|
||||
}
|
||||
evChannel, err := events.Consume(g.eventsConsumer, "graph", _registeredEvents...)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("cannot consume from nats")
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for loop := true; loop; {
|
||||
select {
|
||||
case e := <-evChannel:
|
||||
switch ev := e.Event.(type) {
|
||||
default:
|
||||
l.Error().Interface("event", e).Msg("unhandled event")
|
||||
case events.UserSignedIn:
|
||||
if err := g.identityBackend.UpdateLastSignInDate(ctx, ev.Executant.OpaqueId, utils.TSToTime(ev.Timestamp)); err != nil {
|
||||
l.Error().Err(err).Str("userid", ev.Executant.OpaqueId).Msg("Error updating last sign in date")
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
l.Info().Msg("context cancelled")
|
||||
loop = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseHeaderPurge parses the 'Purge' header.
|
||||
// '1', 't', 'T', 'TRUE', 'true', 'True' are parsed as true
|
||||
// all other values are false.
|
||||
|
||||
Reference in new issue
Block a user