Compare commits
18 Commits
24fd96c8b2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 66fa985a33 | |||
| df0db226a9 | |||
| 57418213f5 | |||
| 9fd843da78 | |||
| 5e74228959 | |||
| 370912a091 | |||
| 5a74e1cb5b | |||
| c293f20559 | |||
| 8943641baf | |||
| 2ca997fca9 | |||
| aa669a98a9 | |||
| 5032ce5d63 | |||
| 07b90d1256 | |||
| 22bde7ef79 | |||
| cafde860a7 | |||
| ded34c5f43 | |||
| 018285c109 | |||
| 350e1a8a43 |
@@ -7,7 +7,8 @@ FROM mvn as builder
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ADD . .
|
ADD . .
|
||||||
RUN mvn package
|
ARG KC_HOSTNAME
|
||||||
|
RUN mvn package -DKC_HOSTNAME=${KC_HOSTNAME}
|
||||||
|
|
||||||
|
|
||||||
FROM eclipse-temurin:21-jdk-alpine
|
FROM eclipse-temurin:21-jdk-alpine
|
||||||
|
|||||||
19
pom.xml
19
pom.xml
@@ -37,7 +37,24 @@
|
|||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>42.7.3</version>
|
<version>42.7.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
public class JodelPost {
|
public class JodelPost {
|
||||||
// id of the post
|
// id of the post
|
||||||
public Long id;
|
public Long id;
|
||||||
|
// Parent id
|
||||||
|
public Optional<Long> parent = Optional.empty();
|
||||||
// id of the author in db
|
// id of the author in db
|
||||||
private final Long authorID;
|
private final Long authorID;
|
||||||
// anonymized authorID
|
// anonymized authorID
|
||||||
@@ -25,31 +27,24 @@ public class JodelPost {
|
|||||||
public Timestamp date;
|
public Timestamp date;
|
||||||
// location if the post
|
// location if the post
|
||||||
public Location location;
|
public Location location;
|
||||||
// list of all comments for the post
|
|
||||||
public Vector<JodelPost> comments = new Vector<>();
|
|
||||||
// the own reaction (null = none, true = positive, false = negative)
|
// the own reaction (null = none, true = positive, false = negative)
|
||||||
public Optional<Boolean> reaction;
|
public Optional<Boolean> reaction;
|
||||||
// all other reactions
|
// all other reactions
|
||||||
public Reactions reactions;
|
public Reactions reactions;
|
||||||
// Parent id
|
// list of all comments for the post
|
||||||
public Optional<Long> parent = Optional.empty();
|
public Vector<JodelPost> comments = new Vector<>();
|
||||||
|
|
||||||
// anonymize function to recursively anonymize the posts
|
// anonymize function to recursively anonymize the posts
|
||||||
public void anonymize(Optional<Vector<Long>> idCache) {
|
public void anonymize(Vector<Long> idCache) {
|
||||||
// check if this is the first post in this process
|
|
||||||
if (idCache.isEmpty()) {
|
|
||||||
// create a new Vector as cache
|
|
||||||
idCache = Optional.of(new Vector<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the anonymized id as index in cached authorIDs
|
// get the anonymized id as index in cached authorIDs
|
||||||
int i = idCache.get().indexOf(this.authorID);
|
int i = idCache.indexOf(this.authorID);
|
||||||
// if the index is -1 the authorID has not been cached jet
|
// if the index is -1 the authorID has not been cached jet
|
||||||
if (i == -1) {
|
if (i == -1) {
|
||||||
// set the current anonymousID as length of the cache (== next index)
|
// set the current anonymousID as length of the cache (== next index)
|
||||||
this.anonymousID = (long) idCache.get().size();
|
this.anonymousID = (long) idCache.size();
|
||||||
// push the current authorID to to cache
|
// push the current authorID to to cache
|
||||||
idCache.get().add(this.authorID);
|
idCache.add(this.authorID);
|
||||||
}
|
}
|
||||||
// the authorID has been anonymized once before, so we can get it from cache
|
// the authorID has been anonymized once before, so we can get it from cache
|
||||||
else this.anonymousID = (long) i;
|
else this.anonymousID = (long) i;
|
||||||
@@ -88,6 +83,7 @@ public class JodelPost {
|
|||||||
this.content = rs.getString("content");
|
this.content = rs.getString("content");
|
||||||
this.date = rs.getTimestamp("postdate");
|
this.date = rs.getTimestamp("postdate");
|
||||||
this.location = new Location(rs.getFloat("longitude"), rs.getFloat("latitude"));
|
this.location = new Location(rs.getFloat("longitude"), rs.getFloat("latitude"));
|
||||||
|
this.reaction = rs.getString("reaction") == null ? Optional.empty() : Optional.of(rs.getBoolean("reaction"));
|
||||||
this.reactions = new Reactions(rs.getLong("positive"), rs.getLong("negative"));
|
this.reactions = new Reactions(rs.getLong("positive"), rs.getLong("negative"));
|
||||||
this.parent = Optional.of(rs.getLong("parent"));
|
this.parent = Optional.of(rs.getLong("parent"));
|
||||||
}
|
}
|
||||||
@@ -102,10 +98,8 @@ public class JodelPost {
|
|||||||
@JsonProperty("content") String content,
|
@JsonProperty("content") String content,
|
||||||
@JsonProperty("date") Timestamp date,
|
@JsonProperty("date") Timestamp date,
|
||||||
@JsonProperty("location") Location location,
|
@JsonProperty("location") Location location,
|
||||||
@JsonProperty("parent") Optional<Long> parent,
|
@JsonProperty("parent") Optional<Long> parent) {
|
||||||
// TODO: getter from Keycloak
|
this.authorID = User.getID(); // TODO: getter from Keycloak
|
||||||
@JsonProperty("authorID") long author) {
|
|
||||||
this.authorID = author; // TODO: getter from Keycloak
|
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
public class Location {
|
public class Location {
|
||||||
public float longitude;
|
|
||||||
public float latitude;
|
public float latitude;
|
||||||
|
public float longitude;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public Location(@JsonProperty("longitude") float longitude, @JsonProperty("latitude") float latitude ) {
|
public Location(@JsonProperty("longitude") float longitude, @JsonProperty("latitude") float latitude ) {
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ import java.util.Vector;
|
|||||||
@CrossOrigin(origins = "*", allowedHeaders = "*", maxAge = 3600)
|
@CrossOrigin(origins = "*", allowedHeaders = "*", maxAge = 3600)
|
||||||
public class Routes {
|
public class Routes {
|
||||||
|
|
||||||
@GetMapping("/posts/{longitude}/{latitude}")
|
@GetMapping("/posts/{latitude}/{longitude}")
|
||||||
public Vector<JodelPost> getPostsByLocation(@PathVariable("longitude") float longitude, @PathVariable("latitude") float latitude) {
|
public Vector<JodelPost> getPostsByLocation(@PathVariable("longitude") float longitude, @PathVariable("latitude") float latitude) {
|
||||||
// list of all posts (not comments) in range
|
// list of all posts (not comments) in range
|
||||||
Vector<JodelPost> posts = new Vector<>();
|
Vector<JodelPost> posts = new Vector<>();
|
||||||
|
|
||||||
|
// get UserID
|
||||||
|
Long userID = User.getID();
|
||||||
|
|
||||||
// DB connection and statement
|
// DB connection and statement
|
||||||
Connection c;
|
Connection c;
|
||||||
PreparedStatement stmt;
|
PreparedStatement stmt;
|
||||||
@@ -44,8 +47,8 @@ public class Routes {
|
|||||||
posts.title,
|
posts.title,
|
||||||
posts.content,
|
posts.content,
|
||||||
posts.postdate,
|
posts.postdate,
|
||||||
posts.postlocation[0] AS longitude,
|
posts.postlocation[0] AS latitude,
|
||||||
posts.postlocation[1] AS latitude,
|
posts.postlocation[1] AS longitude,
|
||||||
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = TRUE) AS positive,
|
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = TRUE) AS positive,
|
||||||
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = FALSE) AS negative
|
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = FALSE) AS negative
|
||||||
FROM
|
FROM
|
||||||
@@ -73,10 +76,11 @@ public class Routes {
|
|||||||
(SELECT * FROM comments inner join posts ON comments.child = posts.id) com
|
(SELECT * FROM comments inner join posts ON comments.child = posts.id) com
|
||||||
inner join targets ON targets.id = com.parent
|
inner join targets ON targets.id = com.parent
|
||||||
)
|
)
|
||||||
SELECT * FROM targets;""");
|
SELECT * FROM targets LEFT JOIN (SELECT post, positive as reaction FROM reactions WHERE userid = (?)) ON post = id;""");
|
||||||
|
|
||||||
stmt.setObject(1, longitude);
|
stmt.setObject(1, latitude);
|
||||||
stmt.setObject(2, latitude);
|
stmt.setObject(2, longitude);
|
||||||
|
stmt.setObject(3, userID);
|
||||||
|
|
||||||
// query recursively for posts inside a 10km radius
|
// query recursively for posts inside a 10km radius
|
||||||
ResultSet rs = stmt.executeQuery();
|
ResultSet rs = stmt.executeQuery();
|
||||||
@@ -115,7 +119,12 @@ public class Routes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculate anonymous IDs for the posts
|
// calculate anonymous IDs for the posts
|
||||||
posts.forEach(post -> post.anonymize(Optional.empty()));
|
Vector<Long> anonymousIDs = new Vector<Long>();
|
||||||
|
anonymousIDs.add(userID);
|
||||||
|
posts.forEach(post -> {
|
||||||
|
post.anonymize(anonymousIDs);
|
||||||
|
});
|
||||||
|
|
||||||
// return the posts
|
// return the posts
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
@@ -125,6 +134,9 @@ public class Routes {
|
|||||||
// list of all posts (not comments) in range
|
// list of all posts (not comments) in range
|
||||||
Optional<JodelPost> root_post = Optional.empty();
|
Optional<JodelPost> root_post = Optional.empty();
|
||||||
|
|
||||||
|
// get UserID
|
||||||
|
Long userID = User.getID();
|
||||||
|
|
||||||
// DB connection and statement
|
// DB connection and statement
|
||||||
Connection c;
|
Connection c;
|
||||||
PreparedStatement stmt;
|
PreparedStatement stmt;
|
||||||
@@ -151,8 +163,8 @@ public class Routes {
|
|||||||
posts.title,
|
posts.title,
|
||||||
posts.content,
|
posts.content,
|
||||||
posts.postdate,
|
posts.postdate,
|
||||||
posts.postlocation[0] AS longitude,
|
posts.postlocation[0] AS latitude,
|
||||||
posts.postlocation[1] AS latitude,
|
posts.postlocation[1] AS longitude,
|
||||||
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = TRUE) AS positive,
|
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = TRUE) AS positive,
|
||||||
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = FALSE) AS negative
|
(SELECT count(*) FROM reactions WHERE reactions.post = posts.id AND reactions.positive = FALSE) AS negative
|
||||||
FROM
|
FROM
|
||||||
@@ -177,9 +189,10 @@ public class Routes {
|
|||||||
(SELECT * FROM comments inner join posts ON comments.child = posts.id) com
|
(SELECT * FROM comments inner join posts ON comments.child = posts.id) com
|
||||||
inner join targets ON targets.id = com.parent
|
inner join targets ON targets.id = com.parent
|
||||||
)
|
)
|
||||||
SELECT * FROM targets;""");
|
SELECT * FROM targets LEFT JOIN (SELECT post, positive as reaction FROM reactions WHERE userid = (?)) ON post = id;""");
|
||||||
|
|
||||||
stmt.setObject(1, id);
|
stmt.setObject(1, id);
|
||||||
|
stmt.setObject(2, userID);
|
||||||
|
|
||||||
// query recursively for posts inside a 10km radius
|
// query recursively for posts inside a 10km radius
|
||||||
ResultSet rs = stmt.executeQuery();
|
ResultSet rs = stmt.executeQuery();
|
||||||
@@ -218,7 +231,9 @@ public class Routes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculate anonymous IDs for the post
|
// calculate anonymous IDs for the post
|
||||||
root_post.ifPresent(root -> root.anonymize(Optional.empty()));
|
Vector<Long> anonymousIDs = new Vector<Long>();
|
||||||
|
anonymousIDs.add(userID);
|
||||||
|
root_post.ifPresent(root -> root.anonymize(anonymousIDs));
|
||||||
|
|
||||||
// return the posts
|
// return the posts
|
||||||
if (root_post.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No post found");
|
if (root_post.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No post found");
|
||||||
@@ -251,8 +266,8 @@ public class Routes {
|
|||||||
stmt.setObject(2, post.title);
|
stmt.setObject(2, post.title);
|
||||||
stmt.setObject(3, post.content);
|
stmt.setObject(3, post.content);
|
||||||
stmt.setObject(4, post.date);
|
stmt.setObject(4, post.date);
|
||||||
stmt.setObject(5, post.location.longitude);
|
stmt.setObject(5, post.location.latitude);
|
||||||
stmt.setObject(6, post.location.latitude);
|
stmt.setObject(6, post.location.longitude);
|
||||||
|
|
||||||
// insert post and get its id
|
// insert post and get its id
|
||||||
ResultSet rs = stmt.executeQuery();
|
ResultSet rs = stmt.executeQuery();
|
||||||
@@ -294,11 +309,73 @@ public class Routes {
|
|||||||
post.reaction = Optional.empty();
|
post.reaction = Optional.empty();
|
||||||
|
|
||||||
// anonymize code
|
// anonymize code
|
||||||
post.anonymize(Optional.empty());
|
Vector<Long> anonymousIDs = new Vector<Long>();
|
||||||
|
anonymousIDs.add(0L);
|
||||||
|
post.anonymize(anonymousIDs);
|
||||||
|
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/post/{id}")
|
||||||
|
public void reactToPost(@PathVariable long id, @RequestBody JodelPost post) {
|
||||||
|
System.out.println(post.reaction);
|
||||||
|
long userID = User.getID();
|
||||||
|
|
||||||
|
// DB connection and statement
|
||||||
|
Connection c;
|
||||||
|
PreparedStatement stmt;
|
||||||
|
|
||||||
|
// try to get data from db
|
||||||
|
try {
|
||||||
|
// check for the driver
|
||||||
|
Class.forName("org.postgresql.Driver");
|
||||||
|
// get the connection with credentials from env variables
|
||||||
|
c = DriverManager
|
||||||
|
.getConnection("jdbc:postgresql://"+
|
||||||
|
System.getenv("POSTGRES_IP")+"/"+System.getenv("POSTGRES_DB"),
|
||||||
|
System.getenv("POSTGRES_USER"), System.getenv("POSTGRES_PASSWORD"));
|
||||||
|
// disable auto commits
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
|
||||||
|
// create a new statement
|
||||||
|
stmt = c.prepareStatement("UPDATE Reactions SET positive = (?) WHERE post = (?) AND userid = (?) RETURNING id, positive");
|
||||||
|
|
||||||
|
stmt.setObject(1, post.reaction.orElse(null));
|
||||||
|
stmt.setObject(2, id);
|
||||||
|
stmt.setObject(3, userID);
|
||||||
|
|
||||||
|
// insert post and get its id
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
|
||||||
|
// check if there is a parent
|
||||||
|
if (!rs.next()) {
|
||||||
|
// create a new statement
|
||||||
|
stmt = c.prepareStatement("INSERT INTO Reactions(userid, post, positive) VALUES ((?), (?), (?))");
|
||||||
|
|
||||||
|
// fill statement
|
||||||
|
stmt.setObject(1, userID);
|
||||||
|
stmt.setObject(2, id);
|
||||||
|
stmt.setObject(3, post.reaction.orElse(null));
|
||||||
|
|
||||||
|
// execute statement
|
||||||
|
stmt.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// commit the changes
|
||||||
|
c.commit();
|
||||||
|
|
||||||
|
// close all connections to db
|
||||||
|
stmt.close();
|
||||||
|
c.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// else log the error
|
||||||
|
catch ( Exception e ) {
|
||||||
|
System.err.println( e.getClass().getName()+": "+ e.getMessage() );
|
||||||
|
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Database is offline");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/post/{id}")
|
@DeleteMapping("/post/{id}")
|
||||||
public void deletePost(@PathVariable long id) {
|
public void deletePost(@PathVariable long id) {
|
||||||
// DB connection and statement
|
// DB connection and statement
|
||||||
@@ -318,9 +395,10 @@ public class Routes {
|
|||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
|
|
||||||
// create a new statement
|
// create a new statement
|
||||||
stmt = c.prepareStatement("UPDATE Posts SET deleted = now() WHERE id = ? AND deleted IS NULL");
|
stmt = c.prepareStatement("UPDATE Posts SET deleted = now() WHERE id = ? AND author = (?) AND deleted IS NULL");
|
||||||
|
|
||||||
stmt.setObject(1, id);
|
stmt.setObject(1, id);
|
||||||
|
stmt.setObject(2, User.getID());
|
||||||
|
|
||||||
// insert delete time
|
// insert delete time
|
||||||
stmt.execute();
|
stmt.execute();
|
||||||
|
|||||||
77
src/main/java/de/anxietyprime/swajodel/User.java
Normal file
77
src/main/java/de/anxietyprime/swajodel/User.java
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package de.anxietyprime.swajodel;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
|
||||||
|
public class User {
|
||||||
|
static public String getUUID () {
|
||||||
|
return SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static public Long getID() {
|
||||||
|
String uuid = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
long id;
|
||||||
|
|
||||||
|
// DB connection and statement
|
||||||
|
Connection c;
|
||||||
|
PreparedStatement stmt;
|
||||||
|
|
||||||
|
// try to get data from db
|
||||||
|
try {
|
||||||
|
// check for the driver
|
||||||
|
Class.forName("org.postgresql.Driver");
|
||||||
|
// get the connection with credentials from env variables
|
||||||
|
c = DriverManager
|
||||||
|
.getConnection("jdbc:postgresql://"+
|
||||||
|
System.getenv("POSTGRES_IP")+"/"+System.getenv("POSTGRES_DB"),
|
||||||
|
System.getenv("POSTGRES_USER"), System.getenv("POSTGRES_PASSWORD"));
|
||||||
|
// disable auto commits
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
|
||||||
|
// create a new statement
|
||||||
|
stmt = c.prepareStatement("SELECT id FROM users WHERE username = (?)");
|
||||||
|
|
||||||
|
stmt.setObject(1, uuid);
|
||||||
|
|
||||||
|
// insert post and get its id
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
|
||||||
|
// check if there is a parent
|
||||||
|
if (!rs.next()) {
|
||||||
|
// create a new statement
|
||||||
|
stmt = c.prepareStatement("INSERT INTO users(username, password) VALUES (?, 'unused') RETURNING id");
|
||||||
|
|
||||||
|
// fill statement
|
||||||
|
stmt.setObject(1, uuid);
|
||||||
|
|
||||||
|
// execute statement
|
||||||
|
rs = stmt.executeQuery();
|
||||||
|
|
||||||
|
// commit the changes
|
||||||
|
c.commit();
|
||||||
|
|
||||||
|
rs.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
id = rs.getLong("id");
|
||||||
|
|
||||||
|
// close all connections to db
|
||||||
|
stmt.close();
|
||||||
|
c.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// else log the error
|
||||||
|
catch ( Exception e ) {
|
||||||
|
System.err.println( e.getClass().getName()+": "+ e.getMessage() );
|
||||||
|
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Database is offline");
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package de.anxietyprime.swajodel.security;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||||
|
|
||||||
|
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||||
|
|
||||||
|
private final JwtConverterProperties properties;
|
||||||
|
|
||||||
|
public JwtConverter(JwtConverterProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractAuthenticationToken convert(Jwt jwt) {
|
||||||
|
Collection<GrantedAuthority> authorities = Stream.concat(
|
||||||
|
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
|
||||||
|
extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
|
||||||
|
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPrincipalClaimName(Jwt jwt) {
|
||||||
|
String claimName = JwtClaimNames.SUB;
|
||||||
|
if (properties.getPrincipalAttribute() != null) {
|
||||||
|
claimName = properties.getPrincipalAttribute();
|
||||||
|
}
|
||||||
|
return jwt.getClaim(claimName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
|
||||||
|
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
|
||||||
|
Map<String, Object> resource;
|
||||||
|
Collection<String> resourceRoles;
|
||||||
|
|
||||||
|
if (resourceAccess == null
|
||||||
|
|| (resource = (Map<String, Object>) resourceAccess.get(properties.getResourceId())) == null
|
||||||
|
|| (resourceRoles = (Collection<String>) resource.get("roles")) == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
return resourceRoles.stream()
|
||||||
|
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.anxietyprime.swajodel.security;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Validated
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "jwt.auth.converter")
|
||||||
|
|
||||||
|
|
||||||
|
public class JwtConverterProperties {
|
||||||
|
|
||||||
|
private String resourceId;
|
||||||
|
private String principalAttribute;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.anxietyprime.swajodel.security;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
public static final String ADMIN = "admin";
|
||||||
|
public static final String USER = "user";
|
||||||
|
private final JwtConverter jwtConverter;
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.authorizeHttpRequests((authz) ->
|
||||||
|
authz.requestMatchers(HttpMethod.GET, "/messages/").permitAll()
|
||||||
|
.anyRequest().authenticated());
|
||||||
|
|
||||||
|
http.sessionManagement(sess -> sess.sessionCreationPolicy(
|
||||||
|
SessionCreationPolicy.STATELESS));
|
||||||
|
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter)));
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1 +1,5 @@
|
|||||||
spring.application.name=SWA-Jodel
|
spring.application.name=SWA-Jodel
|
||||||
|
|
||||||
|
# Security Configuration
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://${KC_HOSTNAME}/realms/Jodel
|
||||||
|
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
|
||||||
|
|||||||
@@ -3,44 +3,9 @@ package de.anxietyprime.swajodel;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Vector;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class SwaJodelApplicationTests {
|
class SwaJodelApplicationTests {
|
||||||
|
|
||||||
@Test
|
|
||||||
void anonymousTest() {
|
|
||||||
JodelPost post = new JodelPost(1000);
|
|
||||||
post.comments.add(new JodelPost(1001));
|
|
||||||
post.comments.add(new JodelPost(1002));
|
|
||||||
post.comments.get(0).comments.add(new JodelPost(1000));
|
|
||||||
post.comments.get(0).comments.add(new JodelPost(1001));
|
|
||||||
post.comments.get(0).comments.add(new JodelPost(1000));
|
|
||||||
post.comments.add(new JodelPost(1003));
|
|
||||||
post.comments.get(2).comments.add(new JodelPost(1001));
|
|
||||||
post.comments.get(2).comments.add(new JodelPost(1002));
|
|
||||||
post.comments.get(2).comments.add(new JodelPost(1003));
|
|
||||||
post.comments.get(2).comments.add(new JodelPost(1000));
|
|
||||||
post.comments.add(new JodelPost(1000));
|
|
||||||
post.comments.add(new JodelPost(1001));
|
|
||||||
post.comments.add(new JodelPost(1001));
|
|
||||||
|
|
||||||
post.anonymize(Optional.empty());
|
|
||||||
|
|
||||||
assert (post.anonymousID == 0);
|
|
||||||
assert (post.comments.get(0).anonymousID == 1);
|
|
||||||
assert (post.comments.get(1).anonymousID == 2);
|
|
||||||
assert (post.comments.get(2).anonymousID == 3);
|
|
||||||
assert (post.comments.get(3).anonymousID == 0);
|
|
||||||
assert (post.comments.get(4).anonymousID == 1);
|
|
||||||
assert (post.comments.get(5).anonymousID == 1);
|
|
||||||
assert (post.comments.get(0).comments.get(0).anonymousID == 0);
|
|
||||||
assert (post.comments.get(0).comments.get(1).anonymousID == 1);
|
|
||||||
assert (post.comments.get(0).comments.get(2).anonymousID == 0);
|
|
||||||
assert (post.comments.get(2).comments.get(0).anonymousID == 1);
|
|
||||||
assert (post.comments.get(2).comments.get(1).anonymousID == 2);
|
|
||||||
assert (post.comments.get(2).comments.get(2).anonymousID == 3);
|
|
||||||
assert (post.comments.get(2).comments.get(3).anonymousID == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user