diff --git a/clientSecurity/README.md b/clientSecurity/README.md new file mode 100644 index 0000000..db806c1 --- /dev/null +++ b/clientSecurity/README.md @@ -0,0 +1,119 @@ + + +# Geode security example - Client + +This example demonstrates basic command security and user authentication in a client application +backed by a secured Geode cluster. This example assumes that Java and Geode are installed. + +## Security Basics + +Geode security is based on Apache Shiro. +Permissions are defined by + +1. the resource accessed (`DATA` and `CLUSTER`) +2. the operation performed (`READ`, `WRITE`, or `MANAGE`) +3. the target for this resource operation (typically a region name) +4. the key for the target (e.g., the key of the region's key-value pair) + +A single permission is represented by a `:`-separated string, e.g., `DATA:READ:region1:myKey`. + +Permissions need not be fully specified. +Abridged permissions are hierarchical. +A permission of `CLUSTER` implies `CLUSTER:READ`, `CLUSTER:WRITE`, and `CLUSTER:MANAGE`, + for all target regions and all key values. +Using wildcard annotation, a permission of `CLUSTER` is equivalent to `CLUSTER:*:*:*`. + + +In this example, four users with varying permissions attempt to read and write data + in two regions. +* The `superUser` user has full permissions and may read and write to all regions. +* The `dataReader` user has `DATA:READ` permission, granting read access to all regions. +* The `dataWriter` user has `DATA:WRITE` permission, granting write access to all regions. +* The `region1dataAdmin` has permissions `DATA:READ:region1` and `DATA:WRITE:region1`, + granting read and write access only to `/region1`. + +For more information on what permission is required for a given operation, + refer to the documentation. + +## Required Implementations + + Two interfaces must be implemented to secure a Geode cluster: `AuthInitialize` + and `SecurityManager`. + + Your implementation of `org.apache.geode.security.AuthInitialize` should handle the interaction + with any existing security infrastructure (e.g., ldap). In this example, we provide a trivial + implementation in `org.apache.geode.examples.clientSecurity.ExampleAuthInit`. + + These credentials are then given to your implementation + of `org.apache.geode.security.SecurityManager` + to authenticate the user (i.e., to log in). + The security manager also handles authorization of the authenticated user + for particular operations. + How permissions are assigned to users is also determined by the security manager. + In this example, + we group permissions by *role*, and assign each user one or more roles in a JSON file. + This file is located at `src/main/resources/example_security.json`. + +## Demonstration of Security +1. Set directory `geode-examples/clientSecurity` to be the current working directory. +Each step in this example specifies paths relative to that directory. + +2. Build the example + + $ ../gradlew build + +3. Start a secure cluster consisting of one locator with two servers with two regions. + Refer to `scripts/start.gfsh`. + When starting a secure cluster, you must specify a *security manager* + that implements authorization. + In this example, we use the security manager + `org.apache.geode.examples.clientSecurity.ExampleSecurityManager`. + This security manager reads a JSON file that defines which roles are granted which permissions, + as well as each user's username, password, and roles. + The JSON is present in `src/main/resources/example_security.json`. + You can execute the `scripts/start.gfsh` script with the command: + + $ ../gradlew start + +4. Run the example. Each user will attempt to put data to `/region1` and `/region2`, + and then read data from `/region1` and `/region2`. Unauthorized reads and writes throw + exceptions caused by `NotAuthorizedException`, which we catch and print in this example. + + $ ../gradlew run + +5. Stop the cluster using the script `scripts/stop.gfsh`. +You can run this script with the command: + + $ ../gradlew stop + +## Things to Get Right with Security + +- Implement `org.apache.geode.security.AuthInitialize` to pass user credentials from any existing + security infrastructure. + +- Implement `org.apache.geode.security.SecurityManager` to handle user authentication + and operation authorization. + +- Specify the `SecurityManager` by the `security-manager` property of all locator and server +property files. An unsecured member or a member secured by a different security manager will not +be allowed to join the cluster. + +- If additional properties are required by your implementation of the security manager, + these may be defined in your locator or server property files. + For instance, our implementation also requires `security-json` to be defined. + diff --git a/clientSecurity/example_security.properties b/clientSecurity/example_security.properties new file mode 100644 index 0000000..d661639 --- /dev/null +++ b/clientSecurity/example_security.properties @@ -0,0 +1,15 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more contributor license +# agreements. See the NOTICE file distributed with this work for additional information regarding +# copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the License. You may obtain a +# copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing permissions and limitations under +# the License. + +security-manager = org.apache.geode.examples.security.ExampleSecurityManager +security-json = example_security.json diff --git a/clientSecurity/scripts/start.gfsh b/clientSecurity/scripts/start.gfsh new file mode 100644 index 0000000..313d035 --- /dev/null +++ b/clientSecurity/scripts/start.gfsh @@ -0,0 +1,46 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Start a locator. +# The security properties file specifies the ExampleSecurityManager to be the security manager. +# This requires that the example_security.json be on the classpath. +# Recall that the --classpath option is specified relative to the locator's working directory. + +start locator --name=locator --bind-address=127.0.0.1\ + --security-properties-file=example_security.properties --classpath=../build/resources/main/ + +# Now we may start our cluster. +# Servers also require security properties to be set and a copy of the security JSON +# We use `--server-port=0` to use a random available port. +# Note that we can start a server with any user with CLUSTER:MANAGE permissions + +start server --name=server1 --locators=127.0.0.1[10334]\ + --classpath=../build/resources/main/:../build/classes/main/\ + --security-properties-file=./example_security.properties --server-port=0\ + --user=superUser --password=123 + +start server --name=server2 --locators=127.0.0.1[10334]\ + --classpath=../build/resources/main/:../build/classes/main/\ + --security-properties-file=./example_security.properties --server-port=0\ + --user=superUser --password=123 + +# To execute any online commands, we need to connect to the locator +# To create a region, we can connect as any user with CLUSTER:MANAGE + +connect --user=superUser --password=123 +create region --name=region1 --type=REPLICATE +create region --name=region2 --type=PARTITION diff --git a/clientSecurity/scripts/stop.gfsh b/clientSecurity/scripts/stop.gfsh new file mode 100644 index 0000000..ad068e3 --- /dev/null +++ b/clientSecurity/scripts/stop.gfsh @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +connect --locator=127.0.0.1[10334] --user=superUser --password=123 +shutdown --include-locators=true diff --git a/clientSecurity/src/main/java/org/apache/geode/examples/clientSecurity/Example.java b/clientSecurity/src/main/java/org/apache/geode/examples/clientSecurity/Example.java new file mode 100644 index 0000000..c4670cd --- /dev/null +++ b/clientSecurity/src/main/java/org/apache/geode/examples/clientSecurity/Example.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.geode.examples.clientSecurity; + +import java.util.Properties; + +import org.apache.commons.lang.Validate; +import org.apache.logging.log4j.Logger; + +import org.apache.geode.cache.Region; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientCacheFactory; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.internal.logging.LogService; + +public class Example implements AutoCloseable { + private static final Logger logger = LogService.getLogger(); + + private static final String REGION1 = "region1"; + private static final String REGION2 = "region2"; + + // Some example data + private static final String AUTHOR_GROSSMAN = "Grossman"; + private static final String BOOK_BY_GROSSMAN = "Soon I Will Be Invincible"; + + private static final String AUTHOR_ROTHFUSS = "Rothfuss"; + private static final String BOOK_BY_ROTHFUSS = "The Name of the Wind"; + + private static final String AUTHOR_LYNCH = "Lynch"; + private static final String BOOK_BY_LYNCH = "The Lies of Locke Lamora"; + + private static final String AUTHOR_SCALZI = "Scalzi"; + private static final String BOOK_BY_SCALZI = "Old Man's War"; + + private static final String AUTHOR_SANDERSON = "Sanderson"; + private static final String BOOK_BY_SANDERSON = "The Way of Kings"; + + private static final String AUTHOR_ABERCROMBIE = "Abercrombie"; + private static final String BOOK_BY_ABERCROMBIE = "The Blade Itself"; + + // Each example will have its own proxy for the cache and both regions. + private final ClientCache cache; + private final Region region1; + private final Region region2; + + private Example(String username) { + Properties props = new Properties(); + props.setProperty("security-username", username); + props.setProperty("security-client-auth-init", ExampleAuthInit.class.getName()); + + // connect to the locator using default port 10334 + cache = new ClientCacheFactory(props).setPoolSubscriptionEnabled(true) + .addPoolLocator("localhost", 10334).create(); + region1 = cache.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY) + .create(REGION1); + region2 = cache.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY) + .create(REGION2); + } + + public static void main(String[] args) throws Exception { + adminUserCanPutAndGetEverywhere(); + writeOnlyUserCannotGet(); + readOnlyUserCannotPut(); + regionUserIsRestrictedByRegion(); + } + + private static void adminUserCanPutAndGetEverywhere() throws Exception { + String valueFromRegion; + try (Example example = new Example("superUser")) { + // All puts and gets should pass + example.region1.put(AUTHOR_ABERCROMBIE, BOOK_BY_ABERCROMBIE); + example.region2.put(AUTHOR_GROSSMAN, BOOK_BY_GROSSMAN); + + valueFromRegion = example.region1.get(AUTHOR_ABERCROMBIE); + Validate.isTrue(BOOK_BY_ABERCROMBIE.equals(valueFromRegion)); + + valueFromRegion = example.region2.get(AUTHOR_GROSSMAN); + Validate.isTrue(BOOK_BY_GROSSMAN.equals(valueFromRegion)); + } + } + + private static void writeOnlyUserCannotGet() { + try (Example example = new Example("dataWriter")) { + // Writes to any region should pass + example.region1.put(AUTHOR_LYNCH, BOOK_BY_LYNCH); + example.region2.put(AUTHOR_ROTHFUSS, BOOK_BY_ROTHFUSS); + + // This will fail since dataWriter does not have DATA:READ + example.region1.get(AUTHOR_LYNCH); + } catch (Exception e) { + logger.error("This exception should be caused by NotAuthorizedException", e); + } + } + + private static void readOnlyUserCannotPut() { + try (Example example = new Example("dataReader")) { + // This will pass + example.region1.get(AUTHOR_LYNCH); + example.region2.get(AUTHOR_ROTHFUSS); + + // This will fail since dataReader does not have DATA:WRITE + example.region1.put(AUTHOR_SANDERSON, BOOK_BY_SANDERSON); + } catch (Exception e) { + logger.error("This exception should be caused by NotAuthorizedException", e); + } + } + + private static void regionUserIsRestrictedByRegion() { + try (Example example = new Example("region1dataAdmin")) { + // This user can read and write only in region1 + example.region1.put(AUTHOR_SANDERSON, BOOK_BY_SANDERSON); + String valueFromRegion = example.region1.get(AUTHOR_SANDERSON); + Validate.isTrue(BOOK_BY_SANDERSON.equals(valueFromRegion)); + + // This will fail since dataReader does not have DATA:WRITE:region2 + example.region2.put(AUTHOR_SCALZI, BOOK_BY_SCALZI); + } catch (Exception e) { + logger.error("This exception should be caused by NotAuthorizedException", e); + } + } + + /** + * We use AutoCloseable examples to guarantee the cache closes. Failure to close the cache would + * cause failures when attempting to run with a new user. + */ + @Override + public void close() throws Exception { + cache.close(); + } +} diff --git a/clientSecurity/src/main/java/org/apache/geode/examples/clientSecurity/ExampleAuthInit.java b/clientSecurity/src/main/java/org/apache/geode/examples/clientSecurity/ExampleAuthInit.java new file mode 100644 index 0000000..5f859f2 --- /dev/null +++ b/clientSecurity/src/main/java/org/apache/geode/examples/clientSecurity/ExampleAuthInit.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.geode.examples.clientSecurity; + +import java.util.Properties; + +import org.apache.logging.log4j.Logger; + +import org.apache.geode.LogWriter; +import org.apache.geode.distributed.DistributedMember; +import org.apache.geode.internal.logging.LogService; +import org.apache.geode.security.AuthInitialize; +import org.apache.geode.security.AuthenticationFailedException; + +public class ExampleAuthInit implements AuthInitialize { + + private static final Logger logger = LogService.getLogger(); + + private static final String USER_NAME = "security-username"; + private static final String PASSWORD = "security-password"; + + private static final String INSECURE_PASSWORD_FOR_EVERY_USER = "123"; + + /** + * The implementer would use their existing infrastructure (e.g., ldap) here to populate these + * properties with the user credentials. These properties will in turn be handled by the + * implementer's design of SecurityManager to authenticate users and authorize operations. + */ + @Override + public Properties getCredentials(Properties securityProps) throws AuthenticationFailedException { + Properties credentials = new Properties(); + String userName = securityProps.getProperty(USER_NAME); + if (userName == null) { + throw new AuthenticationFailedException( + "ExampleAuthInit: user name property [" + USER_NAME + "] not set."); + } + credentials.setProperty(USER_NAME, userName); + credentials.setProperty(PASSWORD, INSECURE_PASSWORD_FOR_EVERY_USER); + logger.info("SampleAuthInit: successfully obtained credentials for user " + userName); + return credentials; + } + + @Override + public void close() {} + + @Override + @Deprecated + public void init(LogWriter systemLogger, LogWriter securityLogger) + throws AuthenticationFailedException {} + + @Override + @Deprecated + public Properties getCredentials(Properties securityProps, DistributedMember server, + boolean isPeer) throws AuthenticationFailedException { + return getCredentials(securityProps); + } +} diff --git a/clientSecurity/src/main/resources/example_security.json b/clientSecurity/src/main/resources/example_security.json new file mode 100644 index 0000000..9d401cd --- /dev/null +++ b/clientSecurity/src/main/resources/example_security.json @@ -0,0 +1,72 @@ +{ + "roles": [ + { + "name": "data", + "operationsAllowed": [ + "DATA:MANAGE", + "DATA:WRITE", + "DATA:READ" + ] + }, + { + "name": "cluster", + "operationsAllowed": [ + "CLUSTER:MANAGE", + "CLUSTER:WRITE", + "CLUSTER:READ" + ] + }, + { + "name": "region1data", + "operationsAllowed": [ + "DATA:MANAGE", + "DATA:WRITE", + "DATA:READ" + ], + "regions": ["region1"] + }, + { + "name": "dataReader", + "operationsAllowed": [ + "DATA:READ" + ] + }, + { + "name": "dataWriter", + "operationsAllowed": [ + "DATA:WRITE" + ] + } + ], + "users": [ + { + "name": "superUser", + "password": "123", + "roles": [ + "cluster", + "data" + ] + }, + { + "name": "region1dataAdmin", + "password": "123", + "roles": [ + "region1data" + ] + }, + { + "name": "dataReader", + "password": "123", + "roles": [ + "dataReader" + ] + }, + { + "name": "dataWriter", + "password": "123", + "roles": [ + "dataWriter" + ] + } + ] +} diff --git a/gradle/rat.gradle b/gradle/rat.gradle index fe73400..63e8d7b 100644 --- a/gradle/rat.gradle +++ b/gradle/rat.gradle @@ -59,6 +59,7 @@ rat { '**/*.diff', '**/*.rej', '**/*.orig', + '**/*.json', // working directories '**/locator/**', diff --git a/settings.gradle b/settings.gradle index 7b579bb..240e573 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,3 +21,4 @@ include 'partitioned' include 'lucene' include 'loader' include 'putall' +include 'clientSecurity'