package io.fathom.cloud.identity; import io.fathom.cloud.CloudException; import io.fathom.cloud.OpenstackExtension; import io.fathom.cloud.ServiceType; import io.fathom.cloud.identity.api.os.model.v2.V2AuthCredentials; import io.fathom.cloud.identity.api.os.model.v3.Endpoint; import io.fathom.cloud.identity.api.os.model.v3.Service; import io.fathom.cloud.identity.model.AuthenticatedUser; import io.fathom.cloud.identity.secrets.Secrets; import io.fathom.cloud.identity.secrets.UserWithSecret; import io.fathom.cloud.identity.services.IdentityService; import io.fathom.cloud.identity.state.AuthRepository; import io.fathom.cloud.openstack.client.identity.ChallengeResponses; import io.fathom.cloud.protobuf.CloudCommons.TokenInfo; import io.fathom.cloud.protobuf.CloudCommons.TokenScope; import io.fathom.cloud.protobuf.IdentityModel.CredentialData; import io.fathom.cloud.protobuf.IdentityModel.DomainData; import io.fathom.cloud.protobuf.IdentityModel.ProjectData; import io.fathom.cloud.protobuf.IdentityModel.ProjectRoles; import io.fathom.cloud.protobuf.IdentityModel.UserData; import io.fathom.cloud.server.auth.TokenAuth; import io.fathom.cloud.server.auth.TokenService; import io.fathom.cloud.server.model.Project; import io.fathom.cloud.server.resources.ClientCertificate; import io.fathom.cloud.server.resources.OpenstackDefaults; import java.util.Date; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; import org.keyczar.AesKey; import org.keyczar.KeyczarUtils; import org.keyczar.exceptions.KeyczarException; import org.keyczar.interfaces.KeyczarReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fathomdb.TimeSpan; import com.fathomdb.extensions.ExtensionModule; import com.fathomdb.extensions.Extensions; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.io.BaseEncoding; import com.google.inject.persist.Transactional; import com.google.protobuf.ByteString; @Singleton public class LoginServiceImpl implements LoginService { private static final Logger log = LoggerFactory.getLogger(LoginServiceImpl.class); protected static final TimeSpan TOKEN_VALIDITY = new TimeSpan("1h"); @Inject Secrets secretService; @Inject AuthRepository authRepository; @Inject TokenService tokenService; @Inject IdentityService identityService; @Inject Extensions extensions; @Override public List<Service> buildServiceMap(String baseUrl, ProjectData project) { List<Service> services = Lists.newArrayList(); // { // Service service = new Service(); // services.add(service); // // service.id = service.name = "keystone"; // service.type = ServiceType.IDENTITY.getType(); // // addEndpoint(service, baseUrl + "/openstack/identity/v2.0"); // } if (project != null) { Service service = new Service(); services.add(service); service.id = service.name = "nova"; service.type = ServiceType.COMPUTE.getType(); addEndpoint(service, baseUrl + "/openstack/compute/" + project.getId()); } if (project != null) { Service service = new Service(); services.add(service); service.id = service.name = "trove"; service.type = ServiceType.DBAAS.getType(); addEndpoint(service, baseUrl + "/openstack/dbaas/" + project.getId()); } // if (project != null) { // Service service = new Service(); // services.add(service); // // service.id = service.name = "heat"; // service.type = ServiceTypes.ORCHESTRATION; // // addEndpoint(service, baseUrl + "/openstack/orchestration/" + // project.getId()); // } { Service service = new Service(); services.add(service); service.name = "glance"; service.type = ServiceType.IMAGE.getType(); addEndpoint(service, baseUrl + "/openstack/images"); } if (project != null) { Service service = new Service(); services.add(service); service.name = "swift"; service.type = ServiceType.OBJECT_STORE.getType(); addEndpoint(service, baseUrl + "/openstack/storage/" + project.getId()); } List<ServiceType> enabledServices = Lists.newArrayList(); Project genericProject = new Project(project.getId()); for (ExtensionModule extension : extensions.getExtensions()) { if (extension instanceof OpenstackExtension) { List<ServiceType> extensionServices = ((OpenstackExtension) extension).getServices(genericProject, baseUrl); enabledServices.addAll(extensionServices); } } for (ServiceType serviceType : enabledServices) { Service service = new Service(); services.add(service); service.name = serviceType.getName(); service.id = service.name; service.type = serviceType.getType(); String url = baseUrl + "/openstack/" + serviceType.getUrlSuffix(); if (serviceType != ServiceType.IDENTITY) { // TODO: Transform to attribute on ServiceType? url += "/" + project.getId(); } else { // Yuk... identity needs v2.0 url += "/v2.0"; } addEndpoint(service, url); } return services; } private void addEndpoint(Service service, String url) { if (service.endpoints == null) { service.endpoints = Lists.newArrayList(); } for (String interfaceName : new String[] { "public", "internal", "admin" }) { Endpoint endpoint = new Endpoint(); service.endpoints.add(endpoint); endpoint.id = service.id + "-" + interfaceName; endpoint.name = service.name; endpoint.interfaceName = interfaceName; endpoint.region = OpenstackDefaults.DEFAULT_REGION; endpoint.serviceId = service.id; endpoint.url = url; // Link link = new Link(); // link.self = url; // endpoint.links.add(link); } } @Override @Transactional public AuthenticatedUser authenticate(V2AuthCredentials authRequest, ClientCertificate clientCertificate) throws CloudException { DomainData domain = null; UserWithSecret userWithSecret = null; log.info("V2 Auth request: " + authRequest); ProjectSpec projectSpec = new ProjectSpec(); if (!Strings.isNullOrEmpty(authRequest.tenantId)) { projectSpec.projectId = Long.valueOf(authRequest.tenantId); } projectSpec.projectName = authRequest.tenantName; if (authRequest.passwordCredentials != null) { domain = identityService.getDefaultDomain(); userWithSecret = authenticate(domain, authRequest.passwordCredentials.username, authRequest.passwordCredentials.password); } else if (authRequest.tokenCredentials != null) { String tokenId = authRequest.tokenCredentials.id; TokenInfo tokenInfo = findTokenInfo(tokenId); if (tokenInfo == null) { return null; } userWithSecret = checkSecret(tokenInfo); domain = authRepository.getDomains().find(tokenInfo.getDomainId()); if (domain == null) { throw new IllegalStateException(); } if (projectSpec.projectId == 0 && projectSpec.projectName == null) { log.info("Token login with scope: {}", tokenInfo.getTokenScope()); if (tokenInfo.getTokenScope() == TokenScope.Project) { projectSpec.projectId = tokenInfo.getProjectId(); log.info("Set projectId to: {}", projectSpec.projectId); } } // This is weird, but valid with V3's deprecation of unscoped // tokens... // if (tokenInfo.getTokenScope() != TokenScope.Unscoped) { // log.warn("Use of scoped token in authenticate request: {} with {}", // authRequest, tokenInfo); // throw new IllegalStateException(); // } // if (tokenInfo.hasProjectId()) { // projectSpec.projectId = tokenInfo.getProjectId(); // projectSpec.projectName = null; // } else { // // Not sure what to do here.. // throw new UnsupportedOperationException(); // } } else if (authRequest.challengeResponse != null && clientCertificate != null) { domain = identityService.getDefaultDomain(); ByteString response = ByteString.copyFrom(BaseEncoding.base64().decode( authRequest.challengeResponse.response)); ByteString challenge = ByteString.copyFrom(BaseEncoding.base64().decode( authRequest.challengeResponse.challenge)); userWithSecret = authenticate(domain, clientCertificate, challenge, response); } if (userWithSecret == null) { return null; } AuthenticatedUser user = toAuthenticationV2(domain, projectSpec, userWithSecret); return user; } @Override @Transactional public AuthenticatedUser authenticate(String tokenId) throws CloudException { TokenInfo tokenInfo = findTokenInfo(tokenId); if (tokenInfo == null) { return null; } return authenticate(tokenInfo); } @Override @Transactional public AuthenticatedUser authenticate(TokenInfo tokenInfo) throws CloudException { DomainData domain = findDomainFromToken(tokenInfo); UserWithSecret userWithSecret = checkSecret(tokenInfo); if (userWithSecret == null) { return null; } TokenScope scope = tokenInfo.getTokenScope(); switch (scope) { case Domain: return buildDomainToken(domain, userWithSecret); case Project: return buildProjectToken(domain, tokenInfo.getProjectId(), userWithSecret); case Unscoped: throw new UnsupportedOperationException(); default: throw new IllegalStateException(); } } private UserWithSecret checkSecret(TokenInfo tokenInfo) throws CloudException { if (TokenAuth.hasExpired(tokenInfo)) { // This is treated the same as an invalid token return null; } UserData userData = authRepository.getUsers().find(tokenInfo.getUserId()); if (userData == null) { return null; } UserWithSecret userWithSecret = null; try { userWithSecret = secretService.getFromToken(userData, tokenInfo); } catch (KeyczarException e) { log.info("Error while checking token secret", e); } if (userWithSecret == null) { return null; } return userWithSecret; } public static class ProjectSpec { public long projectId; public String projectName; } private AuthenticatedUser toAuthenticationV2(DomainData domain, ProjectSpec projectSpec, UserWithSecret userWithSecret) throws CloudException { ProjectData project = null; ProjectRoles projectRoles = null; UserData user = userWithSecret.getUserData(); if (projectSpec.projectId != 0) { return buildProjectToken(domain, projectSpec.projectId, userWithSecret); } else if (!Strings.isNullOrEmpty(projectSpec.projectName)) { for (ProjectRoles i : user.getProjectRolesList()) { ProjectData p = authRepository.getProjects().find(i.getProject()); if (p == null) { continue; } if (projectSpec.projectName.equals(p.getName())) { projectRoles = i; project = p; break; } } if (projectRoles == null) { return null; } return buildProjectToken(domain, project, userWithSecret); } // else if (user.hasDefaultProjectId()) { // long projectId = user.getDefaultProjectId(); // // projectRoles = Users.findProjectRoles(user, projectId); // // if (projectRoles != null) { // project = authRepository.getProjects().find(projectId); // if (project == null) { // log.warn("Cannot find project {}", projectId); // projectRoles = null; // } // } // // if (projectRoles == null) { // // Not an error // log.info("User {} does not have access to their default project", // user.getId()); // } else { // scope = TokenScope.Project; // } // } assert (project == null); // For V2, we treat the scope as a domain if it is unspecified return buildDomainToken(domain, userWithSecret); } protected UserWithSecret authenticate(DomainData domain, String username, String password) throws CloudException { if (Strings.isNullOrEmpty(username)) { return null; } if (Strings.isNullOrEmpty(password)) { return null; } CredentialData credential = authRepository.getUsernames(domain).find(username); if (credential == null) { return null; } UserData user = authRepository.getUsers().find(credential.getUserId()); if (user == null) { return null; } UserWithSecret userWithSecret = secretService.checkPassword(user, credential, password); if (userWithSecret == null) { // TODO: Throttle? log.debug("Password mismatch for {}", username); return null; } return userWithSecret; } @Override @Transactional public void changePassword(DomainData domain, String username, String password, KeyczarReader recoveryKey) throws CloudException { if (Strings.isNullOrEmpty(username)) { throw new IllegalArgumentException(); } if (Strings.isNullOrEmpty(password)) { throw new IllegalArgumentException(); } CredentialData credential = authRepository.getUsernames(domain).find(username); if (credential == null) { throw new IllegalArgumentException(); } UserData user = authRepository.getUsers().find(credential.getUserId()); if (user == null) { throw new IllegalArgumentException(); } secretService.changePassword(user, credential, password, recoveryKey); } protected TokenInfo findTokenInfo(String tokenId) { try { TokenInfo tokenInfo = tokenService.findValidToken(tokenId); return tokenInfo; } catch (Exception e) { log.warn("Unexpected error while reading token", e); return null; } } // protected User findUserFromToken(TokenInfo tokenInfo) throws // CloudException { // if (tokenInfo == null) { // return null; // } // // if (TokenAuth.hasExpired(tokenInfo)) { // // This is treated the same as an invalid token // return null; // } // // long userId = tokenInfo.getUserId(); // return new User(userId); // // authStore.getUsers().find(userId); // } protected DomainData findDomainFromToken(TokenInfo tokenInfo) throws CloudException { if (tokenInfo == null) { return null; } long domainId = -1; switch (tokenInfo.getTokenScope()) { case Domain: domainId = tokenInfo.getDomainId(); break; case Project: if (tokenInfo.hasDomainId()) { domainId = tokenInfo.getDomainId(); } else { // boolean resolveFromProject = true; // if (resolveFromProject) { // log.warn("Resolving domain from project for token"); // // long projectId = tokenInfo.getProjectId(); // ProjectData project = // authRepository.getProjects().find(projectId); // if (project != null) { // domainId = project.getDomainId(); // } // } } if (domainId == -1 || domainId == 0) { throw new UnsupportedOperationException("No domain set in project-scoped token"); } break; default: break; } if (domainId >= 0) { return authRepository.getDomains().find(domainId); } else { return null; } } @Override public TokenInfo buildTokenInfo(AuthenticatedUser authentication) { TokenScope tokenScope = authentication.getScope(); Date expiration = TOKEN_VALIDITY.addTo(new Date()); TokenInfo.Builder token = TokenInfo.newBuilder(); token.setUserId(authentication.getUserId()); // Millisecond resolution is overkill // (and size matters, because this token gets passed as a cookie)) token.setExpiration(expiration.getTime() / 1000L); token.setTokenScope(tokenScope); ProjectData project = authentication.getProject(); if (project != null) { token.setProjectId(project.getId()); if (authentication.getProjectRoleIds() != null) { for (long projectRoleId : authentication.getProjectRoleIds()) { token.addRoles(projectRoleId); } } } token.setDomainId(authentication.getDomainId()); for (long domainRoleId : authentication.getDomainRoleIds(authentication.getDomainId())) { token.addDomainRoles(domainRoleId); } { ByteString tokenSecret = secretService.buildTokenSecret(authentication); token.setTokenSecret(tokenSecret); } return token.build(); } @Override @Transactional public AuthenticatedUser authenticate(Long projectId, String username, String password) throws CloudException { DomainData domain = identityService.getDefaultDomain(); UserWithSecret userWithSecret = authenticate(domain, username, password); if (userWithSecret == null) { return null; } if (projectId == null) { return buildDomainToken(domain, userWithSecret); } else { return buildProjectToken(domain, projectId, userWithSecret); } } @Override @Transactional public AuthenticatedUser authenticate(Long projectId, ClientCertificate clientCertificate, ByteString challenge, ByteString response) throws CloudException { DomainData domain = identityService.getDefaultDomain(); UserWithSecret userWithSecret = authenticate(domain, clientCertificate, challenge, response); if (userWithSecret == null) { return null; } if (projectId == null) { return buildDomainToken(domain, userWithSecret); } else { return buildProjectToken(domain, projectId, userWithSecret); } } @Override @Transactional public ByteString getChallenge(ClientCertificate clientCertificate) throws CloudException { DomainData domain = identityService.getDefaultDomain(); String keyId = toCredentialKey(clientCertificate.getPublicKeySha1()); CredentialData credential = authRepository.getPublicKeyCredentials(domain.getId()).find(keyId); if (credential == null) { log.info("No credential found for {}", keyId); return null; } UserData user = authRepository.getUsers().find(credential.getUserId()); if (user == null) { log.warn("User not found for credential {}", credential); return null; } return secretService.buildAuthChallenge(user, credential, clientCertificate); } private UserWithSecret authenticate(DomainData domain, ClientCertificate clientCertificate, ByteString challenge, ByteString response) throws CloudException { String keyId = toCredentialKey(clientCertificate.getPublicKeySha1()); CredentialData credential = authRepository.getPublicKeyCredentials(domain.getId()).find(keyId); if (credential == null) { return null; } UserData user = authRepository.getUsers().find(credential.getUserId()); if (user == null) { return null; } UserWithSecret userWithSecret = secretService.checkPublicKey(user, credential, clientCertificate, challenge, response); if (userWithSecret == null) { // TODO: Throttle? log.debug("Key mismatch for {}", user); return null; } return userWithSecret; } public static String toCredentialKey(ByteString publicKeySha1) { return BaseEncoding.base16().encode(publicKeySha1.toByteArray()); } private AuthenticatedUser buildDomainToken(DomainData domain, UserWithSecret userWithSecret) throws CloudException { TokenScope scope = null; ProjectData project = null; ProjectRoles projectRoles = null; scope = TokenScope.Domain; return new AuthenticatedUser(scope, userWithSecret, project, projectRoles, domain); } private AuthenticatedUser buildProjectToken(DomainData domain, ProjectData project, UserWithSecret userWithSecret) throws CloudException { if (project == null) { throw new IllegalStateException(); } TokenScope scope = TokenScope.Project; ProjectRoles projectRoles = null; UserData user = userWithSecret.getUserData(); projectRoles = Users.findProjectRoles(user, project.getId()); if (projectRoles == null) { return null; } return new AuthenticatedUser(scope, userWithSecret, project, projectRoles, domain); } private AuthenticatedUser buildProjectToken(DomainData domain, long projectId, UserWithSecret userWithSecret) throws CloudException { UserData user = userWithSecret.getUserData(); ProjectRoles projectRoles = Users.findProjectRoles(user, projectId); if (projectRoles == null) { return null; } ProjectData project = authRepository.getProjects().find(projectRoles.getProject()); if (project == null) { log.warn("Cannot find project {}", projectRoles.getProject()); return null; } TokenScope scope = TokenScope.Project; return new AuthenticatedUser(scope, userWithSecret, project, projectRoles, domain); } @Override public ByteString createRegistrationChallenge(ClientCertificate clientCertificate) throws CloudException { AesKey secretKey = KeyczarUtils.generateSymmetricKey(); byte[] payload = KeyczarUtils.pack(secretKey); byte[] plaintext = ChallengeResponses.addHeader(payload); // We can't encrypt because http proxies don't pass the public key :-( // It shouldn't add anything to security anyway // byte[] ciphertext = ChallengeResponses.encrypt(publicKey, plaintext); byte[] ciphertext = plaintext; ciphertext = ChallengeResponses.addHeader(ciphertext); return ByteString.copyFrom(ciphertext); } }