Skip to content

Commit acbc3f3

Browse files
authored
Merge pull request #583 from telefonicaid/fix/local_pdp_permit_when_any_role
Fix/local pdp not stop at first match
2 parents 8f6b9d5 + d4a9f39 commit acbc3f3

3 files changed

Lines changed: 58 additions & 31 deletions

File tree

CHANGES_NEXT_RELEASE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
- Upgrade express from 4.21.2 to 4.22.1
2+
- Fix: local PDP evaluates all roles and grants access if any applicable role permits the action, not stop at first match (#582)

lib/services/pdp.js

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,14 @@ const ACTION_MAP = {
5050

5151

5252
/**
53-
* Helper para cacheo y respuesta
53+
* Helper for caching and response
5454
*/
5555
function cacheAndReturn(cacheKey, decision, callback) {
5656
const finalDecision = decision || 'Invalid';
5757
cacheUtils.get().data.validation.set(cacheKey, finalDecision);
5858
callback(null, finalDecision);
5959
}
6060

61-
6261
/**
6362
* validation request using local PDP implementation
6463
*/
@@ -77,47 +76,51 @@ function validationRequest(logger, roles, frn, action, headers, callback) {
7776
return callback(new Error('Invalid FRN format'));
7877
}
7978

80-
const component = frnParts[1].toUpperCase(); // ORION, PERSEO, IOTA, STH
81-
const subserviceRaw = frnParts[3]; // "/tourism", "///", etc
79+
const component = frnParts[1].toUpperCase(); // ORION, PERSEO, IOTAGENT, STH
80+
const subserviceRaw = frnParts[3]; // "/tourism", "/", etc
8281
const subservice = subserviceRaw.replace(/\//g, '') || null; // "tourism" o null
8382

8483
const isServiceOperation = subservice === null;
8584
const isSubserviceOperation = subservice !== null;
8685

8786
// 2. For each role: get roleType and component
88-
let matchedRole = null;
87+
let hasMatchingRole = false;
88+
let isPermitted = false;
8989

9090
for (const role of roles) {
9191
const name = (role.name || '').trim();
9292

9393
// if name role with '#', then get right part; otherwise name as is
9494
const hashParts = name.split('#');
9595
const roleInfoRaw = (hashParts.length === 2 ? hashParts[1] : hashParts[0]).trim();
96+
9697
// Alias: admin (without #) = ServiceAdmin for all components
9798
const roleInfo = /^admin$/i.test(roleInfoRaw) ? 'ServiceAdmin' : roleInfoRaw;
9899

99-
// Try extrat type and component (i.e.: ServiceCustomerORION)
100+
// Try extract type and component (i.e.: ServiceCustomerORION)
100101
let match = roleInfo.match(
101-
/(ServiceCustomer|ServiceAdmin|SubServiceCustomer|SubServiceAdmin)([A-Z]+)$/i
102+
/^(ServiceCustomer|ServiceAdmin|SubServiceCustomer|SubServiceAdmin)([A-Z]+)$/i
102103
);
103104

104-
let roleType, roleComponent;
105+
let roleType;
106+
let roleComponent;
105107

106-
// Case 1: rol includes component
108+
// Case 1: role includes component
107109
if (match) {
108110
roleType = match[1];
109111
roleComponent = match[2].toUpperCase();
110112
}
111-
// Caso 2: rol does NOT includes component -> apply over all components
113+
// Case 2: role does NOT include component -> apply over all components
112114
else {
113115
match = roleInfo.match(
114116
/^(ServiceCustomer|ServiceAdmin|SubServiceCustomer|SubServiceAdmin)$/i
115117
);
118+
116119
if (!match) {
117120
continue;
118121
}
119122

120-
roleType = match[1];
123+
roleType = match[1];
121124
roleComponent = 'ANY';
122125
}
123126

@@ -137,27 +140,20 @@ function validationRequest(logger, roles, frn, action, headers, callback) {
137140
continue;
138141
}
139142

140-
matchedRole = { roleType, roleComponent };
141-
break;
142-
} // end for
143-
144-
if (!matchedRole) {
145-
cacheAndReturn(cacheKey, 'Deny', callback);
146-
return;
147-
}
148-
149-
// 3. Final decision
150-
const { roleType } = matchedRole;
151-
const ROLE_CLASS = roleType.endsWith('Customer') ? 'CUSTOMER' : 'ADMIN';
143+
hasMatchingRole = true;
152144

153-
let allowedActions = [];
145+
// 3) Check whether THIS role permits the action
146+
const ROLE_CLASS = roleType.endsWith('Customer') ? 'CUSTOMER' : 'ADMIN';
147+
const allowedActions =
148+
(ACTION_MAP[component] && ACTION_MAP[component][ROLE_CLASS]) || [];
154149

155-
if (ACTION_MAP[component] && ACTION_MAP[component][ROLE_CLASS]) {
156-
allowedActions = ACTION_MAP[component][ROLE_CLASS];
150+
if (allowedActions.includes(action)) {
151+
isPermitted = true;
152+
break; // one permitting role is enough
153+
}
157154
}
158155

159-
const decision = allowedActions.includes(action) ? 'Permit' : 'Deny';
160-
156+
const decision = (hasMatchingRole && isPermitted) ? 'Permit' : 'Deny';
161157
cacheAndReturn(cacheKey, decision, callback);
162158

163159
} catch (err) {
@@ -166,5 +162,4 @@ function validationRequest(logger, roles, frn, action, headers, callback) {
166162
}
167163
}
168164

169-
170165
exports.validationRequest = validationRequest;

test/unit/validate_user_local_pdp_action_test.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ describe('Local PDP validationRequest decision tree', function () {
133133
.catch(done);
134134
});
135135

136+
it('admin and ServiceCustomer without component can CREATE in ORION', function (done) {
137+
runValidation({
138+
roles: [{ id: '1', name: 'x#ServiceCustomer' },
139+
{ id: '2', name: 'admin' }
140+
],
141+
frn: 'fiware:orion:smartcity:/:::',
142+
action: 'create'
143+
})
144+
.then(function (decision) {
145+
decision.should.equal('Permit');
146+
done();
147+
})
148+
.catch(done);
149+
});
150+
136151
it('ServiceAdminORION can DELETE in ORION', function (done) {
137152
runValidation({
138153
roles: [{ id: '1', name: 'x#ServiceAdminORION' }],
@@ -146,9 +161,24 @@ describe('Local PDP validationRequest decision tree', function () {
146161
.catch(done);
147162
});
148163

164+
it('ServiceCustomer and ServiceAdminORION can DELETE in ORION', function (done) {
165+
runValidation({
166+
roles: [{ id: '1', name: 'x#ServiceAdminORION' },
167+
{ id: '2', name: 'x#ServiceCustomer' }],
168+
frn: 'fiware:orion:smartcity:/:::',
169+
action: 'delete'
170+
})
171+
.then(function (decision) {
172+
decision.should.equal('Permit');
173+
done();
174+
})
175+
.catch(done);
176+
});
177+
149178
it('ServiceAdminORION cannot operate on PERSEO', function (done) {
150179
runValidation({
151-
roles: [{ id: '1', name: 'x#ServiceAdminORION' }],
180+
roles: [{ id: '1', name: 'x#ServiceAdminORION' },
181+
{ id: '3', name: 'x#ServiceAdminSTH' } ],
152182
frn: 'fiware:perseo:smartcity:/:::',
153183
action: 'readRule'
154184
})
@@ -161,7 +191,8 @@ describe('Local PDP validationRequest decision tree', function () {
161191

162192
it('SubServiceCustomer can READ ORION at subservice level', function (done) {
163193
runValidation({
164-
roles: [{ id: '1', name: 'x#SubServiceCustomer' }],
194+
roles: [{ id: '4', name: 'x#SubServiceCustomerPERSEO' },
195+
{ id: '1', name: 'x#SubServiceCustomer' }],
165196
frn: 'fiware:orion:smartcity:/tourism:::',
166197
action: 'read'
167198
})

0 commit comments

Comments
 (0)