/*
* Copyright 2014-2016 Groupon, Inc
* Copyright 2014-2016 The Billing Project, LLC
*
* The Billing Project 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.killbill.billing.util.security.shiro.dao;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import org.joda.time.DateTime;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.security.SecurityApiException;
import org.killbill.billing.util.config.definition.SecurityConfig;
import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher;
import org.killbill.clock.Clock;
import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.IDBI;
import org.skife.jdbi.v2.TransactionCallback;
import org.skife.jdbi.v2.TransactionStatus;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
public class DefaultUserDao implements UserDao {
private static final RandomNumberGenerator rng = new SecureRandomNumberGenerator();
private final IDBI dbi;
private final Clock clock;
private final SecurityConfig securityConfig;
@Inject
public DefaultUserDao(final IDBI dbi, final Clock clock, final SecurityConfig securityConfig) {
this.dbi = dbi;
this.clock = clock;
this.securityConfig = securityConfig;
}
@Override
public void insertUser(final String username, final String password, final List<String> roles, final String createdBy) throws SecurityApiException {
final ByteSource salt = rng.nextBytes();
final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME,
password, salt.toBase64(), securityConfig.getShiroNbHashIterations()).toBase64();
final DateTime createdDate = clock.getUTCNow();
inTransactionWithExceptionHandling(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final UserRolesSqlDao userRolesSqlDao = handle.attach(UserRolesSqlDao.class);
for (final String role : roles) {
userRolesSqlDao.create(new UserRolesModelDao(username, role, createdDate, createdBy));
}
final UsersSqlDao usersSqlDao = handle.attach(UsersSqlDao.class);
final UserModelDao userModelDao = usersSqlDao.getByUsername(username);
if (userModelDao != null) {
throw new SecurityApiException(ErrorCode.SECURITY_USER_ALREADY_EXISTS, username);
}
usersSqlDao.create(new UserModelDao(username, hashedPasswordBase64, salt.toBase64(), createdDate, createdBy));
return null;
}
});
}
public List<UserRolesModelDao> getUserRoles(final String username) {
return dbi.inTransaction(new TransactionCallback<List<UserRolesModelDao>>() {
@Override
public List<UserRolesModelDao> inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final UserRolesSqlDao userRolesSqlDao = handle.attach(UserRolesSqlDao.class);
return userRolesSqlDao.getByUsername(username);
}
});
}
@Override
public void addRoleDefinition(final String role, final List<String> permissions, final String createdBy) throws SecurityApiException {
final DateTime createdDate = clock.getUTCNow();
inTransactionWithExceptionHandling(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final RolesPermissionsSqlDao rolesPermissionsSqlDao = handle.attach(RolesPermissionsSqlDao.class);
final List<RolesPermissionsModelDao> existingRole = rolesPermissionsSqlDao.getByRoleName(role);
if (!existingRole.isEmpty()) {
throw new SecurityApiException(ErrorCode.SECURITY_ROLE_ALREADY_EXISTS, role);
}
for (final String permission : permissions) {
rolesPermissionsSqlDao.create(new RolesPermissionsModelDao(role, permission, createdDate, createdBy));
}
return null;
}
});
}
@Override
public void updateRoleDefinition(final String role, final List<String> permissions, final String createdBy) throws SecurityApiException {
final DateTime createdDate = clock.getUTCNow();
inTransactionWithExceptionHandling(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final RolesPermissionsSqlDao rolesPermissionsSqlDao = handle.attach(RolesPermissionsSqlDao.class);
final List<RolesPermissionsModelDao> existingPermissions = rolesPermissionsSqlDao.getByRoleName(role);
// A empty list of permissions means we should remove all current permissions
final Iterable<RolesPermissionsModelDao> toBeDeleted = existingPermissions.isEmpty() ?
existingPermissions :
Iterables.filter(existingPermissions, new Predicate<RolesPermissionsModelDao>() {
@Override
public boolean apply(final RolesPermissionsModelDao input) {
return !permissions.contains(input.getPermission());
}
});
final Iterable<String> toBeAdded = Iterables.filter(permissions, new Predicate<String>() {
@Override
public boolean apply(final String input) {
for (RolesPermissionsModelDao e : existingPermissions) {
if (e.getPermission().equals(input)) {
return false;
}
}
return true;
}
});
for (RolesPermissionsModelDao d : toBeDeleted) {
rolesPermissionsSqlDao.unactiveEvent(d.getRecordId(), createdDate, createdBy);
}
for (final String permission : toBeAdded) {
rolesPermissionsSqlDao.create(new RolesPermissionsModelDao(role, permission, createdDate, createdBy));
}
return null;
}
});
}
@Override
public List<RolesPermissionsModelDao> getRoleDefinition(final String role) {
return dbi.inTransaction(new TransactionCallback<List<RolesPermissionsModelDao>>() {
@Override
public List<RolesPermissionsModelDao> inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final RolesPermissionsSqlDao rolesPermissionsSqlDao = handle.attach(RolesPermissionsSqlDao.class);
return rolesPermissionsSqlDao.getByRoleName(role);
}
});
}
@Override
public void updateUserPassword(final String username, final String password, final String updatedBy) throws SecurityApiException {
final ByteSource salt = rng.nextBytes();
final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME,
password, salt.toBase64(), securityConfig.getShiroNbHashIterations()).toBase64();
inTransactionWithExceptionHandling(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final DateTime updatedDate = clock.getUTCNow();
final UsersSqlDao usersSqlDao = handle.attach(UsersSqlDao.class);
final UserModelDao userModelDao = usersSqlDao.getByUsername(username);
if (userModelDao == null) {
throw new SecurityApiException(ErrorCode.SECURITY_INVALID_USER, username);
}
usersSqlDao.updatePassword(username, hashedPasswordBase64, salt.toBase64(), updatedDate.toDate(), updatedBy);
return null;
}
});
}
@Override
public void updateUserRoles(final String username, final List<String> roles, final String updatedBy) throws SecurityApiException {
inTransactionWithExceptionHandling(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final DateTime updatedDate = clock.getUTCNow();
final UsersSqlDao usersSqlDao = handle.attach(UsersSqlDao.class);
final UserModelDao userModelDao = usersSqlDao.getByUsername(username);
if (userModelDao == null) {
throw new SecurityApiException(ErrorCode.SECURITY_INVALID_USER, username);
}
// Remove stale entries
final UserRolesSqlDao userRolesSqlDao = handle.attach(UserRolesSqlDao.class);
final List<UserRolesModelDao> existingRoles = userRolesSqlDao.getByUsername(username);
for (final UserRolesModelDao curRole : existingRoles) {
if (Iterables.tryFind(roles, new Predicate<String>() {
@Override
public boolean apply(final String input) {
return input.equals(curRole.getRoleName());
}
}).orNull() == null) {
userRolesSqlDao.invalidate(username, curRole.getRoleName(), updatedDate.toDate(), updatedBy);
}
}
// Add new entries
for (final String curNewRole : roles) {
if (Iterables.tryFind(existingRoles, new Predicate<UserRolesModelDao>() {
@Override
public boolean apply(final UserRolesModelDao input) {
return input.getRoleName().equals(curNewRole);
}
}).orNull() == null) {
userRolesSqlDao.create(new UserRolesModelDao(username, curNewRole, updatedDate, updatedBy));
}
}
return null;
}
});
}
@Override
public void invalidateUser(final String username, final String updatedBy) throws SecurityApiException {
inTransactionWithExceptionHandling(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
final DateTime updatedDate = clock.getUTCNow();
final UsersSqlDao usersSqlDao = handle.attach(UsersSqlDao.class);
final UserModelDao userModelDao = usersSqlDao.getByUsername(username);
if (userModelDao == null) {
throw new SecurityApiException(ErrorCode.SECURITY_INVALID_USER, username);
}
usersSqlDao.invalidate(username, updatedDate.toDate(), updatedBy);
return null;
}
});
}
private <T> T inTransactionWithExceptionHandling(final TransactionCallback<T> callback) throws SecurityApiException {
// Similar to EntitySqlDaoTransactionalJdbiWrapper#execute
try {
return dbi.inTransaction(callback);
} catch (final RuntimeException e) {
throwSecurityApiException(e);
return null;
}
}
private void throwSecurityApiException(final Throwable e) throws SecurityApiException {
if (e.getCause() != null && e.getCause().getClass().isAssignableFrom(SecurityApiException.class)) {
throw (SecurityApiException) e.getCause();
} else if (e.getCause() != null) {
throwSecurityApiException(e.getCause());
} else {
throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
}
}
}