Skip to content

Commit

Permalink
Merge pull request #48 from UW-Madison-DoIT/service-one-message
Browse files Browse the repository at this point in the history
feat: add single message path
  • Loading branch information
apetro committed Mar 1, 2018
2 parents 4370efa + 195c7ea commit 2eb9343
Show file tree
Hide file tree
Showing 16 changed files with 913 additions and 47 deletions.
102 changes: 59 additions & 43 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,51 @@

This messaging microservice is intended for use with [uportal-home](https://github.com/uPortal-Project/uportal-home).

uPortal messages include both notifications and announcements, and can be tailored for audiences as small as one person.
uPortal messages include both notifications and announcements, and can be tailored for audiences as small as one person.

## What is a message?

A message is an announcement or notification which can be targeted to an entire user population, a subset of the population, or a single user.
A message is an announcement or notification which can be targeted to an entire user population, a subset of the population, or a single user.

This microservice processes messages in json form. [Details of json format are here](json.md)

## Configuration

Set the source of your messages in the ``application.properties`` file. In this example, we've selected a json file in our resources directory.
Set the source of your messages in the ``application.properties`` file. In this example, we've selected a json file in our resources directory.
``` javascript
message.source=classpath:messages.json
```

## To Build

This project uses maven. ```$ mvn package ``` will build a warfile for deployment.
This project uses maven. ```$ mvn package ``` will build a warfile for deployment.

To run locally, ```$ mvn spring-boot:run ``` will compile and run this microservice.
To run locally, ```$ mvn spring-boot:run ``` will compile and run this microservice.

## Endpoints

### `/`

Implemented in `MessagesController`.

### {*/*}

calls:
```java
@RequestMapping("/")
public @ResponseBody
void index(HttpServletResponse response)
```
returns:
Responds:

```
{"status":"up"}
```
description:
This endpoint returns a small json object indicating that the status of the application is healthy.

### {*/messages*}
Description:
This endpoint returns a small json object indicating that the status of the application is healthy.

calls:
``` java
@RequestMapping(value="/messages", method=RequestMethod.GET)
public @ResponseBody void messages(HttpServletRequest request,
HttpServletResponse response)
```
returns:
### `/messages`

Implemented in `MessagesController`.

Responds:
A JSON object containing messages filtered to the viewing user and the current context.

description:
Intended as the primary endpoint for servicing typical users. The idea is to move all the complication of message
Intended as the primary endpoint for servicing typical users. The idea is to move all the complication of message
resolution server-side into this microservice so that a typical client can request this data and uncritically render it.

Currently filters to:
Expand All @@ -66,34 +57,59 @@ specified in ISO format, as in `2018-02-14` or `2018-02-14:09:46:00`), AND

Expectations:

+ `isMemberOf` header conveying semicolon-delimited group memberships. (This practice is typical in UW-Madison
Shibboleth SP implementation.) Fails gracefully yet safely: if this header is not present, considers the user a member
+ `isMemberOf` header conveying semicolon-delimited group memberships. (This practice is typical in UW-Madison
Shibboleth SP implementation.) Fails gracefully yet safely: if this header is not present, considers the user a member
of no groups.

Versioning:
The details of the filtering are NOT a semantically versioned aspect of the API. That is to say, what is versioned
here is that `/messages` returns the messages appropriate for the requesting user. Increasing sophistication in what
here is that `/messages` returns the messages appropriate for the requesting user. Increasing sophistication in what
"appropriate" means is not a breaking change.

Security:
WARNING: Does not apply any access control other than filtering to messages applicable to the user's groups. If
additional access control is needed (it may not be needed), implement it at the container layer.

### {*/allMessages*}
calls:
``` java
@RequestMapping(value = "/allMessages", method = RequestMethod.GET)
public @ResponseBody
void messages(HttpServletRequest request,
HttpServletResponse response)
```
WARNING: Does not apply any access control other than filtering to messages applicable to the user's groups. If
additional access control is needed (it may not be needed), implement it at the container layer.

returns:
A JSON object, containing every known message, regardless of all criteria.
### `/message/{id}`

description:
Implemented in `MessagesController`.

Responds:

+ A JSON representation of the message with the given `id`, or
+ `404 NOT FOUND` if no message with requested `id`, or
+ `403 FORBIDDEN` if message exists but is expired, premature, or the requesting user is not in its audience.

Description:
Intended as view on a specific message.

### `/admin/allMessages`

Implemented in `MessagesController`.

Responds:
A JSON object, containing every known message, regardless of all criteria.

Description:
Intended as an administrative or troubleshooting view on the data.

Security:
WARNING: Does not apply any access control. Implement access control at the container layer. Whatever access control
is appropriate, apply it to the `/allMessages` path at e.g. the `httpd` layer.
is appropriate, apply it to the `/admin/allMessages` path at e.g. the `httpd` layer. The `/admin` prefix is intended to facilitate this.

### `/admin/message/{id}`

Implemented in `MessagesController`.

Responds:

+ A JSON representation of the message with the given `id`, or
+ 404 NOT FOUND if no message with requested `id`

Description:
Intended as an administrative or troubleshooting view on the data for a specific message.

Security:
WARNING: Does not apply any access control. Implement access control at the container layer.
Whatever access control is appropriate, apply it to the `/admin/message` path at e.g. the `httpd`
layer. The `/admin` prefix is intended to facilitate this.
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package edu.wisc.my.messages.controller;

