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:
committed by
László Monda
parent
6e1f0ded9e
commit
6ccf005750
@@ -1,4 +1,39 @@
|
||||
<span 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>
|
||||
<button class="btn btn-default btn-lg btn-primary" (click)="setUpPermissions()"> Set up permissions </button>
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -9,3 +9,19 @@
|
||||
uhk-message {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
import 'rxjs/add/operator/ignoreElements';
|
||||
import 'rxjs/add/operator/takeWhile';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { AppState } from '../../store/index';
|
||||
import { AppState, getPrivilegePageState } from '../../store';
|
||||
import { SetPrivilegeOnLinuxAction } from '../../store/actions/device';
|
||||
import { LoadAppStartInfoAction, PrivilegeWhatWillThisDoAction } from '../../store/actions/app';
|
||||
import { PrivilagePageSate } from '../../models/privilage-page-sate';
|
||||
|
||||
@Component({
|
||||
selector: 'privilege-checker',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './privilege-checker.component.html',
|
||||
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 {
|
||||
this.store.dispatch(new SetPrivilegeOnLinuxAction());
|
||||
}
|
||||
|
||||
whatWillThisDo(): void {
|
||||
this.store.dispatch(new PrivilegeWhatWillThisDoAction());
|
||||
}
|
||||
|
||||
retry(): void {
|
||||
this.store.dispatch(new LoadAppStartInfoAction());
|
||||
}
|
||||
}
|
||||
|
||||
5
packages/uhk-web/src/app/models/privilage-page-sate.ts
Normal file
5
packages/uhk-web/src/app/models/privilage-page-sate.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface PrivilagePageSate {
|
||||
showWhatWillThisDo: boolean;
|
||||
showWhatWillThisDoContent: boolean;
|
||||
permissionSetupFailed: boolean;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ConfirmationPopoverModule } from 'angular-confirmation-popover';
|
||||
import { DragulaModule } from 'ng2-dragula/ng2-dragula';
|
||||
import { Select2Module } from 'ng2-select2/ng2-select2';
|
||||
import { NouisliderModule } from 'ng2-nouislider';
|
||||
import { ClipboardModule } from 'ngx-clipboard';
|
||||
|
||||
import { AddOnComponent } from './components/add-on';
|
||||
import { KeyboardSliderComponent } from './components/keyboard/slider';
|
||||
@@ -186,7 +187,8 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
|
||||
NotifierModule.withConfig(angularNotifierConfig),
|
||||
ConfirmationPopoverModule.forRoot({
|
||||
confirmButtonType: 'danger' // set defaults here
|
||||
})
|
||||
}),
|
||||
ClipboardModule
|
||||
],
|
||||
providers: [
|
||||
SvgModuleProviderService,
|
||||
|
||||
@@ -17,7 +17,10 @@ export const ActionTypes = {
|
||||
DISMISS_UNDO_NOTIFICATION: type(PREFIX + 'dismiss notification action'),
|
||||
LOAD_HARDWARE_CONFIGURATION_SUCCESS: type(PREFIX + 'load hardware configuration success'),
|
||||
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 {
|
||||
@@ -31,25 +34,29 @@ export class AppStartedAction implements Action {
|
||||
export class ShowNotificationAction implements Action {
|
||||
type = ActionTypes.APP_SHOW_NOTIFICATION;
|
||||
|
||||
constructor(public payload: Notification) { }
|
||||
constructor(public payload: Notification) {
|
||||
}
|
||||
}
|
||||
|
||||
export class ApplyCommandLineArgsAction implements Action {
|
||||
type = ActionTypes.APPLY_COMMAND_LINE_ARGS;
|
||||
|
||||
constructor(public payload: CommandLineArgs) { }
|
||||
constructor(public payload: CommandLineArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProcessAppStartInfoAction implements Action {
|
||||
type = ActionTypes.APP_PROCESS_START_INFO;
|
||||
|
||||
constructor(public payload: AppStartInfo) { }
|
||||
constructor(public payload: AppStartInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
export class UndoLastAction implements Action {
|
||||
type = ActionTypes.UNDO_LAST;
|
||||
|
||||
constructor(public payload: any) {}
|
||||
constructor(public payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export class UndoLastSuccessAction implements Action {
|
||||
@@ -63,19 +70,37 @@ export class DismissUndoNotificationAction implements Action {
|
||||
export class LoadHardwareConfigurationSuccessAction implements Action {
|
||||
type = ActionTypes.LOAD_HARDWARE_CONFIGURATION_SUCCESS;
|
||||
|
||||
constructor(public payload: HardwareConfiguration) {}
|
||||
constructor(public payload: HardwareConfiguration) {
|
||||
}
|
||||
}
|
||||
|
||||
export class ElectronMainLogReceivedAction implements Action {
|
||||
type = ActionTypes.ELECTRON_MAIN_LOG_RECEIVED;
|
||||
|
||||
constructor(public payload: ElectronLogEntry) {}
|
||||
constructor(public payload: ElectronLogEntry) {
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenUrlInNewWindowAction implements Action {
|
||||
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
|
||||
@@ -90,4 +115,7 @@ export type Actions
|
||||
| LoadHardwareConfigurationSuccessAction
|
||||
| ElectronMainLogReceivedAction
|
||||
| OpenUrlInNewWindowAction
|
||||
| PrivilegeWhatWillThisDoAction
|
||||
| SetupPermissionErrorAction
|
||||
| LoadAppStartInfoAction
|
||||
;
|
||||
|
||||
@@ -40,6 +40,13 @@ export class ApplicationEffects {
|
||||
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})
|
||||
showNotification$: Observable<Action> = this.actions$
|
||||
.ofType<ShowNotificationAction>(ActionTypes.APP_SHOW_NOTIFICATION)
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
UpdateFirmwareWithAction
|
||||
} from '../actions/device';
|
||||
import { DeviceRendererService } from '../../services/device-renderer.service';
|
||||
import { ShowNotificationAction } from '../actions/app';
|
||||
import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app';
|
||||
import { AppState } from '../index';
|
||||
import {
|
||||
ActionTypes as UserConfigActions,
|
||||
@@ -78,21 +78,15 @@ export class DeviceEffects {
|
||||
setPrivilegeOnLinuxReply$: Observable<Action> = this.actions$
|
||||
.ofType<SetPrivilegeOnLinuxReplyAction>(ActionTypes.SET_PRIVILEGE_ON_LINUX_REPLY)
|
||||
.map(action => action.payload)
|
||||
.mergeMap((response: any) => {
|
||||
.map((response: any): any => {
|
||||
if (response.success) {
|
||||
return [
|
||||
new ConnectionStateChangedAction({
|
||||
connected: true,
|
||||
hasPermission: true
|
||||
})
|
||||
];
|
||||
return new ConnectionStateChangedAction({
|
||||
connected: true,
|
||||
hasPermission: true
|
||||
});
|
||||
}
|
||||
return [
|
||||
<any>new ShowNotificationAction({
|
||||
type: NotificationType.Error,
|
||||
message: response.error.message || response.error
|
||||
})
|
||||
];
|
||||
|
||||
return new SetupPermissionErrorAction(response.error);
|
||||
});
|
||||
|
||||
@Effect({dispatch: false})
|
||||
@@ -166,8 +160,8 @@ export class DeviceEffects {
|
||||
@Effect() saveResetUserConfigurationToDevice$ = this.actions$
|
||||
.ofType<ApplyUserConfigurationFromFileAction
|
||||
| LoadResetUserConfigurationAction>(
|
||||
UserConfigActions.LOAD_RESET_USER_CONFIGURATION,
|
||||
UserConfigActions.APPLY_USER_CONFIGURATION_FROM_FILE)
|
||||
UserConfigActions.LOAD_RESET_USER_CONFIGURATION,
|
||||
UserConfigActions.APPLY_USER_CONFIGURATION_FROM_FILE)
|
||||
.map(action => action.payload)
|
||||
.switchMap((config: UserConfiguration) => {
|
||||
this.dataStorageRepository.saveConfig(config);
|
||||
|
||||
@@ -51,6 +51,7 @@ export const getHardwareConfiguration = createSelector(appState, fromApp.getHard
|
||||
export const getKeyboardLayout = createSelector(appState, fromApp.getKeyboardLayout);
|
||||
export const deviceConfigurationLoaded = createSelector(appState, fromApp.deviceConfigurationLoaded);
|
||||
export const getAgentVersionInfo = createSelector(appState, fromApp.getAgentVersionInfo);
|
||||
export const getPrivilegePageState = createSelector(appState, fromApp.getPrivilagePageState);
|
||||
|
||||
export const appUpdateState = (state: AppState) => state.appUpdate;
|
||||
export const getShowAppUpdateAvailable = createSelector(appUpdateState, fromAppUpdate.getShowAppUpdateAvailable);
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { ROUTER_NAVIGATION } from '@ngrx/router-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 as UserConfigActionTypes } from '../actions/user-config';
|
||||
import { ActionTypes as DeviceActionTypes } from '../actions/device';
|
||||
import { KeyboardLayout } from '../../keyboard/keyboard-layout.enum';
|
||||
import { getVersions } from '../../util';
|
||||
import { PrivilagePageSate } from '../../models/privilage-page-sate';
|
||||
|
||||
export interface State {
|
||||
started: boolean;
|
||||
@@ -19,6 +26,8 @@ export interface State {
|
||||
configLoading: boolean;
|
||||
hardwareConfig?: HardwareConfiguration;
|
||||
agentVersionInfo?: VersionInformation;
|
||||
privilegeWhatWillThisDoClicked: boolean;
|
||||
permissionError?: any;
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
@@ -27,7 +36,8 @@ export const initialState: State = {
|
||||
navigationCountAfterNotification: 0,
|
||||
runningInElectron: runInElectron(),
|
||||
configLoading: true,
|
||||
agentVersionInfo: getVersions()
|
||||
agentVersionInfo: getVersions(),
|
||||
privilegeWhatWillThisDoClicked: false
|
||||
};
|
||||
|
||||
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:
|
||||
return state;
|
||||
}
|
||||
@@ -134,3 +162,12 @@ export const getKeyboardLayout = (state: State): KeyboardLayout => {
|
||||
};
|
||||
export const deviceConfigurationLoaded = (state: State) => !state.runningInElectron ? true : !!state.hardwareConfig;
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -115,3 +115,43 @@ a.disabled {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user