Skip to content
Snippets Groups Projects
Commit 04115082 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added /ws endpoint and access to programmatic clients in BasicAuth

parent 9f9b7530
No related branches found
No related tags found
No related merge requests found
Showing
with 454 additions and 15 deletions
...@@ -12,6 +12,7 @@ import org.springframework.core.Ordered; ...@@ -12,6 +12,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
...@@ -32,6 +33,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { ...@@ -32,6 +33,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
super.configure(http); super.configure(http);
// CORS are necessary only for development (API access from npm server)
if (Arrays.asList(env.getActiveProfiles()).contains("dev")) { if (Arrays.asList(env.getActiveProfiles()).contains("dev")) {
http.authorizeRequests() http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll(); .antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
...@@ -40,6 +42,31 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { ...@@ -40,6 +42,31 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
http.csrf().disable(); 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 @Bean
@Profile("dev") @Profile("dev")
public FilterRegistrationBean corsFilter() { public FilterRegistrationBean corsFilter() {
......
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);
}
}
}
package it.inaf.ia2.gms.controller; package it.inaf.ia2.gms.controller;
import it.inaf.ia2.gms.authn.SessionData; 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.CreateGroupRequest;
import it.inaf.ia2.gms.model.GroupNode; import it.inaf.ia2.gms.model.GroupNode;
import it.inaf.ia2.gms.model.GroupsModelRequest; import it.inaf.ia2.gms.model.GroupsModelRequest;
import it.inaf.ia2.gms.model.GroupsModelResponse; import it.inaf.ia2.gms.model.GroupsModelResponse;
import it.inaf.ia2.gms.model.PaginatedData; import it.inaf.ia2.gms.model.PaginatedData;
import it.inaf.ia2.gms.model.PaginatedModelRequest; 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.model.RenameGroupRequest;
import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.persistence.model.GroupEntity;
import it.inaf.ia2.gms.service.GroupsModelBuilder; import it.inaf.ia2.gms.service.GroupsModelBuilder;
import it.inaf.ia2.gms.service.GroupsService; import it.inaf.ia2.gms.service.GroupsService;
import it.inaf.ia2.gms.service.GroupsTreeBuilder; import it.inaf.ia2.gms.service.GroupsTreeBuilder;
import it.inaf.ia2.gms.service.PermissionsService;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
...@@ -31,6 +34,9 @@ public class GroupsController { ...@@ -31,6 +34,9 @@ public class GroupsController {
@Autowired @Autowired
private SessionData session; private SessionData session;
@Autowired
private PermissionsService permissionsService;
@Autowired @Autowired
private GroupsService groupsService; private GroupsService groupsService;
...@@ -48,7 +54,13 @@ public class GroupsController { ...@@ -48,7 +54,13 @@ public class GroupsController {
@PostMapping(value = "/group", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @PostMapping(value = "/group", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<PaginatedData<GroupNode>> createGroup(@Valid @RequestBody CreateGroupRequest request) { 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); PaginatedData<GroupNode> groupsPanel = getGroupsPanel(request.getParentGroupId(), request);
......
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() {
}
}
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;
}
}
...@@ -101,6 +101,27 @@ public class GroupsDAO { ...@@ -101,6 +101,27 @@ public class GroupsDAO {
}); });
} }
public Optional<GroupEntity> findGroupByParentAndName(String parentPath, String childName) {
String sql = "SELECT id, path from gms_group WHERE name = ? AND path ~ ?";
return jdbcTemplate.query(conn -> {
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, childName);
ps.setObject(2, getSubGroupsPath(parentPath), Types.OTHER);
return ps;
}, resultSet -> {
if (resultSet.next()) {
GroupEntity group = new GroupEntity();
group.setId(resultSet.getString("id"));
group.setName(childName);
group.setPath(resultSet.getString("path"));
return Optional.of(group);
}
return Optional.empty();
});
}
public List<GroupEntity> listSubGroups(String path) { public List<GroupEntity> listSubGroups(String path) {
String sql = "SELECT id, name, path from gms_group WHERE path ~ ? ORDER BY name"; String sql = "SELECT id, name, path from gms_group WHERE path ~ ? ORDER BY name";
......
package it.inaf.ia2.gms.persistence.model;
import java.util.List;
public class ClientEntity {
private String id;
private String secret;
private List<String> allowedActions;
private String ipFilter;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public List<String> getAllowedActions() {
return allowedActions;
}
public void setAllowedActions(List<String> allowedActions) {
this.allowedActions = allowedActions;
}
public String getIpFilter() {
return ipFilter;
}
public void setIpFilter(String ipFilter) {
this.ipFilter = ipFilter;
}
}
...@@ -9,6 +9,7 @@ import it.inaf.ia2.gms.model.Permission; ...@@ -9,6 +9,7 @@ import it.inaf.ia2.gms.model.Permission;
import it.inaf.ia2.gms.persistence.GroupsDAO; import it.inaf.ia2.gms.persistence.GroupsDAO;
import it.inaf.ia2.gms.persistence.model.GroupEntity; import it.inaf.ia2.gms.persistence.model.GroupEntity;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Service @Service
...@@ -37,13 +38,7 @@ public class GroupsService { ...@@ -37,13 +38,7 @@ public class GroupsService {
} }
} }
public GroupEntity addGroup(String parentId, String groupName, String userId) { public GroupEntity addGroup(GroupEntity parent, String groupName) {
GroupEntity parent = getGroupById(parentId);
if (permissionsService.getGroupPermission(parent, userId) != Permission.ADMIN) {
throw new UnauthorizedException("Missing admin privileges");
}
if (groupsDAO.listSubGroups(parent.getPath()).stream() if (groupsDAO.listSubGroups(parent.getPath()).stream()
.anyMatch(g -> g.getName().equals(groupName))) { .anyMatch(g -> g.getName().equals(groupName))) {
...@@ -107,6 +102,10 @@ public class GroupsService { ...@@ -107,6 +102,10 @@ public class GroupsService {
return parent; return parent;
} }
public GroupEntity getRoot() {
return getGroupById(ROOT);
}
public GroupEntity getGroupById(String groupId) { public GroupEntity getGroupById(String groupId) {
return groupsDAO.findGroupById(groupId) return groupsDAO.findGroupById(groupId)
.orElseThrow(() -> new BadRequestException("Group " + groupId + " not found")); .orElseThrow(() -> new BadRequestException("Group " + groupId + " not found"));
...@@ -120,4 +119,8 @@ public class GroupsService { ...@@ -120,4 +119,8 @@ public class GroupsService {
public List<GroupBreadcrumb> getBreadcrumbs(String path) { public List<GroupBreadcrumb> getBreadcrumbs(String path) {
return groupsDAO.getBreadcrumbs(path); return groupsDAO.getBreadcrumbs(path);
} }
public Optional<GroupEntity> findGroupByParentAndName(GroupEntity parent, String childName) {
return groupsDAO.findGroupByParentAndName(parent.getPath(), childName);
}
} }
...@@ -29,3 +29,11 @@ CREATE TABLE gms_permission ( ...@@ -29,3 +29,11 @@ CREATE TABLE gms_permission (
foreign key (group_id) references gms_group(id), foreign key (group_id) references gms_group(id),
foreign key (group_path) references gms_group(path) 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)
);
\ No newline at end of file
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.ClientsDAO;
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 WebServiceAuthorizationFilterTest {
private WebServiceAuthorizationFilter 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));
filter = spy(new WebServiceAuthorizationFilter());
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/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/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/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());
}
}
...@@ -88,6 +88,15 @@ public class GroupsDAOTest { ...@@ -88,6 +88,15 @@ public class GroupsDAOTest {
assertEquals(1, groups.size()); assertEquals(1, groups.size());
assertEquals("INAF", groups.get(0).getName()); assertEquals("INAF", groups.get(0).getName());
// Group by parent and name
Optional<GroupEntity> optGroup = dao.findGroupByParentAndName(root.getPath(), lbt.getName());
assertTrue(optGroup.isPresent());
assertEquals(lbt.getId(), optGroup.get().getId());
optGroup = dao.findGroupByParentAndName(lbt.getPath(), lbtInaf.getName());
assertTrue(optGroup.isPresent());
assertEquals(lbtInaf.getId(), optGroup.get().getId());
// Children map // Children map
Map<String, Boolean> childrenMap = dao.getHasChildrenMap(Sets.newSet(root.getId())); Map<String, Boolean> childrenMap = dao.getHasChildrenMap(Sets.newSet(root.getId()));
assertEquals(1, childrenMap.size()); assertEquals(1, childrenMap.size());
......
...@@ -49,13 +49,13 @@ public class NestedGroupsIntegrationTest { ...@@ -49,13 +49,13 @@ public class NestedGroupsIntegrationTest {
permissionsDAO.createPermission(superAdminPermission); permissionsDAO.createPermission(superAdminPermission);
// Setup groups // Setup groups
GroupEntity root = groupsService.getGroupById(GroupsService.ROOT); GroupEntity root = groupsService.getRoot();
GroupEntity lbt = groupsService.addGroup(GroupsService.ROOT, "LBT", userId); GroupEntity lbt = groupsService.addGroup(root, "LBT");
GroupEntity tng = groupsService.addGroup(GroupsService.ROOT, "TNG", userId); GroupEntity tng = groupsService.addGroup(root, "TNG");
GroupEntity radio = groupsService.addGroup(GroupsService.ROOT, "Radio", userId); GroupEntity radio = groupsService.addGroup(root, "Radio");
GroupEntity lbtInaf = groupsService.addGroup(lbt.getId(), "INAF", userId); GroupEntity lbtInaf = groupsService.addGroup(lbt, "INAF");
GroupEntity lbtInafProgram1 = groupsService.addGroup(lbtInaf.getId(), "P1", userId); GroupEntity lbtInafProgram1 = groupsService.addGroup(lbtInaf, "P1");
GroupEntity lbtInafProgram2 = groupsService.addGroup(lbtInaf.getId(), "P2", userId); GroupEntity lbtInafProgram2 = groupsService.addGroup(lbtInaf, "P2");
PaginatedModelRequest request = new PaginatedModelRequest(); PaginatedModelRequest request = new PaginatedModelRequest();
request.setPaginatorPage(1); request.setPaginatorPage(1);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment