refactor: remove privileges in favor of SNJS protections

This commit is contained in:
Baptiste Grob
2021-01-18 13:40:10 +01:00
parent 75f02b4a05
commit 42a7cb418c
12 changed files with 30 additions and 434 deletions

View File

@@ -44,8 +44,6 @@ import {
PanelResizer,
PasswordWizard,
PermissionsModal,
PrivilegesAuthModal,
PrivilegesManagementModal,
RevisionPreviewModal,
HistoryMenu,
SyncResolutionMenu,
@@ -140,11 +138,6 @@ const startApplication: StartApplication = async function startApplication(
.directive('panelResizer', () => new PanelResizer())
.directive('passwordWizard', () => new PasswordWizard())
.directive('permissionsModal', () => new PermissionsModal())
.directive('privilegesAuthModal', () => new PrivilegesAuthModal())
.directive(
'privilegesManagementModal',
() => new PrivilegesManagementModal()
)
.directive('revisionPreviewModal', () => new RevisionPreviewModal())
.directive('historyMenu', () => new HistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu())

View File

@@ -8,8 +8,6 @@ export { MenuRow } from './menuRow';
export { PanelResizer } from './panelResizer';
export { PasswordWizard } from './passwordWizard';
export { PermissionsModal } from './permissionsModal';
export { PrivilegesAuthModal } from './privilegesAuthModal';
export { PrivilegesManagementModal } from './privilegesManagementModal';
export { RevisionPreviewModal } from './revisionPreviewModal';
export { HistoryMenu } from './historyMenu';
export { SyncResolutionMenu } from './syncResolutionMenu';

View File

@@ -1,128 +0,0 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import { ProtectedAction, PrivilegeCredential, PrivilegeSessionLength } from '@standardnotes/snjs';
import template from '%/directives/privileges-auth-modal.pug';
type PrivilegesAuthModalScope = {
application: WebApplication
action: ProtectedAction
onSuccess: () => void
onCancel: () => void
}
class PrivilegesAuthModalCtrl implements PrivilegesAuthModalScope {
$element: JQLite
$timeout: ng.ITimeoutService
application!: WebApplication
action!: ProtectedAction
onSuccess!: () => void
onCancel!: () => void
authParameters: Partial<Record<PrivilegeCredential, string>> = {}
sessionLengthOptions!: { value: PrivilegeSessionLength, label: string }[]
selectedSessionLength!: PrivilegeSessionLength
requiredCredentials!: PrivilegeCredential[]
failedCredentials!: PrivilegeCredential[]
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService
) {
this.$element = $element;
this.$timeout = $timeout;
}
$onInit() {
this.sessionLengthOptions = this.application!.privilegesService!
.getSessionLengthOptions();
this.application.privilegesService!.getSelectedSessionLength()
.then((length) => {
this.$timeout(() => {
this.selectedSessionLength = length;
});
});
this.application.privilegesService!.netCredentialsForAction(this.action)
.then((credentials) => {
this.$timeout(() => {
this.requiredCredentials = credentials.sort();
});
});
}
selectSessionLength(length: PrivilegeSessionLength) {
this.selectedSessionLength = length;
}
promptForCredential(credential: PrivilegeCredential) {
return this.application.privilegesService!.displayInfoForCredential(credential).prompt;
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
isCredentialInFailureState(credential: PrivilegeCredential) {
if (!this.failedCredentials) {
return false;
}
return this.failedCredentials.find((candidate) => {
return candidate === credential;
}) != null;
}
validate() {
const failed = [];
for (const cred of this.requiredCredentials) {
const value = this.authParameters[cred];
if (!value || value.length === 0) {
failed.push(cred);
}
}
this.failedCredentials = failed;
return failed.length === 0;
}
async submit() {
if (!this.validate()) {
return;
}
const result = await this.application.privilegesService!.authenticateAction(
this.action,
this.authParameters
);
this.$timeout(() => {
if (result.success) {
this.application.privilegesService!.setSessionLength(this.selectedSessionLength);
this.onSuccess();
this.dismiss();
} else {
this.failedCredentials = result.failedCredentials;
}
});
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class PrivilegesAuthModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesAuthModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
action: '=',
onSuccess: '=',
onCancel: '=',
application: '='
};
}
}

View File

@@ -1,118 +0,0 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/privileges-management-modal.pug';
import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { PrivilegeMutator } from '@standardnotes/snjs';
type DisplayInfo = {
label: string
prompt: string
}
class PrivilegesManagementModalCtrl extends PureViewCtrl {
hasPasscode = false
hasAccount = false
$element: JQLite
application!: WebApplication
privileges!: SNPrivileges
availableActions!: ProtectedAction[]
availableCredentials!: PrivilegeCredential[]
sessionExpirey!: string
sessionExpired = true
credentialDisplayInfo: Partial<Record<PrivilegeCredential, DisplayInfo>> = {}
onCancel!: () => void
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
$element: JQLite
) {
super($timeout);
this.$element = $element;
}
async onAppLaunch() {
super.onAppLaunch();
this.hasPasscode = this.application.hasPasscode();
this.hasAccount = !this.application.noAccount();
this.reloadPrivileges();
}
displayInfoForCredential(credential: PrivilegeCredential) {
const info: any = this.application.privilegesService!.displayInfoForCredential(credential);
if (credential === PrivilegeCredential.LocalPasscode) {
info.availability = this.hasPasscode;
} else if (credential === PrivilegeCredential.AccountPassword) {
info.availability = this.hasAccount;
} else {
info.availability = true;
}
return info;
}
displayInfoForAction(action: ProtectedAction) {
return this.application.privilegesService!.displayInfoForAction(action).label;
}
isCredentialRequiredForAction(action: ProtectedAction, credential: PrivilegeCredential) {
if (!this.privileges) {
return false;
}
return this.privileges.isCredentialRequiredForAction(action, credential);
}
async clearSession() {
await this.application.privilegesService!.clearSession();
this.reloadPrivileges();
}
async reloadPrivileges() {
this.availableActions = this.application.privilegesService!.getAvailableActions();
this.availableCredentials = this.application.privilegesService!.getAvailableCredentials();
const sessionEndDate = await this.application.privilegesService!.getSessionExpirey();
this.sessionExpirey = sessionEndDate.toLocaleString();
this.sessionExpired = new Date() >= sessionEndDate;
for (const cred of this.availableCredentials) {
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
}
const privs = await this.application.privilegesService!.getPrivileges();
this.$timeout(() => {
this.privileges = privs;
});
}
checkboxValueChanged(action: ProtectedAction, credential: PrivilegeCredential) {
this.application.changeAndSaveItem(this.privileges.uuid, (m) => {
const mutator = m as PrivilegeMutator;
mutator.toggleCredentialForAction(action, credential);
})
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class PrivilegesManagementModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesManagementModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
application: '='
};
}
}