import edu.wisc.my.messages.exception.ExpiredMessageException;
import edu.wisc.my.messages.exception.MessageNotFoundException;
import edu.wisc.my.messages.exception.PrematureMessageException;
import edu.wisc.my.messages.exception.UserNotInMessageAudienceException;
import edu.wisc.my.messages.model.Message;
import edu.wisc.my.messages.model.User;
import edu.wisc.my.messages.service.MessagesService;
import java.util.HashMap;
Expand All @@ -10,6 +15,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand Down Expand Up @@ -50,7 +56,12 @@ public Map<String, Object> messages(HttpServletRequest request) {
return responseMap;
}

@GetMapping("/allMessages")
/**
* Get all the messages in the system, regardless of audience, dates, etc.
*
* @return Map where key "messages" has value List of all Messages.
*/
@GetMapping("/admin/allMessages")
public Map<String, Object> allMessages() {
Map<String, Object> responseMap = new HashMap<String, Object>();
responseMap.put("messages", messagesService.allMessages());
Expand All @@ -65,6 +76,50 @@ public Map<String, String> index() {
return statusResponse;
}

/**
* Get a specific message regardless of the message's audience, dates, etc.
*
* @param id message ID to match
* @return Message with matching ID
*/
@RequestMapping("/admin/message/{id}")
public Message adminMessageById(@PathVariable String id) throws MessageNotFoundException {

Message message = messagesService.messageById(id);

if (null == message) {
throw new MessageNotFoundException();
}
return message;
}

/**
* Get a specific message, limited by the requesting user's context.
*
* @throws PrematureMessageException if the message is not yet gone live
* @throws ExpiredMessageException if the message is expired
* @throws UserNotInMessageAudienceException if the requesting user is not in the audience
* @returns the requested message, or null if none matching
*/
@RequestMapping("/message/{id}")
public Message messageById(@PathVariable String id, HttpServletRequest request)
throws UserNotInMessageAudienceException, PrematureMessageException, ExpiredMessageException, MessageNotFoundException {

String isMemberOfHeader = request.getHeader("isMemberOf");
Set<String> groups =
isMemberOfHeaderParser.groupsFromHeaderValue(isMemberOfHeader);
User user = new User();
user.setGroups(groups);

Message message = messagesService.messageByIdForUser(id, user);

if (null == message) {
throw new MessageNotFoundException();
}

return message;
}

@Autowired
public void setMessagesService(MessagesService messagesService) {
this.messagesService = messagesService;
Expand All @@ -75,5 +130,4 @@ public void setIsMemberOfHeaderParser(
IsMemberOfHeaderParser isMemberOfHeaderParser) {
this.isMemberOfHeaderParser = isMemberOfHeaderParser;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package edu.wisc.my.messages.exception;

import edu.wisc.my.messages.model.Message;
import java.time.LocalDateTime;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Requested message is expired.")
public class ExpiredMessageException
extends ForbiddenMessageException {

private final Message expiredMessage;

/**
* Time of consideration. The frame of reference for considering the message expired. Typically,
* this is "now".
*/
private final LocalDateTime asOfWhen;

public ExpiredMessageException(Message messageWithRequestedId, LocalDateTime asOfWhen) {
super("Message " +
((messageWithRequestedId == null) ?
"" :
(" with id [" + messageWithRequestedId.getId() + "] "))
+ "is expired as of " + asOfWhen);
this.expiredMessage = messageWithRequestedId;
this.asOfWhen = asOfWhen;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package edu.wisc.my.messages.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
* Message representing that access to a requested message is forbidden. Sub-classes represent
* specific reasons why access might be forbidden.
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Access denied to requested message.")
public class ForbiddenMessageException
extends Exception {

public ForbiddenMessageException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.wisc.my.messages.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
* Represents case where requested message was not found.
*/
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Did not find requested message.")
public class MessageNotFoundException
extends Exception {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package edu.wisc.my.messages.exception;

import edu.wisc.my.messages.model.Message;
import java.time.LocalDateTime;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
* Thrown on un-privileged request for a message that has not yet gone live.
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Requested message is premature.")
public class PrematureMessageException
extends ForbiddenMessageException {

private Message prematureMessage;
private LocalDateTime asOfWhen;

public PrematureMessageException(Message prematureMessage, LocalDateTime asOfWhen) {
super("Message with id " +
((null == prematureMessage) ?
"" :
prematureMessage.getId() + " ")
+ "is premature as of " + asOfWhen);
this.prematureMessage = prematureMessage;
this.asOfWhen = asOfWhen;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package edu.wisc.my.messages.exception;

import edu.wisc.my.messages.model.Message;
import edu.wisc.my.messages.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Requesting user not in audience for requested message")
public class UserNotInMessageAudienceException extends ForbiddenMessageException {

private final Message messageNotForUser;
private final User userNotInMessageAudience;

public UserNotInMessageAudienceException(Message message, User user) {
super("User not within audience of requested message");
this.messageNotForUser = message;
this.userNotInMessageAudience = user;
}
}
35 changes: 35 additions & 0 deletions src/main/java/edu/wisc/my/messages/service/MessageIdPredicate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package edu.wisc.my.messages.service;

import edu.wisc.my.messages.model.Message;
import java.util.Objects;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Predicate that is true where the tested message has a (potentially null) ID matching that
* (potentially null) ID given at construction.
*/
public class MessageIdPredicate
implements Predicate<Message> {

private final String idToMatch;
protected Logger logger = LoggerFactory.getLogger(getClass());

/**
* Instantiate a MessageIdPredicate that tests for a specific ID.
*
* @param idToMatch potentially null message identifier to look for
*/
public MessageIdPredicate(String idToMatch) {
this.idToMatch = idToMatch;
}

@Override
public boolean test(Message message) {
boolean result = (null != message
&& Objects.equals(this.idToMatch, message.getId()));
logger.trace("{} result testing for id {} in message {}", result, idToMatch, message);
return result;
}
}

0 comments on commit 2eb9343

Please sign in to comment.