// Copyright 2010 Google Inc. All Rights Reseved.
//
// 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 com.google.testing.testify.risk.frontend.server.service.impl;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.api.utils.SystemProperty.Environment.Value;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.testing.testify.risk.frontend.model.LoginStatus;
import com.google.testing.testify.risk.frontend.model.Project;
import com.google.testing.testify.risk.frontend.model.UserInfo;
import com.google.testing.testify.risk.frontend.server.service.UserService;
import com.google.testing.testify.risk.frontend.shared.rpc.UserRpc.ProjectAccess;
import java.util.List;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;
/**
* Implementation of UserService. Returns user and security information.
*
* @author jimr@google.com (Jim Reardon)
*/
@Singleton
public class UserServiceImpl implements UserService {
private static final String LOCAL_DOMAIN =
System.getProperty("com.google.testing.testify.risk.frontend.localdomain");
private static final boolean WHITELISTING_ENABLED =
Boolean.valueOf(System.getProperty("com.google.testing.testify.risk.frontend.whitelisting"));
private static final Logger log = Logger.getLogger(UserServiceImpl.class.getName());
private final PersistenceManagerFactory pmf;
private final com.google.appengine.api.users.UserService userService;
@Inject
public UserServiceImpl(PersistenceManagerFactory pmf) {
this.pmf = pmf;
// TODO(jimr): Inject this.
this.userService = UserServiceFactory.getUserService();
}
@Override
public boolean isUserLoggedIn() {
return getEmail() != null;
}
@Override
public String getEmail() {
User user = userService.getCurrentUser();
return user == null ? null : user.getEmail();
}
@Override
public boolean isWhitelistingEnabled() {
return WHITELISTING_ENABLED;
}
@Override
public boolean isWhitelisted() {
if (isInternalUser()) {
return true;
}
UserInfo user = getCurrentUserInfo(pmf.getPersistenceManager(), false);
if (user == null) {
return false;
}
return user.getIsWhitelisted();
}
@Override
public boolean isInternalUser() {
if (isDevMode()) {
return true;
}
String email = getEmail();
if (email == null || LOCAL_DOMAIN == null) {
return false;
}
return email.endsWith(LOCAL_DOMAIN);
}
@Override
public LoginStatus getLoginStatus(String returnUrl) {
User user = userService.getCurrentUser();
String email = null;
String url;
if (user == null) {
email = "";
url = userService.createLoginURL(returnUrl);
} else {
email = user.getEmail();
url = userService.createLogoutURL(returnUrl);
}
return new LoginStatus(user != null, url, email);
}
@Override
public boolean hasAdministratorAccess() {
return userService.isUserAdmin();
}
@Override
public boolean hasViewAccess(long projectId) {
return hasAccess(ProjectAccess.VIEW_ACCESS, projectId);
}
@Override
public boolean hasViewAccess(Project project) {
return hasAccess(ProjectAccess.VIEW_ACCESS, project, getEmail());
}
@Override
public boolean hasEditAccess(long projectId) {
return hasEditAccess(projectId, getEmail());
}
@Override
public boolean hasEditAccess(Project project) {
return hasAccess(ProjectAccess.VIEW_ACCESS, project, getEmail());
}
@Override
public boolean hasEditAccess(long projectId, String asEmail) {
return hasAccess(ProjectAccess.EDIT_ACCESS, projectId, asEmail);
}
@Override
public boolean hasOwnerAccess(long projectId) {
return hasAccess(ProjectAccess.OWNER_ACCESS, projectId);
}
@Override
public boolean hasAccess(ProjectAccess accessLevel, long projectId) {
return hasAccess(accessLevel, projectId, getEmail());
}
@Override
public boolean hasAccess(ProjectAccess accessLevel, long projectId, String asEmail) {
return hasAccess(accessLevel, getProject(projectId), asEmail);
}
private boolean hasAccess(ProjectAccess accessLevel, Project project, String asEmail) {
if (project == null) {
log.warning("Call to hasAccess with a null project.");
return false;
}
ProjectAccess accessHas = getAccessLevel(project, asEmail);
log.info("Access has: " + accessHas.name() + " Access desired: " + accessLevel.name());
return accessHas.hasAccess(accessLevel);
}
@Override
public ProjectAccess getAccessLevel(long projectId) {
return getAccessLevel(getProject(projectId), getEmail());
}
@Override
public ProjectAccess getAccessLevel(Project project) {
return getAccessLevel(project, getEmail());
}
@Override
public ProjectAccess getAccessLevel(long projectId, String asEmail) {
return getAccessLevel(getProject(projectId), asEmail);
}
private ProjectAccess getAccessLevel(Project project, String asEmail) {
if (project == null) {
log.warning("Call to getAccessLevel with a null project.");
return ProjectAccess.NO_ACCESS;
}
if (asEmail == null) {
if (project.getIsPubliclyVisible()) {
return ProjectAccess.VIEW_ACCESS;
} else {
return ProjectAccess.NO_ACCESS;
}
} else {
if (hasAdministratorAccess()) {
return ProjectAccess.ADMINISTRATOR_ACCESS;
} else if (project.getProjectOwners().contains(asEmail)) {
return ProjectAccess.OWNER_ACCESS;
} else if (project.getProjectEditors().contains(asEmail)) {
return ProjectAccess.EDIT_ACCESS;
} else if (project.getProjectViewers().contains(asEmail)) {
return ProjectAccess.EXPLICIT_VIEW_ACCESS;
} else if (project.getIsPubliclyVisible()) {
return ProjectAccess.VIEW_ACCESS;
} else {
return ProjectAccess.NO_ACCESS;
}
}
}
@Override
public List<Long> getStarredProjects() {
log.info("Getting starred projects for current user.");
PersistenceManager pm = pmf.getPersistenceManager();
List<Long> starredProjects = Lists.newArrayList();
try {
UserInfo userInfo = getCurrentUserInfo(pm, true);
// Copy list items over since we cannot return the server-side list type to client-side code.
if (userInfo != null) {
for (long projectId : userInfo.getStarredProjects()) {
starredProjects.add(projectId);
}
}
} finally {
pm.close();
}
return starredProjects;
}
@Override
public void starProject(long projectId) {
log.info("Starring project: " + projectId);
PersistenceManager pm = pmf.getPersistenceManager();
try {
UserInfo userInfo = getCurrentUserInfo(pm, true);
if (userInfo != null) {
userInfo.starProject(projectId);
pm.makePersistent(userInfo);
}
} finally {
pm.close();
}
}
@Override
public void unstarProject(long projectId) {
log.info("Unstarring project: " + projectId);
PersistenceManager pm = pmf.getPersistenceManager();
try {
UserInfo userInfo = getCurrentUserInfo(pm, true);
if (userInfo != null) {
userInfo.unstarProject(projectId);
pm.makePersistent(userInfo);
}
} finally {
pm.close();
}
}
@Override
public boolean isDevMode() {
Value env = SystemProperty.environment.value();
log.info("System environment: " + env.toString());
return env.equals(SystemProperty.Environment.Value.Development);
}
/**
* Returns all information Testify knows about the currently logged in user. If the logged in user
* is not currently in Testify, a new UserInfo entry will be created. Also, will return null if
* the user is not currently logged in.
*
* @param pm The PersistenceManager session for which the UserInfo object was returned.
*/
private UserInfo getCurrentUserInfo(PersistenceManager pm, boolean createIfMissing) {
User appEngineUser = userService.getCurrentUser();
if (appEngineUser == null) {
log.info("Unable to get user info. User is not logged in.");
return null;
}
// If they are logged in, get the Testify UserInfo record on file. If unavailable, create a new
// entry.
String loggedInUserId = appEngineUser.getUserId();
String currentEmail = appEngineUser.getEmail();
Query jdoQuery = pm.newQuery(UserInfo.class);
jdoQuery.declareParameters("String userIdParam");
jdoQuery.setFilter("userId == userIdParam");
log.info("Querying for user " + currentEmail + " by id " + loggedInUserId);
UserInfo user = queryAndReturnFirst(jdoQuery, loggedInUserId);
if (user != null) {
// Update email address if missing or different.
if (!currentEmail.equals(user.getCurrentEmail())) {
log.info("Adding or updating email for " + currentEmail + " id " + loggedInUserId);
user.setCurrentEmail(currentEmail);
pm.makePersistent(user);
}
} else {
// Try to find by email instead.
log.info("Querying for user " + currentEmail + " by email instead.");
jdoQuery = pm.newQuery(UserInfo.class);
jdoQuery.declareParameters("String currentEmailParam");
jdoQuery.setFilter("currentEmail == currentEmailParam");
user = queryAndReturnFirst(jdoQuery, currentEmail);
if (user != null) {
// Add the user's user ID since we now know it.
if (user.getUserId() == null || user.getUserId().equals("")) {
log.info("Adding user ID for " + currentEmail + ".");
user.setUserId(appEngineUser.getUserId());
pm.makePersistent(user);
} else {
log.severe("Found a user by email but user ID was set to a different ID.");
user = null;
}
}
}
if (user == null) {
log.info("Tried to get info for " + loggedInUserId + " but no entry was found.");
if (createIfMissing) {
log.info("Creating new UserInfo for user: " + loggedInUserId);
user = new UserInfo();
user.setUserId(loggedInUserId);
user.setCurrentEmail(currentEmail);
pm.makePersistent(user);
}
}
return user;
}
@SuppressWarnings("unchecked")
private UserInfo queryAndReturnFirst(Query query, String param) {
List<UserInfo> users = (List<UserInfo>) query.execute(param);
if (users.size() > 0) {
return users.get(0);
}
return null;
}
/**
* Loads a project. This isn't done using ProjectServiceImpl because it would create a circular
* dependency.
*
* @param id projectId to load.
* @return the loaded project.
*/
private Project getProject(long id) {
// TODO(jimr): To reduce this code duplication, project loading should be done at a lower level
// so that object can be injected both here and into project service.
log.info("Getting project: " + Long.toString(id));
PersistenceManager pm = pmf.getPersistenceManager();
try {
return pm.getObjectById(Project.class, id);
} finally {
pm.close();
}
}
}