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

Added /ws endpoint and access to programmatic clients in BasicAuth

parent 9f9b7530
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
@@ -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() {
+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);
        }
    }
}
+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;
@@ -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);

+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() {

    }
}
+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