Skip to content
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

[Resource Access Control] [Part2] Introduces a client for Resource Access Control and adds concrete implementation for resource access control #5194

Open
wants to merge 11 commits into
base: feature/resource-permissions
Choose a base branch
from
Open
446 changes: 312 additions & 134 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/maven-publish.yml
Original file line number Diff line number Diff line change
@@ -32,4 +32,4 @@ jobs:
export SONATYPE_PASSWORD=$(aws secretsmanager get-secret-value --secret-id maven-snapshots-password --query SecretString --output text)
echo "::add-mask::$SONATYPE_USERNAME"
echo "::add-mask::$SONATYPE_PASSWORD"
./gradlew publishPluginZipPublicationToSnapshotsRepository
./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository publishShadowPublicationToSnapshotsRepository
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -43,7 +43,3 @@ out/
build/
gradle-build/
.gradle/

# nodejs
node_modules/
package-lock.json
493 changes: 493 additions & 0 deletions RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md

Large diffs are not rendered by default.

112 changes: 79 additions & 33 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -442,14 +442,6 @@ publishing {
}
}

repositories {
mavenLocal()
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" }
maven { url "https://artifacts.opensearch.org/snapshots/lucene/" }
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases" }
}

tasks.test.finalizedBy(jacocoTestReport) // report is always generated after tests run

@@ -500,13 +492,88 @@ configurations {
force "org.checkerframework:checker-qual:3.49.1"
force "ch.qos.logback:logback-classic:1.5.17"
force "commons-io:commons-io:2.18.0"
force "com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2"
force "org.hamcrest:hamcrest:2.2"
force "org.mockito:mockito-core:5.16.1"
force "net.bytebuddy:byte-buddy:1.15.11"
force "org.ow2.asm:asm:9.7.1"
force "com.google.j2objc:j2objc-annotations:3.0.0"
}
}

integrationTestImplementation.extendsFrom implementation
integrationTestRuntimeOnly.extendsFrom runtimeOnly
}

allprojects {
repositories {
mavenLocal()
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" }
maven { url "https://artifacts.opensearch.org/snapshots/lucene/" }
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases" }
}

configurations {
integrationTestImplementation.extendsFrom implementation
compile.extendsFrom compileOnly
compile.extendsFrom testImplementation
}
dependencies {
// unit test framework
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation 'junit:junit:4.13.2'
testImplementation "org.opensearch:opensearch:${opensearch_version}"
testImplementation "org.mockito:mockito-core:5.16.1"

//integration test framework:
integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') {
exclude(group: 'junit', module: 'junit')
}
integrationTestImplementation 'junit:junit:4.13.2'
integrationTestImplementation("org.opensearch.plugin:reindex-client:${opensearch_version}"){
exclude(group: 'org.slf4j', module: 'slf4j-api')
}
integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}"
integrationTestImplementation 'commons-io:commons-io:2.18.0'
integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}"
integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}"
integrationTestImplementation 'org.hamcrest:hamcrest:2.2'
integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}"
integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}"
integrationTestImplementation('org.awaitility:awaitility:4.2.2') {
exclude(group: 'org.hamcrest', module: 'hamcrest')
}
integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14'
integrationTestImplementation "org.opensearch.plugin:mapper-size:${opensearch_version}"
integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14"
integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14"
integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14"
integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16"
integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5"
integrationTestImplementation "org.mockito:mockito-core:5.16.1"
integrationTestImplementation "org.passay:passay:1.6.6"
integrationTestImplementation "org.opensearch:opensearch:${opensearch_version}"
integrationTestImplementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}"
integrationTestImplementation "org.opensearch.plugin:aggs-matrix-stats-client:${opensearch_version}"
integrationTestImplementation "org.opensearch.plugin:parent-join-client:${opensearch_version}"
integrationTestImplementation 'com.password4j:password4j:1.8.2'
integrationTestImplementation "com.google.guava:guava:${guava_version}"
integrationTestImplementation "org.apache.commons:commons-lang3:${versions.commonslang}"
integrationTestImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
integrationTestImplementation 'org.greenrobot:eventbus-java:3.3.1'
integrationTestImplementation('com.flipkart.zjsonpatch:zjsonpatch:0.4.16'){
exclude(group:'com.fasterxml.jackson.core')
}
integrationTestImplementation 'org.slf4j:slf4j-api:2.0.12'
integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0'
integrationTestImplementation "org.opensearch.plugin:lang-painless:${opensearch_version}"
integrationTestImplementation project(path:":opensearch-resource-sharing-spi", configuration: 'shadow')
integrationTestImplementation project(path: ":${rootProject.name}-client", configuration: 'shadow')
}
}

