-
Notifications
You must be signed in to change notification settings - Fork 383
Authorize rest requests #2753
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
Authorize rest requests #2753
Changes from 46 commits
a8a1660
0fedc03
1793a5b
32d8763
a5a44b8
99bfd94
f62f590
7596b7e
1fb5316
25fda77
aea8ede
15eb6b9
f2cba87
77700e5
d1d9ed1
ccab6ce
174a142
2042a60
0c70700
9837378
a34c3a4
f5afd62
918ab51
98791a2
b639544
4288235
93a2c66
6e9e83d
5ee7b12
34092f9
c41023b
d746435
d68c76e
2814283
3b41475
d5392e4
425c22b
b1ed481
1e3efe2
590f55a
6918a80
427460c
f3c4a77
8557fce
d66294d
4a33b33
6a1c25a
8503358
afc8c81
14fe152
5bae5e8
24be564
2edd319
3c397e2
e7d10c7
54cfcd9
8bfeb25
ccd3d2d
c68083c
dc0c1e2
39314d7
ebc5486
a78582f
a325d8e
0e905a0
25a41b6
1b1f519
ffdd927
9b05d44
a21c4c0
6c5ee8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
peternied marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,9 @@ | |
| package org.opensearch.security.filter; | ||
|
|
||
| import java.nio.file.Path; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
|
|
@@ -37,10 +40,10 @@ | |
| import org.greenrobot.eventbus.Subscribe; | ||
|
|
||
| import org.opensearch.OpenSearchException; | ||
| import org.opensearch.client.node.NodeClient; | ||
| import org.opensearch.common.settings.Settings; | ||
| import org.opensearch.common.util.concurrent.ThreadContext; | ||
| import org.opensearch.rest.BytesRestResponse; | ||
| import org.opensearch.rest.NamedRoute; | ||
| import org.opensearch.rest.RestChannel; | ||
| import org.opensearch.rest.RestHandler; | ||
| import org.opensearch.rest.RestRequest; | ||
|
|
@@ -52,6 +55,8 @@ | |
| import org.opensearch.security.configuration.AdminDNs; | ||
| import org.opensearch.security.configuration.CompatConfig; | ||
| import org.opensearch.security.dlic.rest.api.AllowlistApiAction; | ||
| import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; | ||
| import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; | ||
| import org.opensearch.security.securityconf.impl.AllowlistingSettings; | ||
| import org.opensearch.security.securityconf.impl.WhitelistingSettings; | ||
| import org.opensearch.security.ssl.transport.PrincipalExtractor; | ||
|
|
@@ -70,6 +75,7 @@ public class SecurityRestFilter { | |
|
|
||
| protected final Logger log = LogManager.getLogger(this.getClass()); | ||
| private final BackendRegistry registry; | ||
| private final RestLayerPrivilegesEvaluator evaluator; | ||
| private final AuditLog auditLog; | ||
| private final ThreadContext threadContext; | ||
| private final PrincipalExtractor principalExtractor; | ||
|
|
@@ -88,6 +94,7 @@ public class SecurityRestFilter { | |
|
|
||
| public SecurityRestFilter( | ||
| final BackendRegistry registry, | ||
| final RestLayerPrivilegesEvaluator evaluator, | ||
| final AuditLog auditLog, | ||
| final ThreadPool threadPool, | ||
| final PrincipalExtractor principalExtractor, | ||
|
|
@@ -97,6 +104,7 @@ public SecurityRestFilter( | |
| ) { | ||
| super(); | ||
| this.registry = registry; | ||
| this.evaluator = evaluator; | ||
| this.auditLog = auditLog; | ||
| this.threadContext = threadPool.getThreadContext(); | ||
| this.principalExtractor = principalExtractor; | ||
|
|
@@ -109,28 +117,26 @@ public SecurityRestFilter( | |
|
|
||
| /** | ||
| * This function wraps around all rest requests | ||
| * If the request is authenticated, then it goes through a whitelisting check. | ||
| * The whitelisting check works as follows: | ||
| * If whitelisting is not enabled, then requests are handled normally. | ||
| * If whitelisting is enabled, then SuperAdmin is allowed access to all APIs, regardless of what is currently whitelisted. | ||
| * If whitelisting is enabled, then Non-SuperAdmin is allowed to access only those APIs that are whitelisted in {@link #requests} | ||
| * For example: if whitelisting is enabled and requests = ["/_cat/nodes"], then SuperAdmin can access all APIs, but non SuperAdmin | ||
| * If the request is authenticated, then it goes through a allowlisting check. | ||
| * The allowlisting check works as follows: | ||
| * If allowlisting is not enabled, then requests are handled normally. | ||
| * If allowlisting is enabled, then SuperAdmin is allowed access to all APIs, regardless of what is currently allowlisted. | ||
| * If allowlisting is enabled, then Non-SuperAdmin is allowed to access only those APIs that are allowlisted in {@link #requests} | ||
| * For example: if allowlisting is enabled and requests = ["/_cat/nodes"], then SuperAdmin can access all APIs, but non SuperAdmin | ||
| * can only access "/_cat/nodes" | ||
| * Further note: Some APIs are only accessible by SuperAdmin, regardless of whitelisting. For example: /_opendistro/_security/api/whitelist is only accessible by SuperAdmin. | ||
| * Further note: Some APIs are only accessible by SuperAdmin, regardless of allowlisting. For example: /_opendistro/_security/api/whitelist is only accessible by SuperAdmin. | ||
| * See {@link AllowlistApiAction} for the implementation of this API. | ||
| * SuperAdmin is identified by credentials, which can be passed in the curl request. | ||
| */ | ||
| public RestHandler wrap(RestHandler original, AdminDNs adminDNs) { | ||
| return new RestHandler() { | ||
|
|
||
| @Override | ||
| public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { | ||
| org.apache.logging.log4j.ThreadContext.clearAll(); | ||
| if (!checkAndAuthenticateRequest(request, channel, client)) { | ||
| User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); | ||
| if (userIsSuperAdmin(user, adminDNs) | ||
| || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) | ||
| && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { | ||
| return (request, channel, client) -> { | ||
| org.apache.logging.log4j.ThreadContext.clearAll(); | ||
| if (!checkAndAuthenticateRequest(request, channel)) { | ||
| User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); | ||
| if (userIsSuperAdmin(user, adminDNs) | ||
| || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) | ||
|
DarshitChanpura marked this conversation as resolved.
|
||
| && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { | ||
| if (authorizeRequest(original, request, channel, user)) { | ||
| original.handleRequest(request, channel, client); | ||
| } | ||
| } | ||
|
|
@@ -145,7 +151,56 @@ private boolean userIsSuperAdmin(User user, AdminDNs adminDNs) { | |
| return user != null && adminDNs.isAdmin(user); | ||
| } | ||
|
|
||
| private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { | ||
| private boolean authorizeRequest(RestHandler original, RestRequest request, RestChannel channel, User user) { | ||
|
DarshitChanpura marked this conversation as resolved.
|
||
|
|
||
| List<RestHandler.Route> restRoutes = original.routes(); | ||
| Optional<RestHandler.Route> handler = restRoutes.stream() | ||
| .filter(rh -> rh.getMethod().equals(request.method())) | ||
| .filter(rh -> restPathMatches(request.path(), rh.getPath())) | ||
| .findFirst(); | ||
| if (handler.isPresent() && handler.get() instanceof NamedRoute) { | ||
|
DarshitChanpura marked this conversation as resolved.
Outdated
|
||
| PrivilegesEvaluatorResponse pres = new PrivilegesEvaluatorResponse(); | ||
| NamedRoute route = ((NamedRoute) handler.get()); | ||
| // if actionNames are present evaluate those first | ||
| Set<String> actionNames = route.actionNames(); | ||
| if (actionNames != null && !actionNames.isEmpty()) { | ||
| pres = evaluator.evaluate(user, actionNames); | ||
| } | ||
|
|
||
| // now if pres.allowed is still false check for the NamedRoute name as a permission | ||
| if (!pres.isAllowed()) { | ||
| String action = route.name(); | ||
| pres = evaluator.evaluate(user, Set.of(action)); | ||
| } | ||
|
|
||
| if (log.isDebugEnabled()) { | ||
| log.debug(pres.toString()); | ||
| } | ||
| if (pres.isAllowed()) { | ||
| // TODO make sure this is audit logged | ||
|
DarshitChanpura marked this conversation as resolved.
Outdated
|
||
| log.debug("Request has been granted"); | ||
| // auditLog.logGrantedPrivileges(action, request, task); | ||
| } else { | ||
| // auditLog.logMissingPrivileges(action, request, task); | ||
| String err; | ||
| if (!pres.getMissingSecurityRoles().isEmpty()) { | ||
| err = String.format("No mapping for %s on roles %s", user, pres.getMissingSecurityRoles()); | ||
| } else { | ||
| err = String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); | ||
| } | ||
| log.debug(err); | ||
| // TODO Figure out why ext hangs intermittently after single unauthorized request | ||
|
DarshitChanpura marked this conversation as resolved.
Outdated
|
||
| channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED, err)); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // if handler is not an instance of NamedRoute then we pass through to eval at Transport Layer. | ||
| // TODO Change this to false once all plugins have migrated to use NamedRoutes | ||
|
DarshitChanpura marked this conversation as resolved.
Outdated
|
||
| return true; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I am reading this correctly, we are always going to be evaluating both on the REST and trasnport layer correct? The only case where things would happen only in the REST layer would be if the request were unauthorized?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes.
I'm not quite sure I understand this. Can you elaborate a little?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant to clarify that the only time a request would end in the REST layer would be if it were unauthorized by the REST layer auth checker. Otherwise, it will always hit the transport layer checks. |
||
| } | ||
|
|
||
| private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel) throws Exception { | ||
|
|
||
| threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.REST.toString()); | ||
|
|
||
|
|
@@ -217,4 +272,30 @@ public void onWhitelistingSettingChanged(WhitelistingSettings whitelistingSettin | |
| public void onAllowlistingSettingChanged(AllowlistingSettings allowlistingSettings) { | ||
| this.allowlistingSettings = allowlistingSettings; | ||
| } | ||
|
|
||
| /** | ||
| * Determines if the request's path is a match for the configured handler path. | ||
| * | ||
| * @param requestPath The path from the {@link NamedRoute} | ||
| * @param handlerPath The path from the {@link RestHandler.Route} | ||
| * @return true if the request path matches the route | ||
| */ | ||
| private boolean restPathMatches(String requestPath, String handlerPath) { | ||
|
DarshitChanpura marked this conversation as resolved.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Units like this should have tests with them. I've seen code similar to this in core, can this be extracted to a library?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test file already exists. Check it out https://github.com/opensearch-project/security/blob/9b05d44a6ba15bca521e1ccbd6e33124facddcc0/src/test/java/org/opensearch/security/filter/RestPathMatchesTest.java
It's only used at 2 places and I don't think it will be used at other places in future. I think keeping it here is fine. |
||
| // Check exact match | ||
| if (handlerPath.equals(requestPath)) { | ||
| return true; | ||
| } | ||
| // Split path to evaluate named params | ||
| String[] handlerSplit = handlerPath.split("/"); | ||
| String[] requestSplit = requestPath.split("/"); | ||
| if (handlerSplit.length != requestSplit.length) { | ||
| return false; | ||
| } | ||
| for (int i = 0; i < handlerSplit.length; i++) { | ||
| if (!(handlerSplit[i].equals(requestSplit[i]) || (handlerSplit[i].startsWith("{") && handlerSplit[i].endsWith("}")))) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.