Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#486 api key support #489

Merged
merged 7 commits into from
Jul 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/alfio/config/ConfigurationStatusChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
boolean initCompleted = configurationManager.getBooleanConfigValue(Configuration.getSystemConfiguration(ConfigurationKeys.INIT_COMPLETED), false);
if (!initCompleted) {
String adminPassword = PasswordGenerator.generateRandomPassword();
userRepository.create(UserManager.ADMIN_USERNAME, passwordEncoder.encode(adminPassword), "The", "Administrator", "admin@localhost", true, User.Type.INTERNAL);
userRepository.create(UserManager.ADMIN_USERNAME, passwordEncoder.encode(adminPassword), "The", "Administrator", "admin@localhost", true, User.Type.INTERNAL, null, null);
authorityRepository.create(UserManager.ADMIN_USERNAME, Role.ADMIN.getRoleName());
log.info("*******************************************************");
log.info(" This is the first time you're running alf.io");
Expand Down
128 changes: 124 additions & 4 deletions src/main/java/alfio/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@
import alfio.manager.user.UserManager;
import alfio.model.user.Role;
import alfio.model.user.User;
import alfio.repository.user.AuthorityRepository;
import alfio.repository.user.UserRepository;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
Expand All @@ -49,8 +58,15 @@
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static alfio.model.system.Configuration.getSystemConfiguration;
import static alfio.model.system.ConfigurationKeys.ENABLE_CAPTCHA_FOR_LOGIN;
Expand Down Expand Up @@ -88,12 +104,116 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception {
}
}

private static class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return isTokenAuthentication(request) ? request.getHeader("Authorization").substring("apikey ".length()) : null;
}

@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "N/A";
}
}


public static class APITokenAuthentication extends AbstractAuthenticationToken {

private Object credentials;
private final Object principal;


public APITokenAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.credentials = credentials;
this.principal = principal;
setAuthenticated(true);
}

@Override
public Object getCredentials() {
return credentials;
}

@Override
public Object getPrincipal() {
return principal;
}
}

public static class WrongAccountTypeException extends AccountStatusException {

public WrongAccountTypeException(String msg) {
super(msg);
}
}

