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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Adding Alerting V2 roles to roles.yml ([#5747](https://github.com/opensearch-project/security/pull/5747))
- add suggest api to ad read access role ([#5754](https://github.com/opensearch-project/security/pull/5754))
- Get list of headersToCopy from core and use getHeader(String headerName) instead of getHeaders() ([#5769](https://github.com/opensearch-project/security/pull/5769))
- [Resource Sharing] Keep track of resource_type on resource sharing document ([#5772](https://github.com/opensearch-project/security/pull/5772))

### Bug Fixes
- Create a WildcardMatcher.NONE when creating a WildcardMatcher with an empty string ([#5694](https://github.com/opensearch-project/security/pull/5694))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ public void wipeOutResourceEntries() {
// Helper to create a sample resource and return its ID
public String createSampleResourceAs(TestSecurityConfig.User user, Header... headers) {
try (TestRestClient client = cluster.getRestClient(user)) {
String sample = "{\"name\":\"sample\"}";
String sample = "{\"name\":\"sample\",\"resource_type\":\"" + RESOURCE_TYPE + "\"}";
TestRestClient.HttpResponse resp = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sample, headers);
resp.assertStatusCode(HttpStatus.SC_OK);
return resp.getTextFromJsonBody("/message").split(":")[1].trim();
Expand All @@ -353,7 +353,7 @@ public String createSampleResourceAs(TestSecurityConfig.User user, Header... hea

public String createSampleResourceGroupAs(TestSecurityConfig.User user, Header... headers) {
try (TestRestClient client = cluster.getRestClient(user)) {
String sample = "{\"name\":\"samplegroup\"}";
String sample = "{\"name\":\"samplegroup\",\"resource_type\":\"" + RESOURCE_GROUP_TYPE + "\"}";
TestRestClient.HttpResponse resp = client.putJson(SAMPLE_RESOURCE_GROUP_CREATE_ENDPOINT, sample, headers);
resp.assertStatusCode(HttpStatus.SC_OK);
return resp.getTextFromJsonBody("/message").split(":")[1].trim();
Expand All @@ -362,7 +362,7 @@ public String createSampleResourceGroupAs(TestSecurityConfig.User user, Header..

public String createRawResourceAs(CertificateData adminCert) {
try (TestRestClient client = cluster.getRestClient(adminCert)) {
String sample = "{\"name\":\"sample\"}";
String sample = "{\"name\":\"sample\",\"resource_type\":\"" + RESOURCE_TYPE + "\"}";
TestRestClient.HttpResponse resp = client.postJson(RESOURCE_INDEX_NAME + "/_doc", sample);
resp.assertStatusCode(HttpStatus.SC_CREATED);
return resp.getTextFromJsonBody("/_id");
Expand Down Expand Up @@ -680,6 +680,8 @@ public void awaitSharingEntry(String resourceId, String expectedString) {
TestRestClient.HttpResponse response = client.get(RESOURCE_SHARING_INDEX + "/_doc/" + resourceId);
response.assertStatusCode(200);
String body = response.getBody();
String resourceType = response.getTextFromJsonBody("/_source/resource_type");
assert resourceType != null : "resource_type cannot be null";
assertThat(body, containsString(expectedString));
assertThat(body, containsString(resourceId));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ private ArrayNode expectedHits(String resourceId, String accessLevel) {
// 3) Build the _source sub-object
ObjectNode source = hit.putObject("_source");
source.put("resource_id", resourceId);
source.put("resource_type", RESOURCE_TYPE);

ObjectNode createdBy = source.putObject("created_by");
createdBy.put("user", MIGRATION_USER.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ public SampleResource(StreamInput in) throws IOException {
}
s.setName((String) a[0]);
s.setDescription((String) a[1]);
s.setAttributes((Map<String, String>) a[2]);
s.setUser((User) a[3]);
// ignore a[2] as we know the type
s.setAttributes((Map<String, String>) a[3]);
s.setUser((User) a[4]);
return s;
});

static {
PARSER.declareString(constructorArg(), new ParseField("name"));
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("description"));
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("resource_type"));
PARSER.declareObjectOrNull(optionalConstructorArg(), (p, c) -> p.mapStrings(), null, new ParseField("attributes"));
PARSER.declareObjectOrNull(optionalConstructorArg(), (p, c) -> User.parse(p), null, new ParseField("user"));
}
Expand All @@ -80,6 +82,7 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par
return builder.startObject()
.field("name", name)
.field("description", description)
.field("resource_type", RESOURCE_TYPE)
.field("attributes", attributes)
.field("user", user)
.endObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public String resourceType() {
public String resourceIndexName() {
return RESOURCE_INDEX_NAME;
}

@Override
public String typeField() {
return "resource_type";
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public String resourceType() {
public String resourceIndexName() {
return RESOURCE_INDEX_NAME;
}

@Override
public String typeField() {
return "resource_type";
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ public CreateResourceGroupTransportAction(TransportService transportService, Act

@Override
protected void doExecute(Task task, CreateResourceGroupRequest request, ActionListener<CreateResourceGroupResponse> listener) {
createResource(request, listener);
createResourceGroup(request, listener);
}

private void createResource(CreateResourceGroupRequest request, ActionListener<CreateResourceGroupResponse> listener) {
private void createResourceGroup(CreateResourceGroupRequest request, ActionListener<CreateResourceGroupResponse> listener) {
SampleResourceGroup sampleGroup = request.getResourceGroup();

// 1. Read mapping JSON from the config file
Expand Down
3 changes: 3 additions & 0 deletions sample-resource-plugin/src/main/resources/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"schema_version": 1
},
"properties": {
"resource_type": {
"type": "keyword"
},
"all_shared_principals": {
"type": "keyword"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ public interface ResourceProvider {

String resourceIndexName();

/**
* Returns the name of the field representing the resource type in the resource document.
*
* @return the field name containing the resource type
*/
default String typeField() {
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re

log.debug("postIndex called on {}", resourceIndex);

String resourceType = resourcePluginInfo.getResourceTypeForIndexOp(resourceIndex, index);

String resourceId = index.id();

// Only proceed if this was a create operation and for primary shard
Expand Down Expand Up @@ -107,8 +109,10 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re
resourceIndex
);
}, e -> { log.debug(e.getMessage()); });
// User.getRequestedTenant() is null if multi-tenancy is disabled
ResourceSharing.Builder builder = ResourceSharing.builder()
.resourceId(resourceId)
.resourceType(resourceType)
.createdBy(new CreatedBy(user.getName(), user.getRequestedTenant()));
ResourceSharing sharingInfo = builder.build();
// User.getRequestedTenant() is null if multi-tenancy is disabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,24 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.index.IndexableField;

import org.opensearch.OpenSearchSecurityException;
import org.opensearch.core.xcontent.ToXContentObject;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.index.engine.Engine;
import org.opensearch.security.securityconf.FlattenedActionGroups;
import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration;
import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7;
import org.opensearch.security.setting.OpensearchDynamicSetting;
import org.opensearch.security.spi.resources.ResourceProvider;
import org.opensearch.security.spi.resources.ResourceSharingExtension;
import org.opensearch.security.spi.resources.client.ResourceSharingClient;

Expand All @@ -40,14 +44,16 @@
*/
public class ResourcePluginInfo {

private static final Logger log = LogManager.getLogger(ResourcePluginInfo.class);

private ResourceSharingClient resourceAccessControlClient;

private OpensearchDynamicSetting<List<String>> protectedTypesSetting;

private final Set<ResourceSharingExtension> resourceSharingExtensions = new HashSet<>();

// type <-> index
private final Map<String, String> typeToIndex = new HashMap<>();
// type <-> resource provider
private final Map<String, ResourceProvider> typeToProvider = new HashMap<>();

// UI: access-level *names* per type
private final Map<String, LinkedHashSet<String>> typeToAccessLevels = new HashMap<>();
Expand All @@ -57,8 +63,6 @@ public class ResourcePluginInfo {

// cache current protected types and their indices
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // make the updates/reads thread-safe
private Set<String> currentProtectedTypes = Collections.emptySet(); // snapshot of last set
private Set<String> cachedProtectedTypeIndices = Collections.emptySet(); // precomputed indices

public void setProtectedTypesSetting(OpensearchDynamicSetting<List<String>> protectedTypesSetting) {
this.protectedTypesSetting = protectedTypesSetting;
Expand All @@ -68,7 +72,7 @@ public void setResourceSharingExtensions(Set<ResourceSharingExtension> extension
lock.writeLock().lock();
try {
resourceSharingExtensions.clear();
typeToIndex.clear();
typeToProvider.clear();

// Enforce resource-type unique-ness
Set<String> resourceTypes = new HashSet<>();
Expand All @@ -78,7 +82,7 @@ public void setResourceSharingExtensions(Set<ResourceSharingExtension> extension
// add name seen so far to the resource-types set
resourceTypes.add(rp.resourceType());
// also cache type->index and index->type mapping
typeToIndex.put(rp.resourceType(), rp.resourceIndexName());
typeToProvider.put(rp.resourceType(), rp);
} else {
throw new OpenSearchSecurityException(
String.format(
Expand All @@ -91,10 +95,6 @@ public void setResourceSharingExtensions(Set<ResourceSharingExtension> extension
}
}
resourceSharingExtensions.addAll(extensions);

// Whenever providers change, invalidate protected caches so next update refreshes them
currentProtectedTypes = Collections.emptySet();
cachedProtectedTypeIndices = Collections.emptySet();
} finally {
lock.writeLock().unlock();
}
Expand All @@ -104,35 +104,63 @@ public void updateProtectedTypes(List<String> protectedTypes) {
lock.writeLock().lock();
try {
// Rebuild mappings based on the current allowlist
typeToIndex.clear();
typeToProvider.clear();

if (protectedTypes == null || protectedTypes.isEmpty()) {
// No protected types -> leave maps empty
currentProtectedTypes = Collections.emptySet();
cachedProtectedTypeIndices = Collections.emptySet();
return;
}

// Cache current protected set as an unmodifiable snapshot
currentProtectedTypes = Collections.unmodifiableSet(new LinkedHashSet<>(protectedTypes));

for (ResourceSharingExtension extension : resourceSharingExtensions) {
for (var rp : extension.getResourceProviders()) {
final String type = rp.resourceType();
if (!currentProtectedTypes.contains(type)) continue;

final String index = rp.resourceIndexName();
typeToIndex.put(type, index);
if (!protectedTypes.contains(type)) continue;
typeToProvider.put(rp.resourceType(), rp);
}
}

// pre-compute indices for current protected set
cachedProtectedTypeIndices = Collections.unmodifiableSet(new LinkedHashSet<>(typeToIndex.values()));
} finally {
lock.writeLock().unlock();
}
}

public static String extractFieldFromIndexOp(String fieldName, Engine.Index indexOp) {
String fieldValue = null;
for (IndexableField f : indexOp.parsedDoc().rootDoc().getFields(fieldName)) {
if (f.stringValue() != null) {
fieldValue = f.stringValue();
break;
}
if (f.binaryValue() != null) { // e.g., BytesRef-backed
fieldValue = f.binaryValue().utf8ToString();
break;
}
}
return fieldValue;
}

public String getResourceTypeForIndexOp(String resourceIndex, Engine.Index indexOp) {
lock.readLock().lock();
try {
// Eagerly use type field from first matching provider of same index as the indexOp
// If typeField is not present, assume single resource type per index and return type from provider
var provider = typeToProvider.values()
.stream()
.filter(p -> p.resourceIndexName().equals(resourceIndex))
.findFirst()
.orElse(null);
if (provider == null) {
// should not happen
return null;
}
if (provider.typeField() != null) {
return extractFieldFromIndexOp(provider.typeField(), indexOp);
}
// If `typeField` is not defined, assume single type to index and return type from provider
return provider.resourceType();
} finally {
lock.readLock().unlock();
}
}

public Set<ResourceSharingExtension> getResourceSharingExtensions() {
return ImmutableSet.copyOf(resourceSharingExtensions);
}
Expand Down Expand Up @@ -179,23 +207,22 @@ public FlattenedActionGroups flattenedForType(String resourceType) {
}
}

public String indexByType(String type) {
public ResourceProvider getResourceProvider(String type) {
lock.readLock().lock();
try {
return typeToIndex.get(type);
return typeToProvider.get(type);
} finally {
lock.readLock().unlock();
}
}

public Set<String> typesByIndex(String index) {
public String indexByType(String type) {
lock.readLock().lock();
try {
return typeToIndex.entrySet()
.stream()
.filter(entry -> Objects.equals(entry.getValue(), index))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
if (!typeToProvider.containsKey(type)) {
return null;
}
return typeToProvider.get(type).resourceIndexName();
} finally {
lock.readLock().unlock();
}
Expand All @@ -204,7 +231,7 @@ public Set<String> typesByIndex(String index) {
public Set<ResourceDashboardInfo> getResourceTypes() {
lock.readLock().lock();
try {
return typeToIndex.keySet()
return typeToProvider.keySet()
.stream()
.map(
s -> new ResourceDashboardInfo(
Expand All @@ -221,7 +248,7 @@ public Set<ResourceDashboardInfo> getResourceTypes() {
public Set<String> getResourceIndices() {
lock.readLock().lock();
try {
return new HashSet<>(typeToIndex.values());
return typeToProvider.values().stream().map(ResourceProvider::resourceIndexName).collect(Collectors.toSet());
} finally {
lock.readLock().unlock();
}
Expand All @@ -235,15 +262,10 @@ public Set<String> getResourceIndicesForProtectedTypes() {

lock.readLock().lock();
try {
// If caller is asking for the current protected set, return the cache
if (new LinkedHashSet<>(resourceTypes).equals(currentProtectedTypes)) {
return cachedProtectedTypeIndices;
}

return typeToIndex.entrySet()
return typeToProvider.entrySet()
.stream()
.filter(e -> resourceTypes.contains(e.getKey()))
.map(Map.Entry::getValue)
.map(e -> e.getValue().resourceIndexName())
.collect(Collectors.toSet());
} finally {
lock.readLock().unlock();
Expand Down
Loading
Loading