Skip to content

Commit

Permalink
refactoring of plugin
Browse files Browse the repository at this point in the history
ldap context is created every login
fixed caching of ldap users
  • Loading branch information
smiklosovic committed Dec 16, 2020
1 parent d54c629 commit 52e3f44
Show file tree
Hide file tree
Showing 57 changed files with 1,214 additions and 705 deletions.
47 changes: 30 additions & 17 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,27 @@ The content of the configuration file is as follows:
|service_password
|Service user password

|ldap_naming_attribute
|Use this setting if you need to change your naming attribute from the usual cn=
|filter_template
|template for searching in LDAP, explanation further in this readme, defaults to `(cn=%s)`

|auth_cache_enabled
|relevant for Cassandra 3.11 and 4.0 plugins, defaults to `false`

|consistency_for_role
|consistency level to use for retrieval of a role to check if it can log in - defaults to LOCAL_ONE

|auth_bcrypt_gensalt_log2_rounds
|number of rounds to hash passwords

|load_ldap_service
|defaults to false, if it is true, SPI mechanism will look on class path to load custom implementation of `LDAPUserRetriever`.
|===


## Configuration of Cassandra

If is *strongly* recommended to use `NetworkTopologyStrategy` for your `system_auth keyspace`.


Please be sure that `system_auth` keyspace uses `NetworkTopologyStrategy` with number of replicas equal to number of nodes in DC. If it is not
the case, you can alter your keyspace as follows:

Expand Down Expand Up @@ -145,7 +150,15 @@ For fast testing there is Debian OpenLDAP Docker container
The `ldap.configuration` file in the `conf` directory does not need to be changed, and with the above `docker run` it will work out of the box. You just
have to put it in `$CASSANDRA_CONF` or set respective configuration property as described above.

## How It Works
## Explanation of filter_template property

`filter_template` property is by default `(cn=%s)` where `%s` will be replaced by name you want to log in with.
For example if you do `cqlsh -u myuserinldap`, a search filter for LDAP will be `(cn=myuserinldap)`. You
may have a different search filter based on your need, a lot of people use e.g. SAM or something similar.
If you try to log in with `cqlsh -u cn=myuserinldap`, there will be no replacement done and this will be
used as a search filter instead.

## How it Works

LDAPAuthenticator currently supports plain text authorization requests only in the form of a username and password.
This request is made to the LDAP server over plain text, so you should be using client encryption on the Cassandra
Expand All @@ -165,14 +178,8 @@ the operator still has a possibility of logging in via `cassandra` user as usual
Users meant to be authenticated against the LDAP server will not be able to log in but all "normal" users will be able to
login and the disruption of LDAP communication will not affect their ability to do so as they live in Cassandra natively.

In case there are two logins of same name (e.g. `admin` in LDAP and `admin` in C*),
in order to distinguish them, if you want to login with LDAP user, you have to
specify its full account name, e.g.

cqlsh -u cn=admin,dn=example,dn=org

In case a user specifies just `admin` as login name (or any other name, for that matter), it will try to
authenticate against database first and if not successful against LDAP, adding all details (cn= etc. ...) to username automatically.
In case a user specifies just `test` as login name (or any other name, for that matter), it will try to
authenticate against database first and if not successful against LDAP using filter `filter_template` which defaults to `(cn=%s)`

It is possible to delete administration role (e.g. role `cassandra`) but if one does that, all administration operations are only able to
be done via LDAP account. In case LDAP is down, the operator would not have any control over DB as `cassandra` is not present anymore.
Expand All @@ -184,21 +191,27 @@ If you delete `cassandra` user, there is suddenly not such user. You have to res

Where `dba` is _new_ superuser which is able to write to `system_auth.roles` and acts as Cassandra admin.

Upon login via LDAP user, this plugin will create a dummy role just to be able to play as a normal Cassandra role
with all its permissions and so on. Passwords for LDAP users are not stored in Cassandra, obviously.

Credentials are cached for implementations for Cassandra 3.11 and 4.0 so that way we are not hitting LDAP server
all the time when there is a lot of login attempts with same login name. An administrator can increase
relevant validity settings in `cassandra.yaml` to increase these periods even more.

## SPI for LDAP server implementations (advanced)

In order to talk to a LDAP server, there is `DefaultLDAPServer` class in `base` module which all modules are using.
However, it might not be enough - there is a lot of LDAP servers out there and their internals and configuration
might render the default implementation incompatible. If you have special requirements, you might provide your
own implementation by extending `DefaultLDAPServer` and overriding what is necessary. You might as well
extend and implement `LDAPPasswordRetriever` class. `DefaultLDAPServer` just extends it.
own implementation by implementing `LDAPUserRetriever`. You have to have `load_ldap_service` set to `true` as well.

