Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] GDPR - Right to access and Data Portability #9906

Merged
merged 26 commits into from Apr 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4726593
Implemented basic JSON generation
Hudell Feb 20, 2018
0945340
Added new data to message json
Hudell Feb 21, 2018
6759890
Merge branch 'develop' into right-to-access
Hudell Feb 21, 2018
5ad3755
Changed export operation structure
Hudell Feb 21, 2018
e3c8f0a
Changed code to use forEach instead of fetch
Hudell Feb 21, 2018
cdec24a
Split the operation into two independent commands
Hudell Feb 21, 2018
b41d67c
File download, zip generation, admin settings
Hudell Feb 23, 2018
3c45bad
Merge branch 'develop' into right-to-access
Hudell Feb 23, 2018
0b73219
Merge branch 'develop' into right-to-access
Hudell Feb 26, 2018
41fa0db
Use syncedcron to process data downloads
Hudell Feb 26, 2018
e8e006e
Added download URL
Hudell Feb 26, 2018
a726286
Sending emails when the download file is ready
Hudell Feb 26, 2018
500e00e
Merge branch 'develop' into right-to-access
Hudell Feb 26, 2018
2a04ef7
Merge branch 'develop' into right-to-access
Hudell Feb 27, 2018
649f1ac
Allow usage of GridFS as storage for the finished file.
Hudell Feb 27, 2018
8122ab1
Merge branch 'develop' into right-to-access
Hudell Feb 27, 2018
b013ff1
Lint
Hudell Feb 27, 2018
3580f6c
Merge branch 'develop' into right-to-access
Hudell Feb 28, 2018
a606f73
Added support for Google and Amazon as storage types
Hudell Feb 28, 2018
4f10a4f
Split the options to download and export data
Hudell Mar 5, 2018
3883fcf
Merge branch 'develop' into right-to-access
Hudell Mar 5, 2018
3b76b64
Merge branch 'develop' into data-portability
Hudell Mar 5, 2018
dd2cccc
Merge branch 'data-portability' into right-to-access
Hudell Mar 5, 2018
79743a8
Removed commented code
Hudell Mar 5, 2018
a1086d7
Merge branch 'develop' into right-to-access
Hudell Apr 17, 2018
30fca01
Merge branch 'develop' into right-to-access
Hudell Apr 18, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .meteor/packages
Expand Up @@ -146,6 +146,7 @@ rocketchat:ui-master
rocketchat:ui-message
rocketchat:ui-sidenav
rocketchat:ui-vrecord
rocketchat:user-data-download
rocketchat:version
rocketchat:videobridge
rocketchat:webrtc
Expand Down
1 change: 1 addition & 0 deletions .meteor/versions
Expand Up @@ -235,6 +235,7 @@ rocketchat:ui-master@0.1.0
rocketchat:ui-message@0.1.0
rocketchat:ui-sidenav@0.1.0
rocketchat:ui-vrecord@0.0.1
rocketchat:user-data-download@1.0.0
rocketchat:version@1.0.0
rocketchat:version-check@0.0.1
rocketchat:videobridge@0.2.0
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -113,6 +113,7 @@
"@google-cloud/storage": "^1.6.0",
"@google-cloud/vision": "^0.15.2",
"adm-zip": "^0.4.7",
"archiver": "^2.1.1",
"atlassian-crowd": "^0.5.0",
"autolinker": "^1.6.2",
"aws-sdk": "^2.199.0",
Expand Down
Expand Up @@ -17,7 +17,6 @@ new UploadFS.Store({
})
});


