From 2722454066fca3a35842aa00e35134760c5cff0a Mon Sep 17 00:00:00 2001 From: David Christofas Date: Tue, 5 Apr 2022 18:35:50 +0200 Subject: [PATCH] implement user and group audit events --- audit/pkg/service/service.go | 14 +++++ audit/pkg/types/constants.go | 46 ++++++++++++++ audit/pkg/types/conversion.go | 70 ++++++++++++++++++++++ audit/pkg/types/events.go | 7 +++ audit/pkg/types/types.go | 46 ++++++++++++++ changelog/unreleased/user-group-audit.md | 12 ++++ go.mod | 2 + go.sum | 4 +- graph/pkg/config/config.go | 7 +++ graph/pkg/config/defaults/defaultconfig.go | 4 ++ graph/pkg/service/v0/graph.go | 10 ++++ graph/pkg/service/v0/groups.go | 7 +++ graph/pkg/service/v0/service.go | 13 ++++ graph/pkg/service/v0/users.go | 20 +++++++ 14 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 changelog/unreleased/user-group-audit.md diff --git a/audit/pkg/service/service.go b/audit/pkg/service/service.go index f2f09766a01..adf16f29146 100644 --- a/audit/pkg/service/service.go +++ b/audit/pkg/service/service.go @@ -85,6 +85,20 @@ func StartAuditLogger(ctx context.Context, ch <-chan interface{}, log log.Logger auditEvent = types.SpaceEnabled(ev) case events.SpaceDeleted: auditEvent = types.SpaceDeleted(ev) + case events.UserCreated: + auditEvent = types.UserCreated(ev) + case events.UserDeleted: + auditEvent = types.UserDeleted(ev) + case events.UserFeatureChanged: + auditEvent = types.UserFeatureChanged(ev) + case events.GroupCreated: + auditEvent = types.GroupCreated(ev) + case events.GroupDeleted: + auditEvent = types.GroupDeleted(ev) + case events.GroupMemberAdded: + auditEvent = types.GroupMemberAdded(ev) + case events.GroupMemberRemoved: + auditEvent = types.GroupMemberRemoved(ev) default: log.Error().Interface("event", ev).Msg(fmt.Sprintf("can't handle event of type '%T'", ev)) continue diff --git a/audit/pkg/types/constants.go b/audit/pkg/types/constants.go index 4f2f79d5e21..afd426491c5 100644 --- a/audit/pkg/types/constants.go +++ b/audit/pkg/types/constants.go @@ -30,6 +30,17 @@ const ( ActionSpaceDisabled = "space_disabled" ActionSpaceEnabled = "space_enabled" ActionSpaceDeleted = "space_deleted" + + // Users + ActionUserCreated = "user_created" + ActionUserDeleted = "user_deleted" + ActionUserFeatureChanged = "user_feature_changed" + + // Groups + ActionGroupCreated = "group_created" + ActionGroupDeleted = "group_deleted" + ActionGroupMemberAdded = "group_member_added" + ActionGroupMemberRemoved = "group_member_removed" ) // MessageShareCreated returns the human readable string that describes the action @@ -136,3 +147,38 @@ func MessageSpaceEnabled(spaceID string) string { func MessageSpaceDeleted(spaceID string) string { return fmt.Sprintf("Space '%s' was deleted", spaceID) } + +// MessageUserCreated returns the human readable string that describes the action +func MessageUserCreated(userID string) string { + return fmt.Sprintf("User '%s' was created", userID) +} + +// MessageUserDeleted returns the human readable string that describes the action +func MessageUserDeleted(userID string) string { + return fmt.Sprintf("User '%s' was deleted", userID) +} + +// MessageUserFeatureChanged returns the human readable string that describes the action +func MessageUserFeatureChanged(userID, feature, value string) string { + return fmt.Sprintf("User '%s's' feature '%s' was changed to '%s'", userID, feature, value) +} + +// MessageGroupCreated returns the human readable string that describes the action +func MessageGroupCreated(groupID string) string { + return fmt.Sprintf("Group '%s' was created", groupID) +} + +// MessageGroupDeleted returns the human readable string that describes the action +func MessageGroupDeleted(groupID string) string { + return fmt.Sprintf("Group '%s' was deleted", groupID) +} + +// MessageGroupMemberAdded returns the human readable string that describes the action +func MessageGroupMemberAdded(userID, groupID string) string { + return fmt.Sprintf("User '%s' was added to group '%s'", userID, groupID) +} + +// MessageGroupMemberRemoved returns the human readable string that describes the action +func MessageGroupMemberRemoved(userID, groupID string) string { + return fmt.Sprintf("User '%s' was removed from group '%s'", userID, groupID) +} diff --git a/audit/pkg/types/conversion.go b/audit/pkg/types/conversion.go index f03732daf01..6413bee6ddd 100644 --- a/audit/pkg/types/conversion.go +++ b/audit/pkg/types/conversion.go @@ -369,6 +369,76 @@ func SpaceDeleted(ev events.SpaceDeleted) AuditEventSpaceDeleted { } } +// UserCreated converts a UserCreated event to an AuditEventUserCreated +func UserCreated(ev events.UserCreated) AuditEventUserCreated { + base := BasicAuditEvent("", "", MessageUserCreated(ev.UserID), ActionUserCreated) + return AuditEventUserCreated{ + AuditEvent: base, + UserID: ev.UserID, + } +} + +// UserDeleted converts a UserCreated event to an AuditEventUserDeleted +func UserDeleted(ev events.UserDeleted) AuditEventUserDeleted { + base := BasicAuditEvent("", "", MessageUserDeleted(ev.UserID), ActionUserDeleted) + return AuditEventUserDeleted{ + AuditEvent: base, + UserID: ev.UserID, + } +} + +// UserFeatureChanged converts a UserFeatureChanged event to an AuditEventUserFeatureChanged +func UserFeatureChanged(ev events.UserFeatureChanged) AuditEventUserFeatureChanged { + msg := MessageUserFeatureChanged(ev.UserID, ev.Feature, ev.Value) + base := BasicAuditEvent("", "", msg, ActionUserFeatureChanged) + return AuditEventUserFeatureChanged{ + AuditEvent: base, + UserID: ev.UserID, + Feature: ev.Feature, + Value: ev.Value, + } +} + +// GroupCreated converts a GroupCreated event to an AuditEventGroupCreated +func GroupCreated(ev events.GroupCreated) AuditEventGroupCreated { + base := BasicAuditEvent("", "", MessageGroupCreated(ev.GroupID), ActionGroupCreated) + return AuditEventGroupCreated{ + AuditEvent: base, + GroupID: ev.GroupID, + } +} + +// GroupDeleted converts a GroupDeleted event to an AuditEventGroupDeleted +func GroupDeleted(ev events.GroupDeleted) AuditEventGroupDeleted { + base := BasicAuditEvent("", "", MessageGroupDeleted(ev.GroupID), ActionGroupDeleted) + return AuditEventGroupDeleted{ + AuditEvent: base, + GroupID: ev.GroupID, + } +} + +// GroupMemberAdded converts a GroupMemberAdded event to an AuditEventGroupMemberAdded +func GroupMemberAdded(ev events.GroupMemberAdded) AuditEventGroupMemberAdded { + msg := MessageGroupMemberAdded(ev.GroupID, ev.UserID) + base := BasicAuditEvent("", "", msg, ActionGroupMemberAdded) + return AuditEventGroupMemberAdded{ + AuditEvent: base, + GroupID: ev.GroupID, + UserID: ev.UserID, + } +} + +// GroupMemberRemoved converts a GroupMemberRemoved event to an AuditEventGroupMemberRemove +func GroupMemberRemoved(ev events.GroupMemberRemoved) AuditEventGroupMemberRemoved { + msg := MessageGroupMemberRemoved(ev.GroupID, ev.UserID) + base := BasicAuditEvent("", "", msg, ActionGroupMemberRemoved) + return AuditEventGroupMemberRemoved{ + AuditEvent: base, + GroupID: ev.GroupID, + UserID: ev.UserID, + } +} + func extractGrantee(uid *user.UserId, gid *group.GroupId) (string, string) { switch { case uid != nil && uid.OpaqueId != "": diff --git a/audit/pkg/types/events.go b/audit/pkg/types/events.go index 743044a049f..1862b1f8070 100644 --- a/audit/pkg/types/events.go +++ b/audit/pkg/types/events.go @@ -28,5 +28,12 @@ func RegisteredEvents() []events.Unmarshaller { events.SpaceEnabled{}, events.SpaceDisabled{}, events.SpaceDeleted{}, + events.UserCreated{}, + events.UserDeleted{}, + events.UserFeatureChanged{}, + events.GroupCreated{}, + events.GroupDeleted{}, + events.GroupMemberAdded{}, + events.GroupMemberRemoved{}, } } diff --git a/audit/pkg/types/types.go b/audit/pkg/types/types.go index d4227a196be..78baca3b9f5 100644 --- a/audit/pkg/types/types.go +++ b/audit/pkg/types/types.go @@ -197,3 +197,49 @@ type AuditEventSpaceEnabled struct { type AuditEventSpaceDeleted struct { AuditEventSpaces } + +// AuditEventUserCreated is the event logged when a user is created +type AuditEventUserCreated struct { + AuditEvent + UserID string +} + +// AuditEventUserDeleted is the event logged when a user is deleted +type AuditEventUserDeleted struct { + AuditEvent + UserID string +} + +// AuditEventUserFeatureChanged is the event logged when a user feature is changed +type AuditEventUserFeatureChanged struct { + AuditEvent + UserID string + Feature string + Value string +} + +// AuditEventGroupCreated is the event logged when a group is created +type AuditEventGroupCreated struct { + AuditEvent + GroupID string +} + +// AuditEventGroupDeleted is the event logged when a group is deleted +type AuditEventGroupDeleted struct { + AuditEvent + GroupID string +} + +// AuditEventGroupMemberAdded is the event logged when a group member is added +type AuditEventGroupMemberAdded struct { + AuditEvent + GroupID string + UserID string +} + +// AuditEventGroupMemberRemoved is the event logged when a group member is removed +type AuditEventGroupMemberRemoved struct { + AuditEvent + GroupID string + UserID string +} diff --git a/changelog/unreleased/user-group-audit.md b/changelog/unreleased/user-group-audit.md new file mode 100644 index 00000000000..ff3f6310a46 --- /dev/null +++ b/changelog/unreleased/user-group-audit.md @@ -0,0 +1,12 @@ +Enhancement: Implement audit events for user and groups + +Added audit events for users and groups. This will log: +* User creation +* User deletion +* User property change (currently only email) +* Group creation +* Group deletion +* Group member add +* Group member remove + +https://github.com/owncloud/ocis/pull/3467 diff --git a/go.mod b/go.mod index e44ea50d3cc..bed5b83c602 100644 --- a/go.mod +++ b/go.mod @@ -271,3 +271,5 @@ require ( // we need to use a fork to make the windows build pass replace github.com/pkg/xattr => github.com/micbar/xattr v0.4.6-0.20220215112335-88e74d648fb7 + +replace github.com/cs3org/reva/v2 => github.com/c0rby/reva/v2 v2.0.0-20220405162342-c89800919b67 diff --git a/go.sum b/go.sum index 994eae0056b..a9aa0eddcd0 100644 --- a/go.sum +++ b/go.sum @@ -261,6 +261,8 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= +github.com/c0rby/reva/v2 v2.0.0-20220405162342-c89800919b67 h1:0jWkpLmG9b/nkz+TyPiT7vyWjPTS+ZF4BBwa8j12KPw= +github.com/c0rby/reva/v2 v2.0.0-20220405162342-c89800919b67/go.mod h1:1siLO6MV57uSyzQxPbfM6qNA9NP6aagN3/yKOE/FwtM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -337,8 +339,6 @@ github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD9 github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= github.com/cs3org/go-cs3apis v0.0.0-20220328105952-297bef33e13f h1:emnlOWc1s2gx77MViLnZH9yh5TRHKsykRu6rJjx3lkM= github.com/cs3org/go-cs3apis v0.0.0-20220328105952-297bef33e13f/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= -github.com/cs3org/reva/v2 v2.0.0-20220404075659-19fd0b28297b h1:CqHYID4t286wle5kXcFfUtxxw6Vz0XlbGCiB/Z8rDbI= -github.com/cs3org/reva/v2 v2.0.0-20220404075659-19fd0b28297b/go.mod h1:1siLO6MV57uSyzQxPbfM6qNA9NP6aagN3/yKOE/FwtM= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= diff --git a/graph/pkg/config/config.go b/graph/pkg/config/config.go index 3501a30a032..fe59ea6901d 100644 --- a/graph/pkg/config/config.go +++ b/graph/pkg/config/config.go @@ -23,6 +23,7 @@ type Config struct { Spaces Spaces `yaml:"spaces"` Identity Identity `yaml:"identity"` + Events Events `yaml:"events"` Context context.Context `yaml:"-"` } @@ -62,3 +63,9 @@ type Identity struct { Backend string `yaml:"backend" env:"GRAPH_IDENTITY_BACKEND"` LDAP LDAP `yaml:"ldap"` } + +// Events combines the configuration options for the event bus. +type Events struct { + Endpoint string `yaml:"events_endpoint" env:"GRAPH_EVENTS_ENDPOINT" desc:"the address of the streaming service"` + Cluster string `yaml:"events_cluster" env:"GRAPH_EVENTS_CLUSTER" desc:"the clusterID of the streaming service. Mandatory when using nats"` +} diff --git a/graph/pkg/config/defaults/defaultconfig.go b/graph/pkg/config/defaults/defaultconfig.go index 9c2eba50cb8..5bab47e892b 100644 --- a/graph/pkg/config/defaults/defaultconfig.go +++ b/graph/pkg/config/defaults/defaultconfig.go @@ -57,6 +57,10 @@ func DefaultConfig() *config.Config { GroupIDAttribute: "owncloudUUID", }, }, + Events: config.Events{ + Endpoint: "127.0.0.1:9233", + Cluster: "ocis-cluster", + }, } } diff --git a/graph/pkg/service/v0/graph.go b/graph/pkg/service/v0/graph.go index 2dd618d09ae..fe138f51270 100644 --- a/graph/pkg/service/v0/graph.go +++ b/graph/pkg/service/v0/graph.go @@ -7,6 +7,7 @@ import ( "github.com/ReneKroon/ttlcache/v2" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/events" "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/graph/pkg/config" "github.com/owncloud/ocis/graph/pkg/identity" @@ -69,6 +70,7 @@ type Graph struct { httpClient HTTPClient roleService settingssvc.RoleService spacePropertiesCache *ttlcache.Cache + eventsPublisher events.Publisher } // ServeHTTP implements the Service interface. @@ -86,6 +88,14 @@ func (g Graph) GetHTTPClient() HTTPClient { return g.httpClient } +func (g Graph) publishEvent(ev interface{}) { + if err := events.Publish(g.eventsPublisher, ev); err != nil { + g.logger.Error(). + Err(err). + Msg("could not publish user created event") + } +} + type listResponse struct { Value interface{} `json:"value,omitempty"` } diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 0baa907595c..3124da1edf5 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -13,6 +13,7 @@ import ( libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" + "github.com/cs3org/reva/v2/pkg/events" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) @@ -81,6 +82,7 @@ func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) { return } + g.publishEvent(events.GroupCreated{GroupID: *grp.Id}) render.Status(r, http.StatusOK) render.JSON(w, r, grp) } @@ -197,6 +199,8 @@ func (g Graph) DeleteGroup(w http.ResponseWriter, r *http.Request) { } return } + + g.publishEvent(events.GroupDeleted{GroupID: groupID}) render.Status(r, http.StatusNoContent) render.NoContent(w, r) } @@ -279,6 +283,8 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) { } return } + + g.publishEvent(events.GroupMemberAdded{GroupID: groupID, UserID: id}) render.Status(r, http.StatusNoContent) render.NoContent(w, r) } @@ -322,6 +328,7 @@ func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { } return } + g.publishEvent(events.GroupMemberRemoved{GroupID: groupID, UserID: memberID}) render.Status(r, http.StatusNoContent) render.NoContent(w, r) } diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 7ac1d3590c4..9b5924bb18b 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -7,6 +7,8 @@ import ( "time" "github.com/ReneKroon/ttlcache/v2" + "github.com/asim/go-micro/plugins/events/natsjs/v4" + "github.com/cs3org/reva/v2/pkg/events/server" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -90,12 +92,23 @@ func NewService(opts ...Option) Service { return nil } + publisher, err := server.NewNatsStream( + natsjs.Address(options.Config.Events.Endpoint), + natsjs.ClusterID(options.Config.Events.Cluster), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing events publisher") + } + svc := Graph{ config: options.Config, mux: m, logger: &options.Logger, identityBackend: backend, spacePropertiesCache: ttlcache.NewCache(), + eventsPublisher: publisher, } if options.GatewayClient == nil { var err error diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index 0cc29954f96..4de118f954e 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -12,6 +12,7 @@ import ( "github.com/CiscoM31/godata" revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/events" "github.com/go-chi/chi/v5" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" @@ -133,6 +134,9 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, fmt.Sprintf("could not assign role to account %s", err.Error())) return } + + g.publishEvent(events.UserCreated{UserID: *u.Id}) + render.Status(r, http.StatusOK) render.JSON(w, r, u) } @@ -187,6 +191,9 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) } } + + g.publishEvent(events.UserDeleted{UserID: userID}) + render.Status(r, http.StatusNoContent) render.NoContent(w, r) } @@ -211,12 +218,18 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { return } + var ( + changedFeature string + newValue string + ) if mail, ok := changes.GetMailOk(); ok { if !isValidEmail(*mail) { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("'%s' is not a valid email address", *mail)) return } + changedFeature = "email" + newValue = *mail } u, err := g.identityBackend.UpdateUser(r.Context(), nameOrID, *changes) @@ -229,6 +242,13 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) { } } + g.publishEvent( + events.UserFeatureChanged{ + UserID: nameOrID, + Feature: changedFeature, + Value: newValue, + }, + ) render.Status(r, http.StatusOK) render.JSON(w, r, u)