diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java index 7b15dffe6d780f65ca87d4a1dcc5aab88d4ecc5f..0f561c65ca11bb57579fd9feb50a644d74b6b31a 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java +++ b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java @@ -67,18 +67,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { web.ignoring().antMatchers("/ws/basic/**", "/ws/jwt/**", "/error", "/logout"); } - /** - * Checks the BasicAuth for GMS clients. - */ - @Bean - public FilterRegistrationBean serviceBasicAuthFilter(LoggingDAO loggingDAO) { - FilterRegistrationBean bean = new FilterRegistrationBean(); - bean.setFilter(new ServiceBasicAuthFilter(loggingDAO)); - bean.addUrlPatterns("/ws/basic/*"); - bean.setOrder(Ordered.HIGHEST_PRECEDENCE); - return bean; - } - /** * Checks JWT for web services. */ diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/ServiceBasicAuthFilter.java b/gms/src/main/java/it/inaf/ia2/gms/authn/ServiceBasicAuthFilter.java deleted file mode 100644 index ff82f610b5ae7a950ad1c88334f8b1251a9e07d7..0000000000000000000000000000000000000000 --- a/gms/src/main/java/it/inaf/ia2/gms/authn/ServiceBasicAuthFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -package it.inaf.ia2.gms.authn; - -import it.inaf.ia2.gms.exception.UnauthorizedException; -import it.inaf.ia2.gms.persistence.ClientsDAO; -import it.inaf.ia2.gms.persistence.LoggingDAO; -import it.inaf.ia2.gms.persistence.model.ClientEntity; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.bind.DatatypeConverter; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; - -public class ServiceBasicAuthFilter implements Filter { - - private final LoggingDAO loggingDAO; - - public ServiceBasicAuthFilter(LoggingDAO loggingDAO) { - this.loggingDAO = loggingDAO; - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - - if (request.getServletPath().startsWith("/ws/")) { - try { - validateBasicAuth(request); - } catch (UnauthorizedException ex) { - loggingDAO.logAction("Unauthorized BasicAuth WS request"); - ((HttpServletResponse) res).sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage()); - return; - } - } - - loggingDAO.logAction("BasicAuth WS request"); - - chain.doFilter(req, res); - } - - private void validateBasicAuth(HttpServletRequest request) { - - String token = getBasicAuthToken(request); - - int delim = token.indexOf(":"); - - if (delim == -1) { - throw new BadCredentialsException("Invalid basic authentication token"); - } - - String clientId = token.substring(0, delim); - String clientSecret = token.substring(delim + 1); - - ClientsDAO clientsDAO = getClientsDAO(request); - - ClientEntity client = clientsDAO.findClientById(clientId) - .orElseThrow(() -> new BadCredentialsException("Client " + clientId + " not found")); - - String shaSecret = getSha256(clientSecret); - if (!shaSecret.equals(client.getSecret())) { - throw new UnauthorizedException("Wrong secret"); - } - } - - private String getBasicAuthToken(HttpServletRequest request) { - - String header = request.getHeader("Authorization"); - - if (header == null || !header.toLowerCase().startsWith("basic ")) { - throw new UnauthorizedException("Missing Authorization header"); - } - - byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); - byte[] decoded = Base64.getDecoder().decode(base64Token); - - return new String(decoded, StandardCharsets.UTF_8); - } - - protected ClientsDAO getClientsDAO(HttpServletRequest request) { - WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()); - return webApplicationContext.getBean(ClientsDAO.class); - } - - private static String getSha256(String secret) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] sha = md.digest(secret.getBytes(StandardCharsets.UTF_8)); - return DatatypeConverter.printHexBinary(sha).toLowerCase(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } -} diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/ClientsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/ClientsDAO.java deleted file mode 100644 index 0ceb032665fcd9b14a0f9ff2719f71f0fd3e9e4e..0000000000000000000000000000000000000000 --- a/gms/src/main/java/it/inaf/ia2/gms/persistence/ClientsDAO.java +++ /dev/null @@ -1,58 +0,0 @@ -package it.inaf.ia2.gms.persistence; - -import it.inaf.ia2.gms.persistence.model.ClientEntity; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import javax.sql.DataSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; - -@Component -public class ClientsDAO { - - private final JdbcTemplate jdbcTemplate; - - @Autowired - public ClientsDAO(DataSource dataSource) { - jdbcTemplate = new JdbcTemplate(dataSource); - } - - public Optional<ClientEntity> findClientById(String clientId) { - - String sql = "SELECT client_secret, allowed_actions, ip_filter FROM gms_client WHERE client_id = ?"; - - return jdbcTemplate.query(conn -> { - PreparedStatement ps = conn.prepareStatement(sql); - ps.setString(1, clientId); - return ps; - }, resultSet -> { - if (resultSet.next()) { - ClientEntity client = new ClientEntity(); - client.setId(clientId); - client.setSecret(resultSet.getString("client_secret")); - client.setAllowedActions(getAllowedActions(resultSet)); - client.setIpFilter(resultSet.getString("ip_filter")); - return Optional.of(client); - } - return Optional.empty(); - }); - } - - private List<String> getAllowedActions(ResultSet resultSet) throws SQLException { - - List<String> actions = new ArrayList<>(); - - ResultSet items = resultSet.getArray("allowed_actions").getResultSet(); - while (items.next()) { - String action = items.getString(1); - actions.add(action); - } - - return actions; - } -} diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java new file mode 100644 index 0000000000000000000000000000000000000000..e7047fec36e815dc69be34e75677d2a3a76b478e --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java @@ -0,0 +1,99 @@ +package it.inaf.ia2.gms.persistence; + +import it.inaf.ia2.gms.persistence.model.InvitedRegistration; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class InvitedRegistrationDAO { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public InvitedRegistrationDAO(DataSource dataSource) { + jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Transactional + public void addInvitedRegistration(InvitedRegistration invitedRegistration) { + + String sqlReq = "INSERT INTO invited_registration_request (id, token_hash, email) VALUES (?, ?, ?)"; + + jdbcTemplate.update(conn -> { + PreparedStatement ps = conn.prepareStatement(sqlReq); + ps.setString(1, invitedRegistration.getId()); + ps.setString(2, invitedRegistration.getTokenHash()); + ps.setObject(3, invitedRegistration.getEmail()); + return ps; + }); + + for (String groupId : invitedRegistration.getGroupIds()) { + String sqlReqGroup = "INSERT INTO invited_registration_request_group (request_id, group_id) VALUES (?, ?)"; + + jdbcTemplate.update(conn -> { + PreparedStatement ps = conn.prepareStatement(sqlReqGroup); + ps.setString(1, invitedRegistration.getId()); + ps.setString(2, groupId); + return ps; + }); + } + } + + public Optional<InvitedRegistration> getInvitedRegistrationFromToken(String tokenHash) { + + String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND !done"; + + InvitedRegistration registration = jdbcTemplate.query(conn -> { + PreparedStatement ps = conn.prepareStatement(sqlReq); + ps.setString(1, tokenHash); + return ps; + }, resultSet -> { + if (resultSet.next()) { + InvitedRegistration reg = new InvitedRegistration(); + reg.setId(resultSet.getString("id")); + reg.setEmail(resultSet.getString("email")); + return reg; + } + return null; + }); + + if (registration != null) { + + String sqlReqGroup = "SELECT group_id FROM invited_registration_request_group WHERE request_id = ?"; + + List<String> groupIds = jdbcTemplate.query(conn -> { + PreparedStatement ps = conn.prepareStatement(sqlReqGroup); + ps.setString(1, registration.getId()); + return ps; + }, resultSet -> { + List<String> groups = new ArrayList<>(); + while (resultSet.next()) { + groups.add(resultSet.getString("group_id")); + } + return groups; + }); + + registration.setGroupIds(groupIds); + } + + return Optional.ofNullable(registration); + } + + public void setRegistrationDone(String tokenHash) { + + String sql = "UPDATE invited_registration_request SET done = true WHERE token_hash = ?"; + + jdbcTemplate.update(conn -> { + PreparedStatement ps = conn.prepareStatement(sql); + ps.setString(1, tokenHash); + return ps; + }); + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java new file mode 100644 index 0000000000000000000000000000000000000000..da709f7b539e75e4c7bcae44c1fc91a2dde9a94d --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java @@ -0,0 +1,52 @@ +package it.inaf.ia2.gms.persistence.model; + +import java.util.List; + +public class InvitedRegistration { + + private String id; + private String tokenHash; + private String email; + private boolean done; + private List<String> groupIds; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTokenHash() { + return tokenHash; + } + + public void setTokenHash(String tokenHash) { + this.tokenHash = tokenHash; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isDone() { + return done; + } + + public void setDone(boolean done) { + this.done = done; + } + + public List<String> getGroupIds() { + return groupIds; + } + + public void setGroupIds(List<String> groupIds) { + this.groupIds = groupIds; + } +} diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql index d58e44059dbc48e204fd065276c35262d6334bbd..b9c0a942032d62593a2087401ec47d094f627eca 100644 --- a/gms/src/main/resources/sql/init.sql +++ b/gms/src/main/resources/sql/init.sql @@ -31,14 +31,6 @@ CREATE TABLE gms_permission ( foreign key (group_path) references gms_group(path) ); -CREATE TABLE gms_client ( - client_id varchar NOT NULL, - client_secret varchar NOT NULL, - allowed_actions text[] NOT NULL, - ip_filter text NULL, - primary key (client_id) -); - CREATE TABLE error_log ( "date" timestamp DEFAULT NOW(), "exception_class" varchar, @@ -51,3 +43,19 @@ CREATE TABLE audit_log ( "ip_address" varchar, "action" TEXT ); + +CREATE TABLE invited_registration_request ( + id varchar NOT NULL, + token_hash varchar NOT NULL, + email varchar NOT NULL, + creation_time timestamp DEFAULT NOW(), + done boolean, + PRIMARY KEY(id) +); + +CREATE TABLE invited_registration_request_group ( + request_id varchar NOT NULL, + group_id varchar NOT NULL, + PRIMARY KEY (request_id, group_id), + FOREIGN KEY (request_id) REFERENCES invited_registration_request(id) +); diff --git a/gms/src/test/java/it/inaf/ia2/gms/authn/ServiceBasicAuthFilterTest.java b/gms/src/test/java/it/inaf/ia2/gms/authn/ServiceBasicAuthFilterTest.java deleted file mode 100644 index 4889ae7abe1b94940d2abf925606412d9f899002..0000000000000000000000000000000000000000 --- a/gms/src/test/java/it/inaf/ia2/gms/authn/ServiceBasicAuthFilterTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package it.inaf.ia2.gms.authn; - -import it.inaf.ia2.gms.persistence.ClientsDAO; -import it.inaf.ia2.gms.persistence.LoggingDAO; -import it.inaf.ia2.gms.persistence.model.ClientEntity; -import java.util.Collections; -import java.util.Optional; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(JUnit4.class) -public class ServiceBasicAuthFilterTest { - - private ServiceBasicAuthFilter filter; - - private HttpServletRequest request; - private HttpServletResponse response; - private FilterChain chain; - - @Before - public void setUp() { - - ClientsDAO clientsDAO = mock(ClientsDAO.class); - - ClientEntity client = new ClientEntity(); - client.setId("test"); - client.setSecret("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"); // sha256 of "password" - client.setAllowedActions(Collections.singletonList("*")); - - when(clientsDAO.findClientById("test")).thenReturn(Optional.of(client)); - - LoggingDAO loggingDAO = mock(LoggingDAO.class); - filter = spy(new ServiceBasicAuthFilter(loggingDAO)); - doReturn(clientsDAO).when(filter).getClientsDAO(any()); - - request = mock(HttpServletRequest.class); - response = mock(HttpServletResponse.class); - chain = mock(FilterChain.class); - } - - @Test - public void testValidCredentials() throws Exception { - - when(request.getServletPath()).thenReturn("/ws/basic/group"); - when(request.getHeader("Authorization")).thenReturn("Basic dGVzdDpwYXNzd29yZA=="); // test:password - - filter.doFilter(request, response, chain); - - verify(chain, times(1)).doFilter(any(), any()); - } - - @Test - public void testInvalidCredentials() throws Exception { - - when(request.getServletPath()).thenReturn("/ws/basic/group"); - when(request.getHeader("Authorization")).thenReturn("Basic dGVzdDp0ZXN0"); // test:test - - filter.doFilter(request, response, chain); - - verify(response, times(1)).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), any()); - verify(chain, never()).doFilter(any(), any()); - } - - @Test - public void testMissingHeader() throws Exception { - - when(request.getServletPath()).thenReturn("/ws/basic/group"); - - filter.doFilter(request, response, chain); - - verify(response, times(1)).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), any()); - verify(chain, never()).doFilter(any(), any()); - } - - @Test - public void testOutsidePath() throws Exception { - - when(request.getServletPath()).thenReturn("/other/path"); - - filter.doFilter(request, response, chain); - - verify(response, never()).sendError(anyInt(), any()); - verify(chain, times(1)).doFilter(any(), any()); - } -}