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:
Ralf Haferkamp
2024-08-29 10:48:25 +02:00
committed by Ralf Haferkamp
parent cb8934081f
commit 8e158d52bb
10 changed files with 158 additions and 12 deletions

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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),

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}