core: add support for users and acls

This commit is contained in:
Koushik Dutta
2022-12-19 10:02:43 -08:00
parent 3a349205ed
commit bda70cb578
14 changed files with 176 additions and 27 deletions

View File

@@ -66,4 +66,7 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
async getDevice(nativeId: string) {
return this.aggregate.get(nativeId);
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
}

View File

@@ -83,4 +83,7 @@ export class AutomationCore extends ScryptedDeviceBase implements DeviceProvider
async getDevice(nativeId: string) {
return this.automations.get(nativeId);
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
}

View File

@@ -9,6 +9,7 @@ import { AutomationCore, AutomationCoreNativeId } from './automations-core';
import { LauncherMixin } from './launcher-mixin';
import { MediaCore } from './media-core';
import { ScriptCore, ScriptCoreNativeId } from './script-core';
import { UsersCore, UsersNativeId } from './user';
const { systemManager, deviceManager, endpointManager } = sdk;
@@ -23,7 +24,6 @@ interface RoutedHttpRequest extends HttpRequest {
params: { [key: string]: string };
}
class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, EngineIOHandler, DeviceProvider, Settings {
router: any = Router();
publicRouter: any = Router();
@@ -32,6 +32,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
scriptCore: ScriptCore;
aggregateCore: AggregateCore;
automationCore: AutomationCore;
users: UsersCore;
localAddresses: string[];
storageSettings = new StorageSettings(this, {
localAddresses: {
@@ -101,6 +102,19 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
);
this.aggregateCore = new AggregateCore();
})();
(async () => {
await deviceManager.onDeviceDiscovered(
{
name: 'Scrypted Users',
nativeId: UsersNativeId,
interfaces: [ScryptedInterface.DeviceProvider, ScryptedInterface.DeviceCreator, ScryptedInterface.Readme],
type: ScryptedDeviceType.Builtin,
},
);
this.users = new UsersCore();
})();
}
async getSettings(): Promise<Setting[]> {
@@ -127,9 +141,11 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
return this.automationCore;
if (nativeId === AggregateCoreNativeId)
return this.aggregateCore;
if (nativeId === UsersNativeId)
return this.users;
}
async releaseDevice(id: string, nativeId: string, device: any): Promise<void> {
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
checkEngineIoEndpoint(request: HttpRequest, name: string) {
@@ -140,6 +156,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
}
async checkService(request: HttpRequest, ws: WebSocket, name: string): Promise<boolean> {
// only allow admin users to access these services.
if (request.aclId)
return false;
const check = this.checkEngineIoEndpoint(request, name);
if (!check)
return false;

View File

@@ -137,4 +137,7 @@ export class MediaCore extends ScryptedDeviceBase implements DeviceProvider, Buf
if (nativeId === 'files')
return this.filesHost;
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
}

View File

@@ -87,4 +87,7 @@ export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, De
getDevice(nativeId: string) {
return this.scripts.get(nativeId);
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
}

117
plugins/core/src/user.ts Normal file
View File

@@ -0,0 +1,117 @@
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedUser, ScryptedUserAccessControl, Setting, Settings, SettingValue } from "@scrypted/sdk";
import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
export const UsersNativeId = 'users';
type DBUser = { username: string, aclId: string };
export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
return {
devicesAccessControls: [
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
]
};
}
async getSettings(): Promise<Setting[]> {
return [
{
key: 'password',
title: 'Password',
type: 'password',
}
]
}
async putSetting(key: string, value: SettingValue): Promise<void> {
if (key !== 'password')
return;
const usersService = await sdk.systemManager.getComponent('users');
const users: DBUser[] = await usersService.getAllUsers();
const user = users.find(user => user.username === this.nativeId.substring('user:'.length));
if (!user)
return;
await usersService.addUser(user.username, value.toString(), user.aclId);
}
}
export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvider, DeviceCreator {
constructor() {
super(UsersNativeId);
this.syncUsers();
}
async getDevice(nativeId: string): Promise<any> {
return new User(nativeId);
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
const username = nativeId.substring('user:'.length);
const usersService = await sdk.systemManager.getComponent('users');
await usersService.removeUser(username);
}
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
key: 'username',
title: 'User name',
},
{
key: 'admin',
type: 'boolean',
title: 'Admin',
description: 'Grant this user administrator privileges.',
},
{
key: 'password',
type: 'password',
title: 'Password',
}
]
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const { username, password, admin } = settings;
const usersService = await sdk.systemManager.getComponent('users');
const nativeId = `user:${username}`;
const aclId = await sdk.deviceManager.onDeviceDiscovered({
providerNativeId: this.nativeId,
name: username.toString(),
nativeId,
interfaces: [
ScryptedInterface.ScryptedUser,
ScryptedInterface.Settings,
],
type: ScryptedDeviceType.Builtin,
})
await usersService.addUser(username, password, admin ? undefined : aclId);
await this.syncUsers();
return nativeId;
}
async getReadmeMarkdown(): Promise<string> {
return "Create and Manage the users that can log into Scrypted.";
}
async syncUsers() {
const usersService = await sdk.systemManager.getComponent('users');
const users: DBUser[] = await usersService.getAllUsers();
await sdk.deviceManager.onDevicesChanged({
providerNativeId: this.nativeId,
devices: users.map(user => ({
name: user.username,
nativeId: `user:${user.username}`,
interfaces: [
ScryptedInterface.ScryptedUser,
ScryptedInterface.Settings,
],
type: ScryptedDeviceType.Builtin,
})),
})
}
}

View File

@@ -1,5 +1,5 @@
<template>
<Device v-if="id" :id="id"></Device>
<Device v-if="id" :deviceId="id"></Device>
</template>
<script>
import Device from "./Device.vue";

