Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions sample-resource-plugin/src/main/resources/default-roles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sample_full_access:
hidden: false
static: true
description: "Provide full access to the sample resource plugin"
application_id: "sample-resource-plugin"
cluster_permissions:
- "cluster:admin/sample-resource-plugin/*"

Expand All @@ -16,5 +17,6 @@ sample_read_access:
hidden: false
static: true
description: "Provide read access to the sample resource plugin"
application_id: "sample-resource-plugin"
cluster_permissions:
- "cluster:admin/sample-resource-plugin/get"
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.api;

import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import org.junit.ClassRule;
import org.junit.Test;

import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.TestSecurityConfig.Role;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
import static org.opensearch.test.framework.matcher.RestMatchers.isOk;

public class ApplicationPermissionsInfoTest extends AbstractApiIntegrationTest {

private static final String ENDPOINT = PLUGINS_PREFIX + "/applicationpermissions";

// Use predefined all_access from static_roles.yml so the role name is exactly "all_access"
static final Role ALL_ACCESS = new Role("all_access").isPredefined(true);

static final Role ISM_READ_ROLE = new Role("ism_read_access").applicationId("index-management")
._static(true)
.clusterPermissions("cluster:admin/opendistro/ism/get");

static final Role ISM_FULL_ROLE = new Role("ism_full_access").applicationId("index-management")
._static(true)
.clusterPermissions("cluster:admin/opendistro/ism/*");

static final Role ALERTING_READ_ROLE = new Role("alerting_read_access").applicationId("alerting")
._static(true)
.clusterPermissions("cluster:admin/opendistro/alerting/get");

static final Role NO_APP_ROLE = new Role("custom_role_no_app").clusterPermissions("cluster:monitor/health");

// all_access user mapped to the predefined all_access role
static final TestSecurityConfig.User ALL_ACCESS_USER = new TestSecurityConfig.User("all_access_user").roles(ALL_ACCESS);

static final TestSecurityConfig.User ISM_USER = new TestSecurityConfig.User("ism_user").roles(ISM_READ_ROLE);

static final TestSecurityConfig.User MULTI_APP_USER = new TestSecurityConfig.User("multi_app_user").roles(
ISM_FULL_ROLE,
ALERTING_READ_ROLE
);

static final TestSecurityConfig.User NO_APP_USER = new TestSecurityConfig.User("no_app_user").roles(NO_APP_ROLE);

@ClassRule
public static LocalCluster localCluster = clusterBuilder().users(ALL_ACCESS_USER, ISM_USER, MULTI_APP_USER, NO_APP_USER).build();

@Test
public void allAccessUserGetsWildcard() throws Exception {
try (TestRestClient client = localCluster.getRestClient(ALL_ACCESS_USER)) {
TestRestClient.HttpResponse response = client.get(ENDPOINT);
assertThat(response, isOk());

JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
assertThat(appIds.size(), equalTo(1));
assertThat(appIds.get(0).asText(), equalTo("*"));
}
}

@Test
public void userWithSingleAppRole() throws Exception {
try (TestRestClient client = localCluster.getRestClient(ISM_USER)) {
TestRestClient.HttpResponse response = client.get(ENDPOINT);
assertThat(response, isOk());

JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
List<String> ids = new java.util.ArrayList<>();
appIds.forEach(n -> ids.add(n.asText()));
assertThat(ids, containsInAnyOrder("index-management"));
}
}

@Test
public void userWithMultipleAppRoles() throws Exception {
try (TestRestClient client = localCluster.getRestClient(MULTI_APP_USER)) {
TestRestClient.HttpResponse response = client.get(ENDPOINT);
assertThat(response, isOk());

JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
List<String> ids = new java.util.ArrayList<>();
appIds.forEach(n -> ids.add(n.asText()));
assertThat(ids, containsInAnyOrder("index-management", "alerting"));
}
}

@Test
public void userWithNoAppRolesGetsEmptyList() throws Exception {
try (TestRestClient client = localCluster.getRestClient(NO_APP_USER)) {
TestRestClient.HttpResponse response = client.get(ENDPOINT);
assertThat(response, isOk());

JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
assertThat(appIds.size(), equalTo(0));
}
}

@Test
public void responseIncludesUserName() throws Exception {
try (TestRestClient client = localCluster.getRestClient(ISM_USER)) {
TestRestClient.HttpResponse response = client.get(ENDPOINT);
assertThat(response, isOk());
assertThat(response.bodyAsJsonNode().get("user_name").asText(), equalTo("ism_user"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,8 @@ public static class Role implements ToXContentObject {
private Boolean hidden;
private Boolean reserved;
private String description;
private String applicationId;
private Boolean _static;

/**
* If this is true, the role is expected to be defined in static_roles.yml. Thus, it is not necessary to include it
Expand Down Expand Up @@ -732,6 +734,16 @@ public Role reserved(boolean reserved) {
return this;
}

public Role applicationId(String applicationId) {
this.applicationId = applicationId;
return this;
}

public Role _static(boolean _static) {
this._static = _static;
return this;
}

/**
* If this is true, the role is expected to be defined in static_roles.yml. Thus, it is not necessary to include it
* in the written role config.
Expand All @@ -746,6 +758,8 @@ public Role clone() {
role.clusterPermissions.addAll(this.clusterPermissions);
role.indexPermissions.addAll(this.indexPermissions);
role.tenantPermissions.addAll(this.tenantPermissions);
role.applicationId = this.applicationId;
role._static = this._static;
return role;
}

Expand All @@ -769,6 +783,8 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params
xContentBuilder.field("reserved", reserved);
}
if (!Strings.isNullOrEmpty(description)) xContentBuilder.field("description", description);
if (!Strings.isNullOrEmpty(applicationId)) xContentBuilder.field("application_id", applicationId);
if (_static != null) xContentBuilder.field("static", _static);
return xContentBuilder.endObject();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
import org.opensearch.security.resources.api.share.ShareTransportAction;
import org.opensearch.security.resources.settings.ResourceSharingFeatureFlagSetting;
import org.opensearch.security.resources.settings.ResourceSharingProtectedResourcesSetting;
import org.opensearch.security.rest.ApplicationPermissionsInfoAction;
import org.opensearch.security.rest.DashboardsInfoAction;
import org.opensearch.security.rest.SecurityConfigUpdateAction;
import org.opensearch.security.rest.SecurityHealthAction;
Expand Down Expand Up @@ -653,6 +654,13 @@ public List<RestHandler> getRestHandlers(
resourceSharingEnabledSetting
)
);
handlers.add(
new ApplicationPermissionsInfoAction(
Objects.requireNonNull(privilegesConfiguration),
Objects.requireNonNull(cr),
Objects.requireNonNull(threadPool)
)
);
handlers.add(
new TenantInfoAction(
settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public static SecurityDynamicConfiguration<RoleV7> loadDefaultRoles(Set<Security
for (var entry : pluginRoles.getCEntries().entrySet()) {
entry.getValue().setStatic(true);
entry.getValue().setReserved(true);
// application_id is only valid on static (default) roles;
// clear it if a plugin accidentally marks a role as non-static
// (defensive — setStatic(true) above should make this unreachable)
}

log.info(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.rest;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ImmutableList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.common.util.concurrent.ThreadContext;
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.RestRequest;
import org.opensearch.security.configuration.ConfigurationRepository;
import org.opensearch.security.privileges.PrivilegesConfiguration;
import org.opensearch.security.privileges.PrivilegesEvaluationContext;
import org.opensearch.security.privileges.PrivilegesEvaluator;
import org.opensearch.security.securityconf.DynamicConfigFactory;
import org.opensearch.security.securityconf.impl.CType;
import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration;
import org.opensearch.security.securityconf.impl.v7.RoleV7;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.user.User;
import org.opensearch.threadpool.ThreadPool;
import org.opensearch.transport.client.node.NodeClient;

import static org.opensearch.rest.RestRequest.Method.GET;
import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX;
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;

/**
* Returns the list of application IDs the current user has access to, based on
* their mapped roles. Dashboards uses this to show/hide menu items.
*
* <p>Users mapped to {@code all_access} receive {@code ["*"]}.</p>
*/
public class ApplicationPermissionsInfoAction extends BaseRestHandler {

private static final List<Route> routes = addRoutesPrefix(
ImmutableList.of(new Route(GET, "/applicationpermissions")),
PLUGIN_ROUTE_PREFIX
);

private final Logger log = LogManager.getLogger(this.getClass());
private final PrivilegesConfiguration privilegesConfiguration;
private final ConfigurationRepository configurationRepository;
private final ThreadContext threadContext;

private static final String ALL_ACCESS_ROLE = "all_access";

public ApplicationPermissionsInfoAction(
final PrivilegesConfiguration privilegesConfiguration,
final ConfigurationRepository configurationRepository,
final ThreadPool threadPool
) {
super();
this.privilegesConfiguration = privilegesConfiguration;
this.configurationRepository = configurationRepository;
this.threadContext = threadPool.getThreadContext();
}

@Override
public List<Route> routes() {
return routes;
}

@Override
public String getName() {
return "Application Permissions Info Action";
}

@Override
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
return channel -> {
XContentBuilder builder = channel.newBuilder();
BytesRestResponse response = null;

try {
final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);

if (user == null) {
builder.startObject();
builder.field("error", "No user found in thread context");
builder.endObject();
response = new BytesRestResponse(RestStatus.FORBIDDEN, builder);
} else {
PrivilegesEvaluator privilegesEvaluator = privilegesConfiguration.privilegesEvaluator();
PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, "dummy:action");
Set<String> mappedRoles = context.getMappedRoles();

Set<String> applicationIds;

if (mappedRoles.contains(ALL_ACCESS_ROLE)) {
applicationIds = Set.of("*");
} else {
SecurityDynamicConfiguration<RoleV7> rolesConfig = configurationRepository.getConfiguration(CType.ROLES);
DynamicConfigFactory.addStatics(rolesConfig);

applicationIds = new HashSet<>();
for (Map.Entry<String, RoleV7> entry : rolesConfig.getCEntries().entrySet()) {
RoleV7 role = entry.getValue();
if (!mappedRoles.contains(entry.getKey())) {
continue;
}
// Only honour application_id on static (default) roles
if (!role.isStatic()) {
continue;
}
String appId = role.getApplication_id();
if (appId != null && !appId.isEmpty()) {
applicationIds.add(appId);
}
}
}

builder.startObject();
builder.field("user_name", user.getName());
builder.field("application_ids", applicationIds);
builder.endObject();
response = new BytesRestResponse(RestStatus.OK, builder);
}
} catch (final Exception e1) {
log.error("Error building application permissions response", e1);
builder = channel.newBuilder();
builder.startObject();
builder.field("error", e1.toString());
builder.endObject();
response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
} finally {
if (builder != null) {
builder.close();
}
}

channel.sendResponse(response);
};
}
}
Loading
Loading