Skip to content

Commit

Permalink
Merge pull request #342 from sanger/selfcreate
Browse files Browse the repository at this point in the history
x1145 Let sanger users create a Stan enduser account without approval
  • Loading branch information
khelwood committed Feb 12, 2024
2 parents b5e6e86 + 9db519f commit 9014da3
Show file tree
Hide file tree
Showing 12 changed files with 582 additions and 54 deletions.
61 changes: 15 additions & 46 deletions src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import uk.ac.sanger.sccp.stan.config.SessionConfig;
import uk.ac.sanger.sccp.stan.model.*;
import uk.ac.sanger.sccp.stan.repo.UserRepo;
import uk.ac.sanger.sccp.stan.request.*;
Expand All @@ -32,7 +29,7 @@
import uk.ac.sanger.sccp.stan.service.work.WorkService;
import uk.ac.sanger.sccp.stan.service.work.WorkTypeService;

import java.util.*;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

Expand All @@ -45,8 +42,7 @@
@Component
public class GraphQLMutation extends BaseGraphQLResource {
Logger log = LoggerFactory.getLogger(GraphQLMutation.class);
final LDAPService ldapService;
final SessionConfig sessionConfig;
final AuthService authService;
final IRegisterService<RegisterRequest> registerService;
final IRegisterService<SectionRegisterRequest> sectionRegisterService;
final PlanService planService;
Expand Down Expand Up @@ -107,7 +103,7 @@ public class GraphQLMutation extends BaseGraphQLResource {

@Autowired
public GraphQLMutation(ObjectMapper objectMapper, AuthenticationComponent authComp,
LDAPService ldapService, SessionConfig sessionConfig,
AuthService authService,
IRegisterService<RegisterRequest> registerService,
IRegisterService<SectionRegisterRequest> sectionRegisterService,
PlanService planService, LabelPrintService labelPrintService,
Expand Down Expand Up @@ -138,8 +134,7 @@ public GraphQLMutation(ObjectMapper objectMapper, AuthenticationComponent authCo
ReactivateService reactivateService, LibraryPrepService libraryPrepService,
UserAdminService userAdminService) {
super(objectMapper, authComp, userRepo);
this.ldapService = ldapService;
this.sessionConfig = sessionConfig;
this.authService = authService;
this.registerService = registerService;
this.sectionRegisterService = sectionRegisterService;
this.planService = planService;
Expand Down Expand Up @@ -206,48 +201,22 @@ private void logRequest(String name, User user, Object request) {
}

public DataFetcher<LoginResult> logIn() {
return dataFetchingEnvironment -> {
String username = dataFetchingEnvironment.getArgument("username");
if (log.isInfoEnabled()) {
log.info("Login attempt by {}", repr(username));
}
Optional<User> optUser = userRepo.findByUsername(username);
if (optUser.isEmpty()) {
return new LoginResult("Username not in database.", null);
}
User user = optUser.get();
if (user.getRole()==User.Role.disabled) {
return new LoginResult("Username is disabled.", null);
}
String password = dataFetchingEnvironment.getArgument("password");
if (!ldapService.verifyCredentials(username, password)) {
return new LoginResult("Login failed", null);
}
Authentication authentication = new UsernamePasswordAuthenticationToken(user, password, new ArrayList<>());
authComp.setAuthentication(authentication, sessionConfig.getMaxInactiveMinutes());
log.info("Login succeeded for user {}", user);
return new LoginResult("OK", user);
return dfe -> {
String username = dfe.getArgument("username");
String password = dfe.getArgument("password");
return authService.logIn(username, password);
};
}

private String loggedInUsername() {
var auth = authComp.getAuthentication();
if (auth != null) {
var princ = auth.getPrincipal();
if (princ instanceof User) {
return ((User) princ).getUsername();
}
}
return null;
public DataFetcher<String> logOut() {
return dfe -> authService.logOut();
}

public DataFetcher<String> logOut() {
return dataFetchingEnvironment -> {
if (log.isInfoEnabled()) {
log.info("Logout requested by {}", repr(loggedInUsername()));
}
authComp.setAuthentication(null, 0);
return "OK";
public DataFetcher<LoginResult> userSelfRegister(final User.Role role) {
return dfe -> {
String username = dfe.getArgument("username");
String password = dfe.getArgument("password");
return authService.selfRegister(username, password, role);
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import uk.ac.sanger.sccp.stan.model.User;

import javax.annotation.PostConstruct;
import java.io.IOException;
Expand Down Expand Up @@ -134,6 +135,7 @@ private RuntimeWiring buildWiring() {
.dataFetcher("version", graphQLDataFetchers.versionInfo())
)
.type(newTypeWiring("Mutation")
.dataFetcher("registerAsEndUser", graphQLMutation.userSelfRegister(User.Role.enduser))
.dataFetcher("login", graphQLMutation.logIn())
.dataFetcher("logout", graphQLMutation.logOut())
.dataFetcher("register", transact(graphQLMutation.register()))
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/uk/ac/sanger/sccp/stan/repo/UserRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uk.ac.sanger.sccp.stan.model.User;

import javax.persistence.EntityNotFoundException;
import java.util.List;
import java.util.Optional;

import static uk.ac.sanger.sccp.utils.BasicUtils.repr;
Expand All @@ -14,4 +15,6 @@ public interface UserRepo extends CrudRepository<User, Integer> {
default User getByUsername(String username) throws EntityNotFoundException {
return findByUsername(username).orElseThrow(() -> new EntityNotFoundException("User not found: "+repr(username)));
}

List<User> findAllByRole(User.Role role);
}
32 changes: 32 additions & 0 deletions src/main/java/uk/ac/sanger/sccp/stan/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package uk.ac.sanger.sccp.stan.service;

import uk.ac.sanger.sccp.stan.model.User;
import uk.ac.sanger.sccp.stan.request.LoginResult;

/**
* Service dealing with user authentication
*/
public interface AuthService {
/**
* Logs in
* @param username the username to log in
* @param password the password to authenticate the user
* @return a login result describing the result of the login
*/
LoginResult logIn(String username, String password);

/**
* Logs out the current logged in user, if any
* @return a description of the result of logging out
*/
String logOut();

/**
* Creates a Stan user authenticated with the given credentials
* @param username the username to create
* @param password the password to authenticate the user
* @param role the role to create the user with
* @return the result of attempting to log in with the given credentials
*/
LoginResult selfRegister(String username, String password, User.Role role);
}
133 changes: 133 additions & 0 deletions src/main/java/uk/ac/sanger/sccp/stan/service/AuthServiceImp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package uk.ac.sanger.sccp.stan.service;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import uk.ac.sanger.sccp.stan.AuthenticationComponent;
import uk.ac.sanger.sccp.stan.config.SessionConfig;
import uk.ac.sanger.sccp.stan.model.User;
import uk.ac.sanger.sccp.stan.repo.UserRepo;
import uk.ac.sanger.sccp.stan.request.LoginResult;

import java.util.*;

import static uk.ac.sanger.sccp.utils.BasicUtils.repr;

/**
* @author dr6
*/
@Service
public class AuthServiceImp implements AuthService {
Logger log = LoggerFactory.getLogger(AuthServiceImp.class);
private final SessionConfig sessionConfig;
private final UserRepo userRepo;
private final AuthenticationComponent authComp;
private final LDAPService ldapService;
private final EmailService emailService;
private final UserAdminService userAdminService;

@Autowired
public AuthServiceImp(SessionConfig sessionConfig,
UserRepo userRepo, AuthenticationComponent authComp,
LDAPService ldapService, EmailService emailService, UserAdminService userAdminService) {
this.sessionConfig = sessionConfig;
this.userRepo = userRepo;
this.authComp = authComp;
this.ldapService = ldapService;
this.emailService = emailService;
this.userAdminService = userAdminService;
}

/**
* Gets the username of the logged in user, if any
* @return the logged in username, or null
*/
@Nullable
public String loggedInUsername() {
var auth = authComp.getAuthentication();
if (auth != null) {
var princ = auth.getPrincipal();
if (princ instanceof User) {
return ((User) princ).getUsername();
}
}
return null;
}

@Override
public LoginResult logIn(String username, String password) {
if (log.isInfoEnabled()) {
log.info("Login attempt by {}", repr(username));
}
Optional<User> optUser = userRepo.findByUsername(username);
if (optUser.isEmpty()) {
return new LoginResult("Username not in database.", null);
}
User user = optUser.get();
if (user.getRole()==User.Role.disabled) {
return new LoginResult("Username is disabled.", null);
}
if (!ldapService.verifyCredentials(username, password)) {
return new LoginResult("Login failed.", null);
}
Authentication authentication = new UsernamePasswordAuthenticationToken(user, password, new ArrayList<>());
authComp.setAuthentication(authentication, sessionConfig.getMaxInactiveMinutes());
log.info("Login succeeded for user {}", user);
return new LoginResult("OK", user);
}

@Override
public String logOut() {
if (log.isInfoEnabled()) {
log.info("Logout requested by {}", repr(loggedInUsername()));
}
authComp.setAuthentication(null, 0);
return "OK";
}

@Override
public LoginResult selfRegister(String username, String password, User.Role role) {
if (log.isInfoEnabled()) {
log.info("selfRegister attempt by {}", repr(username));
}
username = userAdminService.validateUsername(username);
if (!ldapService.verifyCredentials(username, password)) {
return new LoginResult("Authentication failed.", null);
}
Optional<User> optUser = userRepo.findByUsername(username);
User user;
if (optUser.isEmpty()) {
user = userAdminService.addUser(username, role);
log.info("Login succeeded as new user {}", user);
sendNewUserEmail(user);
} else {
user = optUser.get();
if (user.getRole()== User.Role.disabled) {
return new LoginResult("Username is disabled.", null);
}
log.info("Login succeeded for existing user {}", user);
}

Authentication authentication = new UsernamePasswordAuthenticationToken(user, password, new ArrayList<>());
authComp.setAuthentication(authentication, sessionConfig.getMaxInactiveMinutes());
return new LoginResult("OK", user);
}

/**
* Tries to send an email to admin users about the new user being created.
* @param user the new user
*/
public void sendNewUserEmail(User user) {
List<User> admins = userRepo.findAllByRole(User.Role.admin);
if (admins.isEmpty()) {
return;
}
List<String> usernames = admins.stream().map(User::getUsername).toList();
String body = "User "+user.getUsername()+" has registered themself as "+user.getRole()+" on %service.";
emailService.tryEmail(usernames, "New user created on %service", body);
}
}
47 changes: 43 additions & 4 deletions src/main/java/uk/ac/sanger/sccp/stan/service/EmailService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import uk.ac.sanger.sccp.stan.config.MailConfig;
import uk.ac.sanger.sccp.stan.service.store.StoreService;

import java.util.Collection;
import java.util.List;

import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty;
Expand Down Expand Up @@ -88,13 +89,10 @@ public String getServiceDescription() {
* @return an array of cc recipients, or null
*/
public String[] releaseEmailCCs(List<String> ccList) {
String releaseCc = mailConfig.getReleaseCC();
String releaseCc = usernameToEmail(mailConfig.getReleaseCC());
if (nullOrEmpty(releaseCc)) {
return nullOrEmpty(ccList) ? null : ccList.toArray(String[]::new);
}
if (releaseCc.indexOf('@') < 0) {
releaseCc += "@sanger.ac.uk";
}
if (nullOrEmpty(ccList)) {
return new String[] { releaseCc };
}
Expand Down Expand Up @@ -133,4 +131,45 @@ public boolean tryReleaseEmail(String recipient, List<String> ccList, List<Strin
return false;
}
}

/**
* Adds the sanger email suffix to the end of usernames, if they don't contain an {@code @} symbol
* @param username the username
* @return an email address for the username
*/
public String usernameToEmail(String username) {
if (!nullOrEmpty(username) && username.indexOf('@') < 0) {
return username + "@sanger.ac.uk";
}
return username;
}

/**
* Tries to send the described email.
* Catches and logs any exceptions thrown.
* Recipient usernames are turned to email addresses using {@link #usernameToEmail}
* @param recipients the usernames or email addresses to send the email to
* @param heading the heading of the email
* @param text the text of the email
* @return true if the email was send; false if an exception was caught
*/
public boolean tryEmail(Collection<String> recipients, String heading, String text) {
String serviceDesc = mailConfig.getServiceDescription();
if (heading.contains("%service")) {
heading = heading.replace("%service", serviceDesc);
}
if (text.contains("%service")) {
text = text.replace("%service", serviceDesc);
}
String[] emailRecs = recipients.stream()
.map(this::usernameToEmail)
.toArray(String[]::new);
try {
send(heading, text, emailRecs, null);
return true;
} catch (Exception e) {
log.error("Failed to send email", e);
return false;
}
}
}
Loading

0 comments on commit 9014da3

Please sign in to comment.