diff --git a/CHANGELOG.md b/CHANGELOG.md index ef54cd0a89..3f4a79498c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) - Add support for X509 v3 extensions (SAN) for authentication ([#5701](https://github.com/opensearch-project/security/pull/5701)) ### Bug Fixes diff --git a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md index ed41ac8f7b..68b7f97888 100644 --- a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md +++ b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md @@ -610,7 +610,6 @@ Read documents from a plugin’s index and migrate ownership and backend role-ba | `source_index` | string | yes | Name of the plugin index containing the existing resource documents | | `username_path` | string | yes | JSON Pointer to the username field inside each document | | `backend_roles_path` | string | yes | JSON Pointer to the backend_roles field (must point to a JSON array) | -| `type_path` | string | no | JSON Pointer to the resource type field inside each document (required if multiple resource types in same resource index) | | `default_access_level` | object | yes | Default access level to assign migrated backend_roles. Must be one from the available action-groups for this type. See `resource-action-groups.yml`. | **Example Request** @@ -621,7 +620,6 @@ Read documents from a plugin’s index and migrate ownership and backend role-ba "source_index": ".sample_resource", "username_path": "/owner", "backend_roles_path": "/backend_roles", - "type_path": "/type", "default_access_level": { "sample-resource": "read_only", "sample-resource-group": "read-only-group" diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index f61440b288..57dc6ad270 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -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(); @@ -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(); @@ -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"); @@ -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)); }); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/MigrateApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/MigrateApiTests.java index 372f27a2a8..e09a0bce7b 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/MigrateApiTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/MigrateApiTests.java @@ -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()); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index 63f1a20474..370b6acd0b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -60,14 +60,16 @@ public SampleResource(StreamInput in) throws IOException { } s.setName((String) a[0]); s.setDescription((String) a[1]); - s.setAttributes((Map) a[2]); - s.setUser((User) a[3]); + // ignore a[2] as we know the type + s.setAttributes((Map) 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")); } @@ -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(); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceExtension.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceExtension.java index aa3a03c4f5..6f47436787 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceExtension.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceExtension.java @@ -38,6 +38,11 @@ public String resourceType() { public String resourceIndexName() { return RESOURCE_INDEX_NAME; } + + @Override + public String typeField() { + return "resource_type"; + } }); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java index 2fc0f77ae9..77fb9753f0 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java @@ -27,6 +27,11 @@ public String resourceType() { public String resourceIndexName() { return RESOURCE_INDEX_NAME; } + + @Override + public String typeField() { + return "resource_type"; + } }); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java index c761577f20..e9d2eaf450 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java @@ -53,10 +53,10 @@ public CreateResourceGroupTransportAction(TransportService transportService, Act @Override protected void doExecute(Task task, CreateResourceGroupRequest request, ActionListener listener) { - createResource(request, listener); + createResourceGroup(request, listener); } - private void createResource(CreateResourceGroupRequest request, ActionListener listener) { + private void createResourceGroup(CreateResourceGroupRequest request, ActionListener listener) { SampleResourceGroup sampleGroup = request.getResourceGroup(); // 1. Read mapping JSON from the config file diff --git a/sample-resource-plugin/src/main/resources/mappings.json b/sample-resource-plugin/src/main/resources/mappings.json index d0141e153f..b163ee8c11 100644 --- a/sample-resource-plugin/src/main/resources/mappings.json +++ b/sample-resource-plugin/src/main/resources/mappings.json @@ -4,6 +4,9 @@ "schema_version": 1 }, "properties": { + "resource_type": { + "type": "keyword" + }, "all_shared_principals": { "type": "keyword" } diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java index 82e050244e..e5b7fc39cd 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java @@ -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; + } + } diff --git a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java index 8382a6463c..1d32259ee4 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java @@ -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 @@ -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 diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index a08c6025cd..4ace9be889 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -15,20 +15,22 @@ 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.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; @@ -46,8 +48,8 @@ public class ResourcePluginInfo { private final Set resourceSharingExtensions = new HashSet<>(); - // type <-> index - private final Map typeToIndex = new HashMap<>(); + // type <-> resource provider + private final Map typeToProvider = new HashMap<>(); // UI: access-level *names* per type private final Map> typeToAccessLevels = new HashMap<>(); @@ -57,8 +59,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 currentProtectedTypes = Collections.emptySet(); // snapshot of last set - private Set cachedProtectedTypeIndices = Collections.emptySet(); // precomputed indices public void setProtectedTypesSetting(OpensearchDynamicSetting> protectedTypesSetting) { this.protectedTypesSetting = protectedTypesSetting; @@ -68,7 +68,7 @@ public void setResourceSharingExtensions(Set extension lock.writeLock().lock(); try { resourceSharingExtensions.clear(); - typeToIndex.clear(); + typeToProvider.clear(); // Enforce resource-type unique-ness Set resourceTypes = new HashSet<>(); @@ -78,7 +78,7 @@ public void setResourceSharingExtensions(Set 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( @@ -91,10 +91,6 @@ public void setResourceSharingExtensions(Set 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(); } @@ -104,35 +100,90 @@ public void updateProtectedTypes(List 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(); } } + /** + * Extracts the value of a field from the Lucene document backing an {@link Engine.Index} operation. + *

+ * This method iterates over all {@link IndexableField} instances with the given {@code fieldName} on the + * root document of {@code indexOp}. It returns the first non-{@code null} value, preferring + * {@link IndexableField#stringValue()} and falling back to decoding {@link IndexableField#binaryValue()} + * as UTF-8 if present. + * + * @param fieldName the name of the field to extract from the index operation's root document; must not be {@code null} + * @param indexOp the index operation whose parsed document will be inspected; must not be {@code null} + * @return the first non-{@code null} string or UTF-8-decoded binary value of the field, or {@code null} if no such value exists + */ + 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; + } + + /** + * Resolves the resource type for the given index operation and resource index. + *

+ * The method acquires a read lock and selects the first registered provider whose resource index name + * matches {@code resourceIndex}. If such a provider defines a {@code typeField}, the value of that + * field is extracted from {@code indexOp} via {@link #extractFieldFromIndexOp(String, Engine.Index)}. + * If {@code typeField} is not defined, it is assumed that the index hosts a single resource type and + * the provider's configured resource type is returned. + *

+ * If no provider is found for the supplied {@code resourceIndex}, {@code null} is returned. + * + * @param resourceIndex the name of the resource index associated with the index operation + * @param indexOp the index operation from which a dynamic type field may be extracted + * @return the resolved resource type, or {@code null} if no matching provider exists for {@code resourceIndex} + */ + 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 getResourceSharingExtensions() { return ImmutableSet.copyOf(resourceSharingExtensions); } @@ -179,23 +230,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 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(); } @@ -204,7 +254,7 @@ public Set typesByIndex(String index) { public Set getResourceTypes() { lock.readLock().lock(); try { - return typeToIndex.keySet() + return typeToProvider.keySet() .stream() .map( s -> new ResourceDashboardInfo( @@ -221,7 +271,7 @@ public Set getResourceTypes() { public Set getResourceIndices() { lock.readLock().lock(); try { - return new HashSet<>(typeToIndex.values()); + return typeToProvider.values().stream().map(ResourceProvider::resourceIndexName).collect(Collectors.toSet()); } finally { lock.readLock().unlock(); } @@ -235,15 +285,10 @@ public Set 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(); diff --git a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java index 06c0a1a895..5e3c44f522 100644 --- a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java +++ b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java @@ -60,6 +60,7 @@ import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.spi.resources.ResourceProvider; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -150,29 +151,22 @@ private ValidationResult, List>> l String backendRolesPath = body.get("backend_roles_path").asText(); JsonNode node = body.get("default_access_level"); Map typeToDefaultAccessLevel = Utils.toMapOfStrings(node); - String typePath = null; - if (body.has("type_path")) { - typePath = body.get("type_path").asText(); - } else { - LOGGER.info("No type_path provided, assuming single resource-type for all records."); - if (typeToDefaultAccessLevel.size() > 1) { - String badRequestMessage = "type_path must be provided when multiple resource types are specified in default_access_level."; - return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); - } - } if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(sourceIndex)) { String badRequestMessage = "Invalid resource index " + sourceIndex + "."; return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } + String typePath = null; for (String type : typeToDefaultAccessLevel.keySet()) { + ResourceProvider provider = resourcePluginInfo.getResourceProvider(type); String defaultAccessLevelForType = typeToDefaultAccessLevel.get(type); LOGGER.info("Default access level for resource type [{}] is [{}]", type, typeToDefaultAccessLevel.get(type)); // check that access level exists for given resource-index - if (resourcePluginInfo.indexByType(type) == null) { + if (provider == null) { String badRequestMessage = "Invalid resource type " + type + "."; return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } + typePath = provider.typeField(); // All types in the same index must have same typeField var accessLevels = resourcePluginInfo.flattenedForType(type).actionGroups(); if (!accessLevels.contains(defaultAccessLevelForType)) { LOGGER.error( @@ -227,7 +221,7 @@ private ValidationResult, List>> l String type; if (typePath != null) { - type = rec.at(typePath.startsWith("/") ? typePath : ("/" + typePath)).asText(null); + type = rec.at("/" + typePath.replace(".", "/")).asText(null); } else { type = typeToDefaultAccessLevel.keySet().iterator().next(); } @@ -292,6 +286,8 @@ private ValidationResult createNewSharingRecords(Triple createNewSharingRecords(Triple allowedKeys() { .put("source_index", RequestContentValidator.DataType.STRING) // name of the resource plugin index .put("username_path", RequestContentValidator.DataType.STRING) // path to resource creator's name .put("backend_roles_path", RequestContentValidator.DataType.STRING) // path to backend_roles - .put("type_path", RequestContentValidator.DataType.STRING) // path to resource type .put("default_access_level", RequestContentValidator.DataType.OBJECT) // default access level by type .build(); } diff --git a/src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java b/src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java index 318c960d38..4e58d784f5 100644 --- a/src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java +++ b/src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java @@ -51,6 +51,11 @@ public class ResourceSharing implements ToXContentFragment, NamedWriteable { */ private String resourceId; + /** + * The type of the resource + */ + private String resourceType; + /** * Information about who created the resource */ @@ -61,14 +66,9 @@ public class ResourceSharing implements ToXContentFragment, NamedWriteable { */ private ShareWith shareWith; - public ResourceSharing(String resourceId, CreatedBy createdBy, ShareWith shareWith) { - this.resourceId = resourceId; - this.createdBy = createdBy; - this.shareWith = shareWith; - } - private ResourceSharing(Builder b) { this.resourceId = b.resourceId; + this.resourceType = b.resourceType; this.createdBy = b.createdBy; this.shareWith = b.shareWith; } @@ -137,21 +137,33 @@ public void revoke(String accessLevel, Recipients target) { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ResourceSharing resourceSharing = (ResourceSharing) o; - return Objects.equals(getResourceId(), resourceSharing.getResourceId()) - && Objects.equals(getCreatedBy(), resourceSharing.getCreatedBy()) - && Objects.equals(getShareWith(), resourceSharing.getShareWith()); + if (!(o instanceof ResourceSharing)) return false; + ResourceSharing that = (ResourceSharing) o; + return Objects.equals(resourceId, that.resourceId) + && Objects.equals(resourceType, that.resourceType) + && Objects.equals(createdBy, that.createdBy) + && Objects.equals(shareWith, that.shareWith); } @Override public int hashCode() { - return Objects.hash(getResourceId(), getCreatedBy(), getShareWith()); + return Objects.hash(resourceId, resourceType, createdBy, shareWith); } @Override public String toString() { - return "ResourceSharing {" + "resourceId='" + resourceId + '\'' + ", createdBy=" + createdBy + ", sharedWith=" + shareWith + '}'; + return "ResourceSharing{" + + "resourceId='" + + resourceId + + '\'' + + ", resourceType='" + + resourceType + + '\'' + + ", createdBy=" + + createdBy + + ", shareWith=" + + shareWith + + '}'; } @Override @@ -162,6 +174,7 @@ public String getWriteableName() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(resourceId); + out.writeString(resourceType); createdBy.writeTo(out); if (shareWith != null) { out.writeBoolean(true); @@ -173,7 +186,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject().field("resource_id", resourceId).field("created_by"); + builder.startObject().field("resource_id", resourceId).field("resource_type", resourceType).field("created_by"); createdBy.toXContent(builder, params); if (shareWith != null) { builder.field("share_with"); @@ -196,6 +209,13 @@ public static ResourceSharing fromXContent(XContentParser parser) throws IOExcep case "resource_id": b.resourceId(parser.text()); break; + case "resource_type": + if (token == XContentParser.Token.VALUE_NULL) { + b.resourceType(null); + } else { + b.resourceType(parser.text()); + } + break; case "created_by": b.createdBy(CreatedBy.fromXContent(parser)); break; @@ -359,6 +379,7 @@ public List getAllPrincipals() { public static final class Builder { private String resourceId; + private String resourceType; private CreatedBy createdBy; private ShareWith shareWith; @@ -367,6 +388,11 @@ public Builder resourceId(String resourceId) { return this; } + public Builder resourceType(String resourceType) { + this.resourceType = resourceType; + return this; + } + public Builder createdBy(CreatedBy createdBy) { this.createdBy = createdBy; return this;