Skip to content

Commit 7b4711b

Browse files
authored
Merge pull request #572 from telefonicaid/task/add_pdp_validation
Task/add pdp validation
2 parents 0c246bf + f196f39 commit 7b4711b

12 files changed

Lines changed: 509 additions & 43 deletions

CHANGES_NEXT_RELEASE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
- Add: allow authorize using local PDP implementation (new config.localPDP / AUTHORIZE_BY_LOCAL_PDP setting) (#571)
2+
- Add: get roles from keystone and cache them using role names (#571)
13
- Remove node cache legacy callbacks warning
24
- Fix: fill from log field with real IP
35
- Upgrade NodeJS version from 16-slim to 24-bullseye-slim in Dockerfile

README.md

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@ Account log has three modes: `all`, `matched`, `wrong`. First one `all` includes
519519
* `config.bypassRoleId`: ID of the role that will be considered to have administrative rights over the proxy (so being transparently proxied without validation). Valid values are Role UUIDs. E.g.: `db50362d5f264c8292bebdb5c5783741`.
520520
* `config.dieOnRedirectError`: this flags changes the behavior of the PEP Proxy when an error is received when redirecting a request. If the flag is true, the PEP Proxy process is shut down immediately; if it is false, the behavior is the usual: generate a 501 Code error.
521521
* `config.bodyLimit`: Controls the maximum request body size allowed, in bytes. Default is 1 Mb
522+
* `config.localPDP`: Use local implementation for validate PDP (Policy Decision Point) or not. This validation is done by the logic in pdp.js file (out of scope of this documentation). Default is false
522523
523524
### Authentication configuration
524525
* `config.authentication.checkHeaders`: when the proxy is working with the access control disabled (just user authentication), indicates whether the `fiware-service` and `fiware-servicepath` headers should be checked for existance and validity (checking: the headers exist, thy are not empty and the user is really part of the service and subservice mentioned in the header). This option is ignored when authorization is enabled, and considered to be `true` (as the headers constitute a mandatory part of the authorization process). Default value is `true`.
@@ -550,33 +551,34 @@ The environment variables provide ways of configuring the plugin without taking
550551
### Configuration based on environment variables
551552
Some of the configuration values for the attributes above mentioned can be overriden with values in environment variables. The following table shows the environment variables and what attribute they map to.
552553
553-
| Environment variable | Configuration attribute |
554-
|:-------------------- |:----------------------------------- |
555-
| PROXY_PORT | config.resource.proxy.port |
556-
| ADMIN_PORT | config.resource.proxy.adminPort |
557-
| TARGET_HOST | config.resource.original.host |
558-
| TARGET_PORT | config.resource.original.port |
559-
| LOG_LEVEL | config.logLevel |
560-
| ACCESS_DISABLE | config.access.disable |
561-
| ACCESS_HOST | config.access.host |
562-
| ACCESS_PORT | config.access.port |
563-
| ACCESS_PROTOCOL | config.access.protocol |
564-
| ACCESS_ACCOUNT | config.access.account |
565-
| ACCESS_ACCOUNTFILE | config.access.accountFile |
566-
| ACCESS_ACCOUNTMODE | config.access.accountMode |
567-
| AUTHENTICATION_HOST | config.authentication.options.host |
568-
| AUTHENTICATION_PORT | config.authentication.options.port |
569-
| AUTHENTICATION_PROTOCOL | config.authentication.options.protocol |
570-
| AUTHENTICATION_CACHE_PROJECTIDS | config.authentication.cacheTTLs.projectIds |
571-
| AUTHENTICATION_CACHE_ROLES | config.authentication.cacheTTLs.roles |
572-
| AUTHENTICATION_CACHE_USERS | config.authentication.cacheTTLs.users |
573-
| AUTHENTICATION_CACHE_VALIDATION | config.authentication.cacheTTLs.validation |
574-
| PROXY_USERNAME | config.authentication.user |
575-
| PROXY_PASSWORD | config.authentication.password |
576-
| PROXY_PASSWORD | config.authentication.password |
577-
| COMPONENT_NAME | config.componentName |
578-
| COMPONENT_PLUGIN | config.middlewares and config.componentName if no COMPONENT_NAME provided |
579-
| BODY_LIMIT | config.bodyLimit |
554+
| Environment variable | Configuration attribute |
555+
|:--------------------------------|:--------------------------------------------------------------------------|
556+
| PROXY_PORT | config.resource.proxy.port |
557+
| ADMIN_PORT | config.resource.proxy.adminPort |
558+
| TARGET_HOST | config.resource.original.host |
559+
| TARGET_PORT | config.resource.original.port |
560+
| LOG_LEVEL | config.logLevel |
561+
| ACCESS_DISABLE | config.access.disable |
562+
| ACCESS_HOST | config.access.host |
563+
| ACCESS_PORT | config.access.port |
564+
| ACCESS_PROTOCOL | config.access.protocol |
565+
| ACCESS_ACCOUNT | config.access.account |
566+
| ACCESS_ACCOUNTFILE | config.access.accountFile |
567+
| ACCESS_ACCOUNTMODE | config.access.accountMode |
568+
| AUTHENTICATION_HOST | config.authentication.options.host |
569+
| AUTHENTICATION_PORT | config.authentication.options.port |
570+
| AUTHENTICATION_PROTOCOL | config.authentication.options.protocol |
571+
| AUTHENTICATION_CACHE_PROJECTIDS | config.authentication.cacheTTLs.projectIds |
572+
| AUTHENTICATION_CACHE_ROLES | config.authentication.cacheTTLs.roles |
573+
| AUTHENTICATION_CACHE_USERS | config.authentication.cacheTTLs.users |
574+
| AUTHENTICATION_CACHE_VALIDATION | config.authentication.cacheTTLs.validation |
575+
| PROXY_USERNAME | config.authentication.user |
576+
| PROXY_PASSWORD | config.authentication.password |
577+
| PROXY_PASSWORD | config.authentication.password |
578+
| COMPONENT_NAME | config.componentName |
579+
| COMPONENT_PLUGIN | config.middlewares and config.componentName if no COMPONENT_NAME provided |
580+
| BODY_LIMIT | config.bodyLimit |
581+
| AUTHORIZE_BY_LOCAL_PDP | config.localPDP |
580582
581583
### Component configuration
582584
A special environment variable, called `COMPONENT_PLUGIN` can be set with one of this values: `orion`, `perseo`, `keypass` and `rest`. This variable can be used to select what component plugin to load in order to determine the action of the incoming requests. This variable also rewrites `config.componentName` configuration paramenter.

bin/pepProxy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ function loadConfiguration() {
6262
'COMPONENT_PLUGIN',
6363
'COMPONENT_NAME',
6464
'BODY_LIMIT',
65-
'DISABLE_DOMAIN_MIDDLEWARE'
65+
'DISABLE_DOMAIN_MIDDLEWARE',
66+
'AUTHORIZE_BY_LOCAL_PDP'
6667
];
6768

6869
for (var i = 0; i < environmentValues.length; i++) {
@@ -152,6 +153,9 @@ function loadConfiguration() {
152153
if (process.env.BODY_LIMIT) {
153154
config.bodyLimit = process.env.BODY_LIMIT;
154155
}
156+
if (process.env.AUTHORIZE_BY_LOCAL_PDP) {
157+
config.localPDP = process.env.AUTHORIZE_BY_LOCAL_PDP == 'true';
158+
}
155159
}
156160

157161
loadConfiguration();

config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ config.authentication = {
9393
protocol: 'http',
9494
host: 'localhost',
9595
port: 5000,
96-
path: '/v3/role_assignments',
96+
path: '/v3/role_assignments?include_names',
9797
authPath: '/v3/auth/tokens'
9898
}
9999
};
@@ -174,6 +174,11 @@ config.bypass = false;
174174
*/
175175
config.bypassRoleId = '';
176176

177+
/**
178+
* Uses local PDP (policy decision point) for validate actions instead of remote PDP.
179+
*/
180+
config.localPDP = false;
181+
177182
/**
178183
* Configures the maximum number of clients that can be simultaneously queued while waiting for the PEP to
179184
* authenticate itself against Keystone (due to an expired token).

lib/services/cacheUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function cacheAndHold(logger, cacheType, cacheKey, retrieveRequestFn, processVal
121121
}
122122

123123
if (cachedValue) {
124-
logger.debug('Value found in the cache [%s] for key [%s]: %s', cacheType, cacheKey, cachedValue);
124+
logger.debug('Value found in the cache [%s] for key [%s]: %j', cacheType, cacheKey, cachedValue);
125125

126126
processValueFn(cachedValue, callback);
127127
} else if (cache.updating[cacheType][cacheKey]) {

lib/services/keystoneAuth.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,17 @@ function getRolesFromResponse(logger, rawBody, service, subservice, cacheKey) {
8484
if (body.role_assignments[i].scope &&
8585
body.role_assignments[i].scope.domain &&
8686
String(body.role_assignments[i].scope.domain.id) === String(service)) {
87-
logger.debug('Role assignment [%s] accepted', body.role_assignments[i].role.id);
88-
roles.push(body.role_assignments[i].role.id);
87+
logger.debug('Role ID assignment [%s] accepted', body.role_assignments[i].role.id);
88+
logger.debug('Role NAME assignment [%s] accepted', body.role_assignments[i].role.name);
89+
roles.push({id: body.role_assignments[i].role.id, name: body.role_assignments[i].role.name});
8990
}
9091
} else {
9192
if (body.role_assignments[i].scope &&
9293
body.role_assignments[i].scope.project &&
9394
String(body.role_assignments[i].scope.project.id) === String(subservice)) {
94-
logger.debug('Role assignment [%s] accepted', body.role_assignments[i].role.id);
95-
roles.push(body.role_assignments[i].role.id);
95+
logger.debug('Role ID assignment [%s] accepted', body.role_assignments[i].role.id);
96+
logger.debug('Role NAME assignment [%s] accepted', body.role_assignments[i].role.name);
97+
roles.push({id: body.role_assignments[i].role.id, name: body.role_assignments[i].role.name});
9698
}
9799
}
98100
}
@@ -116,6 +118,7 @@ function retrieveRoles(req, callback) {
116118
cacheKey = userId + ':' + subserviceId;
117119

118120
function processValue(cachedValue, innerCb) {
121+
logger.debug('Roles value processed with value: %j', cachedValue);
119122
innerCb(null, cachedValue);
120123
}
121124

lib/services/pdp.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U
3+
*
4+
* This file is part of fiware-pep-steelskin
5+
*
6+
* fiware-pep-steelskin is free software: you can redistribute it and/or
7+
* modify it under the terms of the GNU Affero General Public License as
8+
* published by the Free Software Foundation, either version 3 of the License,
9+
* or (at your option) any later version.
10+
*
11+
* fiware-pep-steelskin is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14+
* See the GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public
17+
* License along with fiware-pep-steelskin.
18+
* If not, seehttp://www.gnu.org/licenses/.
19+
*
20+
* For those usages not covered by the GNU Affero General Public License
21+
* please contact with::[iot_support@tid.es]
22+
*/
23+
24+
'use strict';
25+
26+
var cacheUtils = require('./cacheUtils');
27+
28+
/**
29+
* Allowed actions by component and role type
30+
*/
31+
const ACTION_MAP = {
32+
ORION: {
33+
CUSTOMER: ['read', 'subscribe', 'discover', 'subscribe-availability'],
34+
ADMIN: ['read', 'create', 'update', 'delete', 'notify', 'register',
35+
'discover', 'subscribe', 'subscribe-availability']
36+
},
37+
PERSEO: {
38+
CUSTOMER: ['readRule'],
39+
ADMIN: ['readRule', 'writeRule', 'notify']
40+
},
41+
IOTAGENT: {
42+
CUSTOMER: ['read'],
43+
ADMIN: ['create', 'delete', 'update', 'read']
44+
},
45+
STH: {
46+
CUSTOMER: ['read'],
47+
ADMIN: ['create', 'delete', 'update', 'read']
48+
}
49+
};
50+
51+
52+
/**
53+
* Helper para cacheo y respuesta
54+
*/
55+
function cacheAndReturn(cacheKey, decision, callback) {
56+
const finalDecision = decision || 'Invalid';
57+
cacheUtils.get().data.validation.set(cacheKey, finalDecision);
58+
callback(null, finalDecision);
59+
}
60+
61+
62+
/**
63+
* validation request using local PDP implementation
64+
*/
65+
function validationRequest(logger, roles, frn, action, headers, callback) {
66+
67+
logger.debug('pdp.validation with roles: %j, frn: %j, action: %j and headers %j',
68+
roles, frn, action, headers);
69+
const cacheKey = frn + '#' + action + '#' + roles.map(r => r.name).join('-');
70+
71+
try {
72+
// 1. parse FRN -> component, service and subservice
73+
// Format example: fiware:orion:smartcity:/tourism:::
74+
const frnParts = frn.split(':');
75+
76+
if (frnParts.length < 4) {
77+
return callback(new Error('Invalid FRN format'));
78+
}
79+
80+
const component = frnParts[1].toUpperCase(); // ORION, PERSEO, IOTA, STH
81+
const subserviceRaw = frnParts[3]; // "/tourism", "///", etc
82+
const subservice = subserviceRaw.replace(/\//g, '') || null; // "tourism" o null
83+
84+
const isServiceOperation = subservice === null;
85+
const isSubserviceOperation = subservice !== null;
86+
87+
// 2. For each role: get roleType and component
88+
let matchedRole = null;
89+
90+
for (const role of roles) {
91+
const name = role.name || '';
92+
const parts = name.split('#');
93+
if (parts.length !== 2) {
94+
continue;
95+
}
96+
97+
const roleInfo = parts[1];
98+
99+
// Try extrat type and component (i.e.: ServiceCustomerORION)
100+
let match = roleInfo.match(
101+
/(ServiceCustomer|ServiceAdmin|SubServiceCustomer|SubServiceAdmin)([A-Z]+)$/i
102+
);
103+
104+
let roleType, roleComponent;
105+
106+
// Case 1: rol includes component
107+
if (match) {
108+
roleType = match[1];
109+
roleComponent = match[2].toUpperCase();
110+
}
111+
// Caso 2: rol does NOT includes component -> apply over all components
112+
else {
113+
match = roleInfo.match(
114+
/^(ServiceCustomer|ServiceAdmin|SubServiceCustomer|SubServiceAdmin)$/i
115+
);
116+
if (!match) {
117+
continue;
118+
}
119+
120+
roleType = match[1];
121+
roleComponent = 'ANY';
122+
}
123+
124+
// 1) Component matches (or ANY)
125+
if (roleComponent !== 'ANY' && roleComponent !== component) {
126+
continue;
127+
}
128+
129+
// 2) Matches at service/subservice level
130+
const roleIsService = roleType.startsWith('Service');
131+
const roleIsSubservice = roleType.startsWith('SubService');
132+
133+
if (isServiceOperation && !roleIsService) {
134+
continue;
135+
}
136+
if (isSubserviceOperation && !roleIsSubservice) {
137+
continue;
138+
}
139+
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';
152+
153+
let allowedActions = [];
154+
155+
if (ACTION_MAP[component] && ACTION_MAP[component][ROLE_CLASS]) {
156+
allowedActions = ACTION_MAP[component][ROLE_CLASS];
157+
}
158+
159+
const decision = allowedActions.includes(action) ? 'Permit' : 'Deny';
160+
161+
cacheAndReturn(cacheKey, decision, callback);
162+
163+
} catch (err) {
164+
logger.error('Validation exception', err);
165+
callback(err);
166+
}
167+
}
168+
169+
170+
exports.validationRequest = validationRequest;

lib/services/validation.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var request = require('request'),
3535
fs = require('fs'),
3636
path = require('path'),
3737
cacheUtils = require('./cacheUtils'),
38+
pdp = require('./pdp'),
3839
requestTemplate,
3940
roleTemplate;
4041

@@ -46,7 +47,7 @@ var request = require('request'),
4647
* @param {String} roles List of the roles of the user.
4748
* @param {String} organization Name of the organization with the frn format.
4849
* @param {String} action Name of the action the request is trying to execute.
49-
g */
50+
*/
5051
function createAccessRequest(logger, roles, organization, action, callback) {
5152
var parameters = {
5253
organization: organization,
@@ -219,7 +220,12 @@ function validationProcess(req, res, next) {
219220
if (req.corr) {
220221
req.headers[constants.CORRELATOR_HEADER] = req.corr;
221222
}
222-
validationRequest(logger, req.roles, req.frn, req.action, req.headers, handleValidation);
223+
if (config.localPDP) {
224+
pdp.validationRequest(logger, req.roles, req.frn, req.action, req.headers, handleValidation);
225+
} else {
226+
var rolesId = req.roles.map(r => r.id);
227+
validationRequest(logger, rolesId, req.frn, req.action, req.headers, handleValidation);
228+
}
223229
}
224230
}
225231

@@ -255,7 +261,7 @@ function loadTemplates(callback) {
255261
function checkBypass(req, res, next) {
256262
if (config.bypass && config.bypassRoleId && req.roles) {
257263
for (var i = 0; i < req.roles.length; i++) {
258-
if (req.roles[i] === config.bypassRoleId) {
264+
if (req.roles[i].id === config.bypassRoleId) {
259265
req.bypass = true;
260266
}
261267
}

test/keystoneResponses/rolesOfUser.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
}
88
},
99
"role": {
10-
"id": 8907
10+
"id": 8907,
11+
"name": "ServiceAdmin"
1112
},
1213
"user": {
1314
"id": 2836
@@ -22,4 +23,4 @@
2223
"previous": null,
2324
"next": null
2425
}
25-
}
26+
}

0 commit comments

Comments
 (0)