Skip to content

Commit

Permalink
Tests, documentation, and a configuration template, for Shiro->LDAP s…
Browse files Browse the repository at this point in the history
…upport

 ref: #605
  • Loading branch information
michaelsembwever committed Jan 31, 2019
1 parent 3fcb7b6 commit f8bb73b
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 38 deletions.
4 changes: 3 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/ci/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 30 additions & 31 deletions src/docs/content/docs/usage/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 :
Expand All @@ -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.
6 changes: 6 additions & 0 deletions src/server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@
<version>2.0M10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.zapodot</groupId>
<artifactId>embedded-ldap-junit</artifactId>
<version>0.7</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
9 changes: 9 additions & 0 deletions src/server/src/main/resources/shiro.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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$")
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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<SearchResultEntry> 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"));
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -15,7 +14,7 @@
#
# Cassandra Reaper Configuration for Acceptance Tests.
#
segmentCount: 200
segmentCountPerNode: 16
repairParallelism: SEQUENTIAL
repairIntensity: 0.95
scheduleDaysBetween: 7
Expand All @@ -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

Expand All @@ -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
Expand Down
Loading

0 comments on commit f8bb73b

Please sign in to comment.