package io.fathom.cloud.identity.services;
import io.fathom.cloud.CloudException;
import io.fathom.cloud.WellKnownRoles;
import io.fathom.cloud.identity.LoginServiceImpl;
import io.fathom.cloud.identity.Users;
import io.fathom.cloud.identity.model.AuthenticatedProject;
import io.fathom.cloud.identity.model.AuthenticatedUser;
import io.fathom.cloud.identity.secrets.Migrations;
import io.fathom.cloud.identity.secrets.SecretToken;
import io.fathom.cloud.identity.secrets.SecretToken.SecretTokenType;
import io.fathom.cloud.identity.secrets.Secrets;
import io.fathom.cloud.identity.state.AuthRepository;
import io.fathom.cloud.openstack.client.identity.ChallengeResponses;
import io.fathom.cloud.protobuf.IdentityModel.CredentialData;
import io.fathom.cloud.protobuf.IdentityModel.DomainData;
import io.fathom.cloud.protobuf.IdentityModel.DomainRoles;
import io.fathom.cloud.protobuf.IdentityModel.GroupData;
import io.fathom.cloud.protobuf.IdentityModel.ProjectData;
import io.fathom.cloud.protobuf.IdentityModel.ProjectRoles;
import io.fathom.cloud.protobuf.IdentityModel.RoleData;
import io.fathom.cloud.protobuf.IdentityModel.UserData;
import io.fathom.cloud.protobuf.IdentityModel.UserSecretData;
import io.fathom.cloud.state.DuplicateValueException;
import io.fathom.cloud.state.NamedItemCollection;
import io.fathom.cloud.tasks.TaskScheduler;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
import org.keyczar.AesKey;
import org.keyczar.DefaultKeyType;
import org.keyczar.KeyMetadata;
import org.keyczar.KeyczarKey;
import org.keyczar.KeyczarUtils;
import org.keyczar.enums.KeyPurpose;
import org.keyczar.exceptions.KeyczarException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.persist.Transactional;
@Singleton
@Transactional
public class IdentityServiceImpl implements IdentityService {
private static final Logger log = LoggerFactory.getLogger(IdentityServiceImpl.class);
@Inject
AuthRepository authRepository;
@Inject
Secrets secretService;
@Inject
TaskScheduler scheduler;
// @Override
// public void deleteUser(long id) throws StateStoreException,
// ConcurrentUpdateException {
//
// // TODO: Mark as deleted?
// // TODO: Deleted related things e.g. credentials?
// // TODO: Block delete if "in use"
// authStore.getUsers().delete(id);
// }
@Override
@Transactional
public UserData createUser(UserCreationData request) throws CloudException {
UserData.Builder user = request.user;
request.user = null;
String username = user.getName();
if (Strings.isNullOrEmpty(username)) {
throw new IllegalArgumentException();
}
user.setDomainId(request.domain.getId());
String challengeKey = null;
if (request.publicKeySha1 != null) {
challengeKey = LoginServiceImpl.toCredentialKey(request.publicKeySha1);
}
if (Strings.isNullOrEmpty(request.password)) {
request.password = null;
}
NamedItemCollection<CredentialData> usernameStore = authRepository.getUsernames(request.domain);
CredentialData credential = usernameStore.find(username);
if (credential != null) {
throw new WebApplicationException(Status.CONFLICT);
}
if (challengeKey != null) {
CredentialData publicKeyCredential = authRepository.getPublicKeyCredentials(request.domain.getId()).find(
challengeKey);
if (publicKeyCredential != null) {
throw new WebApplicationException(Status.CONFLICT);
}
}
if (challengeKey == null && request.password == null) {
// There's going to be no way to log in
throw new IllegalArgumentException();
}
SecretToken userSecret;
if (request.publicKeyChallengeRequest == null) {
userSecret = SecretToken.create(SecretTokenType.USER_SECRET);
} else {
byte[] plaintext = request.publicKeyChallengeRequest.toByteArray();
if (!ChallengeResponses.hasPrefix(plaintext)) {
throw new IllegalArgumentException();
}
byte[] payload = ChallengeResponses.getPayload(plaintext);
payload = ChallengeResponses.getPayload(payload);
AesKey cryptoKey;
try {
cryptoKey = KeyczarUtils.unpack(payload);
} catch (KeyczarException e) {
throw new IllegalArgumentException("Invalid key", e);
}
userSecret = new SecretToken(SecretTokenType.USER_SECRET, cryptoKey, null);
}
if (request.password != null) {
secretService.addPasswordAuth(user, userSecret, request.password);
}
// TODO: Allow users to opt out of password/token recovery??
boolean allowPasswordRecovery = true;
if (allowPasswordRecovery) {
secretService.addTokenRecovery(user, userSecret);
}
if (challengeKey != null) {
secretService.addPublicKeyAuth(user, challengeKey, request.publicKeyChallengeResponse);
}
KeyczarKey keypair = KeyczarUtils.createKey(new KeyMetadata("RSA Key", KeyPurpose.DECRYPT_AND_ENCRYPT,
DefaultKeyType.RSA_PRIV));
user.getPublicKeyBuilder().setKeyczar(KeyczarUtils.getPublicKey(keypair).toString());
// Create the secret data
{
if (user.hasSecretData()) {
throw new IllegalStateException();
}
UserSecretData.Builder s = UserSecretData.newBuilder();
if (Strings.isNullOrEmpty(user.getName())) {
throw new IllegalArgumentException();
}
// s.setVerifyPublicKey(Hashing.md5().hashBytes(publicKey).asLong());
s.getPrivateKeyBuilder().setKeyczar(keypair.toString());
user.setSecretData(Secrets.buildUserSecret(userSecret, s.build()));
}
UserData created = authRepository.getUsers().create(user);
{
CredentialData.Builder credentialBuilder = CredentialData.newBuilder();
credentialBuilder.setUserId(created.getId());
credentialBuilder.setKey(username);
// if (!Strings.isNullOrEmpty(password)) {
// PasswordHashData passwordHash = hasher.hash(password);
// credentialBuilder.setPasswordHash(passwordHash);
// }
try {
usernameStore.create(credentialBuilder);
} catch (DuplicateValueException e) {
// TODO: We need to be atomic! ZK supports multi, but it looks
// complicated
throw new WebApplicationException(Status.CONFLICT);
}
}
if (challengeKey != null) {
CredentialData.Builder b = CredentialData.newBuilder();
b.setUserId(created.getId());
b.setKey(challengeKey);
try {
authRepository.getPublicKeyCredentials(request.domain.getId()).create(b);
} catch (DuplicateValueException e) {
// TODO: We need to be atomic! ZK supports multi, but it looks
// complicated
throw new WebApplicationException(Status.CONFLICT);
}
}
return created;
}
@Override
public DomainData getDefaultDomain() throws CloudException {
return authRepository.getDomains().getDefaultDomain();
}
@Override
public ProjectData createProject(ProjectData.Builder b, AuthenticatedUser owner, long ownerRoleId)
throws CloudException {
// TODO: Policy that only domain admins can create projects?
// Don't see why!
// Auth.Domain domain = findDomainWithAdminRole();
// if (domain == null) {
// throw new WebApplicationException(Status.FORBIDDEN);
// }
ProjectData created = authRepository.getProjects().create(b);
SecretToken secretToken = SecretToken.create(SecretTokenType.PROJECT_SECRET);
AuthenticatedProject authenticatedProject = new AuthenticatedProject(created, secretToken);
grantRoleToUserOnProject(authenticatedProject, owner.getUserId(), ownerRoleId);
return created;
}
@Override
@Transactional
public void grantRoleToUserOnProject(AuthenticatedProject authenticatedProject, long granteeUserId, long roleId)
throws CloudException {
RoleData role = authRepository.getRoles().find(roleId);
if (role == null) {
throw new IllegalArgumentException("Cannot find role");
}
long projectId = authenticatedProject.getProjectId();
UserData granteeData = authRepository.getUsers().find(granteeUserId);
if (granteeData == null) {
throw new IllegalArgumentException();
}
UserData.Builder b = UserData.newBuilder(granteeData);
{
ProjectRoles.Builder pb = null;
for (ProjectRoles.Builder i : b.getProjectRolesBuilderList()) {
if (i.getProject() == projectId) {
pb = i;
break;
}
}
if (pb == null) {
pb = b.addProjectRolesBuilder();
pb.setProject(projectId);
}
if (!pb.hasSecretData()) {
try {
pb.setSecretData(Secrets.buildProjectRolesSecret(granteeData, authenticatedProject));
} catch (KeyczarException e) {
throw new CloudException("Crypto error granting project role", e);
}
}
if (!pb.getRoleList().contains(role.getId())) {
pb.addRole(role.getId());
}
authRepository.getUsers().update(b);
}
}
@Override
@Transactional
public void grantDomainRoleToUser(long domainId, long granteeUserId, long roleId) throws CloudException {
RoleData role = authRepository.getRoles().find(roleId);
if (role == null) {
throw new IllegalArgumentException("Cannot find role");
}
UserData granteeData = authRepository.getUsers().find(granteeUserId);
if (granteeData == null) {
throw new IllegalArgumentException();
}
DomainData domain = authRepository.getDomains().find(domainId);
if (domain == null) {
throw new IllegalArgumentException();
}
UserData.Builder b = UserData.newBuilder(granteeData);
{
DomainRoles.Builder rb = null;
for (DomainRoles.Builder i : b.getDomainRolesBuilderList()) {
if (i.getDomain() == domainId) {
rb = i;
break;
}
}
if (rb == null) {
rb = b.addDomainRolesBuilder();
rb.setDomain(domainId);
}
if (!rb.getRoleList().contains(role.getId())) {
rb.addRole(role.getId());
}
authRepository.getUsers().update(b);
}
}
@Override
public ProjectData findProject(AuthenticatedUser user, long projectId) throws CloudException {
ProjectData project = authRepository.getProjects().find(projectId);
boolean authorized = false;
if (project != null) {
ProjectRoles projectRoles = Users.findProjectRoles(user.getUserData(), project.getId());
if (projectRoles != null && projectRoles.getRoleCount() != 0) {
authorized = true;
}
if (!authorized) {
if (user.isDomainAdmin(project.getDomainId())) {
authorized = true;
}
}
}
if (!authorized) {
log.info("User {} not authorized on project {}", user, project);
project = null;
}
return project;
}
@Override
public void deleteUser(UserData user) throws CloudException {
// TODO: Mark as deleted?
// TODO: Deleted related things e.g. credentials?
// TODO: Block delete if "in use"
authRepository.getUsers().delete(user.getId());
}
@Inject
Provider<Sweeper> sweeper;
@Override
public void sweep() throws CloudException {
sweeper.get().sweep();
}
@Override
public RoleData findRole(long roleId) {
return authRepository.getRoles().find(roleId);
}
@Override
public List<RoleData> listRoles() {
return authRepository.getRoles().list();
}
@Override
public void fixupProject(AuthenticatedUser user, long projectId) throws CloudException {
ProjectData project = findProject(user, projectId);
if (project == null) {
log.warn("Could not find project");
return;
}
ProjectRoles projectRoles = Users.findProjectRoles(user.getUserData(), project.getId());
if (projectRoles == null) {
log.warn("Could not find role on project");
// TODO: We probably need another path for domain admins
return;
}
if (!projectRoles.hasSecretData()) {
log.warn("Project role has no secret data");
if (projectRoles.getRoleList().contains(WellKnownRoles.ROLE_ID_ADMIN)) {
// TODO: Remove once we've migrated all the projects
log.warn("Creating project key for project: {}", projectId);
Migrations.report(project);
AesKey cryptoKey = KeyczarUtils.generateSymmetricKey();
long userId = user.getUserId();
SecretToken secretToken = new SecretToken(SecretTokenType.PROJECT_SECRET, cryptoKey, null);
AuthenticatedProject authenticatedProject = new AuthenticatedProject(project, secretToken);
grantRoleToUserOnProject(authenticatedProject, userId, WellKnownRoles.ROLE_ID_ADMIN);
} else {
log.warn("User is not admin, cannot create secret");
}
}
}
@Override
public AuthenticatedProject authenticateToProject(AuthenticatedUser user, long projectId) throws CloudException {
ProjectData project = findProject(user, projectId);
if (project == null) {
return null;
}
AuthenticatedProject authenticatedProject = secretService.authenticate(project, user);
if (authenticatedProject == null) {
return null;
}
return authenticatedProject;
}
@Override
public UserData findUser(long userId) throws CloudException {
return authRepository.getUsers().find(userId);
}
@Override
public void start() throws CloudException {
// TODO: Just support method annotations??
scheduler.schedule(SweepTask.class);
}
@Override
public List<DomainData> listDomains(UserData user) throws CloudException {
List<DomainData> ret = Lists.newArrayList();
for (DomainData domain : authRepository.getDomains().list()) {
// TODO: Other domains?
if (domain.getId() != user.getDomainId()) {
continue;
}
ret.add(domain);
}
return ret;
}
@Override
public DomainData findDomain(UserData user, String id) throws CloudException {
DomainData domain = authRepository.getDomains().find(Long.valueOf(id));
// TODO: Other domains?
if (domain != null && domain.getId() != user.getDomainId()) {
domain = null;
}
return domain;
}
@Override
public List<GroupData> listGroups(AuthenticatedUser user) throws CloudException {
long domainId = user.getDomainId();
boolean isAdmin = user.isDomainAdmin(domainId);
Set<Long> userGroups = Sets.newHashSet(user.getUserData().getGroupsList());
List<GroupData> ret = Lists.newArrayList();
for (GroupData group : authRepository.getGroups(domainId).list()) {
if (!isAdmin) {
if (!userGroups.contains(group.getId())) {
continue;
}
}
ret.add(group);
}
return ret;
}
@Override
public UserData findUserByName(long domainId, String userName) throws CloudException {
DomainData domain = authRepository.getDomains().find(domainId);
if (domain == null) {
throw new IllegalArgumentException();
}
CredentialData credentialData = authRepository.getUsernames(domain).find(userName);
if (credentialData == null) {
return null;
}
long userId = credentialData.getUserId();
UserData user = authRepository.getUsers().find(userId);
if (user == null) {
// Unexpected!
log.warn("Unable to find user for credential: {}", userName);
}
return user;
}
@Override
public List<ProjectData> listProjects() throws CloudException {
return authRepository.getProjects().list();
}
}