fileUploadHandler = (directive, meta, file) => {
const store = UploadFS.getStore(directive);

Expand Down
16 changes: 15 additions & 1 deletion packages/rocketchat-file-upload/lib/FileUploadBase.js
Expand Up @@ -4,7 +4,21 @@ import _ from 'underscore';

UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({
insert(userId, doc) {
return userId || (doc && doc.message_id && doc.message_id.indexOf('slack-') === 0); // allow inserts from slackbridge (message_id = slack-timestamp-milli)
if (userId) {
return true;
}

// allow inserts from slackbridge (message_id = slack-timestamp-milli)
if (doc && doc.message_id && doc.message_id.indexOf('slack-') === 0) {
return true;
}

// allow inserts to the UserDataFiles store
if (doc && doc.store && doc.store.split(':').pop() === 'UserDataFiles') {
return true;
}

return false;
},
update(userId, doc) {
return RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', doc.rid) || (RocketChat.settings.get('Message_AllowDeleting') && userId === doc.userId);
Expand Down
25 changes: 23 additions & 2 deletions packages/rocketchat-file-upload/server/config/AmazonS3.js
Expand Up @@ -25,15 +25,35 @@ const get = function(file, req, res) {
}
};

const copy = function(file, out) {
const fileUrl = this.store.getRedirectURL(file);

if (fileUrl) {
const request = /^https:/.test(fileUrl) ? https : http;
request.get(fileUrl, fileRes => fileRes.pipe(out));
} else {
out.end();
}
};

const AmazonS3Uploads = new FileUploadClass({
name: 'AmazonS3:Uploads',
get
get,
copy
// store setted bellow
});

const AmazonS3Avatars = new FileUploadClass({
name: 'AmazonS3:Avatars',
get
get,
copy
// store setted bellow
});

const AmazonS3UserDataFiles = new FileUploadClass({
name: 'AmazonS3:UserDataFiles',
get,
copy
// store setted bellow
});

Expand Down Expand Up @@ -74,6 +94,7 @@ const configure = _.debounce(function() {

AmazonS3Uploads.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Uploads.name, config);
AmazonS3Avatars.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Avatars.name, config);
AmazonS3UserDataFiles.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3UserDataFiles.name, config);
}, 500);

RocketChat.settings.get(/^FileUpload_S3_/, configure);
42 changes: 42 additions & 0 deletions packages/rocketchat-file-upload/server/config/FileSystem.js
Expand Up @@ -28,6 +28,22 @@ const FileSystemUploads = new FileUploadClass({
res.end();
return;
}
},

copy(file, out) {
const filePath = this.store.getFilePath(file._id, file);
try {
const stat = Meteor.wrapAsync(fs.stat)(filePath);

if (stat && stat.isFile()) {
file = FileUpload.addExtensionTo(file);

this.store.getReadStream(file._id, file).pipe(out);
}
} catch (e) {
out.end();
return;
}
}
});

Expand All @@ -54,6 +70,31 @@ const FileSystemAvatars = new FileUploadClass({
}
});

