Skip to content

Commit

Permalink
#486 api key support (#489)
Browse files Browse the repository at this point in the history
* #486 initial work for an api key solution

* #486 refactor

* #486 expose user type, refactor, hide api_key type from user list

* #486 expose ui for handling api key

* #486 expose ui for handling api key

* 486 initial work for valid-to

* #486 add description/rename token in apikey, cleanup
  • Loading branch information
syjer authored and cbellone committed Jul 19, 2018
1 parent ef00805 commit d2199eb
Show file tree
Hide file tree
Showing 19 changed files with 357 additions and 55 deletions.
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"));
}
}

0 comments on commit d2199eb

Please sign in to comment.