-
Notifications
You must be signed in to change notification settings - Fork 300
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
Subset of permissions check on creation #5012
Changes from all commits
9f695fa
d3fcc4a
6904317
17bca93
92d4e60
22cfbe8
665b9e9
e39df0d
ad63974
73eb2ab
6418226
bc8aacf
b90bae9
552aeda
aa506e7
8299645
eebe5fb
c079d09
5b48fb9
e13c055
a1057ce
b6aa196
bf31791
c95d256
1314851
280e00c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
package org.opensearch.security.action.apitokens; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import java.time.Instant; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
@@ -24,14 +25,29 @@ | |
import org.apache.logging.log4j.Logger; | ||
|
||
import org.opensearch.client.node.NodeClient; | ||
import org.opensearch.cluster.metadata.IndexNameExpressionResolver; | ||
import org.opensearch.cluster.service.ClusterService; | ||
import org.opensearch.common.settings.Settings; | ||
import org.opensearch.common.util.concurrent.ThreadContext; | ||
import org.opensearch.core.action.ActionListener; | ||
import org.opensearch.core.rest.RestStatus; | ||
import org.opensearch.core.xcontent.XContentBuilder; | ||
import org.opensearch.rest.BaseRestHandler; | ||
import org.opensearch.rest.BytesRestResponse; | ||
import org.opensearch.rest.RestChannel; | ||
import org.opensearch.rest.RestHandler; | ||
import org.opensearch.rest.RestRequest; | ||
import org.opensearch.security.auditlog.AuditLog; | ||
import org.opensearch.security.configuration.AdminDNs; | ||
import org.opensearch.security.configuration.ConfigurationRepository; | ||
import org.opensearch.security.dlic.rest.api.Endpoint; | ||
import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator; | ||
import org.opensearch.security.dlic.rest.api.RestApiPrivilegesEvaluator; | ||
import org.opensearch.security.dlic.rest.api.SecurityApiDependencies; | ||
import org.opensearch.security.dlic.rest.support.Utils; | ||
import org.opensearch.security.privileges.PrivilegesEvaluator; | ||
import org.opensearch.security.ssl.transport.PrincipalExtractor; | ||
import org.opensearch.security.support.ConfigConstants; | ||
import org.opensearch.threadpool.ThreadPool; | ||
|
||
import static org.opensearch.rest.RestRequest.Method.DELETE; | ||
import static org.opensearch.rest.RestRequest.Method.GET; | ||
|
@@ -44,23 +60,57 @@ | |
import static org.opensearch.security.action.apitokens.ApiToken.INDEX_PERMISSIONS_FIELD; | ||
import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD; | ||
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; | ||
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; | ||
import static org.opensearch.security.util.ParsingUtils.safeMapList; | ||
import static org.opensearch.security.util.ParsingUtils.safeStringList; | ||
|
||
public class ApiTokenAction extends BaseRestHandler { | ||
private ApiTokenRepository apiTokenRepository; | ||
private final ApiTokenRepository apiTokenRepository; | ||
public Logger log = LogManager.getLogger(this.getClass()); | ||
|
||
private static final List<RestHandler.Route> ROUTES = addRoutesPrefix( | ||
ImmutableList.of( | ||
new RestHandler.Route(POST, "/apitokens"), | ||
new RestHandler.Route(DELETE, "/apitokens"), | ||
new RestHandler.Route(GET, "/apitokens") | ||
) | ||
private final ThreadPool threadPool; | ||
private final ConfigurationRepository configurationRepository; | ||
private final PrivilegesEvaluator privilegesEvaluator; | ||
private final SecurityApiDependencies securityApiDependencies; | ||
private final ClusterService clusterService; | ||
private final IndexNameExpressionResolver indexNameExpressionResolver; | ||
|
||
private static final List<Route> ROUTES = addRoutesPrefix( | ||
ImmutableList.of(new Route(POST, "/apitokens"), new Route(DELETE, "/apitokens"), new Route(GET, "/apitokens")) | ||
); | ||
|
||
public ApiTokenAction(ApiTokenRepository apiTokenRepository) { | ||
public ApiTokenAction( | ||
ThreadPool threadpool, | ||
ConfigurationRepository configurationRepository, | ||
PrivilegesEvaluator privilegesEvaluator, | ||
Settings settings, | ||
AdminDNs adminDns, | ||
AuditLog auditLog, | ||
Path configPath, | ||
PrincipalExtractor principalExtractor, | ||
ApiTokenRepository apiTokenRepository, | ||
ClusterService clusterService, | ||
IndexNameExpressionResolver indexNameExpressionResolver | ||
) { | ||
this.apiTokenRepository = apiTokenRepository; | ||
this.threadPool = threadpool; | ||
this.configurationRepository = configurationRepository; | ||
this.privilegesEvaluator = privilegesEvaluator; | ||
this.securityApiDependencies = new SecurityApiDependencies( | ||
adminDns, | ||
configurationRepository, | ||
privilegesEvaluator, | ||
new RestApiPrivilegesEvaluator(settings, adminDns, privilegesEvaluator, principalExtractor, configPath, threadPool), | ||
new RestApiAdminPrivilegesEvaluator( | ||
threadPool.getThreadContext(), | ||
privilegesEvaluator, | ||
adminDns, | ||
settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) | ||
), | ||
auditLog, | ||
settings | ||
); | ||
this.clusterService = clusterService; | ||
this.indexNameExpressionResolver = indexNameExpressionResolver; | ||
} | ||
|
||
@Override | ||
|
@@ -69,22 +119,28 @@ | |
} | ||
|
||
@Override | ||
public List<RestHandler.Route> routes() { | ||
public List<Route> routes() { | ||
return ROUTES; | ||
} | ||
|
||
@Override | ||
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { | ||
// TODO: Authorize this API properly | ||
switch (request.method()) { | ||
case POST: | ||
return handlePost(request, client); | ||
case DELETE: | ||
return handleDelete(request, client); | ||
case GET: | ||
return handleGet(request, client); | ||
default: | ||
throw new IllegalArgumentException(request.method() + " not supported"); | ||
authorizeSecurityAccess(request); | ||
return doPrepareRequest(request, client); | ||
} | ||
|
||
RestChannelConsumer doPrepareRequest(RestRequest request, NodeClient client) { | ||
final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(client.threadPool().getThreadContext()); | ||
try (final ThreadContext.StoredContext ctx = client.threadPool().getThreadContext().stashContext()) { | ||
client.threadPool() | ||
.getThreadContext() | ||
.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUserAndRemoteAddress.getLeft()); | ||
return switch (request.method()) { | ||
case POST -> handlePost(request, client); | ||
case DELETE -> handleDelete(request, client); | ||
case GET -> handleGet(request, client); | ||
default -> throw new IllegalArgumentException(request.method() + " not supported"); | ||
}; | ||
} | ||
} | ||
|
||
|
@@ -119,8 +175,6 @@ | |
|
||
private RestChannelConsumer handlePost(RestRequest request, NodeClient client) { | ||
return channel -> { | ||
final XContentBuilder builder = channel.newBuilder(); | ||
BytesRestResponse response; | ||
try { | ||
final Map<String, Object> requestBody = request.contentOrSourceParamParser().map(); | ||
validateRequestParameters(requestBody); | ||
|
@@ -245,8 +299,6 @@ | |
|
||
private RestChannelConsumer handleDelete(RestRequest request, NodeClient client) { | ||
return channel -> { | ||
final XContentBuilder builder = channel.newBuilder(); | ||
BytesRestResponse response; | ||
try { | ||
final Map<String, Object> requestBody = request.contentOrSourceParamParser().map(); | ||
|
||
|
@@ -295,4 +347,11 @@ | |
} | ||
} | ||
|
||
protected void authorizeSecurityAccess(RestRequest request) throws IOException { | ||
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. What do you think about having a dedicated transport action backing the REST action? Then, one would get access control out of the box without needing explicit checks within the action code. 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. Agree with this though it depends on the action. If the security admin is going to a page in the security dashboards plugin to manage the existing tokens (listing them and performing actions such as revoke) then these should follow the authorization model of other security APIs. If regular users are allowed to request a token (with a subset of their own permissions) then that could be authorized like other cluster actions in OpenSearch. 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 think (for now) my operating assumption would be that this is an admin-level action to issue api tokens to connect with other services programmatically. Since API tokens (currently) do not follow the new optimized privileges evaluation, and thus have higher performance penalty than other means of authc/z, and thus we should restrict this as much as possible. However, if there is community feedback/requests for this feature, then we can consider it at a later time. Since we want to authorize these APIs like the others, I added this logic. 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. To be honest, it is a bit difficult for me to review this without a proper scope definition. During review, I learned from you about a couple of assumptions and assumed limitations (such as not using the optimized privilege evaluation code paths, not supporting regexes, etc). IMHO, there should be some definitions in the issue #1504 what's the initial goal to be achieved and what not. That will be then an information to guide the code review process. Otherwise, this review process will get quite inefficient. If the assumption is indeed that issuing tokens is an admin-level action, I do wonder why we need the matching to existing privileges at all. Won't an admin-level action just be similar to granting new privileges? 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 its rolled out to admin users initially at first than I agree, but if the longer term plan is for other users to be able to request tokens then the logic to determine whether the requested permissions are a subset of the user's permissions would be needed. I was thinking to keep the logic simple and only consider: 1) In practice, I have not come across many roles definitions where cluster administrators use regex when defining the index patterns in a role. I'm sure its used, but not prevalent from my experience. 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. Should we consider following a similar model to AbstractApiAction here? I know this action is not a subset of AbstractApiAction (since it doesn't alter anything in the security index), but maybe it can be modeled similarly? Part of that block also registers an updateHandler to synchronize across the cluster. |
||
// Check if user has security API access | ||
if (!(securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(Endpoint.APITOKENS) | ||
|| securityApiDependencies.restApiPrivilegesEvaluator().checkAccessPermissions(request, Endpoint.APITOKENS) == null)) { | ||
throw new SecurityException("User does not have required security API access"); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we can start creating these inline.