//create source set 'integrationTest'
//add classes from the main source set to the compilation and runtime classpaths of the integrationTest
sourceSets {
@@ -527,6 +594,9 @@ sourceSets {

//add new task that runs integration tests
task integrationTest(type: Test) {
filter {
excludeTestsMatching 'org.opensearch.sample.*ResourcePlugin*'
}
doFirst {
// Only run resources tests on resource-test CI environments or locally
if (System.getenv('CI_ENVIRONMENT') != 'resource-test' && System.getenv('CI_ENVIRONMENT') != null) {
@@ -575,6 +645,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate
check.dependsOn integrationTest

dependencies {
implementation project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow')
implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}"
implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}"
implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}"
@@ -730,31 +801,6 @@ dependencies {

compileOnly "org.opensearch:opensearch:${opensearch_version}"

//integration test framework:
integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') {
exclude(group: 'junit', module: 'junit')
}
integrationTestImplementation 'junit:junit:4.13.2'
integrationTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}"
integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}"
integrationTestImplementation 'commons-io:commons-io:2.18.0'
integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}"
integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}"
integrationTestImplementation 'org.hamcrest:hamcrest:2.2'
integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}"
integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}"
integrationTestImplementation('org.awaitility:awaitility:4.3.0') {
exclude(group: 'org.hamcrest', module: 'hamcrest')
}
integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14'
integrationTestImplementation "org.opensearch.plugin:mapper-size:${opensearch_version}"
integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14"
integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14"
integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14"
integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16"
integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5"
integrationTestImplementation "org.mockito:mockito-core:5.16.1"

