Add "request.id" to file audit logs #35536
Conversation
This generates a synthesized "id" for each incoming request that is included in the audit logs (file only). This id can be used to correlate events for the same request (e.g. authentication success with access granted). This request.id is specific to the audit logs and is not used for any other purpose
Time based uuids are good for lucene ids, but are not great for audit request.id For a request.id we want a generation system where consequative ids arevisually distinct, so that it is easy to spot correlations.
|
Pinging @elastic/es-security |
| if (Operations.subsetOf(aliasPermissions, indexPermissions) == false) { | ||
| // TODO we've already audited a access granted event so this is going to look ugly | ||
| auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); | ||
| auditTrailService.accessDenied(null, authentication, action, request, userPermissions.names()); |
There was a problem hiding this comment.
I prefer us to be explicit here: threadContext.getTransient(AUDIT_REQUEST_ID).
It is a case of tamper request , if audit request Id is missing.
There was a problem hiding this comment.
I think, in this case, it's better to be null, because it's already a denial.
I'd rather fail on the denied rather than fail on a tampered request.
There was a problem hiding this comment.
I've expressed myself poorly. I agree failing with access_denied is better and we should not change this.
I meant that since these request interceptors are called after AuthorizationService#authorize
we can be confident that they are assigned an audit request id already. If they don't have it, then it must be a case of tamper request. Does this sound reasonable?
There was a problem hiding this comment.
Yes, it's reasonable, but the question is what should we do if there is not request.id?
We don't want to fail because then we'd lose the accessDenied audit, so I think we're better off logging it with no request.id
It certainly implies something is wrong, but I don't think there's any reasonable action to take about it.
| if (Operations.subsetOf(targetIndexPermissions, sourceIndexPermissions) == false) { | ||
| // TODO we've already audited a access granted event so this is going to look ugly | ||
| auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); | ||
| auditTrailService.accessDenied(null, authentication, action, request, userPermissions.names()); |
There was a problem hiding this comment.
idem:
I prefer us to be explicit here: threadContext.getTransient(AUDIT_REQUEST_ID).
It is a case of tamper request , if audit request Id is missing.
There was a problem hiding this comment.
As above - but let's chat if you disagree.
There was a problem hiding this comment.
I've clarified my thoughts in the previous comment. Judging from the code flow, I believe we can be explicit about the id and not pass null.
|
|
||
| public static String getOrGenerateRequestId(ThreadContext threadContext) { | ||
| String requestId = extractRequestId(threadContext); | ||
| if (Strings.isEmpty(requestId)) { |
There was a problem hiding this comment.
I think we can get rid of this.
There was a problem hiding this comment.
As in: be explicit when we generate these.
I know it would be a bummer if we hit an assertion about an event not assigned to a request, but I think there's value if we catch such an error, because such an event might reveal hidden bugs.
There was a problem hiding this comment.
It's not really feasible to do that unfortunately.
When we process a transport action, we simply don't/can't know whether that action was spawned from an existing rest/transport request or whether it's standalone.
Well, I guess we could refactor a lot of stuff to make that explicit, but it's not right now.
Maybe, between system context, and withOrigin we could pull something together to know when a transport action is isolated from any originating request, but it's going to be fragile.
| } | ||
|
|
||
| LogEntryBuilder withRequestId(String requestId, ThreadContext threadContext) { | ||
| if (Strings.isNullOrEmpty(requestId)) { |
There was a problem hiding this comment.
This is again a place where I think we should not generate if missing. Missing is an indication that we don't know how this request came to be which is a problem in itself.
There was a problem hiding this comment.
We don't generate if missing - we just pull it from the thread context if it was passed in as null.
We can remove that (by making all callers do the extraction explicitly) if we want.
There was a problem hiding this comment.
Ahhhh, yes, it makes sense now. I was mistaken in my belief that they're also generated here. All precautionary recommendations in the other comments assumed they're also generated here.
All good, sorry.
| Authenticator(RestRequest request, ActionListener<Authentication> listener) { | ||
| this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, listener); | ||
| this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request, | ||
| AuditUtil.getOrGenerateRequestId(threadContext)), null, listener); |
There was a problem hiding this comment.
How do you feel if we move AuditUtil.getOrGenerateRequestId(threadContext) inside the AuditableRestRequest and AuditableTransportRequest ? This is oboviously equivalent, but I think the requestId fits better at the Request abstraction level, rather than Authenticator . The consistent abstraction argument, I believe, is more important when dealing with the threadContext since it is important when (on what thread) we pin down the request id. But, again, this is just a abstraction consistency type of argument.
There was a problem hiding this comment.
I think you're right.
I think, originally the generate... method didn't exist, so I the generation was pushed up tothe Authenticator, but now that it's on AuditUtil it should be generated in the request.
| if (auditId == null) { | ||
| if (isInternalUser(authentication.getUser()) == false) { | ||
| logger.warn("Authorizing action without an existing request-id"); | ||
| assert false : "Audit request.id should be generated during authentication"; |
There was a problem hiding this comment.
I think this is a case for tampered request and authz failed ?
There was a problem hiding this comment.
This is one of those cases where I'm worried about breaking something important to support a feature that is only relevant to some people.
On balance, I think you're correct here, but I still have some reservations.
There was a problem hiding this comment.
Okay, I agree. Let's keep it as is.
| final boolean sameUser = samePrincipal && sameRealmType; | ||
| if (sameUser == false) { | ||
| auditTrailService.accessDenied(current, action, request, roleNames); | ||
| auditTrailService.accessDenied(null, current, action, request, roleNames); |
There was a problem hiding this comment.
Here should also be a request id present, otherwise tampered request?
There was a problem hiding this comment.
As above, it's already a denied, which I think is the better failure than complaining about request ids.
|
I have left a bunch of comments, all about being more strict about when we generate the request id and not simply generating a new one for a request without it. I like though that you've chosen to pass the |
|
I've been mistaken about where you generated the ids, I though you generated them on each event if the parameter is null and if they're not in the context. Instead they are not generated at all, they are pulled from the context. |
|
I actually changed a bunch of things based on your comments, but I had some unrelated test failures so it didn't get pushed. |
- Explicitly generate request.id when we know we should - Explicitly extract request.id instead of doing this in the audit trail - Reject authz of external requests without a request.id
| if (Operations.subsetOf(targetIndexPermissions, sourceIndexPermissions) == false) { | ||
| // TODO we've already audited a access granted event so this is going to look ugly | ||
| auditTrailService.accessDenied(null, authentication, action, request, userPermissions.names()); | ||
| auditTrailService.accessDenied(extractRequestId(threadContext), authentication, action, request, userPermissions.names()); |
albertzaharovits
left a comment
There was a problem hiding this comment.
LGTM Thank you!
| .with(REALM_FIELD_NAME, realm) | ||
| .withRestUri(request) | ||
| .withRequestId(requestId, threadContext) | ||
| .withRequestId(requestId) |
|
I plan to merge this as as (pending any additional review comments) but I also want to investigate passing the I don't expect any issues, but I'd like to keep it to its on PR. |
|
Scrap the comment above. It was a 2 line change to make it a header rather than transient, so I've added it into this PR. |
|
@elasticmachine test this please |
|
@elasticmachine: run gradle build tests 1 |
This generates a synthesized "id" for each incoming request that is included in the audit logs (file only). This id can be used to correlate events for the same request (e.g. authentication success with access granted). This request.id is specific to the audit logs and is not used for any other purpose The request.id is consistent across nodes if a single request requires execution on multiple nodes (e.g. search acros multiple shards).
This generates a synthesized "id" for each incoming request that is included in the audit logs (file only). This id can be used to correlate events for the same request (e.g. authentication success with access granted). This request.id is specific to the audit logs and is not used for any other purpose The request.id is consistent across nodes if a single request requires execution on multiple nodes (e.g. search across multiple shards).
This generates a synthesized "id" for each incoming request that is
included in the audit logs (file only).
This id can be used to correlate events for the same request (e.g.
authentication success with access granted).
This request.id is specific to the audit logs and is not used for any
other purpose