To tell LDAP plugin to use your implementation, you need to create a file in `src/main/resources/META-INF/services`
called `com.instaclustr.cassandra.ldap.auth.LDAPPasswordRetriever` and the content of that file needs to
called `LDAPUserRetriever` and the content of that file needs to
be just one line - the fully qualified class name (with package) of your custom implementation.

After you build such plugin, the SPI mechanism upon plugin's initialisation during Cassandra node startup
will pick up your custom LDAP server connection / authentication logic.

## Further Information
- See blog by Kurt Greaves https://www.instaclustr.com/apache-cassandra-ldap-authentication/[Apache Cassandra LDAP Authentication]
- Please see https://www.instaclustr.com/support/documentation/announcements/instaclustr-open-source-project-status/ for Instaclustr support status of this project
- See blog by Stefan Miklosovic about https://www.instaclustr.com/the-instaclustr-ldap-plugin-for-cassandra/[Apache Cassandra LDAP Authentication]
- Please see https://www.instaclustr.com/support/documentation/announcements/instaclustr-open-source-project-status/[Instaclustr support status] of this project
2 changes: 1 addition & 1 deletion base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
-->

<artifactId>cassandra-ldap-base</artifactId>
<version>1.0.2</version>
<version>1.1.0</version>

<name>Cassandra LDAP Authenticator common code</name>
<description>Common code for Apache Cassandra LDAP plugin</description>
Expand Down
43 changes: 39 additions & 4 deletions base/src/main/java/com/instaclustr/cassandra/ldap/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
*/
package com.instaclustr.cassandra.ldap;

import java.util.StringJoiner;

import org.apache.commons.lang3.builder.HashCodeBuilder;

public class User
{

private final String username;
private String username;

private final String password;
private String password;

private String ldapDN = null;

Expand All @@ -44,6 +46,14 @@ public User(String username, String password)
this.password = password;
}

public void setUsername(final String username) {
this.username = username;
}

public void setPassword(final String password) {
this.password = password;
}

public String getLdapDN()
{
return ldapDN;
Expand Down Expand Up @@ -83,11 +93,36 @@ public boolean equals(Object obj)

final User other = (User) obj;

return this.getUsername().equals(other.getUsername());
if (this.ldapDN != null && other.ldapDN != null)
{
return this.ldapDN.equals(other.ldapDN);
}
else if (this.username != null && other.username != null)
{
return this.username.equals(other.username);
}

return false;
}

public int hashCode()
{
return new HashCodeBuilder(19, 29).append(getUsername()).toHashCode();
if (ldapDN != null)
{
return new HashCodeBuilder(19, 29).append(ldapDN).toHashCode();
}

assert username != null;

return new HashCodeBuilder(19, 29).append(username).toHashCode();
}

@Override
public String toString() {
return new StringJoiner(", ", User.class.getSimpleName() + "[", "]")
.add("username='" + username + "'")
.add("password=redacted")
.add("ldapDN='" + ldapDN + "'")
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractCassandraRolePasswordRetriever implements CassandraPasswordRetriever
public abstract class AbstractCassandraUserRetriever implements CassandraUserRetriever
{

private static final Logger logger = LoggerFactory.getLogger(AbstractCassandraRolePasswordRetriever.class);
private static final Logger logger = LoggerFactory.getLogger(AbstractCassandraUserRetriever.class);

protected static final String LEGACY_CREDENTIALS_TABLE = "credentials";
protected static final String AUTH_KEYSPACE = "system_auth";
Expand All @@ -47,7 +47,7 @@ public abstract class AbstractCassandraRolePasswordRetriever implements Cassandr
protected boolean legacyTableExists;

@Override
public String retrieveHashedPassword(User user)
public User retrieve(User user)
{
try
{
Expand All @@ -67,7 +67,7 @@ public String retrieveHashedPassword(User user)
throw new NoSuchCredentialsException();
}

return result.one().getString("salted_hash");
return new User(user.getUsername(), result.one().getString("salted_hash"));
} catch (NoSuchRoleException ex)
{
logger.trace(format("User %s does not exist in the Cassandra database.", user.getUsername()));
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.instaclustr.cassandra.ldap.auth;

import com.instaclustr.cassandra.ldap.User;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.service.ClientState;

public interface PasswordRetriever
public interface CassandraUserRetriever extends UserRetriever
{

void init(ClientState clientState) throws ConfigurationException;

String retrieveHashedPassword(User user);
}
Loading

0 comments on commit 52e3f44

Please sign in to comment.