/*
* Copyright (C) 2007-2014 Crafter Software Corporation.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.craftercms.profile.management.web.controllers;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.craftercms.commons.security.exception.ActionDeniedException;
import org.craftercms.commons.security.permissions.PermissionEvaluator;
import org.craftercms.profile.api.Profile;
import org.craftercms.profile.api.ProfileConstants;
import org.craftercms.profile.api.SortOrder;
import org.craftercms.profile.api.exceptions.ProfileException;
import org.craftercms.profile.api.services.ProfileService;
import org.craftercms.profile.management.exceptions.InvalidRequestParameterException;
import org.craftercms.profile.management.exceptions.ResourceNotFoundException;
import org.craftercms.profile.management.security.permissions.Action;
import org.craftercms.security.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* MVC Controller for displaying and modifying profiles.
*
* @author avasquez
*/
@Controller
@RequestMapping(ProfileController.BASE_URL_PROFILE)
public class ProfileController {
public static final String BASE_URL_PROFILE = "/profile";
public static final String PATH_VAR_ID = "id";
public static final String URL_VIEW_PROFILE_LIST = "/list/view";
public static final String URL_VIEW_NEW_PROFILE = "/new/view";
public static final String URL_VIEW_PROFILE = "/view";
public static final String URL_GET_PROFILE_COUNT = "/count";
public static final String URL_GET_PROFILE_LIST = "/list";
public static final String URL_GET_PROFILE = "/{" + PATH_VAR_ID + "}";
public static final String URL_CREATE_PROFILE = "/create";
public static final String URL_UPDATE_PROFILE = "/update";
public static final String URL_DELETE_PROFILE = "/{" + PATH_VAR_ID + "}/delete";
public static final String PARAM_TENANT_NAME = "tenantName";
public static final String PARAM_QUERY = "query";
public static final String PARAM_SORT_BY = "sortBy";
public static final String PARAM_SORT_ORDER = "sortOrder";
public static final String PARAM_START = "start";
public static final String PARAM_COUNT = "count";
public static final String VIEW_PROFILE_LIST = "profile-list";
public static final String VIEW_NEW_PROFILE = "new-profile";
public static final String VIEW_PROFILE = "profile";
public static final String MODEL_MESSAGE = "message";
public static final String MSG_PROFILE_CREATED_FORMAT = "Profile '%s' created";
public static final String MSG_PROFILE_UPDATED_FORMAT = "Profile '%s' updated";
public static final String MSG_PROFILE_DELETED_FORMAT = "Profile '%s' deleted";
public static final Pattern QUERY_PATTERN = Pattern.compile("\\w+");
public static final String FINAL_QUERY_FORMAT = "{username: {$regex: '.*%s.*', $options: 'i'}}";
private String verificationUrl;
private ProfileService profileService;
private PermissionEvaluator<Profile, String> tenantPermissionEvaluator;
private PermissionEvaluator<Profile, Profile> profilePermissionEvaluator;
public void setVerificationUrl(String verificationUrl) {
this.verificationUrl = verificationUrl;
}
@Required
public void setProfileService(ProfileService profileService) {
this.profileService = profileService;
}
@Required
public void setTenantPermissionEvaluator(PermissionEvaluator<Profile, String> tenantPermissionEvaluator) {
this.tenantPermissionEvaluator = tenantPermissionEvaluator;
}
@Required
public void setProfilePermissionEvaluator(PermissionEvaluator<Profile, Profile> profilePermissionEvaluator) {
this.profilePermissionEvaluator = profilePermissionEvaluator;
}
@RequestMapping(value = URL_VIEW_PROFILE_LIST, method = RequestMethod.GET)
public String viewProfileList() {
return VIEW_PROFILE_LIST;
}
@RequestMapping(value = URL_VIEW_NEW_PROFILE, method = RequestMethod.GET)
public String viewNewProfile() {
return VIEW_NEW_PROFILE;
}
@RequestMapping(value = URL_VIEW_PROFILE, method = RequestMethod.GET)
public String viewProfile() {
return VIEW_PROFILE;
}
@RequestMapping(value = URL_GET_PROFILE_COUNT, method = RequestMethod.GET)
@ResponseBody
public long getProfileCount(@RequestParam(value = PARAM_TENANT_NAME, required = false) String tenantName,
@RequestParam(value = PARAM_QUERY, required = false) String query)
throws ProfileException {
if (StringUtils.isEmpty(tenantName)) {
tenantName = SecurityUtils.getCurrentProfile().getTenant();
} else {
checkIfAllowed(tenantName, Action.GET_PROFILE_COUNT);
}
if (StringUtils.isNotEmpty(query)) {
if (QUERY_PATTERN.matcher(query).matches()) {
query = String.format(FINAL_QUERY_FORMAT, query);
return profileService.getProfileCountByQuery(tenantName, query);
} else {
throw new InvalidRequestParameterException("Parameter '" + PARAM_QUERY + "' must match regex " +
QUERY_PATTERN.pattern());
}
} else {
return profileService.getProfileCount(tenantName);
}
}
@RequestMapping(value = URL_GET_PROFILE_LIST, method = RequestMethod.GET)
@ResponseBody
public List<Profile> getProfileList(@RequestParam(value = PARAM_TENANT_NAME, required = false) String tenantName,
@RequestParam(value = PARAM_QUERY, required = false) String query,
@RequestParam(value = PARAM_SORT_BY, required = false) String sortBy,
@RequestParam(value = PARAM_SORT_ORDER, required = false) SortOrder sortOrder,
@RequestParam(value = PARAM_START, required = false) Integer start,
@RequestParam(value = PARAM_COUNT, required = false) Integer limit)
throws ProfileException {
if (StringUtils.isEmpty(tenantName)) {
tenantName = SecurityUtils.getCurrentProfile().getTenant();
} else {
checkIfAllowed(tenantName, Action.GET_PROFILE_LIST);
}
if (StringUtils.isNotEmpty(query)) {
if (QUERY_PATTERN.matcher(query).matches()) {
query = String.format(FINAL_QUERY_FORMAT, query);
return profileService.getProfilesByQuery(tenantName, query, sortBy, sortOrder, start, limit);
} else {
throw new InvalidRequestParameterException("Parameter '" + PARAM_QUERY + "' must match regex " +
QUERY_PATTERN.pattern());
}
} else {
return profileService.getProfileRange(tenantName, sortBy, sortOrder, start, limit);
}
}
@RequestMapping(value = URL_GET_PROFILE, method = RequestMethod.GET)
@ResponseBody
public Profile getProfile(@PathVariable(PATH_VAR_ID) String id) throws ProfileException {
Profile profile = profileService.getProfile(id);
if (profile != null) {
checkIfAllowed(profile, Action.GET_PROFILE);
return profile;
} else {
throw new ResourceNotFoundException("No profile found for ID '" + id + "'");
}
}
@RequestMapping(value = URL_CREATE_PROFILE, method = RequestMethod.POST)
@ResponseBody
public Map<String, String> createProfile(@RequestBody Profile profile) throws ProfileException {
checkIfAllowed(profile, Action.CREATE_PROFILE);
profile = profileService.createProfile(profile.getTenant(), profile.getUsername(), profile.getPassword(),
profile.getEmail(), profile.isEnabled(), profile.getRoles(),
profile.getAttributes(), verificationUrl);
return Collections.singletonMap(MODEL_MESSAGE, String.format(MSG_PROFILE_CREATED_FORMAT, profile.getId()));
}
@RequestMapping(value = URL_UPDATE_PROFILE, method = RequestMethod.POST)
@ResponseBody
public Map<String, String> updateProfile(@RequestBody Profile profile) throws ProfileException {
String id = profile.getId().toString();
Profile currentProfile = profileService.getProfile(id);
if (currentProfile != null) {
checkIfAllowed(currentProfile, Action.UPDATE_PROFILE);
profileService.updateProfile(id, profile.getUsername(), profile.getPassword(), profile.getEmail(),
profile.isEnabled(), profile.getRoles(), profile.getAttributes(),
ProfileConstants.NO_ATTRIBUTE);
return Collections.singletonMap(MODEL_MESSAGE, String.format(MSG_PROFILE_UPDATED_FORMAT, id));
} else {
throw new ResourceNotFoundException("No profile found for ID '" + id + "'");
}
}
@RequestMapping(value = URL_DELETE_PROFILE, method = RequestMethod.POST)
@ResponseBody
public Map<String, String> deleteProfile(@PathVariable(PATH_VAR_ID) String id) throws ProfileException {
Profile profile = profileService.getProfile(id);
if (profile != null) {
checkIfAllowed(profile, Action.DELETE_PROFILE);
profileService.deleteProfile(id);
return Collections.singletonMap(MODEL_MESSAGE, String.format(MSG_PROFILE_DELETED_FORMAT, id));
} else {
throw new ResourceNotFoundException("No profile found for ID '" + id + "'");
}
}
private void checkIfAllowed(String tenant, Action action) throws ActionDeniedException {
if (!tenantPermissionEvaluator.isAllowed(tenant, action.toString())) {
if (tenant != null) {
throw new ActionDeniedException(action.toString(), tenant);
} else {
throw new ActionDeniedException(action.toString());
}
}
}
private void checkIfAllowed(Profile profile, Action action) throws ActionDeniedException {
if (!profilePermissionEvaluator.isAllowed(profile, action.toString())) {
if (profile != null) {
throw new ActionDeniedException(action.toString(), profile);
} else {
throw new ActionDeniedException(action.toString());
}
}
}
}