@Configuration
@Order(0)
public static class APITokenAuthWebSecurity extends WebSecurityConfigurerAdapter {

@Autowired
private UserRepository userRepository;

@Autowired
private AuthorityRepository authorityRepository;

//https://stackoverflow.com/a/48448901
@Override
protected void configure(HttpSecurity http) throws Exception {

APIKeyAuthFilter filter = new APIKeyAuthFilter();
filter.setAuthenticationManager(authentication -> {
//
String apiKey = (String) authentication.getPrincipal();
//check if user type ->
User user = userRepository.findByUsername(apiKey).orElseThrow(() -> new BadCredentialsException("Api key " + apiKey + " don't exists"));
if (!user.isEnabled()) {
throw new DisabledException("Api key " + apiKey + " is disabled");
}
if (User.Type.API_KEY != user.getType()) {
throw new WrongAccountTypeException("Wrong account type for username " + apiKey);
}
if (!user.isCurrentlyValid(ZonedDateTime.now(ZoneId.of("UTC")))) {
throw new DisabledException("Api key " + apiKey + " is expired");
}

APITokenAuthentication auth = new APITokenAuthentication(
authentication.getPrincipal(),
authentication.getCredentials(),
authorityRepository.findRoles(apiKey).stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
return auth;
});


http.requestMatcher(WebSecurityConfig::isTokenAuthentication)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
.authorizeRequests()
.antMatchers(ADMIN_PUBLIC_API + "/**").hasRole(API_CLIENT)
.antMatchers(ADMIN_API + "/check-in/**").hasAnyRole(OPERATOR, SUPERVISOR)
.antMatchers(HttpMethod.GET, ADMIN_API + "/events").hasAnyRole(OPERATOR, SUPERVISOR, SPONSOR)
.antMatchers(ADMIN_API + "/user-type").hasAnyRole(OPERATOR, SUPERVISOR, SPONSOR)
.antMatchers(ADMIN_API + "/**").denyAll()
.antMatchers(HttpMethod.POST, "/api/attendees/sponsor-scan").hasRole(SPONSOR)
.antMatchers(HttpMethod.GET, "/api/attendees/*/ticket/*").hasAnyRole(OPERATOR, SUPERVISOR)
.antMatchers("/**").authenticated()
.and().addFilter(filter);
}
}

private static boolean isTokenAuthentication(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
return authorization != null && authorization.toLowerCase(Locale.ENGLISH).startsWith("apikey ");
}

/**
* Basic auth configuration for Public APIs.
* The rules are valid only if the Authorization header is present and if the context path starts with /api/v1/admin
*/
@Configuration
@Order(0)
@Order(1)
public static class APIBasicAuthWebSecurity extends BaseWebSecurity {

@Override
Expand All @@ -113,7 +233,7 @@ protected void configure(HttpSecurity http) throws Exception {
* FormBasedWebSecurity rules.
*/
@Configuration
@Order(1)
@Order(2)
public static class BasicAuthWebSecurity extends BaseWebSecurity {

@Override
Expand All @@ -136,7 +256,7 @@ protected void configure(HttpSecurity http) throws Exception {
* Default form based configuration.
*/
@Configuration
@Order(2)
@Order(3)
public static class FormBasedWebSecurity extends BaseWebSecurity {

@Autowired
Expand Down Expand Up @@ -300,7 +420,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
String username = req.getParameter("username");
if(!userManager.usernameExists(username)) {
int orgId = userManager.createOrganization(username, "Demo organization", username);
userManager.insertUser(orgId, username, "", "", username, Role.OWNER, User.Type.DEMO, req.getParameter("password"));
userManager.insertUser(orgId, username, "", "", username, Role.OWNER, User.Type.DEMO, req.getParameter("password"), null, null);
}
}

Expand Down
27 changes: 20 additions & 7 deletions src/main/java/alfio/controller/api/admin/UsersApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,21 @@ public ValidationResult validateOrganization(@RequestBody OrganizationModificati

@RequestMapping(value = "/users/check", method = POST)
public ValidationResult validateUser(@RequestBody UserModification userModification) {
return userManager.validateUser(userModification.getId(), userModification.getUsername(),
userModification.getOrganizationId(), userModification.getRole(), userModification.getFirstName(),
userModification.getLastName(), userModification.getEmailAddress());
if(userModification.getType() == User.Type.API_KEY) {
return ValidationResult.success();
} else {
return userManager.validateUser(userModification.getId(), userModification.getUsername(),
userModification.getOrganizationId(), userModification.getRole(), userModification.getFirstName(),
userModification.getLastName(), userModification.getEmailAddress());
}
}

@RequestMapping(value = "/users/edit", method = POST)
public String editUser(@RequestBody UserModification userModification, Principal principal) {
userManager.editUser(userModification.getId(), userModification.getOrganizationId(), userModification.getUsername(), userModification.getFirstName(), userModification.getLastName(), userModification.getEmailAddress(), Role.valueOf(userModification.getRole()), principal.getName());
userManager.editUser(userModification.getId(), userModification.getOrganizationId(),
userModification.getUsername(), userModification.getFirstName(), userModification.getLastName(),
userModification.getEmailAddress(), userModification.getDescription(),
Role.valueOf(userModification.getRole()), principal.getName());
return OK;
}

Expand All @@ -143,10 +150,12 @@ public ValidationResult updatePassword(@RequestBody PasswordModification passwor
public UserWithPasswordAndQRCode insertUser(@RequestBody UserModification userModification, @RequestParam("baseUrl") String baseUrl, Principal principal) {
Role requested = Role.valueOf(userModification.getRole());
Validate.isTrue(userManager.getAvailableRoles(principal.getName()).stream().anyMatch(requested::equals), String.format("Requested role %s is not available for current user", userModification.getRole()));
User.Type type = userModification.getType();
UserWithPassword userWithPassword = userManager.insertUser(userModification.getOrganizationId(), userModification.getUsername(),
userModification.getFirstName(), userModification.getLastName(),
userModification.getEmailAddress(), requested,
User.Type.INTERNAL);
type == null ? User.Type.INTERNAL : type,
userModification.getValidToAsDateTime(), userModification.getDescription());
return new UserWithPasswordAndQRCode(userWithPassword, toBase64QRCode(userWithPassword, baseUrl));
}

Expand Down Expand Up @@ -175,14 +184,18 @@ public String enableUser(@PathVariable("id") int userId, @PathVariable("enable")
public UserModification loadUser(@PathVariable("id") int userId) {
User user = userManager.findUser(userId);
List<Organization> userOrganizations = userManager.findUserOrganizations(user.getUsername());
return new UserModification(user.getId(), userOrganizations.get(0).getId(), userManager.getUserRole(user).name(), user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmailAddress());
return new UserModification(user.getId(), userOrganizations.get(0).getId(), userManager.getUserRole(user).name(),
user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmailAddress(),
user.getType(), user.getValidToEpochSecond(), user.getDescription());
}

@RequestMapping(value = "/users/current", method = GET)
public UserModification loadCurrentUser(Principal principal) {
User user = userManager.findUserByUsername(principal.getName());
Optional<Organization> userOrganization = userManager.findUserOrganizations(user.getUsername()).stream().findFirst();
return new UserModification(user.getId(), userOrganization.map(Organization::getId).orElse(-1), userManager.getUserRole(user).name(), user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmailAddress());
return new UserModification(user.getId(), userOrganization.map(Organization::getId).orElse(-1),
userManager.getUserRole(user).name(), user.getUsername(), user.getFirstName(), user.getLastName(),
user.getEmailAddress(), user.getType(), user.getValidToEpochSecond(), user.getDescription());
}

@RequestMapping(value = "/users/{id}/reset-password", method = PUT)
Expand Down
23 changes: 18 additions & 5 deletions src/main/java/alfio/manager/user/UserManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -166,13 +167,13 @@ public ValidationResult validateOrganization(Integer id, String name, String ema
}

@Transactional
public void editUser(int id, int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, String currentUsername) {
public void editUser(int id, int organizationId, String username, String firstName, String lastName, String emailAddress, String description, Role role, String currentUsername) {
boolean admin = ADMIN_USERNAME.equals(username) && Role.ADMIN == role;
if(!admin) {
int userOrganizationResult = userOrganizationRepository.updateUserOrganization(id, organizationId);
Assert.isTrue(userOrganizationResult == 1, "unexpected error during organization update");
}
int userResult = userRepository.update(id, username, firstName, lastName, emailAddress);
int userResult = userRepository.update(id, username, firstName, lastName, emailAddress, description);
Assert.isTrue(userResult == 1, "unexpected error during user update");
if(!admin) {
Assert.isTrue(getAvailableRoles(currentUsername).contains(role), "cannot assign role "+role);
Expand All @@ -183,14 +184,26 @@ public void editUser(int id, int organizationId, String username, String firstNa

@Transactional
public UserWithPassword insertUser(int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, User.Type userType) {
return insertUser(organizationId, username, firstName, lastName, emailAddress, role, userType, null, null);
}

@Transactional
public UserWithPassword insertUser(int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, User.Type userType, ZonedDateTime validTo, String description) {
if (userType == User.Type.API_KEY) {
username = UUID.randomUUID().toString();
firstName = "apikey";
lastName = "";
emailAddress = "";
}

String userPassword = PasswordGenerator.generateRandomPassword();
return insertUser(organizationId, username, firstName, lastName, emailAddress, role, userType, userPassword);
return insertUser(organizationId, username, firstName, lastName, emailAddress, role, userType, userPassword, validTo, description);
}

@Transactional
public UserWithPassword insertUser(int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, User.Type userType, String userPassword) {
public UserWithPassword insertUser(int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, User.Type userType, String userPassword, ZonedDateTime validTo, String description) {
Organization organization = organizationRepository.getById(organizationId);
AffectedRowCountAndKey<Integer> result = userRepository.create(username, passwordEncoder.encode(userPassword), firstName, lastName, emailAddress, true, userType);
AffectedRowCountAndKey<Integer> result = userRepository.create(username, passwordEncoder.encode(userPassword), firstName, lastName, emailAddress, true, userType, validTo, description);
userOrganizationRepository.create(result.getKey(), organization.getId());
authorityRepository.create(username, role.getRoleName());
return new UserWithPassword(userRepository.findById(result.getKey()), userPassword, UUID.randomUUID().toString());
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/alfio/model/modification/UserModification.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
*/
package alfio.model.modification;

import alfio.model.user.User;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

@Getter
public class UserModification {

Expand All @@ -30,6 +35,9 @@ public class UserModification {
private final String firstName;
private final String lastName;
private final String emailAddress;
private final User.Type type;
private final Long validTo;
private final String description;

@JsonCreator
public UserModification(@JsonProperty("id") Integer id,
Expand All @@ -38,13 +46,23 @@ public UserModification(@JsonProperty("id") Integer id,
@JsonProperty("username") String username,
@JsonProperty("firstName") String firstName,
@JsonProperty("lastName") String lastName,
@JsonProperty("emailAddress") String emailAddress) {
@JsonProperty("emailAddress") String emailAddress,
@JsonProperty("type") User.Type type,
@JsonProperty("validTo") Long validTo,
@JsonProperty("description") String description) {
this.id = id;
this.organizationId = organizationId;
this.role = role;
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.emailAddress = emailAddress;
this.type = type;
this.validTo = validTo;
this.description = description;
}

public ZonedDateTime getValidToAsDateTime() {
return validTo == null ? null : ZonedDateTime.ofInstant(Instant.ofEpochSecond(validTo), ZoneId.of("UTC"));
}
}