Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task/add access matches (about users, headers, path. .. ) to access Logger #544

Merged
merged 35 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e3d3ab0
add user name to access log
AlvaroVega Oct 4, 2024
632cfb9
fix userName when token is from Trust
AlvaroVega Oct 4, 2024
9b5d750
fix linter
AlvaroVega Oct 4, 2024
5f12d00
add userName to account InfoError
AlvaroVega Oct 4, 2024
72a9d5d
add path and body to account log
AlvaroVega Oct 4, 2024
efbb8f7
add check access match
AlvaroVega Oct 7, 2024
103ce8b
fix linter
AlvaroVega Oct 7, 2024
80f368b
add checks about body, subpaths and headers
AlvaroVega Oct 7, 2024
2ea4f83
add match value
AlvaroVega Oct 7, 2024
e893ca7
add found matches to wrong access also
AlvaroVega Oct 8, 2024
70ca4f5
clean javadoc
AlvaroVega Oct 8, 2024
7445593
Merge branch 'master' into task/add_access_match
AlvaroVega Oct 8, 2024
30e566e
update doc
AlvaroVega Oct 8, 2024
041d9fc
update CNR
AlvaroVega Oct 8, 2024
e491a13
add messages match·
AlvaroVega Oct 8, 2024
20e2610
update doc
AlvaroVega Oct 8, 2024
c939833
update example users
AlvaroVega Oct 8, 2024
b78220e
update doc
AlvaroVega Oct 8, 2024
b6d412a
add subqueries
AlvaroVega Oct 9, 2024
e762d49
Log query in accessLogger
AlvaroVega Oct 9, 2024
2f78d1d
update account log examples
AlvaroVega Oct 9, 2024
8bdd7b5
add accountMode
AlvaroVega Oct 9, 2024
a9135d0
generic header checker
AlvaroVega Oct 10, 2024
555301b
fix origin to (req.ip || req.connection.remoteAddress) to ensure head…
AlvaroVega Oct 10, 2024
4a0db45
update CNR and configAccessMatch file
AlvaroVega Oct 10, 2024
11d7487
just log message in accessLogger instead of full json with timestamp …
AlvaroVega Oct 10, 2024
7c77636
Homogenizes format for Access Logger with pep logs
AlvaroVega Oct 10, 2024
77bf0f0
update path of file configAccessMatch.js
AlvaroVega Oct 10, 2024
9f497ab
Update CHANGES_NEXT_RELEASE
AlvaroVega Oct 11, 2024
4308e23
Update README.md
fgalan Oct 11, 2024
504e01d
Update README.md
fgalan Oct 11, 2024
af04774
Apply suggestions from code review
fgalan Oct 11, 2024
10bfeeb
Apply suggestions from code review
fgalan Oct 11, 2024
692e17d
Update CHANGES_NEXT_RELEASE
fgalan Oct 11, 2024
6e954fe
Update operations.md
fgalan Oct 11, 2024
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
4 changes: 4 additions & 0 deletions CHANGES_NEXT_RELEASE
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
- Add: user name, request path, query and body to AccessLogger (#541)
- Add: Access Logger Patterns to match to Access Logger (#541)
- Fix: ensure Origin of AccesLogger show x-forwarderd-for header if exists
- Fix: Homogenizes format for Access Logger with pep logs
fgalan marked this conversation as resolved.
Show resolved Hide resolved
- Upgrade body-parser dep from 1.20.0 to 1.20.3
- Upgrade express from 4.19.2 to 4.20.0
69 changes: 63 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* [Development documentation](#development)

## <a name="overview"/> Overview
The Orion Policy Enforcement Point (PEP) is a proxy meant to secure independent FiWare components, by intercepting every request sent to the component, validating it against the Access Control component. This validation is based in several pieces of data:
The Policy Enforcement Point (PEP) is a proxy meant to secure independent FiWare components, by intercepting every request sent to the component, validating it against the Access Control component. This validation is based in several pieces of data:

* User token: comes from the OAuth authorization server and is taken from the `x-auth-token` header.
* ServiceId: is read from the `fiware-service` header and identifies the protected component.
Expand Down Expand Up @@ -439,23 +439,80 @@ In order to have the proxy running, there are several basic pieces of informatio
port: 7070,
path: '/pdp/v3',
account: false,
accountFile: '/tmp/pepAccount.log'
accountFile: '/tmp/pepAccount.log',
accountMode: 'all'
}
```
Accounting log is only activated when account flag is true, and the logs are produced in a fixed INFO level for accountLogger, redardless of the pep log level.
Note that accunting log is not rotate, so you should make sure you configure your own rotation system.
Accounting access log include daba about:
Accounting log is only activated when account flag is true, and the logs are produced in a fixed INFO level for accessLogger, redardless of the pep log level.
Note that accounting log is not rotated, so you should make sure you configure your own rotation system.
Accounting access log include data about:
* Attempt was right or not
* Token
* Origin
* UserId
* UserName
* ServiceId
* Service
* SubServiceId
* SubService
* Action
* Path
* Body
* Date
* Query
Example of access log:
fgalan marked this conversation as resolved.
Show resolved Hide resolved
```
Right Attempt | ResponseStatus=200 | Token=860864fb6d1a4c8a8cb7d59d16daaa52 | Origin=192.168.1.125 | UserId=62c63ada8694451fb67a341346172499 | ServiceId=a9b38dd2a97e4944b2daebdb74ed60ff | Service=smartgondor | SubServiceId=/ | SubService=/ | Action=read | Date=2017-09-21T12:46:57.844Z
"Right Attempt | ResponseStatus=200 | Token=gAAAAABnBPgPrgwpcAkbQOZIryu5ADUIScyorN3vbPYbTJxTE5AF3RO1y25Tf-sL3EKzvfr_1U3u8IL8ylB4e4B_vD5yZjc9rnrSIqoiC77B7uZ1O1xZCyukq_MkjRxJLqA9yQ5lQtAQCC6ig7Kn5uPhpPD-mhVb7kyQjUw1QjtCiyP7UKXZvKU | Origin=172.17.0.22 | UserId=753b954985bf460fabbd6953c71d50c7 | UserName=adm1 | ServiceId=9f710408f5944c3993db600810e97c83 | Service=smartcity | SubServiceId=/ | SubService=/ | Action=read | Path=/v2/entities | Query={\"limit\":\"15\",\"offset\":\"0\",\"options\":\"count\"} | Body={} | Date=2024-10-08T09:25:30.441Z"
```

fgalan marked this conversation as resolved.
Show resolved Hide resolved
Note that the above format is not the same than the regular PEP log (although it is also based in fields separated by `|`, the fields themselves are not the same).

Additionally a file configAccessMatch could be provided to pep to check matches about some elements involved in current access, regardless is right or not right access. For example:
* List for users involved
* List of headers and values
* List of subpaths in URL request
* List of subqueries in query request
* List of strings in body

fgalan marked this conversation as resolved.
Show resolved Hide resolved
PEP reloads this file each time it changes without needing restarting PEP itself.

This is an example of file `configAccessMatch.js` (full path `/opt/fiware-pep-steelskin/configAccessMatch.js` i.e. in a docker image):

```
// Activity related with a list of users
configAccessMatch.users = [
'cracker1', 'cracker2',
];

// Activity related with request which the following headers
configAccessMatch.headers = [
{ "fiware-service": "smartcity" },
{ "x-real-ip": "127.0.0.1" }
];

// Activity related with request including the following subpaths
configAccessMatch.subpaths = [
'/v1',
];

// Activity related with request including the following subqueries
configAccessMatch.subqueries = [
'flowControl', 'options',
];

// Activity related with request including the following strings in body
configAccessMatch.body = [
'legacy',
];
```

When any of theses patterns matches in current access, message access is added with `MATCHED <element> <value>` , where `<element>` would be: `USER`, `HEADER <header-name>`, `SUBPATH`, `SUBQUERY`, `BODY` and `<value>` the value which matches. For example:

```
Right Attempt MATCHED HEADER fiware-service smartcity | ResponseStatus=200 | Token=gAAAAABnBPgPrgwpcAkbQOZIryu5ADUIScyorN3vbPYbTJxTE5AF3RO1y25Tf-sL3EKzvfr_1U3u8IL8ylB4e4B_vD5yZjc9rnrSIqoiC77B7uZ1O1xZCyukq_MkjRxJLqA9yQ5lQtAQCC6ig7Kn5uPhpPD-mhVb7kyQjUw1QjtCiyP7UKXZvKU | Origin=172.17.0.22 | UserId=753b954985bf460fabbd6953c71d50c7 | UserName=adm1 | ServiceId=9f710408f5944c3993db600810e97c83 | Service=smartcity | SubServiceId=/ | SubService=/ | Action=read | Path=/v2/entities | Query={\"limit\":\"15\",\"offset\":\"0\",\"options\":\"count\"} | Body={} | Date=2024-10-08T09:25:30.441Z"
```
Account log has three modes: `all`, `matched`, `wrong`. First one `all` includes right and wrong access regardles if matches or not. Second one `matched` includes all wrong and just rigth matches acess. And `wrong` mode only includes all wrong access, regardless is matches or not with patterns.

```
* `config.componentName`: name of the component that will be used to compose the FRN that will identify the resource to be accessed. E.g.: `orion`.
* `config.resourceNamePrefix`: string prefix that will be used to compose the FRN that will identify the resource to be accessed. E.g.: `fiware:`.
Expand Down
4 changes: 4 additions & 0 deletions bin/pepProxy
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function loadConfiguration() {
'ACCESS_PROTOCOL',
'ACCESS_ACCOUNT',
'ACCESS_ACCOUNTFILE',
'ACCESS_ACCOUNTMODE',
'ADMIN_PORT',
'AUTHENTICATION_HOST',
'AUTHENTICATION_PORT',
Expand Down Expand Up @@ -103,6 +104,9 @@ function loadConfiguration() {
if (process.env.ACCESS_ACCOUNTFILE) {
config.access.accountFile = process.env.ACCESS_ACCOUNTFILE;
}
if (process.env.ACCESS_ACCOUNTMODE) {
config.access.accountMode = process.env.ACCESS_ACCOUNTMODE;
}
if (process.env.AUTHENTICATION_HOST) {
config.authentication.options.host = process.env.AUTHENTICATION_HOST;
}
Expand Down
6 changes: 5 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ config.access = {
/**
* Log Account file
*/
accountFile: '/tmp/pepAccount.log'
accountFile: '/tmp/pepAccount.log',
/**
* Account mode: `all`, `matched`, `wrong`
*/
accountMode: 'all'
};

// User identity configuration
Expand Down
35 changes: 35 additions & 0 deletions configAccessMatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

/**
* List of access match
*/
var configAccessMatch = {};

// Activity related with a list of users
configAccessMatch.users = [
// 'user1', 'user2',
];

// Activity related with request which the following headers
configAccessMatch.headers = [
// { "fiware-service": "smartcity" },
// { "x-real-ip": "127.0.0.1" }
];

// Activity related with request including the following subpaths
configAccessMatch.subpaths = [
// '/v1',
];

// Activity related with request including the following subqueries
configAccessMatch.subqueries = [
// 'flowControl',
];

// Activity related with request including the following strings in body
configAccessMatch.body = [
// 'legacy'
];


exports.configAccessMatch = configAccessMatch;
3 changes: 2 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ var constants = {
X_REAL_IP_HEADER: 'x-real-ip',
CORRELATOR_HEADER: 'fiware-correlator',

GET_ROLES_PATH: '/user'
GET_ROLES_PATH: '/user',
NA: 'N/A'
};


Expand Down
18 changes: 15 additions & 3 deletions lib/fiware-pep-steelskin.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,22 @@ function handleError(error, req, res, next) {
* @param {Function} next Call to the next error handler in the chain.
*/
function accountInfoError(error, req, res, next) {
accessLogger.info('Wrong Attempt' +
var accessMsg = 'Wrong Attempt';
accessMsg = proxyMiddleware.checkAccessMatches(req, accessMsg);
accessLogger.info(accessMsg +
' | Error=' + error.name +
' | Token=' + req.headers['x-auth-token'] +
' | Origin=' + req.connection.remoteAddress +
' | Origin=' + (req.ip || req.connection.remoteAddress) +
' | UserId=' + req.userId +
' | ServiceId=' + req.serviceId +
' | UserName=' + req.userName +
' | Service=' + req.service +
' | SubServiceId=' + req.subserviceId +
' | SubService=' + req.subService +
' | Action=' + req.action +
' | Path=' + req.path +
' | Query=' + JSON.stringify(req.query) +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

En el fichero lib/middleware/proxy.js se trunca el body a 100 caracteres ( ' | Body=' + JSON.stringify(req.body).slice(0, 100) + // not all body), y aquí no, ¿cual es el motivo?

Copy link
Contributor

@rg2011 rg2011 Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hablado con Álvaro, por lo visto se loga el body entero cuando hay error, y solo una parte cuando no lo hay. NTC

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El body se trunca a 100 caracteres en caso de acceso valido y se deja entero en caso de error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faltaba documentarlo: #545

' | Body=' + JSON.stringify(req.body) +
' | Date=' + new Date().toJSON());
next(error);
}
Expand Down Expand Up @@ -114,10 +122,14 @@ function setAccessLogger() {
level: 'info',
transports: [
new(winston.transports.File)({
filename: config.access.accountFile
filename: config.access.accountFile,
json: false,
timestamp: false,
showLevel: false
})
]
});
proxyMiddleware.watchConfigAccessMatchFile();
}

/**
Expand Down
103 changes: 91 additions & 12 deletions lib/middleware/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,30 @@ var config = require('../../config'),
'x-auth-token'
],
winston = require('winston'),
logger = require('logops'),
configAccessMatch = require('../../configAccessMatch.js').configAccessMatch,
accessLogger;

const fs = require('fs');
const configAccessMatchFilePath = './configAccessMatch.js';

function requireUncached(module) {
delete require.cache[require.resolve(module)];
return require(module);
}

function watchConfigAccessMatchFile() {
fs.watch(configAccessMatchFilePath, (event, filename) => {
logger.info('watchConfigAccessMatchFile changed by %s detected in file %s', event, filename);
try {
configAccessMatch = requireUncached('../../configAccessMatch.js').configAccessMatch;
logger.debug('reloaded configAccessMatch %j', configAccessMatch);
} catch (err) {
logger.error('Error %s reloading module: %s ', err, filename);
}
});
}

/**
* Middleware to extract the organization data from the request.
*
Expand Down Expand Up @@ -175,6 +197,44 @@ function sendRequest(req, res, next) {
next();
}

/**
* Check here MATCH file patterns
*
* @param {Object} req Incoming request.
* @param {String} accessMsg Incoming accessMsg
* @return {String} String message corresponding with accessMsg and found matches
*/
function checkAccessMatches(req, accessMsg) {
if (req.userName in configAccessMatch.users ) {
accessMsg += ' MATCHED USER ' + req.userName;
}
for (var header of configAccessMatch.headers) {
var headerName = Object.keys(header)[0];
if (Object.keys(req.headers).includes(headerName)) {
if (req.headers[headerName] === header[headerName]) {
accessMsg += ' MATCHED HEADER ' + headerName + ' ' + header[headerName];
}
}
}
for (var subpath of configAccessMatch.subpaths) {
if (req.path.includes(subpath)) {
accessMsg += ' MATCHED SUBPATH ' + subpath;
}
}
for (var subquery of configAccessMatch.subqueries) {
if (JSON.stringify(req.query).includes(subquery)) {
accessMsg += ' MATCHED SUBQUERY ' + subquery;
}
}
for (var text of configAccessMatch.body) {
if (JSON.stringify(req.body).includes(text)) {
accessMsg += ' MATCHED BODY ' + text;
}
}
return accessMsg;
}


/**
* Account Log
* read to guess its type.
Expand All @@ -190,23 +250,40 @@ function accountInfo(req, res, next) {
level: 'info',
transports: [
new(winston.transports.File)({
filename: config.access.accountFile
filename: config.access.accountFile,
json: false,
timestamp: false,
showLevel: false
})
]
});
}
req.fwdResponse = req.fwdResponse.on('response', function(res) {
accessLogger.info('Right Attempt' +
' | ResponseStatus=' + req.fwdResponse.response.statusCode +
' | Token=' + req.headers['x-auth-token'] +
' | Origin=' + req.connection.remoteAddress +
' | UserId=' + req.userId +
' | ServiceId=' + req.serviceId +
' | Service=' + req.service +
' | SubServiceId=' + req.subserviceId +
' | SubService=' + req.subService +
' | Action=' + req.action +
' | Date=' + new Date().toJSON());
var accessMsgOrig = 'Right Attempt';
var accessMsg = accessMsgOrig;
if (! ['wrong'].includes(config.access.accountMode) ) {
accessMsg = checkAccessMatches(req, accessMsgOrig);
}
if ( ['all'].includes(config.access.accountMode) ||
(['matched'].includes(config.access.accountMode) &&
accessMsgOrig.length < accessMsg.length) ){

accessLogger.info(accessMsg +
' | ResponseStatus=' + req.fwdResponse.response.statusCode +
' | Token=' + req.headers['x-auth-token'] +
' | Origin=' + (req.ip || req.connection.remoteAddress) +
' | UserId=' + req.userId +
' | UserName=' + req.userName +
' | ServiceId=' + req.serviceId +
' | Service=' + req.service +
' | SubServiceId=' + req.subserviceId +
' | SubService=' + req.subService +
' | Action=' + req.action +
' | Path=' + req.path +
' | Query=' + JSON.stringify(req.query) +
' | Body=' + JSON.stringify(req.body).slice(0, 100) + // not all body
' | Date=' + new Date().toJSON());
}
});
}
next();
Expand Down Expand Up @@ -257,3 +334,5 @@ exports.sendResponse = sendResponse;
exports.accountInfo = accountInfo;
exports.checkMandatoryHeaders = checkMandatoryHeaders(validationHeaders);
exports.checkAuthorizationHeader = checkMandatoryHeaders(authorizationHeaders);
exports.watchConfigAccessMatchFile = watchConfigAccessMatchFile;
exports.checkAccessMatches = checkAccessMatches;
11 changes: 8 additions & 3 deletions lib/services/keystoneAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ function retrieveUser(req, callback) {
req.serviceId = cachedValue.serviceId;
req.domainName = cachedValue.domainName;
req.userId = cachedValue.userId;
req.userName = cachedValue.userName;

logger.debug('User value processed with value: %j', cachedValue);

Expand Down Expand Up @@ -245,9 +246,12 @@ function retrieveUser(req, callback) {
cachedValue = {
domainName: body.token.project.domain.name,
serviceId: body.token.project.domain.id,
userId: body.token['OS-TRUST:trust'].trustor_user.id
userId: body.token['OS-TRUST:trust'].trustor_user.id,
userName: constants.NA
};

if (body.token.user) {
cachedValue.userName = body.token.user.name;
}
innerCb(null, cachedValue);

} else if (body.token && body.token.user && body.token.user.domain &&
Expand All @@ -256,7 +260,8 @@ function retrieveUser(req, callback) {
cachedValue = {
domainName: body.token.user.domain.name,
serviceId: body.token.user.domain.id,
userId: body.token.user.id
userId: body.token.user.id,
userName: body.token.user.name
};

req.userData = cachedValue;
Expand Down
2 changes: 2 additions & 0 deletions operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ curl -X GET "http://localhost:11211/admin/log"

Every error message is identified with a prefix code in brackets. The code convention can be found in Apendix A.

There is another log about accounting access which is produced when access.account flag is enabled and is done over access.accountFile file. This accounting access logs run always in the a fixed `info` level and logs right and wrong access attempts, providing for each one user, service, subservice, path, date and other relevant info about access. Additionallys some patterns could be configured in order to mark some of these access.
fgalan marked this conversation as resolved.
Show resolved Hide resolved

## <a name="cache"/> Cache management

PEP keeps a memory cache with some access about roles, domains, users and subservices. Related with this info is possible
Expand Down
Loading