Compare commits

...

3 Commits

Author SHA1 Message Date
Koushik Dutta
f23ad06eef snapshot: verify acls
Some checks failed
Build SDK / Build (push) Has been cancelled
2026-01-19 22:16:44 -08:00
Koushik Dutta
3c8b513c31 sdk: update 2026-01-19 21:34:46 -08:00
Koushik Dutta
35df17334c sdk: AccessControls 2026-01-19 21:21:12 -08:00
10 changed files with 164 additions and 11 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/snapshot",
"version": "0.2.67",
"version": "0.2.68",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/snapshot",
"version": "0.2.67",
"version": "0.2.68",
"dependencies": {
"@types/node": "^22.10.2",
"sharp": "^0.33.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/snapshot",
"version": "0.2.67",
"version": "0.2.68",
"description": "Snapshot Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",

View File

@@ -3,6 +3,7 @@ import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/ht
import { RefreshPromise, TimeoutError, createMapPromiseDebouncer, singletonPromise, timeoutPromise } from "@scrypted/common/src/promise-utils";
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, Resolution, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, Sleep, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
import { checkUserId } from "@scrypted/sdk/acl";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import fs from 'fs';
import https from 'https';
@@ -776,6 +777,13 @@ export class SnapshotPlugin extends AutoenableMixinProvider implements MixinProv
pathname = pathname.substring('/hotlink-ok'.length);
const [_, id, iface] = pathname.split('/');
if (!request.username || (request.aclId && !await checkUserId(id, request.aclId))) {
response.send('', {
code: 401,
});
return;
}
try {
if (iface !== ScryptedInterface.Camera && iface !== ScryptedInterface.VideoCamera)
throw new Error();

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.5.55",
"version": "0.5.58",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.5.55",
"version": "0.5.58",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.27.1",

View File

@@ -1,11 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.5.55",
"version": "0.5.58",
"description": "",
"main": "dist/src/index.js",
"exports": {
".": "./dist/src/index.js",
"./acl": "./dist/src/acl.js",
"./promise-debounce": "./dist/src/promise-debounce.js",
"./storage-settings": "./dist/src/storage-settings.js",
"./settings-mixin": "./dist/src/settings-mixin.js"
},

View File

@@ -1,4 +1,5 @@
import { ScryptedDeviceAccessControl, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedUserAccessControl } from ".";
import sdk, { EventDetails, ScryptedDeviceAccessControl, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedUser, ScryptedUserAccessControl } from ".";
import { createCachingMapPromiseDebouncer } from './promise-debounce';
export function addAccessControlsForInterface(id: string, ...scryptedInterfaces: ScryptedInterface[]): ScryptedDeviceAccessControl {
const methods = scryptedInterfaces.map(scryptedInterface => ScryptedInterfaceDescriptors[scryptedInterface]?.methods || []).flat();
@@ -20,3 +21,132 @@ export function mergeDeviceAccessControls(accessControls: ScryptedUserAccessCont
accessControls.devicesAccessControls.push(...dacls);
return accessControls;
}
export class AccessControls {
constructor(public acl: ScryptedUserAccessControl) {
}
deny(reason: string = 'User does not have permission') {
throw new Error(reason);
}
shouldRejectDevice(id: string) {
if (this.acl.devicesAccessControls === null)
return false;
if (!this.acl.devicesAccessControls)
return true;
const dacls = this.acl.devicesAccessControls.filter(dacl => dacl.id === id);
return !dacls.length;
}
shouldRejectProperty(id: string, property: string) {
if (this.acl.devicesAccessControls === null)
return false;
if (!this.acl.devicesAccessControls)
return true;
const dacls = this.acl.devicesAccessControls.filter(dacl => dacl.id === id);
for (const dacl of dacls) {
if (!dacl.properties || dacl.properties.includes(property))
return false;
}
return true;
}
shouldRejectEvent(id: string, eventDetails: EventDetails) {
if (this.acl.devicesAccessControls === null)
return false;
if (!this.acl.devicesAccessControls)
return true;
const dacls = this.acl.devicesAccessControls.filter(dacl => dacl.id === id);
const { property } = eventDetails;
if (property) {
for (const dacl of dacls) {
if (!dacl.properties || dacl.properties.includes(property))
return false;
}
}
const { eventInterface } = eventDetails;
for (const dacl of dacls) {
if (!dacl.interfaces || dacl.interfaces.includes(eventInterface!))
return false;
}
return true;
}
shouldRejectInterface(id: string, scryptedInterface: ScryptedInterface) {
if (this.acl.devicesAccessControls === null)
return false;
if (!this.acl.devicesAccessControls)
return true;
const dacls = this.acl.devicesAccessControls.filter(dacl => dacl.id === id);
for (const dacl of dacls) {
if (!dacl.interfaces || dacl.interfaces.includes(scryptedInterface))
return false;
}
return true;
}
shouldRejectMethod(id: string, method: string) {
if (this.acl.devicesAccessControls === null)
return false;
if (!this.acl.devicesAccessControls)
return true;
const dacls = this.acl.devicesAccessControls.filter(dacl => dacl.id === id);
for (const dacl of dacls) {
if (!dacl.methods || dacl.methods.includes(method))
return false;
}
return true;
}
}
const accessControls = createCachingMapPromiseDebouncer<AccessControls|undefined>(60 * 1000);
export async function checkUserId(id: string, userId: string) {
const user = sdk.systemManager.getDeviceById<ScryptedUser>(userId);
if (!user || !user.interfaces?.includes(ScryptedInterface.ScryptedUser)) {
// console.error('Error delivering notification, invalid user id:', userId);
return;
}
if (!sdk.systemManager.getDeviceById(id))
return;
try {
const acl = await accessControls(userId, async () => {
const acls = await user.getScryptedUserAccessControl();
const acl = acls ? new AccessControls(acls) : undefined;
return acl;
});
if (acl?.shouldRejectDevice(id)) {
return;
}
}
catch (e) {
// console.error('Error delivering notification, ACL check failed.', e);
return;
}
return user;
}

View File

@@ -0,0 +1,14 @@
export function createCachingMapPromiseDebouncer<T>(duration: number) {
const map = new Map<string, Promise<T>>();
return (key: any, func: () => Promise<T>): Promise<T> => {
const keyStr = JSON.stringify(key);
let value = map.get(keyStr);
if (!value) {
value = func();
map.set(keyStr, value);
setTimeout(() => map.delete(keyStr), duration);
}
return value;
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.5.52",
"version": "0.5.54",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.5.52",
"version": "0.5.54",
"license": "ISC",
"dependencies": {
"openai": "^6.1.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/types",
"version": "0.5.52",
"version": "0.5.54",
"description": "",
"main": "dist/index.js",
"author": "",

View File

@@ -1131,7 +1131,7 @@ class TamperState(TypedDict):
pass
TYPES_VERSION = "0.5.52"
TYPES_VERSION = "0.5.54"
class AirPurifier: