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

Adds a sample resource plugin to demonstrate resource access control in action #4893

Closed
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
152 changes: 152 additions & 0 deletions sample-resource-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Resource Sharing and Access Control Plugin

This plugin demonstrates resource sharing and access control functionality, providing APIs to create, manage, and verify access to resources. The plugin enables fine-grained permissions for sharing and accessing resources, making it suitable for systems requiring robust security and collaboration.

## Features

- Create and delete resources.
- Share resources with specific users, roles and/or backend_roles with specific scope(s).
- Revoke access to shared resources for a list of or all scopes.
- Verify access permissions for a given user within a given scope.
- List all resources accessible to current user.

## API Endpoints

The plugin exposes the following six API endpoints:

### 1. Create Resource
- **Endpoint:** `POST /_plugins/sample_resource_sharing/create`
- **Description:** Creates a new resource. Also creates a resource sharing entry if security plugin is enabled.
- **Request Body:**
```json
{
"name": "<resource_name>"
}
```
- **Response:**
```json
{
"message": "Resource <resource_name> created successfully."
}
```

### 2. Delete Resource
- **Endpoint:** `DELETE /_plugins/sample_resource_sharing/{resource_id}`
- **Description:** Deletes a specified resource owned by the requesting user.
- **Response:**
```json
{
"message": "Resource <resource_id> deleted successfully."
}
```

### 3. Share Resource
- **Endpoint:** `POST /_plugins/sample_resource_sharing/share`
- **Description:** Shares a resource with specified users or roles with defined scope.
- **Request Body:**
```json
{
"resource_id" : "{{ADMIN_RESOURCE_ID}}",
"share_with" : {
"SAMPLE_FULL_ACCESS": {
"users": ["test"],
"roles": ["test_role"],
"backend_roles": ["test_backend_role"]
},
"READ_ONLY": {
"users": ["test"],
"roles": ["test_role"],
"backend_roles": ["test_backend_role"]
},
"READ_WRITE": {
"users": ["test"],
"roles": ["test_role"],
"backend_roles": ["test_backend_role"]
}
}
}
```
- **Response:**
```json
{
"message": "Resource <resource-id> shared successfully."
}
```

### 4. Revoke Access
- **Endpoint:** `POST /_plugins/sample_resource_sharing/revoke`
- **Description:** Revokes access to a resource for specified users or roles.
- **Request Body:**
```json
{
"resource_id" : "<resource-id>",
"entities" : {
"users": ["test", "admin"],
"roles": ["test_role", "all_access"],
"backend_roles": ["test_backend_role", "admin"]
},
"scopes": ["SAMPLE_FULL_ACCESS", "READ_ONLY", "READ_WRITE"]
}
```
- **Response:**
```json
{
"message": "Resource <resource-id> access revoked successfully."
}
```

### 5. Verify Access
- **Endpoint:** `GET /_plugins/sample_resource_sharing/verify_resource_access`
- **Description:** Verifies if a user or role has access to a specific resource with a specific scope.
- **Request Body:**
```json
{
"resource_id": "<resource-id>",
"scope": "SAMPLE_FULL_ACCESS"
}
```
- **Response:**
```json
{
"message": "User has requested scope SAMPLE_FULL_ACCESS access to <resource-id>"
}
```

### 6. List Accessible Resources
- **Endpoint:** `GET /_plugins/sample_resource_sharing/list`
- **Description:** Lists all resources accessible to the requesting user or role.
- **Response:**
```json
{
"resource-ids": [
"<resource-id-1>",
"<resource-id-2>"
]
}
```

## Installation

1. Clone the repository:
```bash
git clone [email protected]:opensearch-project/security.git
```

2. Navigate to the project directory:
```bash
cd sample-resource-plugin
```

3. Build and deploy the plugin:
```bash
$ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest
$ ./bin/opensearch-plugin install file: <path-to-this-plugin>/sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-3.0.0.0-SNAPSHOT.zip
```

## License

This code is licensed under the Apache 2.0 License.

## Copyright

Copyright OpenSearch Contributors.
166 changes: 166 additions & 0 deletions sample-resource-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

apply plugin: 'opensearch.opensearchplugin'
apply plugin: 'opensearch.testclusters'
apply plugin: 'opensearch.java-rest-test'

import org.opensearch.gradle.test.RestIntegTestTask


opensearchplugin {
name 'opensearch-sample-resource-plugin'
description 'Sample plugin that extends OpenSearch Resource Plugin'
classname 'org.opensearch.sample.SampleResourcePlugin'
}

ext {
projectSubstitutions = [:]
licenseFile = rootProject.file('LICENSE.txt')
noticeFile = rootProject.file('NOTICE.txt')
}

repositories {
mavenLocal()
mavenCentral()
maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" }
}

dependencies {
}

def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile
es_tmp_dir.mkdirs()

File repo = file("$buildDir/testclusters/repo")
def _numNodes = findProperty('numNodes') as Integer ?: 1

licenseHeaders.enabled = true
validateNebulaPom.enabled = false
testingConventions.enabled = false
loggerUsageCheck.enabled = false

javaRestTest.dependsOn(rootProject.assemble)
javaRestTest {
systemProperty 'tests.security.manager', 'false'
}
testClusters.javaRestTest {
testDistribution = 'INTEG_TEST'
}

task integTest(type: RestIntegTestTask) {
description = "Run tests against a cluster"
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
}
tasks.named("check").configure { dependsOn(integTest) }

integTest {
if (project.hasProperty('excludeTests')) {
project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each {
exclude "${it}"
}
}
systemProperty 'tests.security.manager', 'false'
systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath

systemProperty "https", System.getProperty("https")
systemProperty "user", System.getProperty("user")
systemProperty "password", System.getProperty("password")
// Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for
// requests. The 'doFirst' delays reading the debug setting on the cluster till execution time.
doFirst {
// Tell the test JVM if the cluster JVM is running under a debugger so that tests can
// use longer timeouts for requests.
def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null
systemProperty 'cluster.debug', isDebuggingCluster
// Set number of nodes system property to be used in tests
systemProperty 'cluster.number_of_nodes', "${_numNodes}"
// There seems to be an issue when running multi node run or integ tasks with unicast_hosts
// not being written, the waitForAllConditions ensures it's written
getClusters().forEach { cluster ->
cluster.waitForAllConditions()
}
}

// The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable
if (System.getProperty("test.debug") != null) {
jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000'
}
if (System.getProperty("tests.rest.bwcsuite") == null) {
filter {
excludeTestsMatching "org.opensearch.security.sampleextension.bwc.*IT"
}
}
}
project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build'))
Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin");
Zip rootBundle = (Zip) rootProject.getTasks().getByName("bundlePlugin");
integTest.dependsOn(bundle)
integTest.getClusters().forEach{c -> {
c.plugin(rootProject.getObjects().fileProperty().value(rootBundle.getArchiveFile()))
c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile()))
}}

testClusters.integTest {
testDistribution = 'INTEG_TEST'

// Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1
if (_numNodes > 1) numberOfNodes = _numNodes
// When running integration tests it doesn't forward the --debug-jvm to the cluster anymore
// i.e. we have to use a custom property to flag when we want to debug OpenSearch JVM
// since we also support multi node integration tests we increase debugPort per node
if (System.getProperty("cluster.debug") != null) {
def debugPort = 5005
nodes.forEach { node ->
node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}")
debugPort += 1
}
}
setting 'path.repo', repo.absolutePath
}

afterEvaluate {
testClusters.integTest.nodes.each { node ->
def plugins = node.plugins
def firstPlugin = plugins.get(0)
if (firstPlugin.provider == project.bundlePlugin.archiveFile) {
plugins.remove(0)
plugins.add(firstPlugin)
}

node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem"))
node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem"))
node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem"))
node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem"))
node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem"))
node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem")
node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem")
node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem")
node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false")
node.setting("plugins.security.ssl.http.enabled", "true")
node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem")
node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem")
node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem")
node.setting("plugins.security.allow_unsafe_democertificates", "true")
node.setting("plugins.security.allow_default_init_securityindex", "true")
node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de")
node.setting("plugins.security.audit.type", "internal_opensearch")
node.setting("plugins.security.enable_snapshot_restore_privilege", "true")
node.setting("plugins.security.check_snapshot_restore_write_privileges", "true")
node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]")
}
}

run {
doFirst {
// There seems to be an issue when running multi node run or integ tasks with unicast_hosts
// not being written, the waitForAllConditions ensures it's written
getClusters().forEach { cluster ->
cluster.waitForAllConditions()
}
}
useCluster testClusters.integTest
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.sample;

import java.io.IOException;
import java.util.Map;

import org.opensearch.accesscontrol.resources.Resource;
import org.opensearch.core.common.io.stream.StreamInput;
import org.opensearch.core.common.io.stream.StreamOutput;
import org.opensearch.core.xcontent.XContentBuilder;

public class SampleResource implements Resource {

private String name;
private String description;
private Map<String, String> attributes;

public SampleResource() {}

public SampleResource(StreamInput in) throws IOException {
this.name = in.readString();
this.description = in.readString();
this.attributes = in.readMap(StreamInput::readString, StreamInput::readString);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject().field("name", name).field("description", description).field("attributes", attributes).endObject();
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(name);
out.writeString(description);
out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString);
}

@Override
public String getWriteableName() {
return "sample_resource";
}

public void setName(String name) {
this.name = name;
}

public void setDescription(String description) {
this.description = description;
}

public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}

@Override
public String getResourceName() {
return name;
}
}
Loading
Loading