//spotless
implementation('com.google.googlejavaformat:google-java-format:1.25.2') {
exclude group: 'com.google.guava'
229 changes: 229 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# **Resource Sharing Client**

This package provides a **ResourceSharing client** that resource plugins can use to **implement access control** by communicating with the **OpenSearch Security Plugin**.

---

## **Usage**

### **1. Creating a Client Accessor with Singleton Pattern**
To ensure a single instance of the `ResourceSharingNodeClient`, use the **Singleton pattern**:

```java
public class ResourceSharingClientAccessor {
private static ResourceSharingNodeClient INSTANCE;

private ResourceSharingClientAccessor() {}

/**
* Get the resource sharing client instance.
*
* @param nodeClient The OpenSearch NodeClient instance.
* @param settings The OpenSearch settings.
* @return A singleton instance of ResourceSharingNodeClient.
*/
public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) {
if (INSTANCE == null) {
INSTANCE = new ResourceSharingNodeClient(nodeClient, settings);
}
return INSTANCE;
}
}
```

---

### **2. Using the Client in a Transport Action**
The following example demonstrates how to use the **Resource Sharing Client** inside a `TransportAction` to verify **delete permissions** before deleting a resource.

```java
@Override
protected void doExecute(Task task, DeleteResourceRequest request, ActionListener<DeleteResourceResponse> listener) {
String resourceId = request.getResourceId();

ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings);

resourceSharingClient.verifyResourceAccess(
resourceId,
RESOURCE_INDEX_NAME,
SampleResourceScope.PUBLIC.value(),
ActionListener.wrap(isAuthorized -> {
if (!isAuthorized) {
listener.onFailure(new UnauthorizedResourceAccessException("Current user is not authorized to delete resource: " + resourceId));
return;
}

// Authorization successful, proceed with deletion
ThreadContext threadContext = transportService.getThreadPool().getThreadContext();
try (ThreadContext.StoredContext ignored = threadContext.stashContext()) {
deleteResource(resourceId, ActionListener.wrap(deleteResponse -> {
if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) {
listener.onFailure(new ResourceNotFoundException("Resource " + resourceId + " not found."));
} else {
listener.onResponse(new DeleteResourceResponse("Resource " + resourceId + " deleted successfully."));
}
}, exception -> {
log.error("Failed to delete resource: " + resourceId, exception);
listener.onFailure(exception);
}));
}
}, exception -> {
log.error("Failed to verify resource access: " + resourceId, exception);
listener.onFailure(exception);
})
);
}
```

---

## **Available Java APIs**

The **`ResourceSharingClient`** provides **four Java APIs** for **resource access control**, enabling plugins to **verify, share, revoke, and list** shareableResources.

**Package Location:**
[`org.opensearch.security.client.resources.ResourceSharingClient`](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java)

---

### **API Usage Examples**
Below are examples demonstrating how to use each API effectively.

---

### **1. `verifyResourceAccess`**
**Checks if the current user has access to a resource** based on predefined **scopes**.

#### **Method Signature:**
```java
void verifyResourceAccess(String resourceId, String resourceIndex, Set<String> scopes, ActionListener<Boolean> listener);
```

#### **Example Usage:**
```java
Set<String> scopes = Set.of("READ_ONLY");
resourceSharingClient.verifyResourceAccess(
"resource-123",
"resource_index",
scopes,
ActionListener.wrap(isAuthorized -> {
if (isAuthorized) {
System.out.println("User has access to the resource.");
} else {
System.out.println("Access denied.");
}
}, e -> {
System.err.println("Failed to verify access: " + e.getMessage());
})
);
```
> **Use Case:** Before performing operations like **deletion or modifications**, ensure the user has the right permissions.

---

### **2. `shareResource`**
**Grants access to a resource** for specific users, roles, or backend roles.

#### **Method Signature:**
```java
void shareResource(String resourceId, String resourceIndex, Map<String, Object> shareWith, ActionListener<ResourceSharing> listener);
```

#### **Example Usage:**
```java
Map<String, Object> shareWith = Map.of(
"users", List.of("user_1", "user_2"),
"roles", List.of("admin_role"),
"backend_roles", List.of("backend_group")
);

resourceSharingClient.shareResource(
"resource-123",
"resource_index",
shareWith,
ActionListener.wrap(response -> {
System.out.println("Resource successfully shared with: " + shareWith);
}, e -> {
System.err.println("Failed to share resource: " + e.getMessage());
})
);
```
> **Use Case:** Used when an **owner/admin wants to share a resource** with specific users or groups.

---

### **3. `revokeResourceAccess`**
**Removes access permissions** for specified users, roles, or backend roles.

#### **Method Signature:**
```java
void revokeResourceAccess(String resourceId, String resourceIndex, Map<String, Object> entitiesToRevoke, Set<String> scopes, ActionListener<ResourceSharing> listener);
```

#### **Example Usage:**
```java
Map<String, Object> entitiesToRevoke = Map.of(
"users", List.of("user_2"),
"roles", List.of("viewer_role")
);
Set<String> scopesToRevoke = Set.of("READ_ONLY");

resourceSharingClient.revokeResourceAccess(
"resource-123",
"resource_index",
entitiesToRevoke,
scopesToRevoke,
ActionListener.wrap(response -> {
System.out.println("Resource access successfully revoked for: " + entitiesToRevoke);
}, e -> {
System.err.println("Failed to revoke access: " + e.getMessage());
})
);
```
> **Use Case:** When a user no longer needs access to a **resource**, their permissions can be revoked.

---

### **4. `listAllAccessibleResources`**
**Retrieves all shareableResources the current user has access to.**

#### **Method Signature:**
```java
void listAllAccessibleResources(String resourceIndex, ActionListener<Set<? extends Resource>> listener);
```

#### **Example Usage:**
```java
resourceSharingClient.listAllAccessibleResources(
"resource_index",
ActionListener.wrap(shareableResources -> {
for (Resource resource : shareableResources) {
System.out.println("Accessible Resource: " + resource.getId());
}
}, e -> {
System.err.println("Failed to list accessible shareableResources: " + e.getMessage());
})
);
```
> **Use Case:** Helps a user identify **which shareableResources they can interact with**.

---

## **Conclusion**
These APIs provide essential methods for **fine-grained resource access control**, enabling:

✔ **Verification** of resource access.
✔ **Granting and revoking** access dynamically.
✔ **Retrieval** of all accessible shareableResources.

For further details, refer to the [`ResourceSharingClient` Java class](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java).

---

## **License**
This project is licensed under the **Apache 2.0 License**.

---

## **Copyright**
© OpenSearch Contributors.
Loading