fathom.realm.redis.RedisRealm Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fathom-security-redis Show documentation
Show all versions of fathom-security-redis Show documentation
Realm for Redis-based authentication
/*
* Copyright (C) 2015 the original author or authors.
*
* 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 fathom.realm.redis;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.typesafe.config.Config;
import fathom.authc.StandardCredentials;
import fathom.exception.FathomException;
import fathom.realm.Account;
import fathom.realm.CachingRealm;
import fathom.utils.ClassUtil;
import fathom.utils.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
/**
* @author James Moger
*/
public class RedisRealm extends CachingRealm {
private static Logger log = LoggerFactory.getLogger(RedisRealm.class);
private JedisPool pool;
private String redisUrl;
private String redisPassword;
private String passwordMapping;
private String nameMapping;
private String emailMapping;
private String roleMapping;
private String permissionMapping;
private String startScript;
private String stopScript;
public RedisRealm() {
super();
}
@Override
public void setup(Config config) {
super.setup(config);
redisUrl = Strings.emptyToNull(config.getString("url"));
Preconditions.checkNotNull(redisUrl, "Url must be specified!");
if (config.hasPath("password")) {
redisPassword = Strings.emptyToNull(config.getString("password"));
}
passwordMapping = "${username}:password";
if (config.hasPath("passwordMapping")) {
passwordMapping = Strings.emptyToNull(config.getString("passwordMapping"));
}
Preconditions.checkNotNull(passwordMapping, "You must specify 'passwordMapping'");
if (config.hasPath("nameMapping")) {
nameMapping = Strings.emptyToNull(config.getString("nameMapping"));
}
if (config.hasPath("emailMapping")) {
emailMapping = Strings.emptyToNull(config.getString("emailMapping"));
}
if (config.hasPath("roleMapping")) {
roleMapping = Strings.emptyToNull(config.getString("roleMapping"));
}
if (config.hasPath("permissionMapping")) {
permissionMapping = Strings.emptyToNull(config.getString("permissionMapping"));
}
if (config.hasPath("startScript")) {
startScript = Strings.emptyToNull(config.getString("startScript"));
}
if (config.hasPath("stopScript")) {
stopScript = Strings.emptyToNull(config.getString("stopScript"));
}
}
@Override
public void start() {
log.debug("Realm '{}' configuration:", getRealmName());
Util.logSetting(log, "url", redisUrl);
Util.logSetting(log, "password", redisPassword);
Util.logSetting(log, "passwordMapping", passwordMapping);
Util.logSetting(log, "nameMapping", nameMapping);
Util.logSetting(log, "emailMapping", emailMapping);
Util.logSetting(log, "roleMapping", roleMapping);
super.logCacheSettings(log);
try {
this.pool = new JedisPool(redisUrl);
} catch (JedisException e) {
throw new FathomException("Failed to create a Redis pool!", e);
}
if (!Strings.isNullOrEmpty(startScript)) {
executeScript(startScript);
}
}
@Override
public void stop() {
if (this.pool != null) {
if (!Strings.isNullOrEmpty(stopScript)) {
executeScript(stopScript);
}
this.pool.close();
}
}
@Override
public Account authenticate(StandardCredentials requestCredentials) {
final String username = requestCredentials.getUsername();
final String password = requestCredentials.getPassword();
if (hasAccount(username)) {
// account is cached, authenticate against the cache
return super.authenticate(requestCredentials);
}
return authenticate(username, password);
}
@Override
public Account authenticate(final String username, final String password) {
Jedis jedis = pool.getResource();
try {
String storedPassword = asNull(jedis.get(key(username, passwordMapping)));
if (Strings.isNullOrEmpty(storedPassword)) {
log.debug("Account '{}' in '{}' has no password and may not be used for authentication",
username, getRealmName());
return null;
}
StandardCredentials requestCredentials = new StandardCredentials(username, password);
StandardCredentials storedCredentials = new StandardCredentials(username, storedPassword);
if (validatePassword(requestCredentials, storedCredentials)) {
log.debug("Authentication succeeded for '{}' against '{}'", username, getRealmName());
String name = null;
if (!Strings.isNullOrEmpty(nameMapping)) {
name = asNull(jedis.get(key(username, nameMapping)));
}
Account account = new Account(name, new StandardCredentials(username, password));
if (!Strings.isNullOrEmpty(emailMapping)) {
account.addEmailAddresses(jedis.lrange(key(username, emailMapping), 0, -1));
}
if (!Strings.isNullOrEmpty(roleMapping)) {
account.getAuthorizations().addRoles(asArray(jedis.lrange(key(username, roleMapping), 0, -1)));
}
if (!Strings.isNullOrEmpty(permissionMapping)) {
account.getAuthorizations().addPermissions(asArray(jedis.lrange(key(username, permissionMapping), 0, -1)));
}
cacheAccount(account);
return account;
} else {
log.debug("Authentication failed for '{}' against '{}'", username, getRealmName());
}
} catch (JedisException e) {
log.debug("Authentication failed for '{}' against '{}'", username, getRealmName());
pool.returnBrokenResource(jedis);
jedis = null;
} finally {
if (jedis != null) {
pool.returnResource(jedis);
}
}
return null;
}
/**
* Constructs a key for use with a key-value data store.
*
* @param username
* @param pattern
* @return a key
*/
private String key(String username, String pattern) {
return pattern.replace("${username}", username);
}
private String asNull(String value) {
return "nil".equals(value) ? null : value;
}
private String[] asArray(Collection strings) {
return strings.toArray(new String[strings.size()]);
}
private String unwrapQuotes(String value) {
if (Strings.isNullOrEmpty(value)) {
return value;
}
if (value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') {
return value.substring(1, value.length() - 1);
}
return value;
}
/**
* Execute a script located either in the classpath or on the filesystem.
*
* @param scriptPath
*/
protected void executeScript(String scriptPath) {
URL scriptUrl = null;
try {
if (scriptPath.startsWith("classpath:")) {
String script = scriptPath.substring("classpath:".length());
scriptUrl = ClassUtil.getResource(script);
if (scriptUrl == null) {
log.warn("Script '{}' not found!", scriptPath);
}
} else {
File file = new File(scriptPath);
if (file.exists()) {
scriptUrl = file.toURI().toURL();
} else {
log.warn("Script '{}' not found!", scriptPath);
}
}
// execute the script
if (scriptUrl != null) {
executeScript(scriptUrl);
}
} catch (Exception e) {
log.error("Failed to parse '{}' as a url", scriptPath);
}
}
/**
* Execute a script.
*
* @param scriptUrl
*/
protected void executeScript(URL scriptUrl) {
log.debug("Executing script '{}'", scriptUrl);
final Jedis jedis = pool.getResource();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(scriptUrl.openStream(), StandardCharsets.UTF_8))) {
reader.lines()
.filter(line -> !Strings.isNullOrEmpty(line) && line.charAt(0) != '#' && !line.startsWith("//"))
.forEach(line -> {
try {
log.trace("execute: {}", line);
String[] args = line.split(" ", 3);
String command = null;
if (args.length == 1) {
command = String.format("return redis.call('%s')", args[0]);
} else if (args.length == 2) {
command = String.format("return redis.call('%s', '%s')", args[0], args[1]);
} else if (args.length == 3) {
command = String.format("return redis.call('%s', '%s', '%s')", args[0], args[1], unwrapQuotes(args[2]));
} else {
log.error("Unexpected number of script arguments {}", args.length);
}
log.trace("command: {}", command);
Object result = jedis.eval(command);
} catch (Exception e) {
log.error("Failed to execute '{}'", line, e);
}
});
} catch (IOException e) {
log.error("Failed to execute script '{}'", scriptUrl, e);
pool.returnBrokenResource(jedis);
// jedis = null;
} finally {
if (jedis != null) {
pool.returnResource(jedis);
}
}
}
}