From cf0fe88a5f1b0c53fb84b016128878db329141fc Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 19 Apr 2020 18:33:37 +0200 Subject: KB49 Add private news feeds. Change-Id: Ib9488351b9734795e02ddaeb26aa81eeb79c0b4d --- .../mulk/mulkcms2/benki/accesscontrol/PageKey.java | 2 +- .../mulkcms2/benki/bookmarks/BookmarkResource.java | 3 +- .../mulkcms2/benki/lazychat/LazychatResource.java | 5 +- .../mulkcms2/benki/posts/AllPostsResource.java | 3 +- .../java/eu/mulk/mulkcms2/benki/posts/Post.java | 17 ++-- .../eu/mulk/mulkcms2/benki/posts/PostResource.java | 112 ++++++++++++++++----- src/main/resources/hibernate.properties | 1 + 7 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 src/main/resources/hibernate.properties (limited to 'src') diff --git a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java index 2553a11..05e4bb5 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java @@ -26,6 +26,6 @@ public class PageKey extends PanacheEntityBase { public BigInteger key; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user", referencedColumnName = "id", nullable = false) + @JoinColumn(name = "\"user\"", referencedColumnName = "id", nullable = false) public User user; } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java index 6dc76b0..495e511 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java @@ -16,6 +16,7 @@ import io.quarkus.security.Authenticated; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; import java.time.OffsetDateTime; import java.util.Objects; import java.util.Set; @@ -42,7 +43,7 @@ public class BookmarkResource extends PostResource { @Inject Template newBookmark; - public BookmarkResource() { + public BookmarkResource() throws NoSuchAlgorithmException { super(PostFilter.BOOKMARKS_ONLY, "Bookmarks"); } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java index 4f9a9fb..cbdbe76 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java @@ -10,6 +10,7 @@ import eu.mulk.mulkcms2.benki.posts.PostResource; import io.quarkus.security.Authenticated; import java.net.URI; import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; import java.time.OffsetDateTime; import java.util.Objects; import javax.transaction.Transactional; @@ -27,7 +28,7 @@ import javax.ws.rs.core.Response; @Path("/lazychat") public class LazychatResource extends PostResource { - public LazychatResource() { + public LazychatResource() throws NoSuchAlgorithmException { super(PostFilter.LAZYCHAT_MESSAGES_ONLY, "Lazy Chat"); } @@ -76,7 +77,7 @@ public class LazychatResource extends PostResource { throw new NotFoundException(); } - if (!Objects.equals(message.owner.id, user.id)) { + if (message.owner == null || !Objects.equals(message.owner.id, user.id)) { throw new ForbiddenException(); } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java index 47c644c..4047b9e 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java @@ -1,11 +1,12 @@ package eu.mulk.mulkcms2.benki.posts; +import java.security.NoSuchAlgorithmException; import javax.ws.rs.Path; @Path("/posts") public class AllPostsResource extends PostResource { - public AllPostsResource() { + public AllPostsResource() throws NoSuchAlgorithmException { super(PostFilter.ALL, "All Posts"); } } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java index fc0f76f..3a02e4e 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java @@ -6,7 +6,6 @@ import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; import eu.mulk.mulkcms2.benki.users.User; import eu.mulk.mulkcms2.benki.users.User_; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; -import io.quarkus.security.identity.SecurityIdentity; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; @@ -60,6 +59,7 @@ public abstract class Post extends PanacheEntityBase { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner", referencedColumnName = "id") + @CheckForNull @JsonbTransient public User owner; @@ -108,7 +108,7 @@ public abstract class Post extends PanacheEntityBase { protected static CriteriaQuery queryViewable( Class entityClass, - SecurityIdentity readerIdentity, + @CheckForNull User reader, @CheckForNull User owner, @CheckForNull Integer cursor, CriteriaBuilder cb, @@ -118,16 +118,13 @@ public abstract class Post extends PanacheEntityBase { var conditions = new ArrayList(); From post; - if (readerIdentity.isAnonymous()) { + if (reader == null) { post = query.from(entityClass); var target = post.join(Post_.targets); conditions.add(cb.equal(target, Role.getWorld())); } else { - var userName = readerIdentity.getPrincipal().getName(); - var user = User.findByNickname(userName); - var root = query.from(User.class); - conditions.add(cb.equal(root, user)); + conditions.add(cb.equal(root, reader)); if (entityClass.isAssignableFrom(Post.class)) { post = (From) root.join(User_.visiblePosts); } else if (entityClass.isAssignableFrom(Bookmark.class)) { @@ -189,14 +186,14 @@ public abstract class Post extends PanacheEntityBase { } public static List findViewable( - PostFilter postFilter, Session session, SecurityIdentity viewer, @CheckForNull User owner) { + PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) { return findViewable(postFilter, session, viewer, owner, null, null).posts; } public static PostPage findViewable( PostFilter postFilter, Session session, - SecurityIdentity viewer, + @CheckForNull User viewer, @CheckForNull User owner, @CheckForNull Integer cursor, @CheckForNull Integer count) { @@ -217,7 +214,7 @@ public abstract class Post extends PanacheEntityBase { protected static PostPage findViewable( Class entityClass, Session session, - SecurityIdentity viewer, + @CheckForNull User viewer, @CheckForNull User owner, @CheckForNull Integer cursor, @CheckForNull Integer count) { diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java index c718bbc..c100e55 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java @@ -11,6 +11,7 @@ import com.rometools.rome.feed.atom.Link; import com.rometools.rome.feed.synd.SyndPersonImpl; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.WireFeedOutput; +import eu.mulk.mulkcms2.benki.accesscontrol.PageKey; import eu.mulk.mulkcms2.benki.accesscontrol.Role; import eu.mulk.mulkcms2.benki.users.User; import io.quarkus.qute.Template; @@ -18,7 +19,10 @@ import io.quarkus.qute.TemplateExtension; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.api.ResourcePath; import io.quarkus.security.identity.SecurityIdentity; +import java.math.BigInteger; import java.net.URI; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -29,6 +33,7 @@ import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.CheckForNull; @@ -37,6 +42,7 @@ import javax.inject.Inject; import javax.json.spi.JsonProvider; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; import javax.ws.rs.BadRequestException; import javax.ws.rs.ForbiddenException; import javax.ws.rs.GET; @@ -59,6 +65,8 @@ public abstract class PostResource { private static final DateTimeFormatter humanDateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); + private static final int pageKeyBytes = 32; + protected static final JsonProvider jsonProvider = JsonProvider.provider(); @ConfigProperty(name = "mulkcms.posts.default-max-results") @@ -78,12 +86,15 @@ public abstract class PostResource { @PersistenceContext protected EntityManager entityManager; + private final SecureRandom secureRandom; + private final PostFilter postFilter; private final String pageTitle; - public PostResource(PostFilter postFilter, String pageTitle) { + public PostResource(PostFilter postFilter, String pageTitle) throws NoSuchAlgorithmException { this.postFilter = postFilter; this.pageTitle = pageTitle; + secureRandom = SecureRandom.getInstanceStrong(); } @GET @@ -94,12 +105,19 @@ public abstract class PostResource { maxResults = maxResults == null ? defaultMaxResults : maxResults; + @CheckForNull var reader = getCurrentUser(); var session = entityManager.unwrap(Session.class); - var q = Post.findViewable(postFilter, session, identity, null, cursor, maxResults); + var q = Post.findViewable(postFilter, session, reader, null, cursor, maxResults); + + var feedUri = "/posts/feed"; + if (reader != null) { + var pageKey = ensurePageKey(reader, feedUri); + feedUri += "?page-key=" + pageKey.key.toString(36); + } return postList .data("posts", q.posts) - .data("feedUri", "/posts/feed") + .data("feedUri", feedUri) .data("pageTitle", pageTitle) .data("showBookmarkForm", showBookmarkForm()) .data("showLazychatForm", showLazychatForm()) @@ -120,13 +138,20 @@ public abstract class PostResource { maxResults = maxResults == null ? defaultMaxResults : maxResults; + @CheckForNull var reader = getCurrentUser(); var owner = User.findByNickname(ownerName); var session = entityManager.unwrap(Session.class); - var q = Post.findViewable(postFilter, session, identity, owner, cursor, maxResults); + var q = Post.findViewable(postFilter, session, reader, owner, cursor, maxResults); + + var feedUri = String.format("/posts/~%s/feed", ownerName); + if (reader != null) { + var pageKey = ensurePageKey(reader, feedUri); + feedUri += "?page-key=" + pageKey.key.toString(36); + } return postList .data("posts", q.posts) - .data("feedUri", String.format("/posts/~%s/feed", ownerName)) + .data("feedUri", feedUri) .data("pageTitle", pageTitle) .data("showBookmarkForm", showBookmarkForm()) .data("showLazychatForm", showLazychatForm()) @@ -137,23 +162,63 @@ public abstract class PostResource { .data("pageSize", maxResults); } + @Transactional + protected final PageKey ensurePageKey(User reader, String pagePath) { + PageKey pageKey = PageKey.find("page = ?1 AND user = ?2", pagePath, reader).firstResult(); + if (pageKey == null) { + pageKey = new PageKey(); + byte[] keyBytes = new byte[pageKeyBytes]; + secureRandom.nextBytes(keyBytes); + pageKey.key = new BigInteger(keyBytes); + pageKey.page = pagePath; + pageKey.user = reader; + pageKey.persist(); + } + return pageKey; + } + @GET @Path("feed") @Produces(APPLICATION_ATOM_XML) - public String getFeed() throws FeedException { - return makeFeed(null, null); + public String getFeed(@QueryParam("page-key") @CheckForNull String pageKeyBase36) + throws FeedException { + @CheckForNull var pageKey = pageKeyBase36 == null ? null : new BigInteger(pageKeyBase36, 36); + return makeFeed(pageKey, null, null); } @GET @Path("~{ownerName}/feed") @Produces(APPLICATION_ATOM_XML) - public String getUserFeed(@PathParam("ownerName") String ownerName) throws FeedException { + public String getUserFeed( + @QueryParam("page-key") @CheckForNull String pageKeyBase36, + @PathParam("ownerName") String ownerName) + throws FeedException { var owner = User.findByNickname(ownerName); - return makeFeed(owner, ownerName); + @CheckForNull var pageKey = pageKeyBase36 == null ? null : new BigInteger(pageKeyBase36, 36); + return makeFeed(pageKey, ownerName, owner); + } + + private String makeFeed( + @CheckForNull BigInteger pageKey, @CheckForNull String ownerName, @CheckForNull User owner) + throws FeedException { + if (pageKey == null) { + return makeFeed(getCurrentUser(), owner, ownerName); + } + + Optional pageKeyInfo = + PageKey.find("page = ?1 AND key = ?2", uri.getPath(), pageKey).singleResultOptional(); + if (pageKeyInfo.isEmpty()) { + throw new ForbiddenException(); + } + + var pageKeyOwner = pageKeyInfo.get().user; + return makeFeed(pageKeyOwner, owner, ownerName); } - private String makeFeed(@Nullable User owner, @Nullable String ownerName) throws FeedException { - var posts = Post.findViewable(postFilter, entityManager.unwrap(Session.class), identity, owner); + private String makeFeed( + @CheckForNull User reader, @Nullable User owner, @Nullable String ownerName) + throws FeedException { + var posts = Post.findViewable(postFilter, entityManager.unwrap(Session.class), reader, owner); var feed = new Feed("atom_1.0"); var feedSubId = owner == null ? "" : String.format("/%d", owner.id); @@ -181,7 +246,7 @@ public abstract class PostResource { feed.setOtherLinks(List.of(selfLink)); var htmlAltLink = new Link(); - var htmlAltPath = owner == null ? "/posts" : String.format("~%s/posts", ownerName); + var htmlAltPath = ownerName == null ? "/posts" : String.format("~%s/posts", ownerName); htmlAltLink.setHref(uri.resolve(URI.create(htmlAltPath)).toString()); htmlAltLink.setRel("alternate"); htmlAltLink.setType("text/html"); @@ -199,9 +264,11 @@ public abstract class PostResource { entry.setUpdated(Date.from(post.date.toInstant())); } - var author = new SyndPersonImpl(); - author.setName(post.owner.getFirstAndLastName()); - entry.setAuthors(List.of(author)); + if (post.owner != null) { + var author = new SyndPersonImpl(); + author.setName(post.owner.getFirstAndLastName()); + entry.setAuthors(List.of(author)); + } if (post.getTitle() != null) { var title = new Content(); @@ -294,16 +361,6 @@ public abstract class PostResource { } } - @CheckForNull - protected final User getCurrentUser() { - if (identity.isAnonymous()) { - return null; - } - - var userName = identity.getPrincipal().getName(); - return User.findByNickname(userName); - } - protected final Post getPostIfVisible(int id) { @CheckForNull var user = getCurrentUser(); var message = getSession().byId(Post.class).load(id); @@ -315,6 +372,11 @@ public abstract class PostResource { return message; } + @CheckForNull + protected final User getCurrentUser() { + return identity.isAnonymous() ? null : User.findByNickname(identity.getPrincipal().getName()); + } + @GET @Produces(APPLICATION_JSON) @Path("{id}") diff --git a/src/main/resources/hibernate.properties b/src/main/resources/hibernate.properties new file mode 100644 index 0000000..52cfc53 --- /dev/null +++ b/src/main/resources/hibernate.properties @@ -0,0 +1 @@ +hibernate.dialect = io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect -- cgit v1.2.3