Loading gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java +27 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import org.springframework.core.Ordered; import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; Loading @@ -32,6 +33,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { super.configure(http); // CORS are necessary only for development (API access from npm server) if (Arrays.asList(env.getActiveProfiles()).contains("dev")) { http.authorizeRequests() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll(); Loading @@ -40,6 +42,31 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { http.csrf().disable(); } /** * The authentication is ignored for these endpoints. The "/ws" endpoints * (web service API for programmatic access) are protected by the custom * WebServiceAuthorizationFilter that checks BasicAuth for GMS clients. */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/ws/**", "/error"); } /** * Checks the BasicAuth for GMS clients. */ @Bean public FilterRegistrationBean webServiceAuthorizationFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebServiceAuthorizationFilter()); bean.addUrlPatterns("/ws/*"); bean.setOrder(Ordered.HIGHEST_PRECEDENCE); return bean; } /** * CORS are necessary only for development (API access from npm server). */ @Bean @Profile("dev") public FilterRegistrationBean corsFilter() { Loading gms/src/main/java/it/inaf/ia2/gms/authn/WebServiceAuthorizationFilter.java 0 → 100644 +94 −0 Original line number Diff line number Diff line 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.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 WebServiceAuthorizationFilter implements Filter { @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) { ((HttpServletResponse) res).sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage()); return; } } 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); } } } gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java +13 −1 Original line number Diff line number Diff line package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.authn.SessionData; import it.inaf.ia2.gms.exception.UnauthorizedException; import it.inaf.ia2.gms.model.CreateGroupRequest; import it.inaf.ia2.gms.model.GroupNode; import it.inaf.ia2.gms.model.GroupsModelRequest; import it.inaf.ia2.gms.model.GroupsModelResponse; import it.inaf.ia2.gms.model.PaginatedData; import it.inaf.ia2.gms.model.PaginatedModelRequest; import it.inaf.ia2.gms.model.Permission; import it.inaf.ia2.gms.model.RenameGroupRequest; import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.service.GroupsModelBuilder; import it.inaf.ia2.gms.service.GroupsService; import it.inaf.ia2.gms.service.GroupsTreeBuilder; import it.inaf.ia2.gms.service.PermissionsService; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; Loading @@ -31,6 +34,9 @@ public class GroupsController { @Autowired private SessionData session; @Autowired private PermissionsService permissionsService; @Autowired private GroupsService groupsService; Loading @@ -48,7 +54,13 @@ public class GroupsController { @PostMapping(value = "/group", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity<PaginatedData<GroupNode>> createGroup(@Valid @RequestBody CreateGroupRequest request) { GroupEntity newGroup = groupsService.addGroup(request.getParentGroupId(), request.getNewGroupName(), session.getUserId()); GroupEntity parent = groupsService.getGroupById(request.getParentGroupId()); if (permissionsService.getGroupPermission(parent, session.getUserId()) != Permission.ADMIN) { throw new UnauthorizedException("Missing admin privileges"); } GroupEntity newGroup = groupsService.addGroup(parent, request.getNewGroupName()); PaginatedData<GroupNode> groupsPanel = getGroupsPanel(request.getParentGroupId(), request); Loading gms/src/main/java/it/inaf/ia2/gms/controller/WebServiceController.java 0 → 100644 +66 −0 Original line number Diff line number Diff line package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.service.GroupsService; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/ws") public class WebServiceController { @Autowired private GroupsService groupsService; /** * Creates a group and its ancestors if they are missing. It doesn't fail if * the last group already exists. */ @PostMapping("/group") public ResponseEntity<GroupEntity> createGroup(@RequestBody List<String> names) { GroupEntity group = groupsService.getRoot(); for (String name : names) { Optional<GroupEntity> optGroup = groupsService.findGroupByParentAndName(group, name); if (optGroup.isPresent()) { group = optGroup.get(); } else { group = groupsService.addGroup(group, name); } } return new ResponseEntity<>(group, HttpStatus.CREATED); } public void deleteGroup() { } public void addMember() { } public void removeMember() { } public void addPrivilege() { } public void deletePrivilege() { } public void prepareToJoin() { } } gms/src/main/java/it/inaf/ia2/gms/persistence/ClientsDAO.java 0 → 100644 +58 −0 Original line number Diff line number Diff line 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; } } Loading
gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java +27 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import org.springframework.core.Ordered; import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; Loading @@ -32,6 +33,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { super.configure(http); // CORS are necessary only for development (API access from npm server) if (Arrays.asList(env.getActiveProfiles()).contains("dev")) { http.authorizeRequests() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll(); Loading @@ -40,6 +42,31 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { http.csrf().disable(); } /** * The authentication is ignored for these endpoints. The "/ws" endpoints * (web service API for programmatic access) are protected by the custom * WebServiceAuthorizationFilter that checks BasicAuth for GMS clients. */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/ws/**", "/error"); } /** * Checks the BasicAuth for GMS clients. */ @Bean public FilterRegistrationBean webServiceAuthorizationFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebServiceAuthorizationFilter()); bean.addUrlPatterns("/ws/*"); bean.setOrder(Ordered.HIGHEST_PRECEDENCE); return bean; } /** * CORS are necessary only for development (API access from npm server). */ @Bean @Profile("dev") public FilterRegistrationBean corsFilter() { Loading
gms/src/main/java/it/inaf/ia2/gms/authn/WebServiceAuthorizationFilter.java 0 → 100644 +94 −0 Original line number Diff line number Diff line 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.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 WebServiceAuthorizationFilter implements Filter { @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) { ((HttpServletResponse) res).sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage()); return; } } 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); } } }
gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java +13 −1 Original line number Diff line number Diff line package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.authn.SessionData; import it.inaf.ia2.gms.exception.UnauthorizedException; import it.inaf.ia2.gms.model.CreateGroupRequest; import it.inaf.ia2.gms.model.GroupNode; import it.inaf.ia2.gms.model.GroupsModelRequest; import it.inaf.ia2.gms.model.GroupsModelResponse; import it.inaf.ia2.gms.model.PaginatedData; import it.inaf.ia2.gms.model.PaginatedModelRequest; import it.inaf.ia2.gms.model.Permission; import it.inaf.ia2.gms.model.RenameGroupRequest; import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.service.GroupsModelBuilder; import it.inaf.ia2.gms.service.GroupsService; import it.inaf.ia2.gms.service.GroupsTreeBuilder; import it.inaf.ia2.gms.service.PermissionsService; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; Loading @@ -31,6 +34,9 @@ public class GroupsController { @Autowired private SessionData session; @Autowired private PermissionsService permissionsService; @Autowired private GroupsService groupsService; Loading @@ -48,7 +54,13 @@ public class GroupsController { @PostMapping(value = "/group", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity<PaginatedData<GroupNode>> createGroup(@Valid @RequestBody CreateGroupRequest request) { GroupEntity newGroup = groupsService.addGroup(request.getParentGroupId(), request.getNewGroupName(), session.getUserId()); GroupEntity parent = groupsService.getGroupById(request.getParentGroupId()); if (permissionsService.getGroupPermission(parent, session.getUserId()) != Permission.ADMIN) { throw new UnauthorizedException("Missing admin privileges"); } GroupEntity newGroup = groupsService.addGroup(parent, request.getNewGroupName()); PaginatedData<GroupNode> groupsPanel = getGroupsPanel(request.getParentGroupId(), request); Loading
gms/src/main/java/it/inaf/ia2/gms/controller/WebServiceController.java 0 → 100644 +66 −0 Original line number Diff line number Diff line package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.service.GroupsService; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/ws") public class WebServiceController { @Autowired private GroupsService groupsService; /** * Creates a group and its ancestors if they are missing. It doesn't fail if * the last group already exists. */ @PostMapping("/group") public ResponseEntity<GroupEntity> createGroup(@RequestBody List<String> names) { GroupEntity group = groupsService.getRoot(); for (String name : names) { Optional<GroupEntity> optGroup = groupsService.findGroupByParentAndName(group, name); if (optGroup.isPresent()) { group = optGroup.get(); } else { group = groupsService.addGroup(group, name); } } return new ResponseEntity<>(group, HttpStatus.CREATED); } public void deleteGroup() { } public void addMember() { } public void removeMember() { } public void addPrivilege() { } public void deletePrivilege() { } public void prepareToJoin() { } }
gms/src/main/java/it/inaf/ia2/gms/persistence/ClientsDAO.java 0 → 100644 +58 −0 Original line number Diff line number Diff line 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; } }