View File

@@ -496,7 +496,7 @@ export default {
versions: null,
};
const device = this.device;
pluginData.nativeId = await plugins.getNativeId(this.id);
pluginData.nativeId = device.nativeId;
pluginData.storage = await plugins.getStorage(this.id);
pluginData.pluginId = this.device.pluginId;
pluginData.packageJson = await plugins.getPackageJson(this.device.pluginId);
@@ -571,7 +571,7 @@ export default {
return this.$store.state.scrypted.devices;
},
id() {
return this.$route.params.id || this.$props.id;
return this.$route.params.id || this.$props.deviceId;
},
canLoad() {
return this.devices.includes(this.id);
@@ -583,7 +583,7 @@ export default {
return this.$scrypted.systemManager.getDeviceById(this.id);
},
},
props: ['id'],
props: ['deviceId'],
};
</script>
<style>

View File

@@ -1,5 +1,5 @@
<template>
<Device v-if="id" :id="id"></Device>
<Device v-if="id" :deviceId="id"></Device>
</template>
<script>
import Device from "./Device.vue";

View File

@@ -1,5 +1,5 @@
<template>
<Device v-if="id" :id="id"></Device>
<Device v-if="id" :deviceId="id"></Device>
</template>
<script>
import Device from "../Device.vue";

View File

@@ -37,7 +37,6 @@ import EventListener from "../../interfaces/EventListener.vue";
import OnOff from "../../interfaces/OnOff.vue";
import Lock from "../../interfaces/Lock.vue";
import Notifier from "../../interfaces/Notifier.vue";
import SoftwareUpdate from "../../interfaces/SoftwareUpdate.vue";
import ColorSettingHsv from "../../interfaces/ColorSettingHsv.vue";
import StartStop from "../../interfaces/StartStop.vue";
import Dock from "../../interfaces/Dock.vue";
@@ -81,7 +80,6 @@ export default {
Brightness,
Lock,
Notifier,
SoftwareUpdate,
ColorSettingHsv,
ColorSettingRgb,
ColorSettingTemperature,

View File

@@ -12,7 +12,6 @@ export const actionableInterfaces = [
ScryptedInterface.Notifier,
ScryptedInterface.Pause,
ScryptedInterface.Scene,
ScryptedInterface.SoftwareUpdate,
ScryptedInterface.StartStop,
// 'Timer',

View File

@@ -4,7 +4,7 @@ import PluginUpdate from "./PluginUpdate.vue";
import PluginPid from "./PluginPid.vue";
import PluginStats from "./PluginStats.vue";
import { getDeviceViewPath } from "../helpers";
import { snapshotCurrentPlugins } from "./plugin";
import { snapshotCurrentPlugins, getIdForNativeId } from "./plugin";
export default {
mixins: [BasicComponent],
@@ -16,13 +16,7 @@ export default {
return `https://www.npmjs.com/package/${device.pluginId}`;
},
async openAutoupdater() {
const plugins = await this.$scrypted.systemManager.getComponent(
"plugins"
);
const id = await plugins.getIdForNativeId(
"@scrypted/core",
"automation:update-plugins"
);
const id = getIdForNativeId(systemManager, '@scrypted/core', 'scriptcore');
this.$router.push(getDeviceViewPath(id));
},
async snapshotCurrentPlugins() {
@@ -84,17 +78,22 @@ export default {
const ids = Object.keys(this.$store.state.systemState);
const devices = [];
const plugins = await this.$scrypted.systemManager.getComponent(
"plugins"
);
const promises = ids.map(async (id) => {
const device = this.$scrypted.systemManager.getDeviceById(id);
if (device.id !== device.providerId) return;
const { name, type } = device;
const pluginId = device.pluginId;
const pluginInfo = await plugins.getPluginInfo(pluginId);
const { packageJson, pid, stats, rpcObjects, pendingResults } = pluginInfo;
const npmPackageVersion = packageJson.version;
let pluginInfo;
try {
const plugins = await this.$scrypted.systemManager.getComponent(
"plugins"
);
pluginInfo = await plugins.getPluginInfo(pluginId);
}
catch (e) {
}
const { packageJson, pid, stats, rpcObjects, pendingResults } = pluginInfo || {};
const npmPackageVersion = packageJson?.version;
devices.push({
id,
name,

View File

@@ -6,6 +6,7 @@ import { Scriptable } from '@scrypted/types';
import { SystemManager } from '@scrypted/types';
import axios, { AxiosResponse } from 'axios';
import semver from 'semver';
import { getAllDevices } from '../../common/mixin';
const pluginSnapshot = require("!!raw-loader!./plugin-snapshot.ts").default.split('\n')
.filter(line => !line.includes('SCRYPTED_FILTER_EXAMPLE_LINE'))
.join('\n')
@@ -74,11 +75,15 @@ export function getNpmPath(npmPackage: string) {
return `https://www.npmjs.com/package/${npmPackage}`;
}
export function getIdForNativeId(systemManager: SystemManager, pluginId: string, nativeId: string) {
const found = getAllDevices(systemManager).find(device => device.pluginId === pluginId && device, nativeId === nativeId);
return found?.id;
}
export async function snapshotCurrentPlugins(scrypted: ScryptedStatic): Promise<string> {
const { systemManager, deviceManager } = scrypted;
const plugins = await systemManager.getComponent("plugins");
const id = await plugins.getIdForNativeId('@scrypted/core', 'scriptcore');
const id = getIdForNativeId(systemManager, '@scrypted/core', 'scriptcore');
const scriptCore = systemManager.getDeviceById<DeviceCreator & Scriptable>(id);
const backupId = await scriptCore.createDevice({
});