const FileSystemUserDataFiles = new FileUploadClass({
name: 'FileSystem:UserDataFiles',

get(file, req, res) {
const filePath = this.store.getFilePath(file._id, file);

try {
const stat = Meteor.wrapAsync(fs.stat)(filePath);

if (stat && stat.isFile()) {
file = FileUpload.addExtensionTo(file);
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`);
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);

this.store.getReadStream(file._id, file).pipe(res);
}
} catch (e) {
res.writeHead(404);
res.end();
return;
}
}
});

const createFileSystemStore = _.debounce(function() {
const options = {
Expand All @@ -62,6 +103,7 @@ const createFileSystemStore = _.debounce(function() {

FileSystemUploads.store = FileUpload.configureUploadsStore('Local', FileSystemUploads.name, options);
FileSystemAvatars.store = FileUpload.configureUploadsStore('Local', FileSystemAvatars.name, options);
FileSystemUserDataFiles.store = FileUpload.configureUploadsStore('Local', FileSystemUserDataFiles.name, options);

// DEPRECATED backwards compatibililty (remove)
UploadFS.getStores()['fileSystem'] = UploadFS.getStores()[FileSystemUploads.name];
Expand Down
29 changes: 27 additions & 2 deletions packages/rocketchat-file-upload/server/config/GoogleStorage.js
Expand Up @@ -29,15 +29,39 @@ const get = function(file, req, res) {
});
};

const copy = function(file, out) {
this.store.getRedirectURL(file, (err, fileUrl) => {
if (err) {
console.error(err);
}

if (fileUrl) {
const request = /^https:/.test(fileUrl) ? https : http;
request.get(fileUrl, fileRes => fileRes.pipe(out));
} else {
out.end();
}
});
};

const GoogleCloudStorageUploads = new FileUploadClass({
name: 'GoogleCloudStorage:Uploads',
get
get,
copy
// store setted bellow
});

const GoogleCloudStorageAvatars = new FileUploadClass({
name: 'GoogleCloudStorage:Avatars',
get
get,
copy
// store setted bellow
});

const GoogleCloudStorageUserDataFiles = new FileUploadClass({
name: 'GoogleCloudStorage:UserDataFiles',
get,
copy
// store setted bellow
});

Expand All @@ -64,6 +88,7 @@ const configure = _.debounce(function() {

GoogleCloudStorageUploads.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageUploads.name, config);
GoogleCloudStorageAvatars.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageAvatars.name, config);
GoogleCloudStorageUserDataFiles.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageUserDataFiles.name, config);
}, 500);

RocketChat.settings.get(/^FileUpload_GoogleStorage_/, configure);
40 changes: 39 additions & 1 deletion packages/rocketchat-file-upload/server/config/GridFS.js
Expand Up @@ -62,7 +62,6 @@ const getByteRange = function(header) {
return null;
};


// code from: https://github.com/jalik/jalik-ufs/blob/master/ufs-server.js#L310
const readFromGridFS = function(storeName, fileId, file, req, res) {
const store = UploadFS.getStore(storeName);
Expand Down Expand Up @@ -123,10 +122,26 @@ const readFromGridFS = function(storeName, fileId, file, req, res) {
}
};

const copyFromGridFS = function(storeName, fileId, file, out) {
const store = UploadFS.getStore(storeName);
const rs = store.getReadStream(fileId, file);

[rs, out].forEach(stream => stream.on('error', function(err) {
store.onReadError.call(store, err, fileId, file);
out.end();
}));

rs.pipe(out);
};

FileUpload.configureUploadsStore('GridFS', 'GridFS:Uploads', {
collectionName: 'rocketchat_uploads'
});

FileUpload.configureUploadsStore('GridFS', 'GridFS:UserDataFiles', {
collectionName: 'rocketchat_userDataFiles'
});

// DEPRECATED: backwards compatibility (remove)
UploadFS.getStores()['rocketchat_uploads'] = UploadFS.getStores()['GridFS:Uploads'];

Expand All @@ -147,6 +162,29 @@ new FileUploadClass({
res.setHeader('Content-Length', file.size);

return readFromGridFS(file.store, file._id, file, req, res);
},

copy(file, out) {
copyFromGridFS(file.store, file._id, file, out);
}
});

new FileUploadClass({
name: 'GridFS:UserDataFiles',

get(file, req, res) {
file = FileUpload.addExtensionTo(file);

res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`);
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);

return readFromGridFS(file.store, file._id, file, req, res);
},

copy(file, out) {
copyFromGridFS(file.store, file._id, file, out);
}
});

Expand Down
Expand Up @@ -14,6 +14,7 @@ const configStore = _.debounce(() => {
console.log('Setting default file store to', store);
UploadFS.getStores().Avatars = UploadFS.getStore(`${ store }:Avatars`);
UploadFS.getStores().Uploads = UploadFS.getStore(`${ store }:Uploads`);
UploadFS.getStores().UserDataFiles = UploadFS.getStore(`${ store }:UserDataFiles`);
}
}, 1000);

Expand Down
37 changes: 35 additions & 2 deletions packages/rocketchat-file-upload/server/lib/FileUpload.js
Expand Up @@ -58,6 +58,25 @@ Object.assign(FileUpload, {
};
},

defaultUserDataFiles() {
return {
collection: RocketChat.models.UserDataFiles.model,
getPath(file) {
return `${ RocketChat.settings.get('uniqueID') }/uploads/userData/${ file.userId }`;
},
onValidate: FileUpload.uploadsOnValidate,
onRead(fileId, file, req, res) {
if (!FileUpload.requestCanAccessFiles(req)) {
res.writeHead(403);
return false;
}

res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
return true;
}
};
},

