diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2414ff114..42e31cd49 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -147,7 +147,9 @@ jobs:
- ~/.ccm/repository
key: v1-dependencies-{{ checksum "pom.xml" }}
- run:
- command: MAVEN_OPTS="-Xmx384m" mvn -B install
+ command: |
+ MAVEN_OPTS="-Xmx384m" mvn -B install
+ mvn -B surefire:test -Dtest=ReaperShiroIT
c_2-1_memory:
environment:
CASSANDRA_VERSION: 2.1.20
diff --git a/src/ci/script.sh b/src/ci/script.sh
index 7dabd7d61..872be047e 100755
--- a/src/ci/script.sh
+++ b/src/ci/script.sh
@@ -45,8 +45,8 @@ case "${TEST_TYPE}" in
if [ "x${GRIM_MIN}" = "x" ]
then
+ mvn -B surefire:test -Dtest=ReaperShiroIT
mvn -B surefire:test -Dtest=ReaperIT
- mvn -B surefire:test -Dtest=ReaperAuthIT
mvn -B surefire:test -Dtest=ReaperH2IT
mvn -B surefire:test -Dtest=ReaperPostgresIT
else
diff --git a/src/docs/content/docs/usage/authentication.md b/src/docs/content/docs/usage/authentication.md
index a4a94a239..9a9e1efaa 100644
--- a/src/docs/content/docs/usage/authentication.md
+++ b/src/docs/content/docs/usage/authentication.md
@@ -6,42 +6,31 @@ identifier = "auth"
parent = "usage"
+++
-# Activating Web UI Authentication
+# Web UI Authentication
-Authentication can be activated in Reaper for the web UI only. It relies on [Apache Shiro](https://shiro.apache.org/), which allows to store users and password in files, databases or connect through LDAP and Active Directory out of the box.
+Authentication is activated in Reaper by default. It relies on [Apache Shiro](https://shiro.apache.org/), which allows to store users and password in files, databases or connect through LDAP and Active Directory out of the box. The default authentication uses the dummy username and password as found in the default [shiro.ini](https://github.com/thelastpickle/cassandra-reaper/blob/master/src/server/src/main/resources/shiro.ini). It is expected you override this in a production environment.
-To activate authentication, add the following block to your Reaper yaml file :
+This default Shiro authentication configuration is referenced via the following block in the Reaper yaml file :
```ini
accessControl:
sessionTimeout: PT10M
shiro:
- iniConfigs: ["file:/path/to/shiro.ini"]
+ iniConfigs: ["classpath:shiro.ini"]
```
## With clear passwords
-Create a `shiro.ini` file and adapt it from the following sample :
+Copy the default `shiro.ini` file and adapt it overriding the "users" section :
```ini
-[main]
-authc = org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter
-authc.loginUrl = /webui/login.html
+…
[users]
user1 = password1
user2 = password2
-[urls]
-# Allow anonynous access to login page (and dependencies), but no other pages
-/webui/ = authc
-/webui = authc
-/webui/login.html = anon
-/webui/*.html* = authc
-/webui/*.js* = anon
-/ping = anon
-/login = anon
-/** = anon
+…
```
## With encrypted passwords
@@ -58,17 +47,7 @@ iniRealm.credentialsMatcher = $sha256Matcher
[users]
john = 807A09440428C0A8AEF58BD3ECE32938B0D76E638119E47619756F5C2C20FF3A
-
-[urls]
-# Allow anonynous access to login page (and dependencies), but no other pages
-/webui/ = authc
-/webui = authc
-/webui/login.html = anon
-/webui/*.html* = authc
-/webui/*.js* = anon
-/ping = anon
-/login = anon
-/** = anon
+…
```
To generate a password, you case use for example :
@@ -90,6 +69,26 @@ print(hex_dig)
a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e
```
-Then start Reaper.
+## With LDAP accounts
+
+Based on [Shiro's LDAP realm usage](https://shiro.apache.org/static/1.2.4/apidocs/org/apache/shiro/realm/ldap/JndiLdapContextFactory.html).
+
+An example configuration for LDAP authentication exists (commented out) in the default shiro.ini :
+
+```
+
+# Example LDAP realm, see https://shiro.apache.org/static/1.2.4/apidocs/org/apache/shiro/realm/ldap/JndiLdapContextFactory.html
+ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
+ldapRealm.userDnTemplate = uid={0},ou=users,dc=cassandra-reaper,dc=io
+ldapRealm.contextFactory.url = ldap://ldapHost:389
+;ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
+;ldapRealm.contextFactory.systemUsername = cn=Manager, dc=example, dc=com
+;ldapRealm.contextFactory.systemPassword = secret
+;ldapRealm.contextFactory.environment[java.naming.security.credentials] = ldap_password
+
+```
+
+
+## Accessing Reaper via the command line `spreaper`
-Both the REST API and the `/webui/login.html` pages will be accessible anonymously, but all other pages will require to be authenticated.
+The RESTful endpoints to Reaper can also be authenticated using JWT (Java Web Token). A token can be generated from the `/jwt` url.
diff --git a/src/server/pom.xml b/src/server/pom.xml
index f8193bb37..6a2222403 100755
--- a/src/server/pom.xml
+++ b/src/server/pom.xml
@@ -267,6 +267,12 @@
2.0M10
test
+
+ org.zapodot
+ embedded-ldap-junit
+ 0.7
+ test
+
diff --git a/src/server/src/main/resources/shiro.ini b/src/server/src/main/resources/shiro.ini
index cb590bdff..53569c813 100644
--- a/src/server/src/main/resources/shiro.ini
+++ b/src/server/src/main/resources/shiro.ini
@@ -19,6 +19,15 @@ authc.loginUrl = /webui/login.html
# Java Web Token authentication for REST endpoints
jwtv = io.cassandrareaper.resources.auth.ShiroJwtVerifyingFilter
+# Example LDAP realm, see https://shiro.apache.org/static/1.2.4/apidocs/org/apache/shiro/realm/ldap/JndiLdapContextFactory.html
+;ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
+;ldapRealm.userDnTemplate = uid={0},ou=users,dc=cassandra-reaper,dc=io
+;ldapRealm.contextFactory.url = ldap://ldapHost:389
+;ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
+;ldapRealm.contextFactory.systemUsername = cn=Manager, dc=example, dc=com
+;ldapRealm.contextFactory.systemPassword = secret
+;ldapRealm.contextFactory.environment[java.naming.security.credentials] = ldap_password
+
# default authentication is the following hardcoded admin user
[users]
admin = admin
diff --git a/src/server/src/test/java/io/cassandrareaper/acceptance/BasicSteps.java b/src/server/src/test/java/io/cassandrareaper/acceptance/BasicSteps.java
index 88cadb0c0..86909373d 100644
--- a/src/server/src/test/java/io/cassandrareaper/acceptance/BasicSteps.java
+++ b/src/server/src/test/java/io/cassandrareaper/acceptance/BasicSteps.java
@@ -1489,7 +1489,7 @@ public void aRequestIsMade(String method, String requestPath) throws Throwable {
@Then("^a \"([^\"]*)\" response is returned$")
public void aResponseIsReturned(String statusDescription) throws Throwable {
- assertEquals(lastResponse.getStatus(), httpStatus(statusDescription));
+ Assertions.assertThat(lastResponse.getStatus()).isEqualTo(httpStatus(statusDescription));
}
@Then("^the response was redirected to the login page$")
diff --git a/src/server/src/test/java/io/cassandrareaper/acceptance/ReaperShiroIT.java b/src/server/src/test/java/io/cassandrareaper/acceptance/ReaperShiroIT.java
new file mode 100644
index 000000000..26bfbbd85
--- /dev/null
+++ b/src/server/src/test/java/io/cassandrareaper/acceptance/ReaperShiroIT.java
@@ -0,0 +1,59 @@
+/*
+ *
+ * Copyright 2019-2019 The Last Pickle Ltd
+ *
+ * Licensed 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 io.cassandrareaper.acceptance;
+
+import java.util.Optional;
+
+import cucumber.api.CucumberOptions;
+import cucumber.api.junit.Cucumber;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Cucumber.class)
+@CucumberOptions(
+ features = "classpath:io.cassandrareaper.acceptance/access_control.feature",
+ plugin = {"pretty"}
+ )
+public final class ReaperShiroIT {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ReaperIT.class);
+ private static ReaperTestJettyRunner runner;
+ private static final String MEMORY_CONFIG_FILE = "cassandra-reaper-access-control-enabled-at.yaml";
+
+ private ReaperShiroIT() {}
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ LOG.info(
+ "setting up testing Reaper runner with {} seed hosts defined and memory storage",
+ TestContext.TEST_CLUSTER_SEED_HOSTS.size());
+
+ runner = new ReaperTestJettyRunner(MEMORY_CONFIG_FILE, Optional.empty());
+ BasicSteps.addReaperRunner(runner);
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ LOG.info("Stopping reaper service...");
+ runner.runnerInstance.after();
+ }
+
+}
\ No newline at end of file
diff --git a/src/server/src/test/java/io/cassandrareaper/resources/auth/EmbeddedLdapTest.java b/src/server/src/test/java/io/cassandrareaper/resources/auth/EmbeddedLdapTest.java
new file mode 100644
index 000000000..8f8001be5
--- /dev/null
+++ b/src/server/src/test/java/io/cassandrareaper/resources/auth/EmbeddedLdapTest.java
@@ -0,0 +1,79 @@
+/*
+ *
+ * Copyright 2019-2019 The Last Pickle Ltd
+ *
+ * Licensed 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 io.cassandrareaper.resources.auth;
+
+import java.util.List;
+
+
+import com.unboundid.ldap.sdk.LDAPInterface;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+import org.junit.Rule;
+import org.junit.Test;
+import org.zapodot.junit.ldap.EmbeddedLdapRule;
+import org.zapodot.junit.ldap.EmbeddedLdapRuleBuilder;
+
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Test using objectclass: org.zapodot:embedded-ldap-junit
+ */
+public final class EmbeddedLdapTest {
+
+ public static final String DOMAIN_DSN = "dc=cassandra-reaper,dc=io";
+
+ @Rule
+ public EmbeddedLdapRule embeddedLdapRule
+ = EmbeddedLdapRuleBuilder.newInstance().usingDomainDsn(DOMAIN_DSN).importingLdifs("test-ldap-users.ldif").build();
+
+ @Test
+ public void shouldFindAllPersons() throws Exception {
+ LDAPInterface ldapConnection = embeddedLdapRule.ldapConnection();
+ SearchResult searchResult = ldapConnection.search(DOMAIN_DSN, SearchScope.SUB, "(objectClass=person)");
+ assertThat(3, equalTo(searchResult.getEntryCount()));
+ List searchEntries = searchResult.getSearchEntries();
+ assertThat(searchEntries.get(0).getAttribute("cn").getValue(), equalTo("John Steinbeck"));
+ assertThat(searchEntries.get(1).getAttribute("cn").getValue(), equalTo("Micha Kops"));
+ assertThat(searchEntries.get(2).getAttribute("cn").getValue(), equalTo("Santa Claus"));
+ }
+
+ @Test
+ public void shouldFindExactPerson0() throws Exception {
+ LDAPInterface ldapConnection = embeddedLdapRule.ldapConnection();
+
+ SearchResult searchResult = ldapConnection
+ .search("cn=John Steinbeck,ou=Users,dc=cassandra-reaper,dc=io", SearchScope.SUB, "(objectClass=person)");
+
+ assertThat(1, equalTo(searchResult.getEntryCount()));
+ assertThat(searchResult.getSearchEntries().get(0).getAttribute("cn").getValue(), equalTo("John Steinbeck"));
+ }
+
+ @Test
+ public void shouldFindExactPerson1() throws Exception {
+ LDAPInterface ldapConnection = embeddedLdapRule.ldapConnection();
+
+ SearchResult searchResult = ldapConnection
+ .search("uid=sclaus,ou=Users,dc=cassandra-reaper,dc=io", SearchScope.SUB, "(objectClass=person)");
+
+ assertThat(1, equalTo(searchResult.getEntryCount()));
+ assertThat(searchResult.getSearchEntries().get(0).getAttribute("cn").getValue(), equalTo("Santa Claus"));
+ }
+}
diff --git a/src/server/src/test/java/io/cassandrareaper/resources/auth/LoginResourceLdapTest.java b/src/server/src/test/java/io/cassandrareaper/resources/auth/LoginResourceLdapTest.java
new file mode 100644
index 000000000..af0d26fb7
--- /dev/null
+++ b/src/server/src/test/java/io/cassandrareaper/resources/auth/LoginResourceLdapTest.java
@@ -0,0 +1,51 @@
+/*
+ *
+ * Copyright 2019-2019 The Last Pickle Ltd
+ *
+ * Licensed 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 io.cassandrareaper.resources.auth;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.io.ResourceUtils;
+import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
+import org.junit.Rule;
+import org.junit.Test;
+import org.zapodot.junit.ldap.EmbeddedLdapRule;
+import org.zapodot.junit.ldap.EmbeddedLdapRuleBuilder;
+
+import static io.cassandrareaper.resources.auth.EmbeddedLdapTest.DOMAIN_DSN;
+
+
+public final class LoginResourceLdapTest {
+
+ @Rule
+ public EmbeddedLdapRule embeddedLdapRule
+ = EmbeddedLdapRuleBuilder.newInstance().usingDomainDsn(DOMAIN_DSN).importingLdifs("test-ldap-users.ldif").build();
+
+ @Test
+ public void testLoginLdap() throws IOException {
+ try (InputStream is = ResourceUtils.getInputStreamForPath("classpath:test-shiro-ldap.ini")) {
+ Ini ini = new Ini();
+ ini.load(is);
+ int port = embeddedLdapRule.embeddedServerPort();
+ ini.setSectionProperty("main", "ldapRealm.contextFactory.url", "ldap://localhost:" + port);
+ new WebIniSecurityManagerFactory(ini).getInstance().authenticate(new UsernamePasswordToken("sclaus", "abcdefg"));
+ }
+ }
+}
diff --git a/src/server/src/test/resources/cassandra-reaper-at-access-control-enabled.yaml b/src/server/src/test/resources/cassandra-reaper-access-control-enabled-at.yaml
similarity index 93%
rename from src/server/src/test/resources/cassandra-reaper-at-access-control-enabled.yaml
rename to src/server/src/test/resources/cassandra-reaper-access-control-enabled-at.yaml
index 740ad4288..e59910f03 100644
--- a/src/server/src/test/resources/cassandra-reaper-at-access-control-enabled.yaml
+++ b/src/server/src/test/resources/cassandra-reaper-access-control-enabled-at.yaml
@@ -1,5 +1,4 @@
-# Copyright 2014-2017 Spotify AB
-# Copyright 2016-2019 The Last Pickle Ltd
+# Copyright 2018-2019 The Last Pickle Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,7 +14,7 @@
#
# Cassandra Reaper Configuration for Acceptance Tests.
#
-segmentCount: 200
+segmentCountPerNode: 16
repairParallelism: SEQUENTIAL
repairIntensity: 0.95
scheduleDaysBetween: 7
@@ -26,11 +25,13 @@ incrementalRepair: false
blacklistTwcsTables: true
enableDynamicSeedList: false
jmxConnectionTimeoutInSeconds: 300
+datacenterAvailability: LOCAL
logging:
level: INFO
loggers:
io.cassandrareaper: DEBUG
+ com.datastax.driver: WARN
appenders:
- type: console
@@ -46,7 +47,6 @@ server:
bindHost: 127.0.0.1
jmxPorts:
- 127.0.0.1: 7100
127.0.0.2: 7200
127.0.0.3: 7300
127.0.0.4: 7400
diff --git a/src/server/src/test/resources/io.cassandrareaper.acceptance/access_control.feature b/src/server/src/test/resources/io.cassandrareaper.acceptance/access_control.feature
index 3d4cdd419..78b10b382 100644
--- a/src/server/src/test/resources/io.cassandrareaper.acceptance/access_control.feature
+++ b/src/server/src/test/resources/io.cassandrareaper.acceptance/access_control.feature
@@ -27,6 +27,22 @@ Feature: Access Control
Given that we are going to use "127.0.0.1@test" as cluster seed host
When a is made
Then a "OK" response is returned
+ Examples:
+ | path | request |
+ | GET | /webui/login.html |
+
+ Scenario Outline: Request to ping resource is allowed but not healthy
+ Given that we are going to use "127.0.0.1@test" as cluster seed host
+ When a is made
+ Then a "NOT_ACCEPTABLE" response is returned
+ Examples:
+ | path | request |
+ | GET | /ping |
+
+ Scenario Outline: Request to protected resource without login returns forbidden
+ Given that we are going to use "127.0.0.1@test" as cluster seed host
+ When a is made
+ Then a "FORBIDDEN" response is returned
Examples:
| path | request |
| GET | /cluster |
diff --git a/src/server/src/test/resources/test-ldap-users.ldif b/src/server/src/test/resources/test-ldap-users.ldif
new file mode 100644
index 000000000..f168b1a3c
--- /dev/null
+++ b/src/server/src/test/resources/test-ldap-users.ldif
@@ -0,0 +1,44 @@
+dn: dc=cassandra-reaper,dc=io
+objectClass: domain
+objectClass: top
+dc: example
+
+dn: ou=Users,dc=cassandra-reaper,dc=io
+objectClass: organizationalUnit
+objectClass: top
+ou: Users
+
+dn: ou=Groups,dc=cassandra-reaper,dc=io
+objectClass: organizationalUnit
+objectClass: top
+ou: Groups
+
+dn: cn=Micha Kops,ou=Users,dc=cassandra-reaper,dc=io
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Micha Kops
+sn: Kops
+uid: mkops
+userPassword: abcdefg
+
+dn: uid=sclaus,ou=Users,dc=cassandra-reaper,dc=io
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+cn: Santa Claus
+sn: Claus
+uid: sclaus
+userPassword: abcdefg
+
+dn: cn=John Steinbeck,ou=Users,dc=cassandra-reaper,dc=io
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+cn: John Steinbeck
+sn: Steinbeck
+uid: jsteinbeck
+userPassword: abcdefg
diff --git a/src/server/src/test/resources/test-shiro-ldap.ini b/src/server/src/test/resources/test-shiro-ldap.ini
new file mode 100644
index 000000000..bf1903d3b
--- /dev/null
+++ b/src/server/src/test/resources/test-shiro-ldap.ini
@@ -0,0 +1,45 @@
+# Copyright 2019-2019 The Last Pickle Ltd
+#
+# Licensed 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.
+
+[main]
+authc = org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter
+authc.loginUrl = /webui/login.html
+
+# Java Web Token authentication for REST endpoints
+jwtv = io.cassandrareaper.resources.auth.ShiroJwtVerifyingFilter
+
+# Example LDAP realm, see https://shiro.apache.org/static/1.2.4/apidocs/org/apache/shiro/realm/ldap/JndiLdapContextFactory.html
+ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
+ldapRealm.userDnTemplate = uid={0},ou=Users,dc=cassandra-reaper,dc=io
+ldapRealm.contextFactory.url = see_LoginResourceTest.testLoginLdap
+;ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
+;ldapRealm.contextFactory.systemUsername = cn=Manager, dc=example, dc=com
+;ldapRealm.contextFactory.systemPassword = secret
+;ldapRealm.contextFactory.environment[java.naming.security.credentials] = ldap_password
+
+[urls]
+# Web UI requires manual authentication and session cookie
+/webui/ = authc
+/webui = authc
+/jwt = authc
+/webui/*.html* = authc
+
+# login page and all js and css resources do not require authentication
+/webui/login.html = anon
+/webui/** = anon
+/ping = anon
+/login = anon
+
+# REST endpoints require a Java Web Token
+/** = noSessionCreation,jwtv
\ No newline at end of file