diff --git a/backend/src/business-layer/timeoutBl.ts b/backend/src/business-layer/timeoutBl.ts index b5a6e228..9a55dbe8 100644 --- a/backend/src/business-layer/timeoutBl.ts +++ b/backend/src/business-layer/timeoutBl.ts @@ -3,6 +3,7 @@ import { // tslint:disable-next-line:ordered-imports Minion, MinionStatus, + MinionTimeout, Switch, SwitchOptions, Toggle, @@ -92,6 +93,26 @@ export class TimeoutBl { logger.info('Timeout module init done.'); } + /** + * Restart timeout countdown for a minion + */ + public async restartMinionTimeout(minionId: string) { + const minionTimeout = this.minionsTimeoutInfo.find(mti => mti.minionId === minionId); + minionTimeout!.turnOnTimeStump = new Date(); + } + + /** + * Get all minions timeout countdown state + * @returns + */ + public async getTimeoutStatus(): Promise { + return this.minionsTimeoutInfo.map(mti => ({ + minionId: mti?.minionId, + active: mti?.status === 'on', + countdownTimestamp: mti?.turnOnTimeStump?.getTime(), + } as MinionTimeout)); + } + /** * Get minion info sturuct if exist for given minion id. * @param minionId minion id to get info for. @@ -107,7 +128,7 @@ export class TimeoutBl { private async timeoutActivation(): Promise { // If currently the timeoutActivation in action, ignore other calls - if(this.isTimeoutPossessing){ + if (this.isTimeoutPossessing) { return; } @@ -232,6 +253,7 @@ export class TimeoutBl { const timeoutMinion = this.findMinionInfo(minion.minionId); this.minionsTimeoutInfo.splice(this.minionsTimeoutInfo.indexOf(timeoutMinion), 1); } + } export const TimeoutBlSingleton = new TimeoutBl(MinionsBlSingleton); diff --git a/backend/src/controllers/actionsController.ts b/backend/src/controllers/actionsController.ts index 4db6b7f7..1412fd9e 100644 --- a/backend/src/controllers/actionsController.ts +++ b/backend/src/controllers/actionsController.ts @@ -41,7 +41,7 @@ export class ActionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'BLOCK', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) + @MinionsRestriction({ requirePermission: 'READ', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) @Get('{actionId}') public async getAction(actionId: string): Promise { return await actionsService.getActionById(actionId); @@ -54,7 +54,7 @@ export class ActionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'BLOCK', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Get('/minion/{minionId}') public async getActionByMinion(minionId: string): Promise { return await actionsService.getMinionActions(minionId); @@ -68,8 +68,8 @@ export class ActionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 1, extractMinionIds: (action: Action) => action.minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 1, extractMinionIds: (action: Action) => action.minionId }) @Put('{actionId}') public async setAction(actionId: string, @Body() action: Action): Promise { return await actionsService.setAction(actionId, action); @@ -83,7 +83,7 @@ export class ActionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) @Put('set-active/{actionId}') public async setActionActive(actionId: string, @Query() active: boolean): Promise { return await actionsService.setActionActive(actionId, active); @@ -96,7 +96,7 @@ export class ActionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: async (actionId: string) => (await actionsService.getActionById(actionId)).minionId }) @Delete('{actionId}') public async deleteAction(actionId: string): Promise { return await actionsService.deleteAction(actionId); @@ -110,7 +110,7 @@ export class ActionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: async (action: Action) => action.minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: async (action: Action) => action.minionId }) @Post() public async createAction(@Body() action: Action): Promise { return await actionsService.createAction(action); diff --git a/backend/src/controllers/minionsController.ts b/backend/src/controllers/minionsController.ts index 8a338693..33f93ec5 100644 --- a/backend/src/controllers/minionsController.ts +++ b/backend/src/controllers/minionsController.ts @@ -18,6 +18,7 @@ import { } from 'tsoa'; import { MinionsBlSingleton } from '../business-layer/minionsBl'; import { TimelineBlSingleton } from '../business-layer/timelineBl'; +import { TimeoutBlSingleton } from '../business-layer/timeoutBl'; import { ErrorResponse, MinionCalibrate, @@ -26,6 +27,7 @@ import { MinionSetRoomName, MinionStatus, MinionTimeline, + MinionTimeout, RestrictionItem, ScanningStatus, SetMinionAutoTurnOff, @@ -55,12 +57,51 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'BLOCK', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Get('timeline/{minionId}') public async getMinionTimeline(minionId: string): Promise { return await TimelineBlSingleton.getTimeline(minionId); } + /** + * Update minion auto turns off timeout. + */ + @Security('userAuth') + @Security('adminAuth') + @Response(501, 'Server error') + @MinionsResultsRestriction((m: MinionTimeout) => m.minionId) + @Get('timeout') + public async getMinionsTimeout(): Promise { + return await TimeoutBlSingleton.getTimeoutStatus(); + } + + /** + * URestart minion timeout countdown. + */ + @Security('userAuth') + @Security('adminAuth') + @Response(501, 'Server error') + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @Post('timeout/restart/{minionId}') + public async restartMinionTimeout(minionId: string): Promise { + return await TimeoutBlSingleton.restartMinionTimeout(minionId); + } + + /** + * Update minion auto turns off timeout. + * @param minionId Minion id. + * @param setTimeout Timeout property. + */ + @Security('userAuth') + @Security('adminAuth') + @Response(501, 'Server error') + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @Put('timeout/{minionId}') + public async setMinionTimeout(minionId: string, @Body() setTimeout: SetMinionAutoTurnOff): Promise { + return await MinionsBlSingleton.setMinionTimeout(minionId, setTimeout.setAutoTurnOffMS); + } + + /** * Power off all minions */ @@ -79,7 +120,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Put('rename/{minionId}') public async renameMinion(minionId: string, @Body() minionRename: MinionRename): Promise { return await MinionsBlSingleton.renameMinion(minionId, minionRename.name); @@ -93,7 +134,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Put('room/{minionId}') public async renameRoom(minionId: string, @Body() roomName: MinionSetRoomName): Promise { return await MinionsBlSingleton.setMinionRoom(minionId, roomName.room); @@ -107,25 +148,14 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Put('network-device/{minionId}') public async replaceNetworkDevice(minionId: string, @Body() macToSet: MinionSetDevice): Promise { return await MinionsBlSingleton.replaceNetworkDevice(minionId, macToSet.mac); } - /** - * Update minion auto turns off timeout. - * @param minionId Minion id. - * @param setTimeout Timeout property. - */ - @Security('userAuth') - @Security('adminAuth') - @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) - @Put('timeout/{minionId}') - public async setMinionTimeout(minionId: string, @Body() setTimeout: SetMinionAutoTurnOff): Promise { - return await MinionsBlSingleton.setMinionTimeout(minionId, setTimeout.setAutoTurnOffMS); - } + + /** * Update minion auto turns off timeout. @@ -135,7 +165,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Put('calibrate/{minionId}') public async setMinionCalibrate(minionId: string, @Body() calibration: MinionCalibrate): Promise { return await MinionsBlSingleton.setMinionCalibrate(minionId, calibration); @@ -147,7 +177,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Post('rescan/{minionId}') public async rescanMinionStatus(minionId: string): Promise { return await MinionsBlSingleton.scanMinionStatus(minionId); @@ -199,7 +229,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Delete('{minionId}') public async deleteMinion(minionId: string): Promise { return await MinionsBlSingleton.deleteMinion(minionId); @@ -238,7 +268,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @MinionSanitation() - @MinionsRestriction({ restrictPermission: 'BLOCK', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'READ', elementArgIndex: 0, extractMinionIds: (minionId: string) => minionId }) @Get('{minionId}') public async getMinion(minionId: string): Promise { return await MinionsBlSingleton.getMinionById(minionId) @@ -252,7 +282,7 @@ export class MinionsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 1, extractMinionIds: (minionId: string) => minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 1, extractMinionIds: (minionId: string) => minionId }) @Put('{minionId}') public async setMinion(@Request() request, minionId: string, @Body() setStatus: MinionStatus): Promise { return await MinionsBlSingleton.setMinionStatus(minionId, setStatus, 'user', request.user); diff --git a/backend/src/controllers/timingsController.ts b/backend/src/controllers/timingsController.ts index 28b01123..8e8111cf 100644 --- a/backend/src/controllers/timingsController.ts +++ b/backend/src/controllers/timingsController.ts @@ -41,7 +41,7 @@ export class TimingsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'BLOCK', elementArgIndex: 0, extractMinionIds: async (timingId: string) => (await TimingsBlSingleton.getTimingById(timingId))?.triggerDirectAction?.minionId }) + @MinionsRestriction({ requirePermission: 'READ', elementArgIndex: 0, extractMinionIds: async (timingId: string) => (await TimingsBlSingleton.getTimingById(timingId))?.triggerDirectAction?.minionId }) @Get('{timingId}') public async getTiming(timingId: string): Promise { return await TimingsBlSingleton.getTimingById(timingId); @@ -55,8 +55,8 @@ export class TimingsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: async (timingId: string) => (await TimingsBlSingleton.getTimingById(timingId))?.triggerDirectAction?.minionId }) - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 1, extractMinionIds: (timing: Timing) => timing.triggerDirectAction?.minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: async (timingId: string) => (await TimingsBlSingleton.getTimingById(timingId))?.triggerDirectAction?.minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 1, extractMinionIds: (timing: Timing) => timing.triggerDirectAction?.minionId }) @Put('{timingId}') public async setTiming(timingId: string, @Body() timing: Timing): Promise { return await TimingsBlSingleton.SetTiming(timingId, timing); @@ -69,7 +69,7 @@ export class TimingsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: async (timingId: string) => (await TimingsBlSingleton.getTimingById(timingId))?.triggerDirectAction?.minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: async (timingId: string) => (await TimingsBlSingleton.getTimingById(timingId))?.triggerDirectAction?.minionId }) @Delete('{timingId}') public async deleteTiming(timingId: string): Promise { return await TimingsBlSingleton.DeleteTiming(timingId); @@ -82,7 +82,7 @@ export class TimingsController extends Controller { @Security('userAuth') @Security('adminAuth') @Response(501, 'Server error') - @MinionsRestriction({ restrictPermission: 'READ', elementArgIndex: 0, extractMinionIds: (timing: Timing) => timing.triggerDirectAction?.minionId }) + @MinionsRestriction({ requirePermission: 'WRITE', elementArgIndex: 0, extractMinionIds: (timing: Timing) => timing.triggerDirectAction?.minionId }) @Post() public async createTiming(@Body() timing: Timing): Promise { return await TimingsBlSingleton.CreateTiming(timing); diff --git a/backend/src/models/sharedInterfaces.d.ts b/backend/src/models/sharedInterfaces.d.ts index 4a78b6fb..c312d59f 100644 --- a/backend/src/models/sharedInterfaces.d.ts +++ b/backend/src/models/sharedInterfaces.d.ts @@ -196,6 +196,17 @@ export declare interface DeviceKind { isFetchCommandsAvailable: boolean; } +/** + * Minion timeout countdown information + */ +export declare interface MinionTimeout { + minionId: string; + /** IS countdown active */ + active: boolean; + /** EPOCH time when countdown started */ + countdownTimestamp: number; +} + /** * Scopes of authentication, right know in our system there are only 3 scopes. * admin and user. any API route protect by one of them. @@ -597,14 +608,14 @@ export declare interface MinionSetDevice { /** * Type of resection of access */ -export declare type RestrictionType = 'BLOCK' | 'READ' | 'WRITE'; +export declare type RestrictionType = 'BLOCK' | 'READ' | 'WRITE'; export declare interface RestrictionItem { /** The use to restrict */ userEmail: string; /** The limited access type to grant user */ restrictionType: RestrictionType; -} +} /** * Represents a minion in system. diff --git a/backend/src/security/restrictions.ts b/backend/src/security/restrictions.ts index 1f8c5420..66aa237a 100644 --- a/backend/src/security/restrictions.ts +++ b/backend/src/security/restrictions.ts @@ -9,8 +9,8 @@ export interface Minion extends InternalMinion { } export interface MinionsRestrictionOptions { - /** The permission that not allowed by it to authorize access */ - restrictPermission: RestrictionType; + /** The permission required to access this resource */ + requirePermission: RestrictionType; /** The function argument index, to extract from it the minion id */ elementArgIndex: number; /** A function to extract from args payload the minion id */ @@ -24,7 +24,7 @@ export interface MinionsRestrictionOptions { * @param restrictPermission The permission that not allowed by it to authorize access * @returns The restriction object, if exists and has access. */ -async function validateMinionRestriction(user: User, minionId: string, restrictPermission: RestrictionType): Promise { +async function validateMinionRestriction(user: User, minionId: string, requirePermission: RestrictionType): Promise { // No restriction applied to admins if (user.scope === 'adminAuth') { return; @@ -40,8 +40,8 @@ async function validateMinionRestriction(user: User, minionId: string, restrictP return; } - // If user has write access, or action is blocked only for total blocked users, and user has read access - if (restriction.restrictionType === 'WRITE' || (restrictPermission === 'BLOCK' && restriction.restrictionType === 'READ')) { + // If user has write access, or action is allowed to who that has read access, and use has read access + if (restriction.restrictionType === 'WRITE' || (requirePermission === 'READ' && restriction.restrictionType === 'READ')) { return restriction; } @@ -60,7 +60,7 @@ async function validateMinionRestriction(user: User, minionId: string, restrictP async function sanitizeMinion(user: User, minion: Minion): Promise { // First make sure user has no restriction to access read this minion, then get the restriction object belong to the current user - const restriction = await validateMinionRestriction(user, minion.minionId, 'BLOCK'); + const restriction = await validateMinionRestriction(user, minion.minionId, 'READ'); // Copy the payload const minionCopy = DeepCopy(minion); // For now, show device id, and only hide the token @@ -84,7 +84,7 @@ async function sanitizeMinion(user: User, minion: Minion): Promise { * @param options The restriction validator options */ export const MinionsRestriction = (options: MinionsRestrictionOptions) => (target: any, key: string, descriptor: PropertyDescriptor) => { - const { restrictPermission: action, elementArgIndex, extractMinionIds } = options; + const { requirePermission: action, elementArgIndex, extractMinionIds } = options; const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) {