avatarsOnValidate(file) {
if (RocketChat.settings.get('Accounts_AvatarResize') !== true) {
return;
Expand Down Expand Up @@ -229,16 +248,30 @@ Object.assign(FileUpload, {
}
res.writeHead(404);
res.end();
},

copy(file, targetFile) {
const store = this.getStoreByName(file.store);
const out = fs.createWriteStream(targetFile);

file = FileUpload.addExtensionTo(file);

if (store.copy) {
store.copy(file, out);
return true;
}

return false;
}
});


export class FileUploadClass {
constructor({ name, model, store, get, insert, getStore }) {
constructor({ name, model, store, get, insert, getStore, copy }) {
this.name = name;
this.model = model || this.getModelFromName();
this._store = store || UploadFS.getStore(name);
this.get = get;
this.copy = copy;

if (insert) {
this.insert = insert;
Expand Down
14 changes: 14 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Expand Up @@ -567,6 +567,7 @@
"Domain_removed": "Domain Removed",
"Domains": "Domains",
"Domains_allowed_to_embed_the_livechat_widget": "Comma-separated list of domains allowed to embed the livechat widget. Leave blank to allow all domains.",
"Download_My_Data" : "Download My Data",
"Download_Snippet": "Download",
"Drop_to_upload_file": "Drop to upload file",
"Dry_run": "Dry run",
Expand Down Expand Up @@ -724,6 +725,7 @@
"Example_s": "Example: <code class=\"inline\">%s</code>",
"Exclude_Botnames": "Exclude Bots",
"Exclude_Botnames_Description": "Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.",
"Export_My_Data" : "Export My Data",
"External_Service": "External Service",
"External_Queue_Service_URL": "External Queue Service URL",
"Facebook_Page": "Facebook Page",
Expand Down Expand Up @@ -2106,6 +2108,18 @@
"User_uploaded_file": "Uploaded a file",
"User_uploaded_image": "Uploaded an image",
"User_Presence": "User Presence",
"UserDataDownload" : "User Data Download",
"UserData_EnableDownload" : "Enable User Data Download",
"UserData_FileSystemPath" : "System Path (Exported Files)",
"UserData_FileSystemZipPath" : "System Path (Compressed File)",
"UserData_ProcessingFrequency" : "Processing Frequency (Minutes)",
"UserData_MessageLimitPerRequest" : "Message Limit per Request",
"UserDataDownload_EmailSubject" : "Your Data File is Ready to Download",
"UserDataDownload_EmailBody" : "Your data file is now ready to download. Click <a href=\"__download_link__\">here</a> to download it.",
"UserDataDownload_Requested" : "Download File Requested",
"UserDataDownload_Requested_Text" : "Your data file will be generated. A link to download it will be sent to your email address when ready.",
"UserDataDownload_RequestExisted_Text" : "Your data file is already being generated. A link to download it will be sent to your email address when ready.",
"UserDataDownload_CompletedRequestExisted_Text" : "Your data file was already generated. Check your email account for the download link.",
"Username": "Username",
"Username_and_message_must_not_be_empty": "Username and message must not be empty.",
"Username_cant_be_empty": "The username cannot be empty",
Expand Down
6 changes: 6 additions & 0 deletions packages/rocketchat-lib/client/models/UserDataFiles.js
@@ -0,0 +1,6 @@
RocketChat.models.UserDataFiles = new class extends RocketChat.models._Base {
constructor() {
super();
this._initModel('userDataFiles');
}
};
2 changes: 2 additions & 0 deletions packages/rocketchat-lib/package.js
Expand Up @@ -126,6 +126,8 @@ Package.onUse(function(api) {
api.addFiles('server/models/Subscriptions.js', 'server');
api.addFiles('server/models/Uploads.js', 'server');
api.addFiles('server/models/Users.js', 'server');
api.addFiles('server/models/ExportOperations.js', 'server');
api.addFiles('server/models/UserDataFiles.js', 'server');

api.addFiles('server/oauth/oauth.js', 'server');
api.addFiles('server/oauth/facebook.js', 'server');
Expand Down