Repository: owncloud/ocis
Pull request #3332 (2022-03-16 14:57:47)

File events audit logging
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description
Adds file based events for audit logger

## Related Issue
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
- Fixes https://jira.owncloud.com/browse/OCIS-2335

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
- new unit tests
- manual testing

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [x] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eefbf28ee5b..cf7a96d8d80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The following sections list the changes for unreleased.
 * Bugfix - Network configuration in individiual_services example: [#3238](https://github.com/owncloud/ocis/pull/3238)
 * Bugfix - Improve gif thumbnails: [#3305](https://github.com/owncloud/ocis/pull/3305)
 * Change - Settings service now stores its data via metadata service: [#3232](https://github.com/owncloud/ocis/pull/3232)
+* Enhancement - Add password reset link to login page: [#3329](https://github.com/owncloud/ocis/pull/3329)
 * Enhancement - Log sharing events in audit service: [#3301](https://github.com/owncloud/ocis/pull/3301)
 * Enhancement - Add space aliases: [#3283](https://github.com/owncloud/ocis/pull/3283)
 * Enhancement - Include etags in drives listing: [#3267](https://github.com/owncloud/ocis/pull/3267)
@@ -37,6 +38,13 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/pull/3232
 
+* Enhancement - Add password reset link to login page: [#3329](https://github.com/owncloud/ocis/pull/3329)
+
+   Added a configurable passwort reset link to the login page. It can be set via
+   `IDP_PASSWORD_RESET_URI`. If the option is not set the link will not be shown.
+
+   https://github.com/owncloud/ocis/pull/3329
+
 * Enhancement - Log sharing events in audit service: [#3301](https://github.com/owncloud/ocis/pull/3301)
 
    Contains sharing related events. See full list in audit/pkg/types/events.go
diff --git a/audit/pkg/service/service.go b/audit/pkg/service/service.go
index 905e251ee51..5edc11122d8 100644
--- a/audit/pkg/service/service.go
+++ b/audit/pkg/service/service.go
@@ -61,6 +61,20 @@ func StartAuditLogger(ctx context.Context, ch <-chan interface{}, log log.Logger
 				auditEvent = types.LinkAccessed(ev)
 			case events.LinkAccessFailed:
 				auditEvent = types.LinkAccessFailed(ev)
+			case events.FileUploaded:
+				auditEvent = types.FileUploaded(ev)
+			case events.FileDownloaded:
+				auditEvent = types.FileDownloaded(ev)
+			case events.ItemMoved:
+				auditEvent = types.ItemMoved(ev)
+			case events.ItemTrashed:
+				auditEvent = types.ItemTrashed(ev)
+			case events.ItemPurged:
+				auditEvent = types.ItemPurged(ev)
+			case events.ItemRestored:
+				auditEvent = types.ItemRestored(ev)
+			case events.FileVersionRestored:
+				auditEvent = types.FileVersionRestored(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 663996241dd..10b51a35c38 100644
--- a/audit/pkg/types/constants.go
+++ b/audit/pkg/types/constants.go
@@ -4,6 +4,7 @@ import "fmt"
 
 // short identifiers for audit actions
 const (
+	// Sharing
 	ActionShareCreated            = "file_shared"
 	ActionSharePermissionUpdated  = "share_permission_updated"
 	ActionShareDisplayNameUpdated = "share_name_updated"
@@ -13,6 +14,15 @@ const (
 	ActionShareAccepted           = "share_accepted"
 	ActionShareDeclined           = "share_declined"
 	ActionLinkAccessed            = "public_link_accessed"
+
+	// Files
+	ActionFileCreated         = "file_create"
+	ActionFileRead            = "file_read"
+	ActionFileTrashed         = "file_delete"
+	ActionFileRenamed         = "file_rename"
+	ActionFilePurged          = "file_trash_delete"
+	ActionFileRestored        = "file_trash_restore"
+	ActionFileVersionRestored = "file_version_restore"
 )
 
 // MessageShareCreated returns the human readable string that describes the action
@@ -59,3 +69,38 @@ func MessageShareDeclined(userid, shareid, sharerid string) string {
 func MessageLinkAccessed(linkid string, success bool) string {
 	return fmt.Sprintf("link '%s' was accessed. Success: %v", linkid, success)
 }
+
+// MessageFileCreated returns the human readable string that describes the action
+func MessageFileCreated(item string) string {
+	return fmt.Sprintf("File '%s' was created", item)
+}
+
+// MessageFileRead returns the human readable string that describes the action
+func MessageFileRead(item string) string {
+	return fmt.Sprintf("File '%s' was read", item)
+}
+
+// MessageFileTrashed returns the human readable string that describes the action
+func MessageFileTrashed(item string) string {
+	return fmt.Sprintf("File '%s' was trashed", item)
+}
+
+// MessageFileRenamed returns the human readable string that describes the action
+func MessageFileRenamed(item, oldpath, newpath string) string {
+	return fmt.Sprintf("File '%s' was moved from '%s' to '%s'", item, oldpath, newpath)
+}
+
+// MessageFilePurged returns the human readable string that describes the action
+func MessageFilePurged(item string) string {
+	return fmt.Sprintf("File '%s' was removed from trashbin", item)
+}
+
+// MessageFileRestored returns the human readable string that describes the action
+func MessageFileRestored(item, path string) string {
+	return fmt.Sprintf("File '%s' was restored from trashbin to '%s'", item, path)
+}
+
+// MessageFileVersionRestored returns the human readable string that describes the action
+func MessageFileVersionRestored(item string, version string) string {
+	return fmt.Sprintf("File '%s' was restored in version '%s'", item, version)
+}
diff --git a/audit/pkg/types/conversion.go b/audit/pkg/types/conversion.go
index c90ab0eec43..29dde252511 100644
--- a/audit/pkg/types/conversion.go
+++ b/audit/pkg/types/conversion.go
@@ -5,9 +5,11 @@ import (
 	"time"
 
 	"github.com/cs3org/reva/v2/pkg/events"
+	"github.com/cs3org/reva/v2/pkg/utils"
 
 	group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
 	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
 )
 
@@ -220,6 +222,94 @@ func LinkAccessFailed(ev events.LinkAccessFailed) AuditEventLinkAccessed {
 	}
 }
 
+// FilesAuditEvent creates an AuditEventFiles from the given values
+func FilesAuditEvent(base AuditEvent, itemid, owner, path string) AuditEventFiles {
+	return AuditEventFiles{
+		AuditEvent: base,
+		FileID:     itemid,
+		Owner:      owner,
+		Path:       path,
+	}
+}
+
+// FileUploaded converts a FileUploaded event to an AuditEventFileCreated
+func FileUploaded(ev events.FileUploaded) AuditEventFileCreated {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+	base := BasicAuditEvent(uid, "", MessageFileCreated(iid), ActionFileCreated)
+	return AuditEventFileCreated{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+	}
+}
+
+// FileDownloaded converts a FileDownloaded event to an AuditEventFileRead
+func FileDownloaded(ev events.FileDownloaded) AuditEventFileRead {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+	base := BasicAuditEvent(uid, "", MessageFileRead(iid), ActionFileRead)
+	return AuditEventFileRead{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+	}
+}
+
+// ItemMoved converts a ItemMoved event to an AuditEventFileRenamed
+func ItemMoved(ev events.ItemMoved) AuditEventFileRenamed {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+
+	oldpath := ""
+	if ev.OldReference != nil {
+		oldpath = ev.OldReference.GetPath()
+	}
+
+	base := BasicAuditEvent(uid, "", MessageFileRenamed(iid, oldpath, path), ActionFileRenamed)
+	return AuditEventFileRenamed{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+		OldPath:         oldpath,
+	}
+}
+
+// ItemTrashed converts a ItemTrashed event to an AuditEventFileDeleted
+func ItemTrashed(ev events.ItemTrashed) AuditEventFileDeleted {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+	base := BasicAuditEvent(uid, "", MessageFileTrashed(iid), ActionFileTrashed)
+	return AuditEventFileDeleted{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+	}
+}
+
+// ItemPurged converts a ItemPurged event to an AuditEventFilePurged
+func ItemPurged(ev events.ItemPurged) AuditEventFilePurged {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+	base := BasicAuditEvent(uid, "", MessageFilePurged(iid), ActionFilePurged)
+	return AuditEventFilePurged{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+	}
+}
+
+// ItemRestored converts a ItemRestored event to an AuditEventFileRestored
+func ItemRestored(ev events.ItemRestored) AuditEventFileRestored {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+
+	oldpath := ""
+	if ev.OldReference != nil {
+		oldpath = ev.OldReference.GetPath()
+	}
+
+	base := BasicAuditEvent(uid, "", MessageFileRestored(iid, path), ActionFileRestored)
+	return AuditEventFileRestored{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+		OldPath:         oldpath,
+	}
+}
+
+// FileVersionRestored converts a FileVersionRestored event to an AuditEventFileVersionRestored
+func FileVersionRestored(ev events.FileVersionRestored) AuditEventFileVersionRestored {
+	iid, path, uid := extractFileDetails(ev.FileID, ev.Owner)
+	base := BasicAuditEvent(uid, "", MessageFileVersionRestored(iid, ev.Key), ActionFileVersionRestored)
+	return AuditEventFileVersionRestored{
+		AuditEventFiles: FilesAuditEvent(base, iid, uid, path),
+		Key:             ev.Key,
+	}
+}
+
 func extractGrantee(uid *user.UserId, gid *group.GroupId) (string, string) {
 	switch {
 	case uid != nil && uid.OpaqueId != "":
@@ -231,6 +321,20 @@ func extractGrantee(uid *user.UserId, gid *group.GroupId) (string, string) {
 	return "", ""
 }
 
+func extractFileDetails(ref *provider.Reference, owner *user.UserId) (string, string, string) {
+	id, path := "", ""
+	if ref != nil {
+		path = ref.GetPath()
+		id, _ = utils.FormatStorageSpaceReference(ref)
+	}
+
+	uid := ""
+	if owner != nil {
+		uid = owner.GetOpaqueId()
+	}
+	return id, path, uid
+}
+
 func formatTime(t *types.Timestamp) string {
 	if t == nil {
 		return ""
diff --git a/audit/pkg/types/events.go b/audit/pkg/types/events.go
index 801dd41790d..85cb3c99c74 100644
--- a/audit/pkg/types/events.go
+++ b/audit/pkg/types/events.go
@@ -16,5 +16,12 @@ func RegisteredEvents() []events.Unmarshaller {
 		events.ReceivedShareUpdated{},
 		events.LinkAccessed{},
 		events.LinkAccessFailed{},
+		events.FileUploaded{},
+		events.FileDownloaded{},
+		events.ItemTrashed{},
+		events.ItemMoved{},
+		events.ItemPurged{},
+		events.ItemRestored{},
+		events.FileVersionRestored{},
 	}
 }
diff --git a/audit/pkg/types/types.go b/audit/pkg/types/types.go
index 17e2d0a3e16..1b1728f0237 100644
--- a/audit/pkg/types/types.go
+++ b/audit/pkg/types/types.go
@@ -15,6 +15,10 @@ type AuditEvent struct {
 	Level      int    // the log level of the entry (usually 1 for audit events)
 }
 
+/*
+   Sharing
+*/
+
 // AuditEventSharing is the basic audit event for shares
 type AuditEventSharing struct {
 	AuditEvent
@@ -76,3 +80,77 @@ type AuditEventLinkAccessed struct {
 	Success    bool   // If the request was successful.
 	ItemType   string // file or folder
 }
+
+/*
+   Files
+*/
+
+// AuditEventFiles is the basic audit event for files
+type AuditEventFiles struct {
+	AuditEvent
+
+	Path   string // The full path to the create file.
+	Owner  string // The UID of the owner of the file.
+	FileID string // The newly created files identifier.
+}
+
+// AuditEventFileCreated is the event logged when a file is created
+type AuditEventFileCreated struct {
+	AuditEventFiles
+}
+
+// AuditEventFileRead is the event logged when a file is read (aka downloaded)
+type AuditEventFileRead struct {
+	AuditEventFiles
+}
+
+// AuditEventFileUpdated is the event logged when a file is updated
+// TODO: How to differentiate between new uploads and new version uploads?
+// FIXME: implement
+type AuditEventFileUpdated struct {
+	AuditEventFiles
+}
+
+// AuditEventFileDeleted is the event logged when a file is deleted (aka trashed)
+type AuditEventFileDeleted struct {
+	AuditEventFiles
+}
+
+// AuditEventFileCopied is the event logged when a file is copied
+// TODO: copy is a download&upload for now. How to know it was a copy?
+// FIXME: implement
+type AuditEventFileCopied struct {
+	AuditEventFiles
+}
+
+// AuditEventFileRenamed is the event logged when a file is renamed (moved)
+type AuditEventFileRenamed struct {
+	AuditEventFiles
+
+	OldPath string
+}
+
+// AuditEventFilePurged is the event logged when a file is purged (deleted from trashbin)
+type AuditEventFilePurged struct {
+	AuditEventFiles
+}
+
+// AuditEventFileRestored is the event logged when a file is restored (from trashbin)
+type AuditEventFileRestored struct {
+	AuditEventFiles
+
+	OldPath string
+}
+
+// AuditEventFileVersionRestored is the event logged when a file version is restored
+type AuditEventFileVersionRestored struct {
+	AuditEventFiles
+
+	Key string
+}
+
+// AuditEventFileVersionDeleted is the event logged when a file version is deleted
+// TODO: is this even possible?
+type AuditEventFileVersionDeleted struct {
+	AuditEventFiles
+}
diff --git a/changelog/unreleased/file-events-audit-logging.md b/changelog/unreleased/file-events-audit-logging.md
new file mode 100644
index 00000000000..816f0e61d0a
--- /dev/null
+++ b/changelog/unreleased/file-events-audit-logging.md
@@ -0,0 +1,5 @@
+Enhancement: Audit logger will now log file events
+
+See full list of supported events in `audit/pkg/types/types.go`
+
+https://github.com/owncloud/ocis/pull/3332
diff --git a/changelog/unreleased/password-reset-link.md b/changelog/unreleased/password-reset-link.md
new file mode 100644
index 00000000000..01b57657ce8
--- /dev/null
+++ b/changelog/unreleased/password-reset-link.md
@@ -0,0 +1,7 @@
+Enhancement: Add password reset link to login page
+
+Added a configurable passwort reset link to the login page.
+It can be set via `IDP_PASSWORD_RESET_URI`. If the option is not set
+the link will not be shown.
+
+https://github.com/owncloud/ocis/pull/3329
diff --git a/go.mod b/go.mod
index c61033b5f88..5378d6c931a 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/blevesearch/bleve/v2 v2.3.1
 	github.com/coreos/go-oidc/v3 v3.1.0
 	github.com/cs3org/go-cs3apis v0.0.0-20220126114148-64c025ccdd19
-	github.com/cs3org/reva/v2 v2.0.0-20220316045927-99115670eb33
+	github.com/cs3org/reva/v2 v2.0.0-20220317153101-5a93e519610c
 	github.com/disintegration/imaging v1.6.2
 	github.com/glauth/glauth/v2 v2.0.0-20211021011345-ef3151c28733
 	github.com/go-chi/chi/v5 v5.0.7
diff --git a/go.sum b/go.sum
index 156debb180a..f2cea32d57a 100644
--- a/go.sum
+++ b/go.sum
@@ -341,8 +341,8 @@ 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-20220126114148-64c025ccdd19 h1:1jqPH58jCxvbaJ9WLIJ7W2/m622bWS6ChptzljSG6IQ=
 github.com/cs3org/go-cs3apis v0.0.0-20220126114148-64c025ccdd19/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
-github.com/cs3org/reva/v2 v2.0.0-20220316045927-99115670eb33 h1:XK88Fs9FteY9a+iKXqPhUK38zNQbP1jj60zy+Tx7SPI=
-github.com/cs3org/reva/v2 v2.0.0-20220316045927-99115670eb33/go.mod h1:XNtK1HEClNzmz5vyQa2DUw4KH3oqBjQoEsV1LhAGlV0=
+github.com/cs3org/reva/v2 v2.0.0-20220317153101-5a93e519610c h1:tTAuVwgbDNPyeqNJPjMrT1xZ4jZYGSJ2AWqDkvSpXuA=
+github.com/cs3org/reva/v2 v2.0.0-20220317153101-5a93e519610c/go.mod h1:XNtK1HEClNzmz5vyQa2DUw4KH3oqBjQoEsV1LhAGlV0=
 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/idp/pkg/config/service.go b/idp/pkg/config/service.go
index c019b73046e..261a336e2ba 100644
--- a/idp/pkg/config/service.go
+++ b/idp/pkg/config/service.go
@@ -2,5 +2,6 @@ package config
 
 // Service defines the available service configuration.
 type Service struct {
-	Name string `ocisConfig:"-" yaml:"-"`
+	Name             string `ocisConfig:"-" yaml:"-"`
+	PasswordResetURI string `ocisConfig:"password_reset_uri" env:"IDP_PASSWORD_RESET_URI" desc:"The URI where a user can reset their password."`
 }
diff --git a/idp/pkg/service/v0/service.go b/idp/pkg/service/v0/service.go
index cf486f16214..0598f0b59d3 100644
--- a/idp/pkg/service/v0/service.go
+++ b/idp/pkg/service/v0/service.go
@@ -214,6 +214,8 @@ func (idp IDP) Index() http.HandlerFunc {
 	nonce := rndm.GenerateRandomString(32)
 	indexHTML = bytes.Replace(indexHTML, []byte("__CSP_NONCE__"), []byte(nonce), 1)
 
+	indexHTML = bytes.Replace(indexHTML, []byte("__PASSWORD_RESET_LINK__"), []byte(idp.config.Service.PasswordResetURI), 1)
+
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusOK)
 		if _, err := w.Write(indexHTML); err != nil {
diff --git a/idp/ui/public/index.html b/idp/ui/public/index.html
index 9896a1a725a..1004a430603 100644
--- a/idp/ui/public/index.html
+++ b/idp/ui/public/index.html
@@ -13,6 +13,6 @@
     <noscript>
       You need to enable JavaScript to run this app.
     </noscript>
-    <main id="root" class="oc-login-bg" data-path-prefix="__PATH_PREFIX__"></main>
+    <main id="root" class="oc-login-bg" data-path-prefix="__PATH_PREFIX__" passwort-reset-link="__PASSWORD_RESET_LINK__"></main>
   </body>
 </html>
diff --git a/idp/ui/src/containers/Login/Login.js b/idp/ui/src/containers/Login/Login.js
index 65b36c1473c..83e7c662656 100644
--- a/idp/ui/src/containers/Login/Login.js
+++ b/idp/ui/src/containers/Login/Login.js
@@ -9,6 +9,7 @@ import Button from '@material-ui/core/Button';
 import CircularProgress from '@material-ui/core/CircularProgress';
 import green from '@material-ui/core/colors/green';
 import Typography from '@material-ui/core/Typography';
+import Link from '@material-ui/core/Link';
 
 import TextInput from '../../components/TextInput'
 
@@ -55,7 +56,8 @@ class Login extends React.PureComponent {
   }
 
   render() {
-    const { loading, errors, classes, username } = this.props;
+    const { loading, errors, classes, username, passwordResetLink } = this.props;
+    const loginFailed = errors.http;
     const hasError = errors.http || errors.username || errors.password;
     const errorMessage = errors.http
       ? <ErrorMessage error={errors.http}></ErrorMessage>
@@ -109,6 +111,8 @@ class Login extends React.PureComponent {
               {hasError && <Typography id="oc-login-error-message" variant="subtitle2" component="span" color="error"
                                        className={classes.message}>{errorMessage}</Typography>}
               <div className={classes.wrapper}>
+                {loginFailed && passwordResetLink && <Link id="oc-login-password-reset" href={passwordResetLink} variant="subtitle2">{"Reset password?"}</Link>}
+                <br />
                 <Button
                     type="submit"
                     color="primary"
@@ -150,6 +154,7 @@ Login.propTypes = {
   loading: PropTypes.string.isRequired,
   username: PropTypes.string.isRequired,
   password: PropTypes.string.isRequired,
+  passwordResetLink: PropTypes.string.isRequired,
   errors: PropTypes.object.isRequired,
   hello: PropTypes.object,
   query: PropTypes.object.isRequired,
@@ -160,7 +165,7 @@ Login.propTypes = {
 
 const mapStateToProps = (state) => {
   const { loading, username, password, errors} = state.login;
-  const { hello, query } = state.common;
+  const { hello, query, passwordResetLink } = state.common;
 
   return {
     loading,
@@ -168,7 +173,8 @@ const mapStateToProps = (state) => {
     password,
     errors,
     hello,
-    query
+    query,
+    passwordResetLink
   };
 };
 
diff --git a/idp/ui/src/reducers/common.js b/idp/ui/src/reducers/common.js
index 163ecdc177d..3279df0f53a 100644
--- a/idp/ui/src/reducers/common.js
+++ b/idp/ui/src/reducers/common.js
@@ -20,13 +20,24 @@ const defaultPathPrefix = (() => {
   return pathPrefix;
 })();
 
+const defaultPasswordResetLink = (() => {
+  const root = document.getElementById('root');
+  let link = root ? root.getAttribute('passwort-reset-link') : null;
+  if (!link || link === '__PASSWORD_RESET_LINK__') {
+    // Not replaced, probably we are running in debug mode or whatever. Use sane default.
+    link = '';
+  }
+  return link;
+})();
+
 const defaultState = {
   hello: null,
   error: null,
   flow: flow,
   query: query,
   updateAvailable: false,
-  pathPrefix: defaultPathPrefix
+  pathPrefix: defaultPathPrefix,
+  passwordResetLink: defaultPasswordResetLink
 };
 
 function commonReducer(state = defaultState, action) {
diff --git a/settings/package.json b/settings/package.json
index 17cf9ed6c8c..3eedad80c57 100644
--- a/settings/package.json
+++ b/settings/package.json
@@ -32,13 +32,13 @@
   "devDependencies": {
     "@babel/core": "^7.15.5",
     "@babel/plugin-proposal-class-properties": "^7.7.4",
-    "@babel/plugin-proposal-export-default-from": "^7.7.4",
+    "@babel/plugin-proposal-export-default-from": "^7.16.7",
     "@babel/plugin-proposal-object-rest-spread": "^7.15.6",
     "@babel/plugin-syntax-dynamic-import": "^7.7.4",
     "@babel/plugin-transform-runtime": "^7.15.0",
     "@babel/polyfill": "^7.10.1",
     "@babel/preset-env": "^7.16.11",
-    "@babel/register": "^7.14.5",
+    "@babel/register": "^7.17.0",
     "@cucumber/cucumber": "^7.3.1",
     "@cucumber/pretty-formatter": "^1.0.0-alpha.1",
     "@erquhart/rollup-plugin-node-builtins": "^2.1.5",
diff --git a/settings/yarn.lock b/settings/yarn.lock
index 3b6878732ba..c847c66e949 100644
--- a/settings/yarn.lock
+++ b/settings/yarn.lock
@@ -572,13 +572,13 @@
     "@babel/helper-plugin-utils" "^7.16.7"
     "@babel/plugin-syntax-dynamic-import" "^7.8.3"
 
-"@babel/plugin-proposal-export-default-from@^7.7.4":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.14.5.tgz#8931a6560632c650f92a8e5948f6e73019d6d321"
-  integrity sha512-T8KZ5abXvKMjF6JcoXjgac3ElmXf0AWzJwi2O/42Jk+HmCky3D9+i1B7NPP1FblyceqTevKeV/9szeikFoaMDg==
+"@babel/plugin-proposal-export-default-from@^7.16.7":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.16.7.tgz#a40ab158ca55627b71c5513f03d3469026a9e929"
+  integrity sha512-+cENpW1rgIjExn+o5c8Jw/4BuH4eGKKYvkMB8/0ZxFQ9mC0t4z09VsPIwNg6waF69QYC81zxGeAsREGuqQoKeg==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/plugin-syntax-export-default-from" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.16.7"
+    "@babel/plugin-syntax-export-default-from" "^7.16.7"
 
 "@babel/plugin-proposal-export-namespace-from@^7.16.7":
   version "7.16.7"
@@ -702,12 +702,12 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-export-default-from@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.14.5.tgz#cdfa9d43d2b2c89b6f1af3e83518e8c8b9ed0dbc"
-  integrity sha512-snWDxjuaPEobRBnhpqEfZ8RMxDbHt8+87fiEioGuE+Uc0xAKgSD8QiuL3lF93hPVQfZFAcYwrrf+H5qUhike3Q==
+"@babel/plugin-syntax-export-default-from@^7.16.7":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.16.7.tgz#fa89cf13b60de2c3f79acdc2b52a21174c6de060"
+  integrity sha512-4C3E4NsrLOgftKaTYTULhHsuQrGv3FHrBzOMDiS7UYKIpgGBkAdawg4h+EI8zPeK9M0fiIIh72hIwsI24K7MbA==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.16.7"
 
 "@babel/plugin-syntax-export-namespace-from@^7.8.3":
   version "7.8.3"
@@ -1140,15 +1140,15 @@
     "@babel/types" "^7.4.4"
     esutils "^2.0.2"
 
-"@babel/register@^7.14.5":
-  version "7.15.3"
-  resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.15.3.tgz#6b40a549e06ec06c885b2ec42c3dd711f55fe752"
-  integrity sha512-mj4IY1ZJkorClxKTImccn4T81+UKTo4Ux0+OFSV9hME1ooqS9UV+pJ6BjD0qXPK4T3XW/KNa79XByjeEMZz+fw==
+"@babel/register@^7.17.0":
+  version "7.17.0"
+  resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.0.tgz#8051e0b7cb71385be4909324f072599723a1f084"
+  integrity sha512-UNZsMAZ7uKoGHo1HlEXfteEOYssf64n/PNLHGqOKq/bgYcu/4LrQWAHJwSCb3BRZK8Hi5gkJdRcwrGTO2wtRCg==
   dependencies:
     clone-deep "^4.0.1"
     find-cache-dir "^2.0.0"
     make-dir "^2.1.0"
-    pirates "^4.0.0"
+    pirates "^4.0.5"
     source-map-support "^0.5.16"
 
 "@babel/runtime@^7.13.8", "@babel/runtime@^7.8.4":
@@ -6409,12 +6409,10 @@ pirates@^3.0.2:
   dependencies:
     node-modules-regexp "^1.0.0"
 
-pirates@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
-  integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
-  dependencies:
-    node-modules-regexp "^1.0.0"
+pirates@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
+  integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
 
 pixelmatch@^4.0.0:
   version "4.0.2"
diff --git a/storage/pkg/command/storageusers.go b/storage/pkg/command/storageusers.go
index 3edbd6a1b0a..0ebd6811762 100644
--- a/storage/pkg/command/storageusers.go
+++ b/storage/pkg/command/storageusers.go
@@ -112,6 +112,14 @@ func storageUsersConfigFromStruct(c *cli.Context, cfg *config.Config) map[string
 					"tmp_folder":         cfg.Reva.StorageUsers.TempFolder,
 				},
 			},
+			"interceptors": map[string]interface{}{
+				"eventsmiddleware": map[string]interface{}{
+					"group":     "sharing",
+					"type":      "nats",
+					"address":   cfg.Reva.Sharing.Events.Address,
+					"clusterID": cfg.Reva.Sharing.Events.ClusterID,
+				},
+			},
 		},
 		"http": map[string]interface{}{
 			"network": cfg.Reva.StorageUsers.HTTPNetwork,


Test patch:
diff --git a/audit/pkg/service/service_test.go b/audit/pkg/service/service_test.go
index 8bdcac5cd45..0bd1d22966f 100644
--- a/audit/pkg/service/service_test.go
+++ b/audit/pkg/service/service_test.go
@@ -294,6 +294,124 @@ var testCases = []struct {
 			require.Equal(t, "token-123", ev.ShareToken)
 			require.Equal(t, false, ev.Success)
 		},
+	}, {
+		Alias: "File created",
+		SystemEvent: events.FileUploaded{
+			FileID: reference("sto-123", "iid-123", "./item"),
+			Owner:  userID("uid-123"), // NOTE: owner not yet implemented in reva
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFileCreated{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was created", "file_create")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+		},
+	}, {
+		Alias: "File read",
+		SystemEvent: events.FileDownloaded{
+			FileID: reference("sto-123", "iid-123", "./item"),
+			Owner:  userID("uid-123"), // NOTE: owner not yet implemented in reva
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFileRead{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was read", "file_read")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+		},
+	}, {
+		Alias: "File trashed",
+		SystemEvent: events.ItemTrashed{
+			FileID: reference("sto-123", "iid-123", "./item"),
+			Owner:  userID("uid-123"), // NOTE: owner not yet implemented in reva
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFileDeleted{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was trashed", "file_delete")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+		},
+	}, {
+		Alias: "File renamed",
+		SystemEvent: events.ItemMoved{
+			FileID:       reference("sto-123", "iid-123", "./item"),
+			OldReference: reference("sto-123", "iid-123", "./anotheritem"),
+			Owner:        userID("uid-123"), // NOTE: owner not yet implemented in reva
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFileRenamed{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was moved from './anotheritem' to './item'", "file_rename")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+			// AuditEventFileRenamed fields
+			require.Equal(t, "./anotheritem", ev.OldPath)
+
+		},
+	}, {
+		Alias: "File purged",
+		SystemEvent: events.ItemPurged{
+			FileID: reference("sto-123", "iid-123", "./item"),
+			Owner:  userID("uid-123"), // NOTE: owner not yet implemented in reva
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFilePurged{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was removed from trashbin", "file_trash_delete")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+		},
+	}, {
+		Alias: "File restored",
+		SystemEvent: events.ItemRestored{
+			FileID:       reference("sto-123", "iid-123", "./item"),
+			Owner:        userID("uid-123"), // NOTE: owner not yet implemented in reva
+			OldReference: reference("sto-123", "sto-123!iid-123/item", "./oldpath"),
+			Key:          "",
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFileRestored{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was restored from trashbin to './item'", "file_trash_restore")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+			// AuditEventFileRestored fields
+			require.Equal(t, "./oldpath", ev.OldPath)
+
+		},
+	}, {
+		Alias: "File version restored",
+		SystemEvent: events.FileVersionRestored{
+			FileID: reference("sto-123", "iid-123", "./item"),
+			Owner:  userID("uid-123"), // NOTE: owner not yet implemented in reva
+			Key:    "v1",
+		},
+		CheckAuditEvent: func(t *testing.T, b []byte) {
+			ev := types.AuditEventFileVersionRestored{}
+			require.NoError(t, json.Unmarshal(b, &ev))
+
+			// AuditEvent fields
+			checkBaseAuditEvent(t, ev.AuditEvent, "uid-123", "", "File 'sto-123!iid-123/item' was restored in version 'v1'", "file_version_restore")
+			// AuditEventSharing fields
+			checkFilesAuditEvent(t, ev.AuditEventFiles, "sto-123!iid-123/item", "uid-123", "./item")
+			// AuditEventFileRestored fields
+			require.Equal(t, "v1", ev.Key)
+
+		},
 	},
 }
 
@@ -343,6 +461,12 @@ func checkSharingAuditEvent(t *testing.T, ev types.AuditEventSharing, itemID str
 	require.Equal(t, shareID, ev.ShareID)
 }
 
+func checkFilesAuditEvent(t *testing.T, ev types.AuditEventFiles, itemID string, owner string, path string) {
+	require.Equal(t, itemID, ev.FileID)
+	require.Equal(t, owner, ev.Owner)
+	require.Equal(t, path, ev.Path)
+}
+
 func shareID(id string) *collaboration.ShareId {
 	return &collaboration.ShareId{
 		OpaqueId: id,
@@ -376,6 +500,13 @@ func resourceID(sid, oid string) *provider.ResourceId {
 	}
 }
 
+func reference(sid, oid, path string) *provider.Reference {
+	return &provider.Reference{
+		ResourceId: resourceID(sid, oid),
+		Path:       path,
+	}
+}
+
 func timestamp(seconds uint64) *rtypes.Timestamp {
 	return &rtypes.Timestamp{
 		Seconds: seconds,
@@ -394,6 +525,7 @@ func linkPermissions(perms ...string) *link.PublicSharePermissions {
 		Permissions: permissions(perms...),
 	}
 }
+
 func permissions(permissions ...string) *provider.ResourcePermissions {
 	perms := &provider.ResourcePermissions{}