1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-14 02:08:50 +02:00

org export/import

This commit is contained in:
Kyle Spearrin 2017-09-06 09:05:53 -04:00
parent ff9030e7af
commit aaa91e50b7
10 changed files with 345 additions and 3 deletions

View File

@ -28,6 +28,22 @@
}).$promise;
};
$scope.import = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsImport.html',
controller: 'organizationSettingsImportController'
});
};
$scope.export = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsExport.html',
controller: 'organizationSettingsExportController'
});
};
$scope.delete = function () {
$uibModal.open({
animation: true,

View File

@ -0,0 +1,114 @@
angular
.module('bit.organization')
.controller('organizationSettingsExportController', function ($scope, apiService, $uibModalInstance, cipherService,
$q, toastr, $analytics, $state) {
$analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;
var decLogins = [],
decCollections = [];
var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId },
function (collections) {
decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
}).$promise;
var loginsPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
function (ciphers) {
for (var i = 0; i < ciphers.Data.length; i++) {
if (ciphers.Data[i].Type === 1) {
var decLogin = cipherService.decryptLogin(ciphers.Data[i]);
decLogins.push(decLogin);
}
}
}).$promise;
$q.all([collectionsPromise, loginsPromise]).then(function () {
if (!decLogins.length) {
toastr.error('Nothing to export.', 'Error!');
$scope.close();
return;
}
var collectionsDict = {};
for (var i = 0; i < decCollections.length; i++) {
collectionsDict[decCollections[i].id] = decCollections[i];
}
try {
var exportLogins = [];
for (i = 0; i < decLogins.length; i++) {
var login = {
name: decLogins[i].name,
uri: decLogins[i].uri,
username: decLogins[i].username,
password: decLogins[i].password,
notes: decLogins[i].notes,
totp: decLogins[i].totp,
collections: []
};
if (decLogins[i].collectionIds) {
for (var j = 0; j < decLogins[i].collectionIds.length; j++) {
if (collectionsDict.hasOwnProperty(decLogins[i].collectionIds[j])) {
login.collections.push(collectionsDict[decLogins[i].collectionIds[j]].name);
}
}
}
exportLogins.push(login);
}
var csvString = Papa.unparse(exportLogins);
var csvBlob = new Blob([csvString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(csvBlob, makeFileName());
}
else {
var a = window.document.createElement('a');
a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' });
a.download = makeFileName();
document.body.appendChild(a);
// IE: "Access is denied".
// ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access
a.click();
document.body.removeChild(a);
}
$analytics.eventTrack('Exported Organization Data');
toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!');
$scope.close();
}
catch (err) {
toastr.error('Something went wrong. Please try again.', 'Error!');
$scope.close();
}
}, function () {
toastr.error('Something went wrong. Please try again.', 'Error!');
$scope.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
function makeFileName() {
var now = new Date();
var dateString =
now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) +
padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) +
padNumber(now.getSeconds(), 2);
return 'bitwarden_org_export_' + dateString + '.csv';
}
function padNumber(number, width, paddingCharacter) {
paddingCharacter = paddingCharacter || '0';
number = number + '';
return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number;
}
});

View File

@ -0,0 +1,119 @@
angular
.module('bit.organization')
.controller('organizationSettingsImportController', function ($scope, $state, apiService, $uibModalInstance, cipherService,
toastr, importService, $analytics, $sce, validationService, cryptoService) {
$analytics.eventTrack('organizationSettingsImportController', { category: 'Modal' });
$scope.model = { source: '' };
$scope.source = {};
$scope.splitFeatured = false;
$scope.options = [
{
id: 'bitwardencsv',
name: 'bitwarden (csv)',
featured: true,
sort: 1,
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
'Log into the web vault and navigate to your organization\'s admin area. Then to go ' +
'"Settings" > "Tools" > "Export".')
}
];
$scope.setSource = function () {
for (var i = 0; i < $scope.options.length; i++) {
if ($scope.options[i].id === $scope.model.source) {
$scope.source = $scope.options[i];
break;
}
}
};
$scope.setSource();
$scope.import = function (model, form) {
if (!model.source || model.source === '') {
validationService.addError(form, 'source', 'Select the format of the import file.', true);
return;
}
var file = document.getElementById('file').files[0];
if (!file && (!model.fileContents || model.fileContents === '')) {
validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true);
return;
}
$scope.processing = true;
importService.importOrg(model.source, file || model.fileContents, importSuccess, importError);
};
function importSuccess(collections, logins, collectionRelationships) {
if (!collections.length && !logins.length) {
importError('Nothing was imported.');
return;
}
else if (logins.length) {
var halfway = Math.floor(logins.length / 2);
var last = logins.length - 1;
if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) {
importError('CSV data is not formatted correctly. Please check your import file and try again.');
return;
}
}
apiService.ciphers.importOrg({ orgId: $state.params.orgId }, {
collections: cipherService.encryptCollections(collections, $state.params.orgId),
logins: cipherService.encryptLogins(logins, cryptoService.getOrgKey($state.params.orgId)),
collectionRelationships: collectionRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
$state.go('backend.user.vault', { refreshFromServer: true }).then(function () {
$analytics.eventTrack('Imported Org Data', { label: $scope.model.source });
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
});
}, importError);
}
function loginIsBadData(login) {
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
}
function importError(error) {
$analytics.eventTrack('Import Org Data Failed', { label: $scope.model.source });
$uibModalInstance.dismiss('cancel');
if (error) {
var data = error.data;
if (data && data.ValidationErrors) {
var message = '';
for (var key in data.ValidationErrors) {
if (!data.ValidationErrors.hasOwnProperty(key)) {
continue;
}
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
message += (key + ': ' + data.ValidationErrors[key][i] + ' ');
}
}
if (message !== '') {
toastr.error(message);
return;
}
}
else if (data && data.Message) {
toastr.error(data.Message);
return;
}
else {
toastr.error(error);
return;
}
}
toastr.error('Something went wrong. Try again.', 'Oh No!');
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@ -49,6 +49,21 @@
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Import/Export</h3>
</div>
<div class="box-body">
<p>
Quickly import logins, collections, and other data. You can also export all of your organization's
vault data in <code>.csv</code> format.
</p>
</div>
<div class="box-footer">
<button class="btn btn-default btn-flat" type="button" ng-click="import()">Import Data</button>
<button class="btn btn-default btn-flat" type="button" ng-click="export()">Export Data</button>
</div>
</div>
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Danger Zone</h3>

