Commit 04115082 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added /ws endpoint and access to programmatic clients in BasicAuth

parent 9f9b7530
......@@ -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;
......@@ -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();
......@@ -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() {
......
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;
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;
......@@ -31,6 +34,9 @@ public class GroupsController {
@Autowired
private SessionData session;
@Autowired
private PermissionsService permissionsService;
@Autowired
private GroupsService groupsService;
......@@ -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);
......
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 {
});
}
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) {
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;
import it.inaf.ia2.gms.persistence.GroupsDAO;
import it.inaf.ia2.gms.persistence.model.GroupEntity;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
......@@ -37,13 +38,7 @@ public class GroupsService {
}
}
public GroupEntity addGroup(String parentId, String groupName, String userId) {
GroupEntity parent = getGroupById(parentId);
if (permissionsService.getGroupPermission(parent, userId) != Permission.ADMIN) {
throw new UnauthorizedException("Missing admin privileges");
}
public GroupEntity addGroup(GroupEntity parent, String groupName) {
if (groupsDAO.listSubGroups(parent.getPath()).stream()
.anyMatch(g -> g.getName().equals(groupName))) {
......@@ -107,6 +102,10 @@ public class GroupsService {
return parent;
}
public GroupEntity getRoot() {
return getGroupById(ROOT);
}
public GroupEntity getGroupById(String groupId) {
return groupsDAO.findGroupById(groupId)
.orElseThrow(() -> new BadRequestException("Group " + groupId + " not found"));
......@@ -120,4 +119,8 @@ public class GroupsService {
public List<GroupBreadcrumb> getBreadcrumbs(String 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 (
foreign key (group_id) references gms_group(id),
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 {
assertEquals(1, groups.size());
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
Map<String, Boolean> childrenMap = dao.getHasChildrenMap(Sets.newSet(root.getId()));
assertEquals(1, childrenMap.size());
......
......@@ -49,13 +49,13 @@ public class NestedGroupsIntegrationTest {
permissionsDAO.createPermission(superAdminPermission);
// Setup groups
GroupEntity root = groupsService.getGroupById(GroupsService.ROOT);
GroupEntity lbt = groupsService.addGroup(GroupsService.ROOT, "LBT", userId);
GroupEntity tng = groupsService.addGroup(GroupsService.ROOT, "TNG", userId);
GroupEntity radio = groupsService.addGroup(GroupsService.ROOT, "Radio", userId);
GroupEntity lbtInaf = groupsService.addGroup(lbt.getId(), "INAF", userId);
GroupEntity lbtInafProgram1 = groupsService.addGroup(lbtInaf.getId(), "P1", userId);
GroupEntity lbtInafProgram2 = groupsService.addGroup(lbtInaf.getId(), "P2", userId);
GroupEntity root = groupsService.getRoot();
GroupEntity lbt = groupsService.addGroup(root, "LBT");
GroupEntity tng = groupsService.addGroup(root, "TNG");