mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-07 04:40:05 -06:00
graph(oidc): Consume UserSignedIn events in graph service
Pass them to the identity backend to update the last sign-in date of the user.
This commit is contained in:
committed by
Ralf Haferkamp
parent
cb8934081f
commit
8e158d52bb
@@ -12,8 +12,12 @@ packages:
|
||||
DriveItemPermissionsProvider:
|
||||
HTTPClient:
|
||||
Permissions:
|
||||
Publisher:
|
||||
RoleService:
|
||||
github.com/cs3org/reva/v2/pkg/events:
|
||||
config:
|
||||
dir: "mocks"
|
||||
interfaces:
|
||||
Publisher:
|
||||
github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool:
|
||||
config:
|
||||
dir: "mocks"
|
||||
|
||||
@@ -3,6 +3,7 @@ package identity
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
cs3group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
@@ -36,6 +37,7 @@ type Backend interface {
|
||||
UpdateUser(ctx context.Context, nameOrID string, user libregraph.UserUpdate) (*libregraph.User, error)
|
||||
GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (*libregraph.User, error)
|
||||
GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error)
|
||||
UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error
|
||||
|
||||
// CreateGroup creates the supplied group in the identity backend.
|
||||
CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error)
|
||||
|
||||
@@ -3,6 +3,7 @@ package identity
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
@@ -111,6 +112,11 @@ func (i *CS3) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libr
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UpdateLastSignInDate implements the Backend Interface. It's currently not supported for the CS3 backend
|
||||
func (i *CS3) UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error {
|
||||
return errNotImplemented
|
||||
}
|
||||
|
||||
// GetGroups implements the Backend Interface.
|
||||
func (i *CS3) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.Group, error) {
|
||||
logger := i.Logger.SubloggerWithRequestID(ctx)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
@@ -24,6 +25,7 @@ const (
|
||||
_givenNameAttribute = "givenname"
|
||||
_surNameAttribute = "sn"
|
||||
_identitiesAttribute = "oCExternalIdentity"
|
||||
ldapDateFormat = "20060102150405Z0700"
|
||||
)
|
||||
|
||||
// DisableUserMechanismType is used instead of directly using the string values from the configuration.
|
||||
@@ -670,6 +672,38 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UpdateLastSignInDate implements the Backend Interface.
|
||||
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
|
||||
}
|
||||
e, err := i.getLDAPUserByID(userID)
|
||||
switch {
|
||||
case errors.Is(err, ErrNotFound):
|
||||
i.logger.Warn().Err(err).Str("userID", userID).Msg("Failed to update last sign in date for user")
|
||||
return nil
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
mr := ldap.ModifyRequest{DN: e.DN}
|
||||
mr.Replace("oCLastSignInTimestamp", []string{timestamp.UTC().Format(ldapDateFormat)})
|
||||
if err := i.conn.Modify(&mr); err != nil {
|
||||
msg := "error updating last sign in date for user"
|
||||
i.logger.Error().Err(err).Str("userid", userID).Msg(msg)
|
||||
errMap := ldapResultToErrMap{
|
||||
ldap.LDAPResultNoSuchObject: errorcode.New(errorcode.ItemNotFound, msg),
|
||||
ldap.LDAPResultUnwillingToPerform: errorcode.New(errorcode.NotAllowed, msg),
|
||||
ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
|
||||
ldapGenericErr: errorcode.New(errorcode.GeneralException, msg),
|
||||
}
|
||||
return i.mapLDAPError(err, errMap)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *LDAP) changeUserName(ctx context.Context, dn, originalUserName, newUserName string) (*ldap.Entry, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
|
||||
|
||||
@@ -47,8 +47,6 @@ const (
|
||||
schoolPropertiesUpdated
|
||||
)
|
||||
|
||||
const ldapDateFormat = "20060102150405Z0700"
|
||||
|
||||
var (
|
||||
errNotSet = errors.New("attribute not set")
|
||||
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
|
||||
url "net/url"
|
||||
)
|
||||
|
||||
@@ -681,6 +683,54 @@ func (_c *Backend_UpdateGroupName_Call) RunAndReturn(run func(context.Context, s
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateLastSignInDate provides a mock function with given fields: ctx, userID, timestamp
|
||||
func (_m *Backend) UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error {
|
||||
ret := _m.Called(ctx, userID, timestamp)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateLastSignInDate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, time.Time) error); ok {
|
||||
r0 = rf(ctx, userID, timestamp)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Backend_UpdateLastSignInDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateLastSignInDate'
|
||||
type Backend_UpdateLastSignInDate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateLastSignInDate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - timestamp time.Time
|
||||
func (_e *Backend_Expecter) UpdateLastSignInDate(ctx interface{}, userID interface{}, timestamp interface{}) *Backend_UpdateLastSignInDate_Call {
|
||||
return &Backend_UpdateLastSignInDate_Call{Call: _e.mock.On("UpdateLastSignInDate", ctx, userID, timestamp)}
|
||||
}
|
||||
|
||||
func (_c *Backend_UpdateLastSignInDate_Call) Run(run func(ctx context.Context, userID string, timestamp time.Time)) *Backend_UpdateLastSignInDate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Backend_UpdateLastSignInDate_Call) Return(_a0 error) *Backend_UpdateLastSignInDate_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Backend_UpdateLastSignInDate_Call) RunAndReturn(run func(context.Context, string, time.Time) error) *Backend_UpdateLastSignInDate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateUser provides a mock function with given fields: ctx, nameOrID, user
|
||||
func (_m *Backend) UpdateUser(ctx context.Context, nameOrID string, user libregraph.UserUpdate) (*libregraph.User, error) {
|
||||
ret := _m.Called(ctx, nameOrID, user)
|
||||
|
||||
@@ -49,11 +49,11 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
|
||||
}
|
||||
|
||||
var publisher events.Stream
|
||||
var eventsStream events.Stream
|
||||
|
||||
if options.Config.Events.Endpoint != "" {
|
||||
var err error
|
||||
publisher, err = stream.NatsFromConfig(options.Config.Service.Name, false, stream.NatsConfig(options.Config.Events))
|
||||
eventsStream, err = stream.NatsFromConfig(options.Config.Service.Name, false, stream.NatsConfig(options.Config.Events))
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
@@ -130,10 +130,12 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
|
||||
var handle svc.Service
|
||||
handle, err = svc.NewService(
|
||||
svc.Context(options.Context),
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(middlewares...),
|
||||
svc.EventsPublisher(publisher),
|
||||
svc.EventsPublisher(eventsStream),
|
||||
svc.EventsConsumer(eventsStream),
|
||||
svc.WithRoleService(roleService),
|
||||
svc.WithValueService(valueService),
|
||||
svc.WithRequireAdminMiddleware(requireAdminMiddleware),
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"go-micro.dev/v4/client"
|
||||
mevents "go-micro.dev/v4/events"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
@@ -28,11 +27,6 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
|
||||
)
|
||||
|
||||
// Publisher is the interface for events publisher
|
||||
type Publisher interface {
|
||||
Publish(string, interface{}, ...mevents.PublishOption) error
|
||||
}
|
||||
|
||||
// Permissions is the interface used to access the permissions service
|
||||
type Permissions interface {
|
||||
ListPermissions(ctx context.Context, req *settingssvc.ListPermissionsRequest, opts ...client.CallOption) (*settingssvc.ListPermissionsResponse, error)
|
||||
@@ -68,6 +62,7 @@ type Graph struct {
|
||||
valueService settingssvc.ValueService
|
||||
specialDriveItemsCache *ttlcache.Cache[string, interface{}]
|
||||
eventsPublisher events.Publisher
|
||||
eventsConsumer events.Consumer
|
||||
searchService searchsvc.SearchProviderService
|
||||
keycloakClient keycloak.Client
|
||||
historyClient ehsvc.EventHistoryService
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
@@ -22,6 +23,7 @@ type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
@@ -34,6 +36,7 @@ type Options struct {
|
||||
ValueService settingssvc.ValueService
|
||||
RoleManager *roles.Manager
|
||||
EventsPublisher events.Publisher
|
||||
EventsConsumer events.Consumer
|
||||
SearchService searchsvc.SearchProviderService
|
||||
KeycloakClient keycloak.Client
|
||||
EventHistoryClient ehsvc.EventHistoryService
|
||||
@@ -51,6 +54,13 @@ func newOptions(opts ...Option) Options {
|
||||
return opt
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(ctx context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
@@ -142,6 +152,13 @@ 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,6 +1,7 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
@@ -16,10 +17,13 @@ import (
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/store"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
|
||||
ocisldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
@@ -148,6 +152,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
mux: m,
|
||||
specialDriveItemsCache: spacePropertiesCache,
|
||||
eventsPublisher: options.EventsPublisher,
|
||||
eventsConsumer: options.EventsConsumer,
|
||||
searchService: options.SearchService,
|
||||
identityEducationBackend: options.IdentityEducationBackend,
|
||||
keycloakClient: options.KeycloakClient,
|
||||
@@ -515,6 +520,39 @@ func setIdentityBackends(options Options, svc *Graph) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user