View File

@ -32,6 +32,7 @@
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} },
importOrg: { url: _apiUri + '/ciphers/import-organization?organizationId=:orgId', method: 'POST', params: { orgId: '@orgId' } },
favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } },
putPartial: { url: _apiUri + '/ciphers/:id/partial', method: 'POST', params: { id: '@id' } },
putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } },

View File

@ -109,6 +109,22 @@
}
};
_service.importOrg = function (source, file, success, error) {
if (!file) {
error();
return;
}
switch (source) {
case 'bitwardencsv':
importBitwardenOrgCsv(file, success, error);
break;
default:
error();
break;
}
};
var _passwordFieldNames = [
'password', 'pass word', 'passphrase', 'pass phrase',
'pass', 'code', 'code word', 'codeword',
@ -272,6 +288,64 @@
});
}
function importBitwardenOrgCsv(file, success, error) {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var collections = [],
logins = [],
collectionRelationships = [];
angular.forEach(results.data, function (value, key) {
var loginIndex = logins.length;
if (value.collections && value.collections !== '') {
var loginCollections = value.collections.split(',');
for (var i = 0; i < loginCollections.length; i++) {
var addCollection = true;
var collectionIndex = collections.length;
for (var j = 0; j < collections.length; j++) {
if (collections[j].name === loginCollections[i]) {
addCollection = false;
collectionIndex = j;
break;
}
}
if (addCollection) {
collections.push({
name: loginCollections[i]
});
}
collectionRelationships.push({
key: loginIndex,
value: collectionIndex
});
}
}
logins.push({
favorite: false,
uri: value.uri && value.uri !== '' ? trimUri(value.uri) : null,
username: value.username && value.username !== '' ? value.username : null,
password: value.password && value.password !== '' ? value.password : null,
notes: value.notes && value.notes !== '' ? value.notes : null,
name: value.name && value.name !== '' ? value.name : '--',
totp: value.totp && value.totp !== '' ? value.totp : null
});
});
success(collections, logins, collectionRelationships);
}
});
}
function importLastPass(file, success, error) {
if (typeof file !== 'string' && file.type && file.type === 'text/html') {
var reader = new FileReader();

View File

@ -1,8 +1,8 @@
angular
.module('bit.tools')
.controller('toolsExportController', function ($scope, apiService, authService, $uibModalInstance, cryptoService,
cipherService, $q, toastr, $analytics) {
.controller('toolsExportController', function ($scope, apiService, $uibModalInstance, cipherService, $q,
toastr, $analytics) {
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;

View File

@ -6,6 +6,7 @@
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
$scope.model = { source: '' };
$scope.source = {};
$scope.splitFeatured = true;
$scope.options = [
{

View File

@ -16,7 +16,7 @@
<option value="">-- Select --</option>
<option ng-repeat="option in options | filter: { featured: true } | orderBy: ['sort', 'name']"
value="{{option.id}}">{{option.name}}</option>
<option value="-" disabled></option>
<option value="-" disabled ng-if="splitFeatured"></option>
<option ng-repeat="option in options | filter: { featured: '!true' } | orderBy: ['sort', 'name']"
value="{{option.id}}">{{option.name}}</option>
</select>

View File

@ -231,6 +231,8 @@
<script src="app/organization/organizationCollectionsEditController.js"></script>
<script src="app/organization/organizationCollectionsUsersController.js"></script>
<script src="app/organization/organizationSettingsController.js"></script>
<script src="app/organization/organizationSettingsImportController.js"></script>
<script src="app/organization/organizationSettingsExportController.js"></script>
<script src="app/organization/organizationBillingController.js"></script>
<script src="app/organization/organizationBillingChangePaymentController.js"></script>
<script src="app/organization/organizationBillingAdjustSeatsController.js"></script>