View File

@@ -5,6 +5,7 @@ import {
RemoteSession,
SessionStrings,
UuidString,
isNullOrUndefined,
} from '@standardnotes/snjs';
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
import { render, FunctionComponent } from 'preact';
@@ -78,7 +79,9 @@ function useSessions(
setSessions(sessionsDuringRevoke);
const response = await responsePromise;
if ('error' in response) {
if (isNullOrUndefined(response)) {
setSessions(sessionsBeforeRevoke);
} else if ('error' in response) {
if (response.error?.message) {
setErrorMessage(response.error?.message);
} else {

View File

@@ -1,5 +1,10 @@
import { WebApplication } from '@/ui_models/application';
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote, BackupFile } from '@standardnotes/snjs';
import {
EncryptionIntent,
ContentType,
SNNote,
BackupFile
} from '@standardnotes/snjs';
function zippableTxtName(name: string, suffix = ""): string {
const sanitizedName = name
@@ -22,41 +27,26 @@ export class ArchiveManager {
}
public async downloadBackup(encrypted: boolean) {
const run = async () => {
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
const data = await this.application.createBackupFile(intent);
if (!data) {
return;
}
const blobData = new Blob(
[JSON.stringify(data, null, 2)],
{ type: 'text/json' }
const data = await this.application.createBackupFile(intent);
if (!data) {
return;
}
const blobData = new Blob(
[JSON.stringify(data, null, 2)],
{ type: 'text/json' }
);
if (encrypted) {
this.downloadData(
blobData,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`
);
if (encrypted) {
this.downloadData(
blobData,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`
);
} else {
/** download as zipped plain text files */
this.downloadZippedDecryptedItems(data);
}
};
if (
await this.application.privilegesService!
.actionRequiresPrivilege(ProtectedAction.ManageBackups)
) {
this.application.presentPrivilegesModal(
ProtectedAction.ManageBackups,
() => {
run();
});
} else {
run();
/** download as zipped plain text files */
this.downloadZippedDecryptedItems(data);
}
}

View File

@@ -217,7 +217,7 @@ export class AppState {
}
}
async openEditor(noteUuid: string) {
async openEditor(noteUuid: string): Promise<void> {
if (this.getActiveEditor()?.note?.uuid === noteUuid) {
return;
}
@@ -226,7 +226,7 @@ export class AppState {
if (!note) {
console.warn('Tried accessing a non-existant note of UUID ' + noteUuid);
return;
};
}
if (await this.application.authorizeNoteAccess(note)) {
const activeEditor = this.getActiveEditor();

View File

@@ -80,8 +80,7 @@
)
menu-row(
action='self.selectedMenuItem(true); self.toggleProtectNote()'
desc=`'Protecting a note will require credentials to view
it (Manage Privileges via Account menu)'`,
desc=`'Protecting a note will require credentials to view it'`,
label="self.note.protected ? 'Unprotect' : 'Protect'"
)
menu-row(

View File

@@ -42,55 +42,6 @@
}
}
#privileges-modal {
min-width: 400px;
max-width: 700px;
.sk-panel-header {
position: relative;
}
.close-button {
cursor: pointer;
position: absolute;
padding: 1.1rem 2rem;
right: 0;
}
table {
margin-bottom: 12px;
width: 100%;
overflow: auto;
border-collapse: collapse;
border-spacing: 0px;
border-color: var(--sn-stylekit-contrast-border-color);
background-color: var(--sn-stylekit-background-color);
color: var(--sn-stylekit-contrast-foreground-color);
th,
td {
padding: 6px 13px;
border: 1px solid var(--sn-stylekit-contrast-border-color);
}
tr:nth-child(2n) {
background-color: var(--sn-stylekit-contrast-background-color);
}
}
th {
text-align: center;
font-weight: normal;
}
.priv-header {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
#item-preview-modal {
> .sk-modal-content {
width: 800px;

View File

@@ -222,10 +222,6 @@
| {{option.label}}
.sk-p The autolock timer begins when the window or tab loses focus.
.sk-panel-row
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPrivilegesModal('')",
ng-show='!self.state.user'
) Manage Privileges
a.sk-a.info.sk-panel-row.condensed(
ng-click='self.changePasscodePressed()'
) Change Passcode

View File

@@ -1,37 +0,0 @@
.sk-modal-background(ng-click="ctrl.cancel()")
#privileges-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Authentication Required
a.close-button.info(ng-click="ctrl.cancel()") Cancel
.sk-panel-content
.sk-panel-section
div(ng-repeat="credential in ctrl.requiredCredentials")
.sk-p.sk-bold.sk-panel-row
strong {{ctrl.promptForCredential(credential)}}
.sk-panel-row
input.sk-input.contrast(
ng-model="ctrl.authParameters[credential]"
should-focus="$index == 0"
sn-autofocus="true"
sn-enter="ctrl.submit()"
type="password"
)
.sk-panel-row
label.sk-label.danger(
ng-if="ctrl.isCredentialInFailureState(credential)"
) Invalid authentication. Please try again.
.sk-panel-row
.sk-panel-row
.sk-horizontal-group
.sk-p.sk-bold Remember For
a.sk-a.info(
ng-repeat="option in ctrl.sessionLengthOptions"
ng-class="{'boxed' : option.value == ctrl.selectedSessionLength}"
ng-click="ctrl.selectSessionLength(option.value)"
)
| {{option.label}}
.sk-panel-footer.extra-padding
.sk-button.info.big.block.bold(ng-click="ctrl.submit()")
.sk-label Submit

View File

@@ -1,51 +0,0 @@
.sk-modal-background(ng-click='ctrl.cancel()')
#privileges-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Manage Privileges
a.sk-a.close-button.info(ng-click='ctrl.cancel()') Done
.sk-panel-content
.sk-panel-section
table.sk-table
thead
tr
th
th(ng-repeat='cred in ctrl.availableCredentials')
.priv-header
strong {{ctrl.credentialDisplayInfo[cred].label}}
.sk-p.font-small(
ng-show='!ctrl.credentialDisplayInfo[cred].availability',
style='margin-top: 2px'
) Not Configured
tbody
tr(ng-repeat='action in ctrl.availableActions')
td
.sk-p {{ctrl.displayInfoForAction(action)}}
th(ng-repeat='credential in ctrl.availableCredentials')
input(
ng-checked='ctrl.isCredentialRequiredForAction(action, credential)',
ng-click='ctrl.checkboxValueChanged(action, credential)',
ng-disabled='!ctrl.credentialDisplayInfo[credential].availability',
type='checkbox'
)
.sk-panel-section(ng-if='ctrl.sessionExpirey && !ctrl.sessionExpired')
.sk-p.sk-panel-row
| You will not be asked to authenticate until {{ctrl.sessionExpirey}}.
a.sk-a.sk-panel-row.info(ng-click='ctrl.clearSession()') Clear Session
.sk-panel-footer
.sk-h2.sk-bold About Privileges
.sk-panel-section.no-bottom-pad
.sk-panel-row
.text-content
.sk-p
| Privileges represent interface level authentication for accessing
| certain items and features. Note that when your application is unlocked,
| your data exists in temporary memory in an unencrypted state.
| Privileges are meant to protect against unwanted access in the event of
| an unlocked application, but do not affect data encryption state.
p.sk-p
| Privileges sync across your other devices; however, note that if you
| require an "Application Passcode" privilege, and another device does not have
| an application passcode set up, the application passcode requirement will be ignored
| on that device.