feat: Handle privilege escalation gracefully even without PolicyKit (#599)

* feat: Handle privilege escalation gracefully even without PolicyKit

* build: upgrade tslint => 5.9.1

* build: add uhk-agent/package-lock.json

* feat: add error animation

* fix: display agent icon when user use ALT + TAB
This commit is contained in:
Róbert Kiss
2018-04-07 23:09:47 +02:00
committed by László Monda
parent 6e1f0ded9e
commit 6ccf005750
22 changed files with 1680 additions and 85 deletions

81
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "uhk-agent", "name": "uhk-agent",
"version": "1.1.2", "version": "1.1.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -1414,9 +1414,9 @@
"dev": true "dev": true
}, },
"commander": { "commander": {
"version": "2.11.0", "version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true "dev": true
}, },
"compare-func": { "compare-func": {
@@ -9428,9 +9428,9 @@
"dev": true "dev": true
}, },
"resolve": { "resolve": {
"version": "1.4.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz",
"integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==",
"dev": true, "dev": true,
"requires": { "requires": {
"path-parse": "1.0.5" "path-parse": "1.0.5"
@@ -10843,29 +10843,51 @@
} }
}, },
"tslib": { "tslib": {
"version": "1.7.1", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz",
"integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==",
"dev": true "dev": true
}, },
"tslint": { "tslint": {
"version": "5.5.0", "version": "5.9.1",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.5.0.tgz", "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz",
"integrity": "sha1-EOjas+MGH6YelELozuOYKs8gpqo=", "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=",
"dev": true, "dev": true,
"requires": { "requires": {
"babel-code-frame": "6.26.0", "babel-code-frame": "6.26.0",
"colors": "1.1.2", "builtin-modules": "1.1.1",
"commander": "2.11.0", "chalk": "2.3.2",
"commander": "2.15.1",
"diff": "3.4.0", "diff": "3.4.0",
"glob": "7.1.2", "glob": "7.1.2",
"js-yaml": "3.10.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"resolve": "1.4.0", "resolve": "1.6.0",
"semver": "5.4.1", "semver": "5.4.1",
"tslib": "1.7.1", "tslib": "1.9.0",
"tsutils": "2.12.0" "tsutils": "2.26.1"
}, },
"dependencies": { "dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "1.9.1"
}
},
"chalk": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
"integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
"dev": true,
"requires": {
"ansi-styles": "3.2.1",
"escape-string-regexp": "1.0.5",
"supports-color": "5.3.0"
}
},
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
@@ -10879,16 +10901,31 @@
"once": "1.4.0", "once": "1.4.0",
"path-is-absolute": "1.0.1" "path-is-absolute": "1.0.1"
} }
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"supports-color": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
"integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
"dev": true,
"requires": {
"has-flag": "3.0.0"
}
} }
} }
}, },
"tsutils": { "tsutils": {
"version": "2.12.0", "version": "2.26.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.12.0.tgz", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.26.1.tgz",
"integrity": "sha1-yJKoTI8vjeE/jvMsLFw4tFfM6sY=", "integrity": "sha512-bnm9bcjOqOr1UljleL94wVCDlpa6KjfGaTkefeLch4GRafgDkROxPizbB/FxTEdI++5JqhxczRy/Qub0syNqZA==",
"dev": true, "dev": true,
"requires": { "requires": {
"tslib": "1.7.1" "tslib": "1.9.0"
} }
}, },
"tty-browserify": { "tty-browserify": {

View File

@@ -60,7 +60,7 @@
"svg-sprite": "1.3.7", "svg-sprite": "1.3.7",
"ts-loader": "2.3.1", "ts-loader": "2.3.1",
"ts-node": "3.0.4", "ts-node": "3.0.4",
"tslint": "5.5.0", "tslint": "5.9.1",
"typescript": "2.5.2", "typescript": "2.5.2",
"webpack": "2.4.1" "webpack": "2.4.1"
}, },
@@ -76,11 +76,11 @@
"test:uhk-web": "lerna exec --scope uhk-web npm test", "test:uhk-web": "lerna exec --scope uhk-web npm test",
"lint": "run-s -scn lint:ts lint:style", "lint": "run-s -scn lint:ts lint:style",
"lint:ts": "run-p -sn lint:ts:electron-main lint:ts:electron-renderer lint:ts:web lint:ts:test-serializer lint:ts:uhk-usb", "lint:ts": "run-p -sn lint:ts:electron-main lint:ts:electron-renderer lint:ts:web lint:ts:test-serializer lint:ts:uhk-usb",
"lint:ts:electron-main": "tslint --type-check --project ./packages/uhk-agent/tsconfig.json", "lint:ts:electron-main": "tslint --project ./packages/uhk-agent/tsconfig.json",
"lint:ts:electron-renderer": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.renderer.json", "lint:ts:electron-renderer": "tslint --project ./packages/uhk-web/src/tsconfig.renderer.json",
"lint:ts:web": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.app.json", "lint:ts:web": "tslint --project ./packages/uhk-web/src/tsconfig.app.json",
"lint:ts:test-serializer": "tslint --type-check --project ./packages/test-serializer/tsconfig.json", "lint:ts:test-serializer": "tslint --project ./packages/test-serializer/tsconfig.json",
"lint:ts:uhk-usb": "tslint --type-check --project ./packages/uhk-usb/tsconfig.json", "lint:ts:uhk-usb": "tslint --project ./packages/uhk-usb/tsconfig.json",
"lint:style": "stylelint \"packages/uhk-agent/src/**/*.scss\" \"packages/uhk-web/src/**/*.scss\" --syntax scss", "lint:style": "stylelint \"packages/uhk-agent/src/**/*.scss\" \"packages/uhk-web/src/**/*.scss\" --syntax scss",
"build": "run-s build:common build:usb build:web build:electron", "build": "run-s build:common build:usb build:web build:electron",
"build:web": "lerna exec --scope uhk-web npm run build", "build:web": "lerna exec --scope uhk-web npm run build",
@@ -92,6 +92,7 @@
"server:web": "lerna exec --scope uhk-web npm start", "server:web": "lerna exec --scope uhk-web npm start",
"server:electron": "lerna exec --scope uhk-web npm run server:renderer", "server:electron": "lerna exec --scope uhk-web npm run server:renderer",
"electron": "lerna exec --scope uhk-agent npm start", "electron": "lerna exec --scope uhk-agent npm start",
"electron:spe": "lerna exec --scope uhk-agent npm run electron:spe",
"standard-version": "standard-version", "standard-version": "standard-version",
"pack": "node ./scripts/release.js", "pack": "node ./scripts/release.js",
"sprites": "node ./scripts/generate-svg-sprites", "sprites": "node ./scripts/generate-svg-sprites",

