Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authorization checks #52

Merged
merged 2 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import nl.b3p.planmonitorwonen.api.model.Detailplanning;
import nl.b3p.planmonitorwonen.api.model.Plancategorie;
Expand Down Expand Up @@ -71,10 +72,20 @@ private String sqlQuestionMarks(int count) {
return String.join(", ", qs);
}

public Set<Planregistratie> getPlanregistraties() {
public Set<Planregistratie> getPlanregistratiesForProvincie() {
return jdbcClient.sql("select * from planregistratie").query(planregistratieRowMapper).set();
}

public Set<Planregistratie> getPlanregistratiesForGemeentes(Collection<String> gemeentes) {
return jdbcClient
.sql(
"select * from planregistratie where gemeente in (%s)"
.formatted(sqlQuestionMarks(gemeentes.size())))
.params(Arrays.asList(gemeentes.toArray()))
.query(planregistratieRowMapper)
.set();
}

@Transactional
public void deletePlanregistratie(String id) {
jdbcClient.sql("delete from planregistratie where id = ?").param(1, id, Types.OTHER).update();
Expand Down Expand Up @@ -185,13 +196,16 @@ insert into detailplanning(id, plancategorie_id, creator, created_at, editor, ed
}
}

public boolean planregistratieExists(String id) {
return !this.jdbcClient
.sql("select 1 from planregistratie where id = ?")
.param(1, id, Types.OTHER)
.query()
.singleColumn()
.isEmpty();
public String getPlanregistratieGemeente(String id) {
return (String)
this.jdbcClient
.sql("select gemeente from planregistratie where id = ?")
.param(1, id, Types.OTHER)
.query()
.singleColumn()
.stream()
.findFirst()
.orElse(null);
}

public Set<Plancategorie> getPlancategorieen(String planregistratieId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public PopulateTestData(PlanmonitorWonenDatabaseService pmwDb, JdbcClient jdbcCl

@PostConstruct
public void init() throws ParseException {
if (!pmwDb.getPlanregistraties().isEmpty()) {
if (!pmwDb.getPlanregistratiesForProvincie().isEmpty()) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@

package nl.b3p.planmonitorwonen.api.controller;

import static nl.b3p.planmonitorwonen.api.model.auth.PlanmonitorAuthentication.getFromSecurityContext;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.OK;

import java.lang.invoke.MethodHandles;
import java.util.Map;
import java.util.Set;
import nl.b3p.planmonitorwonen.api.PlanmonitorWonenDatabaseService;
import nl.b3p.planmonitorwonen.api.model.Planregistratie;
import nl.b3p.planmonitorwonen.api.model.PlanregistratieComplete;
import nl.b3p.planmonitorwonen.api.model.auth.PlanmonitorAuthentication;
import org.locationtech.jts.io.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -29,6 +35,9 @@
@RestController
@Profile("!test")
public class PlanregistratieController {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private final PlanmonitorWonenDatabaseService pmwDb;

public PlanregistratieController(
Expand All @@ -38,14 +47,33 @@ public PlanregistratieController(

@GetMapping(path = "${planmonitor-wonen-api.base-path}/planregistraties")
public Set<Planregistratie> planregistraties() {
return pmwDb.getPlanregistraties();
PlanmonitorAuthentication auth = getFromSecurityContext();
if (auth.isProvincie()) {
return pmwDb.getPlanregistratiesForProvincie();
} else {
return pmwDb.getPlanregistratiesForGemeentes(auth.getGemeentes());
}
}

@GetMapping(path = "${planmonitor-wonen-api.base-path}/planregistratie/{id}/details")
public Map<String, Object> details(@PathVariable("id") String id) {
if (!pmwDb.planregistratieExists(id)) {
PlanmonitorAuthentication auth = getFromSecurityContext();

String gemeente = pmwDb.getPlanregistratieGemeente(id);
if (gemeente == null) {
throw new ResponseStatusException(NOT_FOUND);
}

if (!auth.isProvincie() && !auth.getGemeentes().contains(gemeente)) {
logger.warn(
"Gemeente user \"{}\" with authorization for gemeentes {} tried to access plan id {} of gemeente {}",
auth.getTmApiAuthentication().getName(),
auth.getGemeentes(),
id,
gemeente);
throw new ResponseStatusException(NOT_FOUND);
}

return Map.of(
"plancategorieen",
pmwDb.getPlancategorieen(id),
Expand All @@ -57,9 +85,38 @@ public Map<String, Object> details(@PathVariable("id") String id) {
public ResponseEntity<?> put(
@PathVariable("id") String id, @RequestBody PlanregistratieComplete planregistratieComplete)
throws ParseException {

if (id == null || !id.equals(planregistratieComplete.planregistratie().getId())) {
throw new ResponseStatusException(BAD_REQUEST);
}

PlanmonitorAuthentication auth = getFromSecurityContext();

if (!auth.isProvincie()
&& !auth.getGemeentes().contains(planregistratieComplete.planregistratie().getGemeente())) {
logger.warn(
"Gemeente user \"{}\" with authorization for gemeentes {} tried to save plan id {}, name \"{}\" with gemeente value {}",
auth.getTmApiAuthentication().getName(),
auth.getGemeentes(),
id,
planregistratieComplete.planregistratie().getPlanNaam(),
planregistratieComplete.planregistratie().getGemeente());
throw new ResponseStatusException(FORBIDDEN);
}

String gemeente = pmwDb.getPlanregistratieGemeente(id);

if (gemeente != null && !auth.isProvincie() && !auth.getGemeentes().contains(gemeente)) {
logger.warn(
"Gemeente user \"{}\" with authorization for gemeentes {} tried to update plan id {}, name \"{}\" of gemeente {}",
auth.getTmApiAuthentication().getName(),
auth.getGemeentes(),
id,
planregistratieComplete.planregistratie().getPlanNaam(),
gemeente);
throw new ResponseStatusException(FORBIDDEN);
}

pmwDb.insertPlanregistratie(
planregistratieComplete.planregistratie(),
planregistratieComplete.plancategorieen(),
Expand All @@ -70,12 +127,28 @@ public ResponseEntity<?> put(
@DeleteMapping(path = "${planmonitor-wonen-api.base-path}/planregistratie/{id}")
public ResponseEntity<?> delete(@PathVariable("id") String id) {
if (id == null) {
return ResponseEntity.status(BAD_REQUEST).build();
} else if (!pmwDb.planregistratieExists(id)) {
return ResponseEntity.status(NOT_FOUND).build();
} else {
pmwDb.deletePlanregistratie(id);
return ResponseEntity.status(OK).build();
throw new ResponseStatusException(BAD_REQUEST);
}

PlanmonitorAuthentication auth = getFromSecurityContext();

String gemeente = pmwDb.getPlanregistratieGemeente(id);

if (gemeente == null) {
throw new ResponseStatusException(NOT_FOUND);
}

if (!auth.isProvincie() && !auth.getGemeentes().contains(gemeente)) {
logger.warn(
"Gemeente user \"{}\" with authorization for gemeentes {} tried to delete plan id {} of gemeente {}",
auth.getTmApiAuthentication().getName(),
auth.getGemeentes(),
id,
gemeente);
throw new ResponseStatusException(FORBIDDEN);
}

pmwDb.deletePlanregistratie(id);
return ResponseEntity.status(OK).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2024 Provincie Zeeland
*
* SPDX-License-Identifier: MIT
*/

package nl.b3p.planmonitorwonen.api.model.auth;

import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import nl.b3p.planmonitorwonen.api.security.TMAPIAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.server.ResponseStatusException;

public class PlanmonitorAuthentication {
private TMAPIAuthenticationToken tmApiAuthentication;
private boolean isProvincie;
private Set<String> gemeentes = new HashSet<>();

public TMAPIAuthenticationToken getTmApiAuthentication() {
return tmApiAuthentication;
}

public boolean isProvincie() {
return isProvincie;
}

public Set<String> getGemeentes() {
return gemeentes;
}

public static PlanmonitorAuthentication getFromSecurityContext() throws ResponseStatusException {
final TMAPIAuthenticationToken authentication =
(TMAPIAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new ResponseStatusException(UNAUTHORIZED);
}

Map<String, Set<String>> groupProperties = new HashMap<>();
authentication
.getAuthResponse()
.get("groupProperties")
.elements()
.forEachRemaining(
n -> {
String key = n.get("key").textValue();
String value = n.get("value").textValue();
if (!groupProperties.containsKey(key)) {
groupProperties.put(key, new HashSet<>());
}
groupProperties.get(key).add(value);
});

PlanmonitorAuthentication result = new PlanmonitorAuthentication();
result.tmApiAuthentication = authentication;
result.isProvincie =
"provincie".equals(groupProperties.get("typeGebruiker").stream().findFirst().orElse(null));
result.gemeentes = groupProperties.get("gemeente");

if (!result.isProvincie && result.gemeentes.isEmpty()) {
throw new ResponseStatusException(FORBIDDEN);
}

return result;
}
}
4 changes: 2 additions & 2 deletions src/main/resources/db/migration/V1__schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ create table planregistratie
edited_at timestamp with time zone,
plan_naam varchar not null unique,
provincie varchar,
gemeente varchar references gemeente (naam),
gemeente varchar not null references gemeente (naam),
regio varchar,
plaatsnaam varchar,
vertrouwelijkheid pmw_vertrouwelijkheid,
Expand All @@ -106,7 +106,7 @@ create table planregistratie
knelpunten_meerkeuze pmw_knelpunten_meerkeuze,
beoogd_woonmilieu_abf13 pmw_woonmilieu_abf13,
aantal_studentenwoningen integer,
sleutelproject boolean not null
sleutelproject boolean not null
);

create type pmw_nieuwbouw as enum (
Expand Down
Loading