Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Requirements](#requirements)
- [Installation](#installation)
- [Business logic for Developers to know](#business-logic-for-developers-to-know)
- [Locks](#locks)
- [Configuration](#configuration)
- [O2Control gRPC](#o2control-grpc)
- [Apricot gRPC](#apricot-grpc)
Expand Down Expand Up @@ -55,6 +56,7 @@ It communicates with [Control agent](https://github.com/AliceO2Group/Control) ov
7. Open browser and navigate to http://localhost:8080

## [Business logic for Developers to know](./docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md)
## [Locks](./docs/LOCKS.md)
## Configuration
### O2Control gRPC
* `hostname` - gRPC hostname
Expand Down
69 changes: 69 additions & 0 deletions Control/docs/LOCKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Locks

## Overview

The locks functionality in Control/ECS GUI provides a mechanism to ensure exclusive access to detectors during operations. This prevents conflicts when multiple users attempt to configure or control the same detector simultaneously.

All LOCKs operations require an authenticated user session and moreover:
- Lock Operations require a role of minimum `Role.DETECTOR`.
- Lock Operations on `ALL` detectors require a minimum role of `Role.GLOBAL`.
- Forced Lock Operations require a role of minimum `Role.GLOBAL`.
- Forced Lock Operations on `ALL` detectors require a minimum role of `Role.ADMIN`.

## Lock Management

### Lock States

Each detector can be in one of the following lock states:
- **Released**: The detector is available to be locked by any user
- **Taken**: The detector is currently locked by a specific user

### Get Lock States

### Take Lock

Acquires a lock on a detector for the current user.

**Parameters**:
- `detectorId`: The individual detector identifier or `ALL` to lock all detectors (excluding TST)
- `action`: `TAKE`
- `shouldForce` (optional): Boolean flag to force taking the lock even if held by another user

**Behavior**:
- When `detectorId` is `ALL` and the user has a role of at least `GLOBAL`, locks all detectors **except** TST
- If a lock is already held by another user and:
- `shouldForce` is `false`, the request will fail.
- `shouldForce` is `true` and the user has a role of at least `GLOBAL` for individual detector and at least `ADMIN` for detector `ALL`, the lock will be taken regardless of current ownership

### Release Lock

Releases a lock on a detector.

**Parameters**:
- `detectorId`: The detector identifier or `ALL` to release all locks (excluding TST)
- `action`: `RELEASE`
- `shouldForce` (optional): Boolean flag to force releasing the lock even if held by another user

**Behavior**:
- When `detectorId` is `ALL` and the user has a role of at least `GLOBAL`, releases locks on all detectors **except** TST
- If a lock is already held by another user and:
- `shouldForce` is `false`, the request will fail.
- `shouldForce` is `true` and the user has a role of at least `GLOBAL` for individual detector and at least `ADMIN` for detector `ALL`, the lock will be released regardless of current ownership

## Special Cases

### TST Detector

The TST (test) detector is treated specially:
- When using `ALL` as the detector ID, TST is excluded from both TAKE and RELEASE operations
- TST must be taken/released explicitly by using `TST` as the detector ID
- Moreover, front-end pages are also:
- excluding TST from being displayed if on `Global` page
- displaying TST at the end with separator if on `+Create` or `Locks` pages

### Error Handling

The lock controller handles the following error cases:
- Missing `detectorId` parameter → Returns `InvalidInputError`
- Invalid `action` parameter → Returns `InvalidInputError`
- Lock conflicts (when not forcing) → Error from LockService
5 changes: 4 additions & 1 deletion Control/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ module.exports.setup = (http, ws) => {
http.post('/execute/o2-roc-config', coreMiddleware, (req, res) => ctrlService.createAutoEnvironment(req, res));

// Lock Service
http.get('/locks', lockController.getLocksStateHandler.bind(lockController));
http.get('/locks',
minimumRoleMiddleware(Role.DETECTOR),
lockController.getLocksStateHandler.bind(lockController)
);

http.put(`/locks/:action/${DetectorId.ALL}`,
minimumRoleMiddleware(Role.GLOBAL),
Expand Down
1 change: 1 addition & 0 deletions Control/lib/common/detectorId.enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
const DetectorId = Object.freeze({
ALL: 'ALL',
TST: 'TST',
});

exports.DetectorId = DetectorId;
38 changes: 21 additions & 17 deletions Control/lib/controllers/Lock.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ const { LogManager, LogLevel } = require('@aliceo2/web-ui');
const { updateAndSendExpressResponseFromNativeError, InvalidInputError } = require('@aliceo2/web-ui');

const { DetectorLockAction } = require('./../common/lock/detectorLockAction.enum.js');
const { DetectorId } = require('./../common/detectorId.enum.js');
const {User} = require('./../dtos/User.js');

const LOG_FACILITY = 'cog/log-ctrl';
const DETECTOR_ALL = 'ALL';

/**
* Controller for dealing with all API requests on actions and state of the locks used for detectors
Expand Down Expand Up @@ -70,27 +70,31 @@ class LockController {
}
const user = new User(username, name, personid, access);
if (action.toLocaleUpperCase() === DetectorLockAction.TAKE) {
if (detectorId === DETECTOR_ALL) {
Object.keys(this._lockService.locksByDetector).forEach((detector) => {
try {
this._lockService.takeLock(detector, user, shouldForce);
} catch (error) {
console.error(error);
}
});
if (detectorId === DetectorId.ALL) {
Object.keys(this._lockService.locksByDetector)
.filter((detector) => detector !== DetectorId.TST) // Skip TST detector when locking all
.forEach((detector) => {
try {
this._lockService.takeLock(detector, user, shouldForce);
} catch (error) {
console.error(error);
}
});
} else {
this._lockService.takeLock(detectorId, user, shouldForce);
}
res.status(200).json(this._lockService.locksByDetectorToJSON());
} else if (action.toLocaleUpperCase() === DetectorLockAction.RELEASE) {
if (detectorId === DETECTOR_ALL) {
Object.keys(this._lockService.locksByDetector).forEach((detector) => {
try {
this._lockService.releaseLock(detector, user, shouldForce);
} catch (error) {
console.error(error);
}
});
if (detectorId === DetectorId.ALL) {
Object.keys(this._lockService.locksByDetector)
.filter((detector) => detector !== DetectorId.TST) // Skip TST detector when releasing all
.forEach((detector) => {
try {
this._lockService.releaseLock(detector, user, shouldForce);
} catch (error) {
console.error(error);
}
});
} else {
this._lockService.releaseLock(detectorId, user, shouldForce);
}
Expand Down
12 changes: 11 additions & 1 deletion Control/test/api/lock/api-get-locks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/

const request = require('supertest');
const { ADMIN_TEST_TOKEN, TEST_URL } = require('../generateToken.js');
const { ADMIN_TEST_TOKEN, GUEST_TEST_TOKEN, TEST_URL } = require('../generateToken.js');

describe(`'API - GET - /locks' test suite`, () => {
before(async () => {
Expand Down Expand Up @@ -50,4 +50,14 @@ describe(`'API - GET - /locks' test suite`, () => {
message: 'Invalid JWT token provided'
});
});

it('should return unauthorized error for insufficient role token requests', async () => {
await request(`${TEST_URL}/api/locks`)
.get(`/?token=${GUEST_TEST_TOKEN}`)
.expect(403, {
status: 403,
message: 'Not enough permissions for this operation',
title: 'Unauthorized Access',
});
});
});
Loading