1309
packages/uhk-agent/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@
}, },
"scripts": { "scripts": {
"start": "electron ./dist/electron-main.js", "start": "electron ./dist/electron-main.js",
"electron:spe": "electron ./dist/electron-main.js --spe",
"build": "webpack && npm run install:build-deps && npm run build:usb && npm run download-firmware && npm run copy-blhost", "build": "webpack && npm run install:build-deps && npm run build:usb && npm run download-firmware && npm run copy-blhost",
"build:usb": "electron-rebuild -w node-hid -p -m ./dist", "build:usb": "electron-rebuild -w node-hid -p -m ./dist",
"install:build-deps": "cd ./dist && npm i", "install:build-deps": "cd ./dist && npm i",

View File

@@ -2,7 +2,7 @@
/// <reference path="./custom_types/command-line-args.d.ts"/> /// <reference path="./custom_types/command-line-args.d.ts"/>
import './polyfills'; import './polyfills';
import { app, BrowserWindow, ipcMain } from 'electron'; import { app, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import * as path from 'path'; import * as path from 'path';
@@ -10,7 +10,7 @@ import * as url from 'url';
import * as commandLineArgs from 'command-line-args'; import * as commandLineArgs from 'command-line-args';
import { UhkHidDevice, UhkOperations } from 'uhk-usb'; import { UhkHidDevice, UhkOperations } from 'uhk-usb';
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service'; // import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { LogRegExps } from 'uhk-common'; import { CommandLineArgs, LogRegExps } from 'uhk-common';
import { DeviceService } from './services/device.service'; import { DeviceService } from './services/device.service';
import { logger } from './services/logger.service'; import { logger } from './services/logger.service';
import { AppUpdateService } from './services/app-update.service'; import { AppUpdateService } from './services/app-update.service';
@@ -18,13 +18,13 @@ import { AppService } from './services/app.service';
import { SudoService } from './services/sudo.service'; import { SudoService } from './services/sudo.service';
import { UhkBlhost } from '../../uhk-usb/src'; import { UhkBlhost } from '../../uhk-usb/src';
import * as isDev from 'electron-is-dev'; import * as isDev from 'electron-is-dev';
import { CommandLineInputs } from './models/command-line-inputs';
const optionDefinitions = [ const optionDefinitions = [
{name: 'addons', type: Boolean} {name: 'addons', type: Boolean},
{name: 'spe', type: Boolean} // simulate privilege escalation error
]; ];
const options: CommandLineInputs = commandLineArgs(optionDefinitions); const options: CommandLineArgs = commandLineArgs(optionDefinitions);
// import './dev-extension'; // import './dev-extension';
// require('electron-debug')({ showDevTools: true, enabled: true }); // require('electron-debug')({ showDevTools: true, enabled: true });
@@ -83,13 +83,13 @@ function createWindow() {
}); });
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);
win.maximize(); win.maximize();
uhkHidDeviceService = new UhkHidDevice(logger); uhkHidDeviceService = new UhkHidDevice(logger, options);
uhkBlhost = new UhkBlhost(logger, packagesDir); uhkBlhost = new UhkBlhost(logger, packagesDir);
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir); uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations); deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations);
appUpdateService = new AppUpdateService(logger, win, app); appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService); appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
sudoService = new SudoService(logger); sudoService = new SudoService(logger, options);
// and load the index.html of the app. // and load the index.html of the app.
win.loadURL(url.format({ win.loadURL(url.format({

View File

@@ -1,3 +1,10 @@
export interface CommandLineInputs { export interface CommandLineInputs {
/**
* addons menu visible or not
*/
addons?: boolean; addons?: boolean;
/**
* simulate privilege escalation error
*/
spe?: boolean;
} }

View File

@@ -5,12 +5,13 @@ import * as sudo from 'sudo-prompt';
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import { emptyDir, copy } from 'fs-extra'; import { emptyDir, copy } from 'fs-extra';
import { IpcEvents, LogService, IpcResponse } from 'uhk-common'; import { CommandLineArgs, IpcEvents, LogService, IpcResponse } from 'uhk-common';
export class SudoService { export class SudoService {
private rootDir: string; private rootDir: string;
constructor(private logService: LogService) { constructor(private logService: LogService,
private options: CommandLineArgs) {
if (isDev) { if (isDev) {
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../../../'); this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../../../');
} else { } else {
@@ -21,6 +22,19 @@ export class SudoService {
} }
private async setPrivilege(event: Electron.Event) { private async setPrivilege(event: Electron.Event) {
if (this.options.spe) {
const error = new Error('No polkit authentication agent found.');
this.logService.error('[SudoService] Simulate privilege escalation error ', error);
const response = new IpcResponse();
response.success = false;
response.error = {message: error.message};
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
return;
}
switch (process.platform) { switch (process.platform) {
case 'linux': case 'linux':
await this.setPrivilegeOnLinux(event); await this.setPrivilegeOnLinux(event);
@@ -28,7 +42,7 @@ export class SudoService {
default: default:
const response: IpcResponse = { const response: IpcResponse = {
success: false, success: false,
error: { message: 'Permissions couldn\'t be set. Invalid platform: ' + process.platform } error: {message: 'Permissions couldn\'t be set. Invalid platform: ' + process.platform}
}; };
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response); event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
@@ -39,7 +53,7 @@ export class SudoService {
private async setPrivilegeOnLinux(event: Electron.Event) { private async setPrivilegeOnLinux(event: Electron.Event) {
const tmpDirectory = dirSync(); const tmpDirectory = dirSync();
const rulesDir = path.join(this.rootDir, 'rules'); const rulesDir = path.join(this.rootDir, 'rules');
this.logService.debug('[SudoService] Copy rules dir', { src: rulesDir, dst: tmpDirectory.name }); this.logService.debug('[SudoService] Copy rules dir', {src: rulesDir, dst: tmpDirectory.name});
await copy(rulesDir, tmpDirectory.name); await copy(rulesDir, tmpDirectory.name);
const scriptPath = path.join(tmpDirectory.name, 'setup-rules.sh'); const scriptPath = path.join(tmpDirectory.name, 'setup-rules.sh');
@@ -55,7 +69,7 @@ export class SudoService {
if (error) { if (error) {
this.logService.error('[SudoService] Error when set privilege: ', error); this.logService.error('[SudoService] Error when set privilege: ', error);
response.success = false; response.success = false;
response.error = error; response.error = {message: error.message};
} else { } else {
response.success = true; response.success = true;
} }

View File

@@ -1,3 +1,10 @@
export interface CommandLineArgs { export interface CommandLineArgs {
addons: boolean; /**
* addons menu visible or not
*/
addons?: boolean;
/**
* simulate privilege escalation error
*/
spe?: boolean;
} }

View File

@@ -1,5 +1,5 @@
import { Device, devices, HID } from 'node-hid'; import { Device, devices, HID } from 'node-hid';
import { LogService } from 'uhk-common'; import { CommandLineArgs, LogService } from 'uhk-common';
import { import {
ConfigBufferId, ConfigBufferId,
@@ -27,7 +27,8 @@ export class UhkHidDevice {
private _device: HID; private _device: HID;
private _hasPermission = false; private _hasPermission = false;
constructor(private logService: LogService) { constructor(private logService: LogService,
private options: CommandLineArgs) {
} }
/** /**
@@ -38,6 +39,10 @@ export class UhkHidDevice {
* @returns {boolean} * @returns {boolean}
*/ */
public hasPermission(): boolean { public hasPermission(): boolean {
if (this.options.spe) {
return false;
}
try { try {
if (this._hasPermission) { if (this._hasPermission) {
return true; return true;

View File

@@ -6167,6 +6167,19 @@
"deep-freeze-strict": "1.1.1" "deep-freeze-strict": "1.1.1"
} }
}, },
"ngx-clipboard": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/ngx-clipboard/-/ngx-clipboard-8.0.0.tgz",
"integrity": "sha1-EJjC3G/oyAmJo1OUvlFvvwvJR3c=",
"requires": {
"ngx-window-token": "0.0.2"
}
},
"ngx-window-token": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/ngx-window-token/-/ngx-window-token-0.0.2.tgz",
"integrity": "sha1-aA7phrvm+V0H2q3xVMDs815YmSA="
},
"no-case": { "no-case": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",

View File

@@ -72,6 +72,7 @@
"ng2-dragula": "1.5.0", "ng2-dragula": "1.5.0",
"ng2-nouislider": "^1.7.6", "ng2-nouislider": "^1.7.6",
"ng2-select2": "1.0.0-beta.10", "ng2-select2": "1.0.0-beta.10",
"ngx-clipboard": "8.0.0",
"ngrx-store-freeze": "0.1.9", "ngrx-store-freeze": "0.1.9",
"node-hid": "0.5.4", "node-hid": "0.5.4",
"nouislider": "^10.1.0", "nouislider": "^10.1.0",

View File

@@ -1,4 +1,39 @@
<span class="privilege-checker-wrapper"> <div class="privilege-checker-wrapper">
<uhk-message header="Cannot talk to your UHK" subtitle="Your UHK has been detected, but its permissions are not set up yet, so Agent can't talk to it."></uhk-message> <uhk-message header="Cannot talk to your UHK"
<button class="btn btn-default btn-lg btn-primary" (click)="setUpPermissions()"> Set up permissions </button> subtitle="Your UHK has been detected, but its permissions are not set up yet, so Agent can't talk to it."></uhk-message>
</span>
<button class="btn btn-default btn-lg btn-primary"
(click)="setUpPermissions()"> Set up permissions
</button>
<div class="mt-10">
<a class="link-inline"
*ngIf="state.showWhatWillThisDo"
(click)="whatWillThisDo()">What will this do?
</a>
<div>
<p class="privilege-error"
#privilegeError
*ngIf="state.permissionSetupFailed">
Agent wasn't able to set up permissions via PolicyKit. This is most likely because the
<code>polkit</code> package is not installed on your system.
</p>
<div *ngIf="state.showWhatWillThisDoContent">
Agent uses the following script to set up permissions. You can run it manually as root, then
<a class="link-inline"
(click)="retry()">retry</a>.
<div class="copy-container">
<span class="fa fa-2x fa-copy"
ngxClipboard
[cbContent]="command"
title="Copy to clipboard"
data-toggle="tooltip"
data-placement="top"></span>
<pre><code>{{ command }}</code></pre>
</div>
</div>
</div>
</div>
</div>

View File

@@ -9,3 +9,19 @@
uhk-message { uhk-message {
max-width: 50%; max-width: 50%;
} }
.privilege-error {
animation: error-fade-in 2s;
}
@keyframes error-fade-in {
0% {
color: white;
background-color: red;
}
100% {
color: inherit;
background-color: inherit;
}
}

View File

@@ -1,26 +1,61 @@
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import 'rxjs/add/observable/of'; import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
import { AppState } from '../../store/index'; import { AppState, getPrivilegePageState } from '../../store';
import { SetPrivilegeOnLinuxAction } from '../../store/actions/device'; import { SetPrivilegeOnLinuxAction } from '../../store/actions/device';
import { LoadAppStartInfoAction, PrivilegeWhatWillThisDoAction } from '../../store/actions/app';
import { PrivilagePageSate } from '../../models/privilage-page-sate';
@Component({ @Component({
selector: 'privilege-checker', selector: 'privilege-checker',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './privilege-checker.component.html', templateUrl: './privilege-checker.component.html',
styleUrls: ['./privilege-checker.component.scss'] styleUrls: ['./privilege-checker.component.scss']
}) })
export class PrivilegeCheckerComponent {
constructor(protected store: Store<AppState>) { export class PrivilegeCheckerComponent implements OnInit, OnDestroy {
state: PrivilagePageSate;
command = `cat <<EOF >/etc/udev/rules.d/50-uhk60.rules
# Ultimate Hacking Keyboard rules
# These are the udev rules for accessing the USB interfaces of the UHK as non-root users.
# Copy this file to /etc/udev/rules.d and physically reconnect the UHK afterwards.
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="612[0-7]", MODE:="0666"
EOF
udevadm trigger
udevadm settle`;
private stateSubscription: Subscription;
constructor(private store: Store<AppState>,
private cdRef: ChangeDetectorRef) {
}
ngOnInit(): void {
this.stateSubscription = this.store.select(getPrivilegePageState)
.subscribe(state => {
this.state = state;
this.cdRef.markForCheck();
});
}
ngOnDestroy(): void {
if (this.stateSubscription) {
this.stateSubscription.unsubscribe();
}
} }
setUpPermissions(): void { setUpPermissions(): void {
this.store.dispatch(new SetPrivilegeOnLinuxAction()); this.store.dispatch(new SetPrivilegeOnLinuxAction());
} }
whatWillThisDo(): void {
this.store.dispatch(new PrivilegeWhatWillThisDoAction());
}
retry(): void {
this.store.dispatch(new LoadAppStartInfoAction());
}
} }

View File

@@ -0,0 +1,5 @@
export interface PrivilagePageSate {
showWhatWillThisDo: boolean;
showWhatWillThisDoContent: boolean;
permissionSetupFailed: boolean;
}

View File

@@ -8,6 +8,7 @@ import { ConfirmationPopoverModule } from 'angular-confirmation-popover';
import { DragulaModule } from 'ng2-dragula/ng2-dragula'; import { DragulaModule } from 'ng2-dragula/ng2-dragula';
import { Select2Module } from 'ng2-select2/ng2-select2'; import { Select2Module } from 'ng2-select2/ng2-select2';
import { NouisliderModule } from 'ng2-nouislider'; import { NouisliderModule } from 'ng2-nouislider';
import { ClipboardModule } from 'ngx-clipboard';
import { AddOnComponent } from './components/add-on'; import { AddOnComponent } from './components/add-on';
import { KeyboardSliderComponent } from './components/keyboard/slider'; import { KeyboardSliderComponent } from './components/keyboard/slider';
@@ -186,7 +187,8 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
NotifierModule.withConfig(angularNotifierConfig), NotifierModule.withConfig(angularNotifierConfig),
ConfirmationPopoverModule.forRoot({ ConfirmationPopoverModule.forRoot({
confirmButtonType: 'danger' // set defaults here confirmButtonType: 'danger' // set defaults here
}) }),
ClipboardModule
], ],
providers: [ providers: [
SvgModuleProviderService, SvgModuleProviderService,

View File

@@ -17,7 +17,10 @@ export const ActionTypes = {
DISMISS_UNDO_NOTIFICATION: type(PREFIX + 'dismiss notification action'), DISMISS_UNDO_NOTIFICATION: type(PREFIX + 'dismiss notification action'),
LOAD_HARDWARE_CONFIGURATION_SUCCESS: type(PREFIX + 'load hardware configuration success'), LOAD_HARDWARE_CONFIGURATION_SUCCESS: type(PREFIX + 'load hardware configuration success'),
ELECTRON_MAIN_LOG_RECEIVED: type(PREFIX + 'Electron main log received'), ELECTRON_MAIN_LOG_RECEIVED: type(PREFIX + 'Electron main log received'),
OPEN_URL_IN_NEW_WINDOW: type(PREFIX + 'Open URL in new Window') OPEN_URL_IN_NEW_WINDOW: type(PREFIX + 'Open URL in new Window'),
PRIVILEGE_WHAT_WILL_THIS_DO: type(PREFIX + 'What will this do clicked'),
SETUP_PERMISSION_ERROR: type(PREFIX + 'Setup permission error'),
LOAD_APP_START_INFO: type(PREFIX + 'Load app start info')
}; };
export class AppBootsrappedAction implements Action { export class AppBootsrappedAction implements Action {
@@ -31,25 +34,29 @@ export class AppStartedAction implements Action {
export class ShowNotificationAction implements Action { export class ShowNotificationAction implements Action {
type = ActionTypes.APP_SHOW_NOTIFICATION; type = ActionTypes.APP_SHOW_NOTIFICATION;
constructor(public payload: Notification) { } constructor(public payload: Notification) {
}
} }
export class ApplyCommandLineArgsAction implements Action { export class ApplyCommandLineArgsAction implements Action {
type = ActionTypes.APPLY_COMMAND_LINE_ARGS; type = ActionTypes.APPLY_COMMAND_LINE_ARGS;
constructor(public payload: CommandLineArgs) { } constructor(public payload: CommandLineArgs) {
}
} }
export class ProcessAppStartInfoAction implements Action { export class ProcessAppStartInfoAction implements Action {
type = ActionTypes.APP_PROCESS_START_INFO; type = ActionTypes.APP_PROCESS_START_INFO;
constructor(public payload: AppStartInfo) { } constructor(public payload: AppStartInfo) {
}
} }
export class UndoLastAction implements Action { export class UndoLastAction implements Action {
type = ActionTypes.UNDO_LAST; type = ActionTypes.UNDO_LAST;
constructor(public payload: any) {} constructor(public payload: any) {
}
} }
export class UndoLastSuccessAction implements Action { export class UndoLastSuccessAction implements Action {
@@ -63,19 +70,37 @@ export class DismissUndoNotificationAction implements Action {
export class LoadHardwareConfigurationSuccessAction implements Action { export class LoadHardwareConfigurationSuccessAction implements Action {
type = ActionTypes.LOAD_HARDWARE_CONFIGURATION_SUCCESS; type = ActionTypes.LOAD_HARDWARE_CONFIGURATION_SUCCESS;
constructor(public payload: HardwareConfiguration) {} constructor(public payload: HardwareConfiguration) {
}
} }
export class ElectronMainLogReceivedAction implements Action { export class ElectronMainLogReceivedAction implements Action {
type = ActionTypes.ELECTRON_MAIN_LOG_RECEIVED; type = ActionTypes.ELECTRON_MAIN_LOG_RECEIVED;
constructor(public payload: ElectronLogEntry) {} constructor(public payload: ElectronLogEntry) {
}
} }
export class OpenUrlInNewWindowAction implements Action { export class OpenUrlInNewWindowAction implements Action {
type = ActionTypes.OPEN_URL_IN_NEW_WINDOW; type = ActionTypes.OPEN_URL_IN_NEW_WINDOW;
constructor(public payload: string) {} constructor(public payload: string) {
}
}
export class PrivilegeWhatWillThisDoAction implements Action {
type = ActionTypes.PRIVILEGE_WHAT_WILL_THIS_DO;
}
export class SetupPermissionErrorAction implements Action {
type = ActionTypes.SETUP_PERMISSION_ERROR;
constructor(public payload: string) {
}
}
export class LoadAppStartInfoAction implements Action {
type = ActionTypes.LOAD_APP_START_INFO;
} }
export type Actions export type Actions
@@ -90,4 +115,7 @@ export type Actions
| LoadHardwareConfigurationSuccessAction | LoadHardwareConfigurationSuccessAction
| ElectronMainLogReceivedAction | ElectronMainLogReceivedAction
| OpenUrlInNewWindowAction | OpenUrlInNewWindowAction
| PrivilegeWhatWillThisDoAction
| SetupPermissionErrorAction
| LoadAppStartInfoAction
; ;

View File

@@ -40,6 +40,13 @@ export class ApplicationEffects {
this.logService.info('Renderer appStart effect end'); this.logService.info('Renderer appStart effect end');
}); });
@Effect({dispatch: false})
appStartInfo$: Observable<Action> = this.actions$
.ofType(ActionTypes.LOAD_APP_START_INFO)
.do(() => {
this.appRendererService.getAppStartInfo();
});
@Effect({dispatch: false}) @Effect({dispatch: false})
showNotification$: Observable<Action> = this.actions$ showNotification$: Observable<Action> = this.actions$
.ofType<ShowNotificationAction>(ActionTypes.APP_SHOW_NOTIFICATION) .ofType<ShowNotificationAction>(ActionTypes.APP_SHOW_NOTIFICATION)

View File

@@ -31,7 +31,7 @@ import {
UpdateFirmwareWithAction UpdateFirmwareWithAction
} from '../actions/device'; } from '../actions/device';
import { DeviceRendererService } from '../../services/device-renderer.service'; import { DeviceRendererService } from '../../services/device-renderer.service';
import { ShowNotificationAction } from '../actions/app'; import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app';
import { AppState } from '../index'; import { AppState } from '../index';
import { import {
ActionTypes as UserConfigActions, ActionTypes as UserConfigActions,
@@ -78,21 +78,15 @@ export class DeviceEffects {
setPrivilegeOnLinuxReply$: Observable<Action> = this.actions$ setPrivilegeOnLinuxReply$: Observable<Action> = this.actions$
.ofType<SetPrivilegeOnLinuxReplyAction>(ActionTypes.SET_PRIVILEGE_ON_LINUX_REPLY) .ofType<SetPrivilegeOnLinuxReplyAction>(ActionTypes.SET_PRIVILEGE_ON_LINUX_REPLY)
.map(action => action.payload) .map(action => action.payload)
.mergeMap((response: any) => { .map((response: any): any => {
if (response.success) { if (response.success) {
return [ return new ConnectionStateChangedAction({
new ConnectionStateChangedAction({ connected: true,
connected: true, hasPermission: true
hasPermission: true });
})
];
} }
return [
<any>new ShowNotificationAction({ return new SetupPermissionErrorAction(response.error);
type: NotificationType.Error,
message: response.error.message || response.error
})
];
}); });
@Effect({dispatch: false}) @Effect({dispatch: false})
@@ -166,8 +160,8 @@ export class DeviceEffects {
@Effect() saveResetUserConfigurationToDevice$ = this.actions$ @Effect() saveResetUserConfigurationToDevice$ = this.actions$
.ofType<ApplyUserConfigurationFromFileAction .ofType<ApplyUserConfigurationFromFileAction
| LoadResetUserConfigurationAction>( | LoadResetUserConfigurationAction>(
UserConfigActions.LOAD_RESET_USER_CONFIGURATION, UserConfigActions.LOAD_RESET_USER_CONFIGURATION,
UserConfigActions.APPLY_USER_CONFIGURATION_FROM_FILE) UserConfigActions.APPLY_USER_CONFIGURATION_FROM_FILE)
.map(action => action.payload) .map(action => action.payload)
.switchMap((config: UserConfiguration) => { .switchMap((config: UserConfiguration) => {
this.dataStorageRepository.saveConfig(config); this.dataStorageRepository.saveConfig(config);

View File

@@ -51,6 +51,7 @@ export const getHardwareConfiguration = createSelector(appState, fromApp.getHard
export const getKeyboardLayout = createSelector(appState, fromApp.getKeyboardLayout); export const getKeyboardLayout = createSelector(appState, fromApp.getKeyboardLayout);
export const deviceConfigurationLoaded = createSelector(appState, fromApp.deviceConfigurationLoaded); export const deviceConfigurationLoaded = createSelector(appState, fromApp.deviceConfigurationLoaded);
export const getAgentVersionInfo = createSelector(appState, fromApp.getAgentVersionInfo); export const getAgentVersionInfo = createSelector(appState, fromApp.getAgentVersionInfo);
export const getPrivilegePageState = createSelector(appState, fromApp.getPrivilagePageState);
export const appUpdateState = (state: AppState) => state.appUpdate; export const appUpdateState = (state: AppState) => state.appUpdate;
export const getShowAppUpdateAvailable = createSelector(appUpdateState, fromAppUpdate.getShowAppUpdateAvailable); export const getShowAppUpdateAvailable = createSelector(appUpdateState, fromAppUpdate.getShowAppUpdateAvailable);

View File

@@ -1,13 +1,20 @@
import { ROUTER_NAVIGATION } from '@ngrx/router-store'; import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { VersionInformation } from 'uhk-common'; import {
HardwareConfiguration,
Notification,
NotificationType,
runInElectron,
UserConfiguration,
VersionInformation
} from 'uhk-common';
import { HardwareConfiguration, Notification, NotificationType, runInElectron, UserConfiguration } from 'uhk-common';
import { ActionTypes, ShowNotificationAction } from '../actions/app'; import { ActionTypes, ShowNotificationAction } from '../actions/app';
import { ActionTypes as UserConfigActionTypes } from '../actions/user-config'; import { ActionTypes as UserConfigActionTypes } from '../actions/user-config';
import { ActionTypes as DeviceActionTypes } from '../actions/device'; import { ActionTypes as DeviceActionTypes } from '../actions/device';
import { KeyboardLayout } from '../../keyboard/keyboard-layout.enum'; import { KeyboardLayout } from '../../keyboard/keyboard-layout.enum';
import { getVersions } from '../../util'; import { getVersions } from '../../util';
import { PrivilagePageSate } from '../../models/privilage-page-sate';
export interface State { export interface State {
started: boolean; started: boolean;
@@ -19,6 +26,8 @@ export interface State {
configLoading: boolean; configLoading: boolean;
hardwareConfig?: HardwareConfiguration; hardwareConfig?: HardwareConfiguration;
agentVersionInfo?: VersionInformation; agentVersionInfo?: VersionInformation;
privilegeWhatWillThisDoClicked: boolean;
permissionError?: any;
} }
export const initialState: State = { export const initialState: State = {
@@ -27,7 +36,8 @@ export const initialState: State = {
navigationCountAfterNotification: 0, navigationCountAfterNotification: 0,
runningInElectron: runInElectron(), runningInElectron: runInElectron(),
configLoading: true, configLoading: true,
agentVersionInfo: getVersions() agentVersionInfo: getVersions(),
privilegeWhatWillThisDoClicked: false
}; };
export function reducer(state = initialState, action: Action & { payload: any }) { export function reducer(state = initialState, action: Action & { payload: any }) {
@@ -115,6 +125,24 @@ export function reducer(state = initialState, action: Action & { payload: any })
}; };
} }
case ActionTypes.PRIVILEGE_WHAT_WILL_THIS_DO:
return {
...state,
privilegeWhatWillThisDoClicked: true
};
case ActionTypes.SETUP_PERMISSION_ERROR:
return {
...state,
permissionError: action.payload
};
case DeviceActionTypes.SET_PRIVILEGE_ON_LINUX:
return {
...state,
permissionError: null
};
default: default:
return state; return state;
} }
@@ -134,3 +162,12 @@ export const getKeyboardLayout = (state: State): KeyboardLayout => {
}; };
export const deviceConfigurationLoaded = (state: State) => !state.runningInElectron ? true : !!state.hardwareConfig; export const deviceConfigurationLoaded = (state: State) => !state.runningInElectron ? true : !!state.hardwareConfig;
export const getAgentVersionInfo = (state: State) => state.agentVersionInfo || {} as VersionInformation; export const getAgentVersionInfo = (state: State) => state.agentVersionInfo || {} as VersionInformation;
export const getPrivilagePageState = (state: State): PrivilagePageSate => {
const permissionSetupFailed = !!state.permissionError;
return {
permissionSetupFailed,
showWhatWillThisDo: !state.privilegeWhatWillThisDoClicked && !permissionSetupFailed,
showWhatWillThisDoContent: state.privilegeWhatWillThisDoClicked || permissionSetupFailed
};
};

View File

@@ -115,3 +115,43 @@ a.disabled {
display: block; display: block;
} }
} }
a.link-inline {
cursor: pointer;
}
@mixin code-style() {
color: #6a737d;
background-color: #f6f8fa;
text-align: left;
}
code {
@include code-style();
}
pre {
code {
@include code-style();
}
}
.mt-10 {
margin-top: 10px;
}
.copy-container {
position: relative;
.fa-copy {
cursor: pointer;
color: #6a737d;
position: absolute;
right: 4px;
top: 4px;
&:hover {
color: darken(#6a737d, 15);
}
}
}