diff --git a/content/authentication/next-steps/_index.md b/content/authentication/next-steps/_index.md index a536e77..0e81077 100644 --- a/content/authentication/next-steps/_index.md +++ b/content/authentication/next-steps/_index.md @@ -7,8 +7,8 @@ originalAuthor: John Woolbright # to be set by page creator originalAuthorGitHub: jwoolbright23 # to be set by page creator reviewer: Sally Steuterman # to be set by the page reviewer reviewerGitHub: gildedgardenia # to be set by the page reviewer -lastEditor: # update any time edits are made after review -lastEditorGitHub: # update any time edits are made after review +lastEditor: Ben Clark # update any time edits are made after review +lastEditorGitHub: brclark # update any time edits are made after review lastMod: # UPDATE ANY TIME CHANGES ARE MADE --- @@ -16,6 +16,9 @@ lastMod: # UPDATE ANY TIME CHANGES ARE MADE Now that you have successfully added authentication to an existing project you can further explore the topics below: +1. For a walkthrough on how to add roles and privileges to user accounts, take a look at the bonus module. 1. For a deeper dive into `spring security` you can visit the [official docs](https://spring.io/projects/spring-security). 1. View [this guide](https://spring.io/guides/gs/securing-web/) that walks through the creation of a spring MVC web application using Spring Security. -1. [Baeldung Spring Security Basic Authentication](https://www.baeldung.com/spring-security-basic-authentication) walkthrough. This will will walk through the process of basic authentication with Spring with an MVC application. \ No newline at end of file +1. [Baeldung Spring Security Basic Authentication](https://www.baeldung.com/spring-security-basic-authentication) walkthrough. This will walk through the process of basic authentication with Spring with an MVC application. + +{{% children style="h4" %}} diff --git a/content/authentication/next-steps/bonus-module/_index.md b/content/authentication/next-steps/bonus-module/_index.md new file mode 100644 index 0000000..e13a06f --- /dev/null +++ b/content/authentication/next-steps/bonus-module/_index.md @@ -0,0 +1,25 @@ +--- +title: "BONUS: User Roles & Spring Security" +date: 2023-11-14T09:28:27-05:00 +draft: false +weight: 1 +originalAuthor: Ben Clark # to be set by page creator +originalAuthorGitHub: brclark # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +This section contains 5 lessons to build out the **authentication** +and **authorization** features in Coding Events. The video shows the +final features added as a result of the lessons. You will add user-owned +data, refactored controllers to make use of services, refactored +authentication and authorization using Spring Security framework, +and roles & privileges to restrict access to the Coding Events based +on assigned user roles. + +### TODO: Add Embedded Intro video + +{{% children %}} diff --git a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md new file mode 100644 index 0000000..b42bb4e --- /dev/null +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -0,0 +1,849 @@ +--- +title: "Data Transfer Objects & Services" +date: 2023-10-17T00:02:24-05:00 +draft: false +weight: 2 +originalAuthor: Ben Clark # to be set by page creator +originalAuthorGitHub: brclark # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +It's time for us to expand on a previous topic used in Chapter 18. +We used **Data Transfer Objects** or DTOs to separate the structure +of our Models from the needs of our client requests or views. + +## Separating Business Logic from Controllers + +For example, we have a `RegisterFormDTO` containing a necessary `verifyPassword` +field when registering a new account, *but* the `verifyPassword` is not a field +that we need to store in the model. Our DTOs allow us to create data objects +that are specific to the requirements of a form or client request. They +translate the database definition in the Model to the client requirement of +a request. + +In order to facilitate the translation of the database Models to a DTO, we will +add another "layer" to our design. The [sequence diagrams](https://en.wikipedia.org/wiki/Sequence_diagram) +below show how we will use modules +called **Services** to manage the interactions between the **Controllers** and the +**Repositories** / **Models**. + +{{% notice blue Note "rocket" %}} +Services are a design concept laid out in [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design). +Any logic that does not fit neatly in to an object, such as the Controller +or Model, can be expressed as a service. + +The benefits of a Service layer in an MVC app are described nicely in +[this StackOverflow piece](https://stackoverflow.com/questions/31180816/mvc-design-pattern-service-layer-purpose). +{{% /notice %}} + +```mermaid { align="left" zoom="false" } +%%{init:{"fontFamily":"monospace", "zoom":"false"}}%% +sequenceDiagram + Title: Our Design Before DTOs/Services + actor User + participant E as EventController + participant A as AuthController + participant Ev as EventRepository + User->>E: POST /events/create
{ Event newEvent } + E->>+A: getUserFromSession + A-->>-E: currUser + Note over E: // business logic
event.setCreator(currUser) + E->>Ev: save(Event newEvent) + E->>User: redirect: /events +``` + +In our current design without **Services**, our "business logic" --- the logic +that validates and configures our data before saving to the database --- is +handled in the **Controllers**. Our `Event` model also contains fields that are +not used in the `Create Event` form that users complete, such as `user_id`. + +We can create a DTO that directly maps to the `Create Event` form, +which gives us flexibility to create customized forms with fields unrelated to +the database models. Again, consider the `RegisterFormDTO` which contained the +`verifyPassword` field, a necessary field for the form that does not end up in +the `User` Model. + +With DTOs, our "business logic" will now include the translation of DTO fields to +Model fields. Rather than having this logic clutter our Controllers, we can +move it to the Service layer. Take a look at the diagram below and notice how +our `EventController` passes the `EventDTO` to the `EventService`, where the +"business logic" now resides. + +```mermaid { align="right" zoom="false" } +sequenceDiagram + Title: Design with DTOs & Services + actor User + participant E as EventController + participant Es as EventService + participant Us as UserService + participant Ev as EventRepository + User->>E: POST /events/create
{ EventDTO newEventDto } + E->>+Es: save(newEventDto) + Note over Es: // business logic
// translate DTO to Model + Es->>+Us: getCurrentUser + Us-->>-Es: currUser + Note over Es: newEvent.setCreator(currUser) + Es->>Ev: save(Event newEvent) + Es-->>E: newEvent + E->>User: redirect: /events +``` + +## Adding DTOs & Services to CodingEvents + +{{% notice blue Note "rocket" %}} +The code for this section begins with the [user-data branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/user-data) +and ends with the [add-service-dto branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/add-service-dto) +of the `CodingEventsJava` repository. +{{% /notice %}} + +### Adding `EventDTO` & `EventCategoryDTO` +We'll start by adding the `EventDTO`. This will be a POJO class that contains +every field from our `Create Event` form. Notice the flattening of the +`EventDetails`, meaning that the DTO has the fields from `EventDetails` +instead of a separate class/object for them. + +Move the `dto` package from `codingevents.models.dto` to `codingevents.dto`, so +that it's no longer nested inside the `models` package. + +Next, let's create the `EventDTO` class in the same package. + +```java +public class EventDTO { + @NotBlank(message = "Name is required") + @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") + private String name; + + @Size(max = 500, message = "Description too long!") + private String description; + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email. Try again.") + private String contactEmail; + + private int categoryId; + + private int[] tagIds; + + public EventDTO() {} + + // Add setters & getters... +} +``` + +Add the getters and setters for the fields. Let's also add the DTO for +`EventCategory`. Create `EventCategoryDTO` in the `dto` package as well. + +```java +public class EventCategoryDTO { + @Size(min=3, message="Name must be at least 3 characters long") + private String name; + + public EventCategoryDTO() {} + + // Add getters & setters... +} +``` + +You should also add a DTO for the `Tag` model. + +### Prepping `User` model for `UserService` + +We have a few updates we need to make to the `User` model to prep it for use +with the `UserService`. Namely, we need to move the `PasswordEncoder` class to +its own managed config. Our password encoder is currently a static instance +in the `User` model, but we will need access to our encoder within the +`UserService` so that we can validate a login password against the user's +encrypted password. + +#### Creating `PasswordEncoder` bean + +This password encoder object will be a managed Java bean, similar to a +controller, that can be referenced using an `@Autowired` field. + +First, create a new package `config` within your `codingevents` package. Then, +create a new class `EncoderConfig` in the package. + +```java +@Configuration +public class EncoderConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +The `@Configuration` annotation tells Spring that this class will contain +`@Bean` definitions for Spring managed objects. Inside the class, we +define a `@Bean` that will return an instance of the `BCryptPasswordEncoder` +that we were using in the `User` class. + +#### Refactoring `User` model + +Next, we need to modify our `User` model for use of the `PasswordEncoder`. +We are going to rework our constructor so that a new `User` instance gets +the encoded password passed in, and the `User` object will not be responsible +for doing any encoding. + +Remove the field containing the `static final BCryptPasswordEncoder`. + +Modify the `User` constructor so that it takes in `String pwHash` as an argument +and uses it to set the field directly, removing the call to `encode`. + +Lastly, remove the `isMatchingPassword` method and replace it with a getter for +the `pwHash` field. This will cause an issue in `AuthenticationController` that +we will fix after adding `UserService`. + +Our `User` class is now refactored. Instead of having the `User` class be +responsible for encoding passwords, we will do password encryption in the +`UserService` and pass encrypted passwords to new `User` instances. + +### Adding `UserService` + +The reponsibilities of the service layer are to translate DTOs to Models +and handle interactions between the Controller and the Repository. To follow +this design, let's add a service to handle interactions between +`AuthenticationController` and `UserRepository`, and translating +`RegisterFormDTO` to `User` models. + +{{% notice blue Note "rocket" %}} +Services are built in to the [Spring Framework](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Service.html). +A class annotated with `@Service` will become a managed component +in the Spring application context, similar to `@Controller`, meaning that the +instance will be created by the Spring context. We can get a reference to a +service object in a different class using the `@Autowired` annotation. +{{% /notice %}} + +Let's first add a package `services` inside the `codingevents` package. Create +the `UserService` class inside this package. + +```java +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + +} +``` + +Our `UserService` is going to expose a few methods: + +1. `User findByUsername(String username)`: retrieves `User` from +`UserRepository` by the `username` +1. `Optional findById(Integer id)`: expose `UserRepository` +functionality for possible use +1. `List findAll()`: expose `UserRepository` functionality +for possible use +1. `User deleteUser(Integer id)`: expose `UserRepository` functionality +for possible use +1. `User save(RegisterFormDTO registration)`: validates data in +`RegisterFormDTO` and creates a new `User` in `UserRepository` +1. `boolean validateUser(User user, String password)`: validate a password +by encoding it and comparing to the encoded `User` password. +1. `User getCurrentUser()`: retrieves currently logged in `User` from +the `user` attribute in `HttpSession` which is loaded from the +current request context + +#### Add methods to expose `UserRepository` functionality + +Our `UserService` needs to expose some of the basic functionalities of the +`UserRepository` to the controllers. Add the following methods to your +`UserRepository` class. Don't worry about the `userNotFoundException` --- we +will add this definition in the next section. + +```java + public User findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public Optional findById(Integer id){ + return userRepository.findById(id); + } + + public List findAll() { + return (List) userRepository.findAll(); + } + + public User deleteUser(Integer id) { + User user = userRepository.findById(id) + .orElseThrow(userNotFoundException(id)); + userRepository.delete(user); + return user; + } +``` + +#### Add custom error handlers + +We have to add proper error handling to +our site. When we throw an error during request handling, we can trigger an +automatic error page template to be shown. The automatic error page +will be covered in the next lesson, but for now, we can add our custom +`Exception` type when a resource is not found. + +First we need to add a definition for the `userNotFoundException`in `UserService`. +This is a method that supplies a more generic `ResourceNotFoundException` with a +custom message. Add this method to the end of your `UserService` class. + +```java + private Supplier userNotFoundException(Integer id) { + return () -> new ResourceNotFoundException("User with id %d could not be found"); + } +``` + +Next we need to define our custom exceptions that will be used to trigger the +error template. Add a new package called `exceptions` in `org.launchcode.codingevents`. +Then create a new class in that package called `ResourceNotFoundException`. This +will extend `RuntimeException` and give us a new exception type for our needs. + +```java +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException() { + + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause, + boolean enableSuppression, + boolean writeableStackTrace) { + super(message, cause, enableSuppression, writeableStackTrace); + } +} +``` + +We need one other custom exception that will be thrown if the user does not +provide matching `password` and `verifyPassword` fields when registering. Create +another new class in `exceptions` named `UserRegistrationException`. + +```java +public class UserRegistrationException extends RuntimeException { + public UserRegistrationException() { } + + public UserRegistrationException(String message) { + super(message); + } + + public UserRegistrationException(String message, Throwable cause) { + super(message, cause); + } + + public UserRegistrationException(Throwable cause) { + super(cause); + } + + public UserRegistrationException(String message, Throwable cause, boolean enableSuppression, + boolean writeableStackTrace) { + super(message, cause, enableSuppression, writeableStackTrace); + } +} +``` + +#### Add `save` and `validateUser` methods + +Two very important methods in our `UserService` will be `save` and +`validateUser` methods. + +The `save` method will be responsible for taking a `RegisterFormDTO` instance, +translating it to a new `User` instance, and saving to the database. It will +be responsible for comparing the `verifyPassword` field and encoding the +password for use in the `User` instance. + +```java + public User save(RegisterFormDTO registration) { + String password = registration.getPassword(); + String verifyPassword = registration.getVerifyPassword(); + if (!password.equals(verifyPassword)) { + throw new UserRegistrationException("Passwords do not match"); + } + + String pwHash = passwordEncoder.encode(registration.getPassword()); + User user = new User(registration.getUsername(), pwHash); + + return userRepository.save(user); + } +``` + +When a user is attempting to login, we will need to validate the password +provided in the `LoginFormDTO` against the `pwHash` of the user. + +```java + public boolean validateUser(User user, String password) { + if (user == null) { + return false; + } + + return passwordEncoder.matches(password, user.getPwHash()); + } +``` + +#### Add `getCurrentUser` method + +One more piece to add in `UserService`, we have to add a `getCurrentUser()` method. + +We will do some fancy Spring Framework logic to retrieve the `HttpSession` from +the current HTTP request context, and then get the `User` object similarly to +how we do it in `AuthenticationController`. + +Add this method to your `UserService` below the fields. + +```java + public User getCurrentUser() { + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpSession session = attr.getRequest().getSession(true); + + Integer userId = (Integer) session.getAttribute("user"); + if (userId == null) { + return null; + } + + Optional user = findById(userId); + + return user.orElse(null); + } +``` + +With these methods added to `UserService`, we can refactor some of our +`AuthenticationController` to make use of the service. + +### Refactor `AuthenticationController` to use `UserService` + +Our goal in `AuthenticationController` is to remove references to +`UserRepository` and replace them with references to methods from +`UserService`. To begin, update the `userRepository` field to: + +```java + private UserService userService; +``` + +With `userRepository` removed, we can update `getUserFromSession`, +`processRegistrationForm`, and `processLoginForm`. + +First, update `getUserFromSession` to use `userService.findById`, along with +helpful code to deal with `Optional`: + +```java{ hl_lines="7" } + public User getUserFromSession(HttpSession session) { + Integer userId = (Integer) session.getAttribute(userSessionKey); + if (userId == null) { + return null; + } + + return userService.findById(userId).orElse(null); + } +``` + +Next, update `processRegistrationForm` to use `userService.save` with our +`RegistrationFormDTO`: + +```java{ hl_lines="4 9 16-21" } + @PostMapping("/register") + public String processRegistrationForm(@ModelAttribute @Valid RegisterFormDTO registerFormDTO, + Errors errors, Model model) { + model.addAttribute("title", "Register"); + if (errors.hasErrors()) { + return "register"; + } + + User existingUser = userService.findByUsername(registerFormDTO.getUsername()); + + if (existingUser != null) { + errors.rejectValue("username", "username.alreadyexists", "A user with that username already exists"); + return "register"; + } + + try { + User newUser = userService.save(registerFormDTO); + } catch (UserRegistrationException ex) { + errors.rejectValue("password", "passwords.mismatch", "Passwords do not match"); + return "register"; + } + + return "redirect:/login"; + } +``` + +Last, we need to update `processLoginForm` to use `userService` and the new +`validateUser` method: + +```java{ hl_lines="11 20" } + @PostMapping("/login") + public String processLoginForm(@ModelAttribute @Valid LoginFormDTO loginFormDTO, + Errors errors, HttpServletRequest request, + Model model) { + model.addAttribute("title", "Log In"); + + if (errors.hasErrors()) { + return "login"; + } + + User theUser = userService.findByUsername(loginFormDTO.getUsername()); + + if (theUser == null) { + errors.rejectValue("username", "user.invalid", "The given username does not exist"); + return "login"; + } + + String password = loginFormDTO.getPassword(); + + if (!userService.validateUser(theUser, password)) { + errors.rejectValue("password", "password.invalid", "Invalid password"); + return "login"; + } + + setUserInSession(request.getSession(), theUser); + + return "redirect:"; + } +``` + +### Adding `EventService` and `EventCategoryService` + +The `EventService` and `EventCategoryService` will be responsible for +translating `EventDTO` and `EventCategoryDTO` to model objects and +communication between the `EventController` and `EventRepository`. + +#### `EventService` + +Let's create another class in the `services` package named `EventService`. + +Our service will need references to the repositories so that it can access +the database, and a reference to `UserService` so that it can retrieve the +currently logged-in user. + +```java +@Service +public class EventService { + + @Autowired + private EventRepository eventRepository; + + @Autowired + private EventCategoryRepository categoryRepository; + + @Autowired + private UserService userService; + +} +``` + +Next let's add some methods that will expose database functionality. Notice +how we use the new `findAllByCreator` and `findByIdAndCreator` repository +methods to filter events by user. We also want to create method definitions +that assume the creator is the current user, such as +`getAllEventsByCurrentUser()`. + +```java + public List getAllEvents() { + return (List) eventRepository.findAll(); + } + + public List getAllEventsByCreator(User creator) { + return eventRepository.findAllByCreator(creator); + } + + public List getAllEventsByCurrentUser() { + return getAllEventsByCreator(userService.getCurrentUser()); + } + + public Event getEventById(int id) { + return eventRepository.findById(id).orElseThrow(ResourceNotFoundException::new); + } + + public Event getEventByIdAndCreator(int id, User creator) { + return eventRepository.findByIdAndCreator(id, creator).orElseThrow(ResourceNotFoundException::new); + } + + public Event getEventByIdForCurrentUser(int id) { + return getEventByIdAndCreator(id, userService.getCurrentUser()); + } + + public void removeEventById(int id) { + eventRepository.deleteById(id); + } +``` + +Last, we must add a `save` method, which +takes in an `EventDTO` object and will translate it to our `Event` and +`EventDetails` models before saving to the database. + +```java + public Event save(EventDTO eventDTO) { + Event event = new Event(); + event.setName(eventDTO.getName()); + + EventDetails details = new EventDetails(eventDTO.getDescription(), + eventDTO.getContactEmail()); + event.setEventDetails(details); + + event.setEventCategory(categoryRepository.findById(eventDTO.getCategoryId()) + .orElse(null)); + + event.setCreator(userService.getCurrentUser()); + + eventRepository.save(event); + + return event; + } +``` + +#### Add `EventCategoryService` + +Similar to our `EventService`, we must add methods to the `EventCategoryService` +to expose functionality of the `EventCategoryRepository` to the controllers. + +We will only need `EventCategoryRepository` and `UserService` autowired fields +in this class. + +Write the following methods, using the previous section on `EventService` as a +guide: + +1. `List getAllCategories()` +1. `List getAllCategoriesByCreator(User creator)` +1. `List getAllCategoriesByCurrentUser()` +1. `EventCategory getCategoryById(int id)` +1. `EventCategory getCategoryByIdAndCreator(int id, User creator)` +1. `EventCategory getCategoryByIdForCurrentUser(int id)` +1. `EventCategory save(EventCategoryDTO categoryDTO)` + +You should add the corresponding service for `Tag` as well. + +Now that our service layer is added, we can refactor our controllers to use them +and our form views to use DTOs. + +### Refactoring Controllers & Views + +We will remove all references to `authController` in `EventController` and +`EventCategoryController`, which is how the sequence diagram above is organized. +We will also make use of the service classes instead of the repository classes. + +#### `EventController` + +Let's start by refactoring our `EventController` to use the `EventService`, +`EventCategoryService` and `EventDTO`. + +Change the `EventRepository` field to be `EventService`, as well as +`EventCategoryRepository` to `EventCategoryService`, like below. Also, remove +the `AuthenticationController` field as we will not be using it to retrieve +the current user anymore. + +```java + @Autowired + private EventService eventService; + + @Autowired + private EventCategoryService eventCategoryService; +``` + +Now, we have to to update the request handlers to use `eventService` and +`eventCategoryService`. + +In `displayEvents` we will add a `try/catch` block to catch +`ResourceNotFoundException` if the category ID is invalid. We will also +remove the use of `HttpSession` and `authController`. Instead, we use +the `eventService.getAllEventsByCurrentUser()` that we wrote to find the +current user information: + +```java {hl_lines="2 5 7-14"} + @GetMapping + public String displayEvents(@RequestParam(required = false) Integer categoryId, Model model) { + if (categoryId == null) { + model.addAttribute("title", "All Events"); + model.addAttribute("events", eventService.getAllEventsByCurrentUser()); + } else { + try { + EventCategory category = eventCategoryService.getCategoryByIdForCurrentUser(categoryId); + + model.addAttribute("title", "Events in category: " + category.getName()); + model.addAttribute("events", category.getEvents()); + } catch(ResourceNotFoundException ex) { + model.addAttribute("title", "Invalid Category ID: " + categoryId); + } + } + + return "events/index"; + } +``` + +In the `displayCreateEventForm` method, we have to make sure we switch to using +`EventDTO` as our model-binding object. We can also remove references to +`currUser` and `authController`. + +```java {hl_lines="4-5"} + @GetMapping("create") + public String displayCreateEventForm(Model model) { + model.addAttribute("title", "Create Event"); + model.addAttribute(new EventDTO()); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCurrentUser()); + return "events/create"; + } +``` + +In our `processCreateEventForm` method, we can remove the logic that prepares +the `newEvent` for saving in `eventRepository`, and instead we can pass our +`newEventDTO` directly to the `eventService` for processing and saving. + +```java {hl_lines="2 6 10"} + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid EventDTO newEventDTO, + Errors errors, Model model) { + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCurrentUser()); + return "events/create"; + } + + eventService.save(newEventDTO); + return "redirect:/events"; + } +``` + +Our `displayDeleteEventForm` method should show events that the current user +has created. We will update our `processDeleteEventsForm` method to call +`eventService`, but recognize that we are leaving this method unsecure. + +Can a user submit a request to delete events that they do not own? It appears +so. How can we protect this method so that we only delete events with ids that +the user owns? + +```java {hl_lines="5 14"} + @GetMapping("delete") + public String displayDeleteEventForm(Model model) { + model.addAttribute("title", "Delete Events"); + model.addAttribute("events", eventService.getAllEventsByCurrentUser()); + return "events/delete"; + } + + @PostMapping("delete") + public String processDeleteEventsForm(@RequestParam(required = false) int[] eventIds) { + + if (eventIds != null) { + for (int id : eventIds) { + eventService.removeEventById(id); + } + } + + return "redirect:/events"; + } +``` + +Lastly, we need to update `displayEventDetails` to validate that a user owns +the `eventId` that is passed in. We can achieve this with the +`getEventByIdForCurrentUser` method that we added in `eventService`. + +```java {hl_lines="4-10"} + @GetMapping("detail") + public String displayEventDetails(@RequestParam Integer eventId, Model model) { + try { + Event event = eventService.getEventByIdForCurrentUser(eventId); + + model.addAttribute("title", event.getName() + " Details"); + model.addAttribute("event", event); + } catch (ResourceNotFoundException ex) { + model.addAttribute("title", "Invalid Event ID: " + eventId); + } + + return "events/detail"; + } +``` + +That takes care of the `EventController`. Updating the `EventCategoryController` +will be very similar and more simple. + +#### `EventCategoryController` + +Update `EventCategoryController` to use the `EventCategoryService` and +`EventCategoryDTO`. + +Change the `EventCategoryRepository` field to be `EventCategoryService`. + +Now, refactor all references to the `eventCategoryRepository` to be +`eventCategoryService` references and use `EventCategoryDTO` in the +create form, similar to how we did it in the previous section. You should +be able to remove all references to `authController` that were needed +to get `currUser`, since we have helpful methods in `EventCategoryService` now. + +#### Updating Views to use DTOs + +Now that we have our controllers updated to use services, we have to update +our views to make use of DTOs for the `create` forms. + +First we will update `events/create.html`. We will use the `eventDTO` +attribute that we passed in to the template for model binding. + +```html {hl_lines="4 6 10 12 16 18 22 28" title="events/create.html"} +
+
+ +

+
+
+ +

+
+
+ +

+
+
+ +
+
+ +
+
+``` + +Lastly, we will update `eventCategories/create.html` and use the +`eventCategoryDTO` that we passed in for model binding. + +```html {hl_lines="4 6"} +
+
+ + +
+ +
+``` + +Make the necessary updates for `TagController` and our updates should be +complete. There should be no change in functionality +for Coding Events. Be sure to test the create, read, and delete functions. + +The next section will begin a process to add user roles and privileges +to Coding Events. First, we will introduce `Role` and `Privilege` models +that can be associated with `User` models. diff --git a/content/authentication/next-steps/bonus-module/role-based-access/_index.md b/content/authentication/next-steps/bonus-module/role-based-access/_index.md new file mode 100644 index 0000000..37fc2da --- /dev/null +++ b/content/authentication/next-steps/bonus-module/role-based-access/_index.md @@ -0,0 +1,629 @@ +--- +title: "Role-Based User Access" +date: 2023-12-07T10:26:50-06:00 +draft: false +weight: 5 +originalAuthor: Ben Clark # to be set by page creator +originalAuthorGitHub: brclark # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +In this final lesson for the Bonus Module, we will add some new features to +Coding Events that make use of the roles & privileges infrastructure. We +will modify some core functionality that has been there from the beginning +of the project. + +Overall, we will define the actions that two of our user types can take, the +base `ROLE_USER` and the more powerful `ROLE_ORGANIZER`. `ROLE_USER` will be +able to view events that have been created by `ROLE_ORGANIZER`, as well as all +categories that have been created as well. `ROLE_USER` will then be able to mark +themselves as interested or **attending** an event, which will store +persistently in the database. `ROLE_USER` will have a limited menu bar that does +not show links to event and category creation. + +`ROLE_ORGANIZER` will be able to create new events and categories that are +associated with their account. We will expand the access and view of the menu +bar for `ROLE_ORGANIZER` so that they can see links to the routes they have +access to. + +## Adding Role-Based Features for Users + +{{% notice blue Note "rocket" %}} +The code for this section begins with the [spring-security-features branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/spring-security-features) +and ends with the [role-based-access branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/role-based-access) +of the `CodingEventsJava` repository. +{{% /notice %}} + +The first portion of this lesson will add event attendance relationships +to our models, as well as the ability to mark in login that you want to be +an event organizer. + +### Add Event Attendance Relationship + +#### Add `User` and `Event` many-to-many relationship + +In the `Event` model, we will add another field that stores `Collection +attendees` as a many-to-many relationship. Add the following field to `Event`, +recognizing that we are manually specifying the join table: + +```java + @ManyToMany + @JoinTable( + name = "users_events", + joinColumns = @JoinColumn( + name = "user_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn( + name = "event_id", referencedColumnName = "id")) + private Collection attendees; +``` + +Be sure to add a getter and a setter as well for the new field. + +Now, in the `User` model we will add the same relationship to connect a +`Collection attendingEvents` field. Add the following field to `User`: + +```java + @ManyToMany + @JoinTable( + name = "users_events", + joinColumns = @JoinColumn( + name = "event_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn( + name = "user_id", referencedColumnName = "id")) + private Collection attendingEvents; +``` + +Be sure to add a getter and setter for this field too. + +#### Add Attendance CRUD logic to `EventService` + +We can add methods to `EventService` that provide functionality for marking +and removing attendance relationships between an event and a user. These +methods will be used specifically in our controllers and views for displaying +and adding attendance. + +Add the following `addAttendanceForUser` method to the `EventService` class. +Notice that we use the `attendees` collection to manage the relationship and +add a `user` object to the collection. + +```java + public void addAttendanceForUser(Integer eventId, User user) { + Event event = eventRepository.findById(eventId) + .orElseThrow(ResourceNotFoundException::new); + + if (!event.getAttendees().contains(user)) { + event.getAttendees().add(user); + eventRepository.save(event); + } + } +``` + +With that in place we can make a more useful method to us --- +`addAttendanceForCurrentUser`: + +```java + public void addAttendanceForCurrentUser(Integer eventId) { + addAttendanceForUser(eventId, userService.getCurrentUser()); + } +``` + +Now we have to provide the similar logic for removing a user from the attendance +collection, with `removeAttendanceForUser` method: + +```java + public void removeAttendanceForUser(Integer eventId, User user) { + Event event = eventRepository.findById(eventId) + .orElseThrow(ResourceNotFoundException::new); + + event.getAttendees().remove(user); + eventRepository.save(event); + } + + public void removeAttendanceForCurrentUser(Integer eventId) { + removeAttendanceForUser(eventId, userService.getCurrentUser()); + } +``` + +Lastly, we will need some helpful methods for checking if the current user +is marked as attending an event and for getting the events they will attend: + +```java + public boolean getUserEventAttendance(Event event) { + return event.getAttendees().contains(userService.getCurrentUser()); + } + + public List getAttendingEventsByCurrentUser() { + return (List) userService.getCurrentUser().getAttendingEvents(); + } +``` + +#### Add Event Attendance CRUD to `EventController` + +We need to set up routes that will allow users to look at the events they have +marked for attendance, and routes to allow for easy setting/unsetting of +attendance for an event by the current user. + +In `EventController`, let's first add a route at `GET /events/attending` that +will load the table of events that the current user has RSVP'd to: + +```java + @GetMapping("attending") + public String displayMyEvents(Model model) { + model.addAttribute("events", eventService.getAttendingEventsByCurrentUser()); + model.addAttribute("title", "My Events"); + return "events/index"; + } +``` + +Next, we need two POST request handlers at `POST /events/{id}/attending` and +`POST /events/{id}/removeAttending`. These methods will assume that the current +user is marking themselves for attendance to a specific event. + +```java + @PostMapping("{id}/attending") + public String processUserEventAttendance(@PathVariable Integer id, Model model) { + + eventService.addAttendanceForCurrentUser(id); + + return "redirect:/events/detail?eventId=" + id; + } + + @PostMapping("{id}/removeAttending") + public String removeUserEventAttendance(@PathVariable Integer id, Model model) { + + eventService.removeAttendanceForCurrentUser(id); + + return "redirect:/events/detail?eventId=" + id; + } +``` + +Last, we want the Event Details page to display a button to RSVP or remove attendance +easily from the event. We'll pass a boolean value as a model attribute to the +details template that says whether the current user is attending the event, which +will help us display the correct form for changing the reservation. + +In `displayEventDetails` method in `EventController`, add another model attribute in +the `try` block: + +```java{ hl_lines="6" } + try { + Event event = eventService.getEventByIdForCurrentUser(eventId); + + model.addAttribute("title", event.getName() + " Details"); + model.addAttribute("event", event); + model.addAttribute("userAttendance", eventService.getUserEventAttendance(event)); + } catch (ResourceNotFoundException ex) { +``` + +#### Adding User Attendance UI in templates + +We want to allow users to mark themselves for attendance to a coding event. We +can put this functionality in multiple places. For now, we will put that choice +in the event details page. We'll have a button that displays the current +attendance and allows users to flip their choice. + +In the `events/detail.html` template, let's dynamically add a button based on +the `userAttendance` attribute value we passed in: + +```html{hl_lines="5-17"} + + Contact Email + + + + + Actions + + +
+ +
+
+ +
+ + +``` + +With that addition, every user should be able to mark themselves for attendance +to an event. + +#### Add `RoleRepository` Field +Our `UserService` implements the `loadUserByUsername` method that is a part +of the `UserDetailsService` interface. In that method, we need to properly +load the *granted authorities* for that user. + +In `UserService` add a new autowired field for the `RoleRepository`: + +```java + @Autowired + private RoleRepository roleRepository; +``` + +#### Update `getAuthorities` to pull roles & privileges for `User` + +We want to modify `getAuthorities` to take an argument for a `Collection` +object that is a list of the roles for a user. We'll refactor this method +and introduce some helper methods. The result will be that `getAuthorities` +returns a collection of granted authories that includes *all roles & +privileges* for a given list of user roles. + +```java + private Collection getAuthorities( + Collection roles) { + return getGrantedAuthorities(getPrivilegesAndRoles(roles)); + } + + private List getPrivilegesAndRoles(Collection roles) { + } + + private List getGrantedAuthorities(List privileges) { + } +``` + +In `getPrivilegesAndRoles`, we will take a list of `Role` objects and return a +list of roles and associated privileges in `String` form. We will heavily use +Java `stream` and `map` methods here to translate between collections: + +Update the `getPrivilegesAndRoles` method as below: + +```java + private List getPrivilegesAndRoles(Collection roles) { + List collection = new ArrayList<>(); + for (Role role : roles) { + collection.addAll(role.getPrivileges()); + } + List rolesAndPrivileges = collection.stream() + .map(Privilege::getName) + .collect(Collectors.toList()); + rolesAndPrivileges.addAll(roles.stream() + .map(Role::getName) + .collect(Collectors.toList()) + ); + return rolesAndPrivileges; + } +``` + +We will use that `List` object to create a `List` +object. Update `getGrantedAuthorities` method as below: + +```java + private List getGrantedAuthorities(List privileges) { + return privileges.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +``` + +Now, the current user in session will have their roles and privileges stored in +the `Authentication` object in `SecurityContext`. + +### Add Event Organizer role capability for users + +#### Add field to `RegisterFormDTO` + +When a user is registering for Coding Events, they should be able to mark +themselves as an event organizer, so that they can have both `ROLE_USER` and +`ROLE_ORGANIZER`. + +In `RegisterFormDTO`, add a field: + +```java + private Boolean eventOrganizer; +``` + +Add a getter and a setter for this field as well. + +#### Update `UserService save` for Event Organizer field in `RegisterFormDTO` + +We have already added a `Boolean` to `RegisterFormDTO` that allows a user to +mark themselves as an organizer. We need to make sure their assigned roles +reflect that. In `UserService save` add the following update: + +```java{ hl_lines="11-20" } + public User save(RegisterFormDTO registration) { + String password = registration.getPassword(); + String verifyPassword = registration.getVerifyPassword(); + if (!password.equals(verifyPassword)) { + throw new UserRegistrationException("Passwords do not match"); + } + + String pwHash = passwordEncoder.encode(registration.getPassword()); + User user = new User(registration.getUsername(), pwHash); + + if (registration.getEventOrganizer()) { + List roles = new ArrayList<>(); + roles.add(roleRepository.findByName(RoleType.ROLE_USER.toString())); + roles.add(roleRepository.findByName(RoleType.ROLE_ORGANIZER.toString())); + user.setRoles(roles); + } else { + user.setRoles(Collections.singletonList( + roleRepository.findByName(RoleType.ROLE_USER.toString()) + )); + } + + return userRepository.save(user); + } +``` + +If the user has marked themselves as an organizer, we add both `ROLE_USER` and +`ROLE_ORGANIZER` to their account. + +#### Add organizer option to register form + +In our registration form, we need to give the option to register as an event +organizer. Add the following input to the `register.html` template: + +```html{hl_lines="6-11"} +
+ +
+
+ +
+ + +``` + +Now when someone checks the option for "I am an event organizer", their user +type for `ROLE_ORGANIZER` is set. + +In the next section, we will add security annotations to the controllers to +limit access depending on user role. + +### Secure controllers with authorization annotations + +We currently have route filtering done in our `WebSecurity` class. This can be +a helpful way to set up route filtering based on prefixes. For example, +we use `.requestMatchers("/login").permitAll()` to allow any client to access +this route. + +We could also design our app to have Admin routes behind `/admin/` prefix, +and with this, we could limit access using +[Ant matchers again](https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html#match-by-ant), +with a method like `.requestMatchers("/admin/**").hasRole("ROLE_ADMIN")`. + +For Coding Events, we are going to set up authorization per controller and +request handler using the `@PreAuthorize` annotation. The basics of +[Authorization with Annotation](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#authorizing-with-annotations) +are described in documentation, and require us to add +`@PreAuthorize("hasRole('ROLE_ORGANIZER')")` to request handlers that we +want to restrict role-based access to. + +#### Secure `EventCategoryController` + +Our first task is to update `EventCategoryController` with `@PreAuthorize` +annotations. The `/eventCategories` route is already protected by our +request matchers in `WebSecurity` for basic user authentication. We will go +above and beyond to add authorization to the controller for `ROLE_USER`. + +Add the following to `EventCategoryController`, which will apply this +authorization to every request handler by default: + +```java{hl_lines="3"} +@Controller +@RequestMapping("eventCategories") +@PreAuthorize("hasRole('ROLE_USER')") +public class EventCategoryController { +``` + +Now we can selectively apply `ROLE_ORGANIZER` authorization to individual +request handlers. We want to restrict the creation of new categories to +the `ROLE_ORGANIZER` type. + +```java{hl_lines="2"} + @GetMapping("create") + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + public String renderCreateEventCategoryForm(Model model) { +``` + +```java{hl_lines="2"} + @PostMapping("create") + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + public String processCreateEventCategoryForm(@Valid @ModelAttribute EventCategoryDTO eventCategoryDto, + Errors errors, Model model, HttpSession session) { +``` + +We had previously prevented users from seeing categories that they did not +create, meaning one could not see another user's categories. With these roles, +however, we want to allow a regular `ROLE_USER` to browse the categories created +by `ROLE_ORGANIZER`. + +Therefore, we should update our `displayAllCategories` handler to show all +categories as below: + +```java{hl_lines="4"} + @GetMapping + public String displayAllCategories(Model model, HttpSession session) { + model.addAttribute("title", "All Categories"); + model.addAttribute("categories", eventCategoryService.getAllCategories()); + return "eventCategories/index"; + } +``` + +#### Secure `EventController` + +Similar to `EventCategoryController`, we will add `@PreAuthorize` by default to +every request: + +```java{hl_lines="3"} +@Controller +@RequestMapping("events") +@PreAuthorize("hasRole('ROLE_USER')") +public class EventController { +``` + +When a `ROLE_USER` user looks at `/events`, they should see a list of all events +created by other organizers for them to browse. Let's update `displayEvents` to +return all events: + +```java{hl_lines="4 7"} + public String displayEvents(@RequestParam(required = false) Integer categoryId, Model model, HttpSession session) { + if (categoryId == null) { + model.addAttribute("title", "All Events"); + model.addAttribute("events", eventService.getAllEvents()); + } else { + try { + EventCategory category = eventCategoryService.getCategoryById(categoryId); +``` + +Next, we'll create a separate `Event` index for `ROLE_ORGANIZER` so that they +can see the events they have created, which will live at `/events/organizer`. +It reuses the `/events/index.html` template but passes different data in. Add +the following method: + +```java + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @GetMapping("organizer") + public String displayOrganizerEvents(Model model) { + + model.addAttribute("title", "My Organizer Events"); + model.addAttribute("events", eventService.getAllEventsByCurrentUser()); + + return "events/index"; + } +``` + +For the rest of CRUD, add the same `@PreAuthorize` annotation so that only +`ROLE_ORGANIZER` can create and delete events. + +In `displayCreateEventForm`, we want to pass all of the categories to the view +so that organizers can use categories created by others. + +```java{hl_lines="1 6"} + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @GetMapping("create") + public String displayCreateEventForm(Model model) { + model.addAttribute("title", "Create Event"); + model.addAttribute(new EventDTO()); + model.addAttribute("categories", eventCategoryService.getAllCategories()); + return "events/create"; + } +``` + +```java{hl_lines="1 7"} + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid EventDTO newEventDto, + Errors errors, Model model) { + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + model.addAttribute("categories", eventCategoryService.getAllCategories()); + return "events/create"; + } + + eventService.save(newEventDTO); + return "redirect:/events"; + } +``` + +```java{hl_lines="1"} + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @GetMapping("delete") + public String displayDeleteEventForm(Model model) { +``` + +```java{hl_lines="1"} + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @PostMapping("delete") + public String processDeleteEventsForm(@RequestParam(required = false) int[] eventIds) { +``` + +Last, we want our `displayEventDetails` handler to show any event id that is +passed. Update the method to retrieve the event without checking the current +user: + +```java{ hl_lines="4" } + @GetMapping("detail") + public String displayEventDetails(@RequestParam Integer eventId, Model model) { + try { + Event event = eventService.getEventById(eventId); + + model.addAttribute("title", event.getName() + " Details"); + model.addAttribute("event", event); + model.addAttribute("userAttendance", eventService.getUserEventAttendance(event)); + } catch (ResourceNotFoundException ex) { + model.addAttribute("title", "Invalid Event ID: " + eventId); + } + + return "events/detail"; + } +``` + + +{{% notice blue Note "rocket" %}} +The `detail` template shows the `Tag` entries associated with an `Event`. +Consider how you might display the tags to all users but restrict the ability +to add tags to `ROLE_ORGANIZER` only. +{{% /notice %}} + +### Update Navigation by Role + +Depending on which role is logged-in, we can selectively display certain content +versus another. [The Spring Security Thymeleaf documentation](https://www.thymeleaf.org/doc/articles/springsecurity.html) +has some background on the `sec:authorize` attribute that we can use. It is +similar to the `th:if` attribute that will selectively include an HTML element +depending on the condition. + +We will use one of the following: + +1. `sec:authorize="isAuthenticated()"` +1. `sec:authorize="hasRole('ROLE_ORGANIZER')"` + +Let's redesign our navbar to show all `ROLE_USER` links first, and then hide +`ROLE_ORGANIZER` links in some dropdown menus. + +In order for Dropdown menus to work in Bootstrap, we need to include PopperJS +as a dependency of Bootstrap in `fragments.html`: + +```html{hl_lines="5"} + + + Coding Events + + + + + +``` + +Next, let's rework the nav list in `fragments.html` to use `sec:authorize`: + +```html{hl_lines="1 5-20"} + + +``` + +That should take care of it! Be sure to test at this point to verify that users +can mark their attendance and that they do not have access to routes like +`/events/create` which are reserved for `ROLE_ORGANIZER`. Also test that a user +with `ROLE_ORGANIZER` can access all of the restricted routes and see the +updated navigation. diff --git a/content/authentication/next-steps/bonus-module/spring-security/_index.md b/content/authentication/next-steps/bonus-module/spring-security/_index.md new file mode 100644 index 0000000..3e01bd5 --- /dev/null +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -0,0 +1,581 @@ +--- +title: "Spring Security Framework" +date: 2023-11-24T10:28:42-06:00 +draft: false +weight: 4 +originalAuthor: Ben Clark # to be set by page creator +originalAuthorGitHub: brclark # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +Up to this point, we have set up user authentication mostly on our own. The +`AuthenticationFilter` class hooks in to the request handler logic and will +prevent the request from accessing certain routes unless there is a HTTP +session key with a logged-in user. We manually set the session key during login. +The benefit of doing this logic manually was to learn the basics of user +authentication and how it can be implemented in the HTTP protocol. + +This lesson will introduce more features of the Spring Security Framework to +handle user authentication. It will provide us classes and interfaces that are +defined within the `org.springframework.security.*` packages. The benefit of +learning and implementing this framework is a more robust set of security +and authentication features. It will require us to learn from documentation +and implement interfaces defined by Spring Security. + +This walkthrough does not make use of all Spring Security features, and there +is much left that can be improved. Take this as an introduction to the more +advanced Spring Security framework. + +{{% notice blue Note "rocket" %}} +The Spring Security documentation can be found +[here](https://docs.spring.io/spring-security/reference/servlet/index.html). +This framework has +evolved and changed from version to version. We will be implementing the +Spring Security 6 framework. + +Another helpful resource in preparing this tutorial was the +[Baeldung blog](https://www.baeldung.com/spring-security-authentication-and-registration), +which provides walkthroughs on a number of Spring related features and +frameworks. +{{% /notice %}} + +## Implementing Spring Security 6 for Authentication + +{{% notice blue Note "rocket" %}} +The code for this section begins with the +[user-roles-privileges branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/user-roles-privileges) +and ends with the +[spring-security-features branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/spring-security-features) +of the `CodingEventsJava` repository. +{{% /notice %}} + +To begin, we need to make sure that our Gradle project includes the necessary +dependencies for Spring Security 6. + +Update the `build.gradle` file to include the following packages in the +`dependencies` object. You will also need to **remove** the +`spring-security-crypto:5.5.1` package + +```groovy + // Remove implementation("org.springframework.security:spring-security-crypto:5.5.1") + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'org.springframework.boot:spring-boot-starter-security' +``` + +This lesson will consist of four parts of implementation to adopt the +Spring Security framework: + +1. Refactor `UserService` to implement Spring Security `UserDetailsService` +interface methods that take care of authentication and user session +1. Remove `AuthenticationFilter` and `WebApplicationConfig` to replace with +Spring Security 6 `WebSecurity` class, which handles authorization and +request filtering +1. Refactor other controllers and services to make use of updated `UserService` +1. Improve error pages and error redirection + +### Authentication in Spring Security 6 + +Before diving in, take a look at the [Spring Security Authentication +documentation](https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html), +which describes the classes and components that implement the +behaviors we want to use. + +We are going to implement username/password verification using the +`UserDetailsService` interface. + +#### Implementing `UserDetailsService` + +The `UserDetailsService` implementation returns a `UserDetails` object which +is a Spring Security representation of username, password, and granted user +authorities. We are going to refactor our `UserService` class to implement +a custom `IUserService` interface that we create. Our `IUserService` interface +will extend the `UserDetailsService` interface that we are required to use for +Spring Security. + +```mermaid{ zoom="false" } +classDiagram + UserDetailsService <|-- IUserService : extends + IUserService <|.. UserService : implements + class UserDetailsService{ + loadUserByUsername(String username) UserDetails + } + class IUserService{ + findByUsername(String username) User + save(RegisterFormDTO registration) User + getCurrentUser() User + findById(Integer id) Optional~User~ + findAll() List~User~ + deleteUser(Integer id) User + } + class UserService{ + loadUserByUsername(String username) UserDetails + save(RegisterFormDTO registration) User + getCurrentUser() User + findByUsername(String username) User + findById(Integer id) Optional~User~ + findAll() List~User~ + deleteUser(Integer id) User + } +``` + +Create a new *interface* in the `codingevents.services` package named +`IUserService`. + +This interface should **extend** the `UserDetailsService` interface, which +is found in the `org.springframework.security.core.userdetails` package. + +`IUserService` should include two method definitions, which mirror the +methods we have already implemented in `UserService`. + +1. `User findByUsername(String username);` +1. `User save(RegisterFormDTO registration);` + +All together, our new interface should look like this: + +```java +import org.springframework.security.core.userdetails.UserDetailsService; + +public interface IUserService extends UserDetailsService { + User findByUsername(String username); + User getCurrentUser(); + User save(RegisterFormDTO registration); + Optional findById(Integer id); + List findAll(); + User deleteUser(Integer id); +} +``` + +Next, we will refactor our `UserService` class to properly implement +the method required by `UserDetailsService` interface. + +First, modify your `UserService` class to be an implementer of the +interface `IUserService`, and add the `@Transactional` annotation as well. +This annotation will make sure that each `UserService` method completes all +of it's actions successfully or none of them will take effect. + +```java +@Service +@Transactional +public class UserService implements IUserService { +``` + +Once we add this interface, we will get an error saying that we must provide an +implementation of `loadUserByUsername` from the `UserDetailsService` interface. + +This method will take in a username string and it needs to return a +`UserDetails` object. For us, that will mean returning a `User` instance from +the `org.springframework.security.core.userdetails` package. The [documentation +for this constructor](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/User.html#%3Cinit%3E(java.lang.String,java.lang.String,java.util.Collection)) +shows that we need the username, the password hash, and a +collection of the granted authorities for this user. + +Add the following method to your `UserService` class. Notice the call to the +`getAuthorities` method that is not yet implemented. + +```java + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("Invalid username or password"); + } + return new org.springframework.security.core.userdetails.User(user.getUsername(), + user.getPwHash(), + getAuthorities()); + } +``` + +For now, we are going to implement the `getAuthorities` method to return the +same authority for all users which is `ROLE_USER`. In the next lesson, we will +make sure that the granted authorities are correct based on the actual role of +each user. + +This helper method will return the authorities in the datatype required by the +`UserDetails` instance, which is `Collection`. Add +the `getAuthorities` method to the `UserService` class as well, preferably +at the bottom as it is a `private` method. + +```java + private Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + } +``` + +Lastly, we can update our `getCurrentUser` method to use the Spring Security +framework for retrieving the current user from the `SecurityContextHolder` +mentioned in the Authentication documentation, which tracks the currently +authenticated user in an `Authentication` object. + +Let's modify our `getCurrentUser` method to make use of the framework: + +```java + public User getCurrentUser() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + return null; + } + return findByUsername(auth.getName()); + } +``` + +### Authorization and Request Filtering with `WebSecurity` + +Our current implementation of request filtering has done a good job of teaching +us about the basics of user authentication. We currently implement the +`HandlerInterceptor` interface so that we can check if a user is stored in the +`HttpSession` before a request is handled by our controllers. If there is not +an authenticated user stored in session, we redirect them to the login page. + +Spring Security 6 framework has some built-in annotations and methods that will +do the same logic but will handle the user authentication and session management +internally. + +Take a look at the [Spring Security documentation](https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html) +that talks about how we will authorize each request. With the authentication +that we set up in the previous section, the `HttpSecurity` instance can be used +to setup request filtering. + +To begin, we can **remove** the `AuthenticationFilter` class entirely, as well +as the `WebApplicationConfig` class. + +Next, create a new class `WebSecurity` in the `security` package. This class +will set up the `SecurityFilterChain` described in the [Authorizing +Requests](https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html#authorize-requests) +section of the documentation. + +This class should also include `UserService` and `PasswordEncoder` autowired +fields. + +```java +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@EnableTransactionManagement +public class WebSecurity { + + @Autowired + private UserService userService; + + @Autowired + private PasswordEncoder passwordEncoder; +} +``` + +1. `@Configuration` annotation lets Spring know that this class will set up +a `@Bean` +1. `@EnableWebSecurity` annotation lets Spring know that this class will set +up the `SecurityFilterChain` bean +1. `@EnableMethodSecurity` annotation gives the ability to use some helpful +annotations to secure specific controller methods +1. `@EnableTransactionManagement` annotation allows our Spring project to use +the `@Transactional` annotation elsewhere + +To set up the `SecurityFilterChain`, we will use method chaining to build an +`http` object that contains filtering rules for requests, meaning which +routes are permitted and which ones require authentication, +as well as error routes. We build this filter chain using a design pattern known +as the **builder pattern**. This [helpful article](https://www.springcloud.io/post/2023-03/spring-security-design-patterns/#gsc.tab=0) +lists some of different design patterns used in the Spring framework, for more +context. + +Add the following method to the `WebSecurity` class: + +```java + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/login", + "/register", + "/logout", + "/error", + "/css/**", + "/js/**", + "/img/**").permitAll() + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.disable()) + .logout(logout -> logout + .logoutUrl("/logout") + .invalidateHttpSession(true) + .clearAuthentication(true) + .logoutSuccessUrl("/login") + .permitAll() + ) + .securityContext((securityContext) -> securityContext + .securityContextRepository(securityContextRepository()) + .requireExplicitSave(true) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(authenticationEntryPoint()) + ); + return http.build(); + } +``` + +With each of the methods in this method chaining sequence, we are setting up +a portion of the security filter. The `exceptionHandling` and `securityContext` +methods require a custom `@Bean` method and class so that we can access the +objects in another class. + +First, we'll add a `SecurityContextRepository` bean which will implement the +storage of our `SecurityContext` across different requests, so that we can +remember who is logged in. Think of this as storing the `SecurityContext` which +contains our `Authentication` information in the session. + +Add the following method to the `WebSecurity` class: + +```java + @Bean + public SecurityContextRepository securityContextRepository() { + return new DelegatingSecurityContextRepository( + new RequestAttributeSecurityContextRepository(), + new HttpSessionSecurityContextRepository() + ); + } +``` + +Next, add the `authenticationEntryPoint` method to the `WebSecurity` class: + +```java + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new CustomAuthenticationEntryPoint(); + } +``` + +After adding this method, we'll need to create the +`CustomAuthenticationEntryPoint` which will be responsible for sending errors +to either `/login` route for unauthenticated requests or `/error` route +otherwise. + +Create this new class in the `security` package. + +```java +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException, ServletException { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + response.sendRedirect(request.getContextPath() + "/login"); + } else { + response.sendRedirect(request.getContextPath() + "/error/403"); + } + } +} +``` + +Our last step to enable authorization in Spring Security is to set up our +custom `AuthenticationManager` which will use our `UserService` and +`PasswordEncoder` instances (from the autowired fields) to get the +`UserDetails` instance. + +Add this last method to the `WebSecurity` class: + +```java + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = + http.getSharedObject(AuthenticationManagerBuilder.class); + + authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder); + + return authenticationManagerBuilder.build(); + } +``` + +### Refactoring `AuthenticationController` + +Recall that we have plugged in to the Spring Security framework by using +`UserDetailsService` interface and relying on `Authentication` objects +from `SecurityContextHolder`. + +In the `AuthenticationController` we can now remove some previous methods +we needed when we were managing users in session. + +First, add a field for the `AuthenticationManager` and the +`SecurityContextRepository`. + +```java + @Autowired + private AuthenticationManager authManager; + + @Autowired + private SecurityContextRepository securityContextRepository; +``` + +Next, we can **remove** the `getUserFromSession` and `setUserInSession` methods +entirely. + +Lastly, we are going to update the `processLoginForm` to use the +`SecurityContextHolder` and `Authentication` objects for user +session management. The `processLoginForm` is going to completely change with +this refactoring, so you can start by **deleting** the contents of the method. +We will start from scratch (including our error check and the title of the page +in case of errors). Also, add the parameter for `HttpServletResponse` to the +method. + +```java{ hl_lines="4" } + @PostMapping("/login") + public String processLoginForm(@ModelAttribute @Valid LoginFormDTO loginFormDTO, + Errors errors, HttpServletRequest request, + HttpServletResponse response, + Model model) { + model.addAttribute("title", "Log In"); + if (errors.hasErrors()) { + return "login"; + } + } +``` + +The next portion of this method will be a `try/catch` block that protects from +the possibility of an `AuthenticationException`. + +Take a look at the [AuthenticationManager documentation](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/authentication/AuthenticationManager.html) +which has a method `authenticate()`. We will create a Username/Password token +that we send to the `authenticate()` method for username/password validation. +We will also use the `securityContextRepository` field to save the new +`SecurityContext` established by the user login. If we catch an +`AuthenticationException` instance, we will let the user know that the +username or password were incorrect. + +Add the following block to your `processLoginForm` method: + +```java + try { + UsernamePasswordAuthenticationToken token = + UsernamePasswordAuthenticationToken.unauthenticated( + loginFormDTO.getUsername(), + loginFormDTO.getPassword() + ); + Authentication authentication = + authManager.authenticate(token); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + this.securityContextRepository.saveContext(context, request, response); + + return "redirect:"; + } catch (AuthenticationException ex) { + errors.rejectValue("username", "bad.credentials", "Invalid e-mail or password"); + return "/login"; + } +``` + +Last, weirdly enough, we can remove our request handler for logout, as we +included the `/logout` route as part of our security filter setup in +`WebSecurity`. Navigating to `/logout` will invalidate the security session +automatically. + +### Updating Error Handling + +When a user tries to access a route that does not exist, or a resource id +that they don't own, we need to send them to a predesigned error page. We +can do that with exception handling and automatic routing to an error +controller. + +#### Adding `ErrorController` + +In the `controllers` package, create a class named `ErrorController`. This +class will handle `ResourceNotFoundException` and `BadRequestException`. + +```java +@ControllerAdvice +public class ErrorController { + + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public String exception(final ResourceNotFoundException ex, final Model model) { + model.addAttribute("message", ex.getMessage()); + return "error/404"; + } + + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String exception(final BadRequestException ex, final Model model) { + model.addAttribute("message", ex.getMessage()); + return "error/400"; + } + +} +``` + +Next, we need to create the `BadRequestException` in the `exception` package: + +```java +public class BadRequestException extends RuntimeException { + public BadRequestException() { + + } + + public BadRequestException(String message) { + super(message); + } + + public BadRequestException(String message, Throwable cause) { + super(message, cause); + } + + public BadRequestException(Throwable cause) { + super(cause); + } + + public BadRequestException(String message, Throwable cause, boolean enableSuppression, + boolean writeableStackTrace) { + super(message, cause, enableSuppression, writeableStackTrace); + } +} +``` + +The `ErrorController` will handle exceptions and the `/error` route, but we need +to add some views for it to display. + +In the `resources/templates/error/` directory, create a Thymeleaf view named +`400.html`: + +```html + + + + + +
+ +
+

Incorrect data provided

+
+ + + +``` + +Create `403.html` that has the same content except a message that says: + +```html +
+

You have no access to this site

+
+``` + +Create `404.html` that has the following message: + +```html +
+

Resource not found

+
+``` + +That wraps up our Spring Security refactoring. Be sure to test and +make sure that register and login work, as well as proper loading of +and creation of user data. + +The next lesson will add some new roled-based features for users, limiting +access to the roles that users are assigned. + + diff --git a/content/authentication/next-steps/bonus-module/user-data/_index.md b/content/authentication/next-steps/bonus-module/user-data/_index.md new file mode 100644 index 0000000..c81da1f --- /dev/null +++ b/content/authentication/next-steps/bonus-module/user-data/_index.md @@ -0,0 +1,298 @@ +--- +title: "User Owned Data" +date: 2023-11-14T09:28:27-05:00 +draft: false +weight: 1 +originalAuthor: Ben Clark # to be set by page creator +originalAuthorGitHub: brclark # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +With the authentication filter additions, our application now requires users +to log in before they can access any features of the site. However, users +will want the data that they create to be associated with their account. How +can we ensure that the events and categories that a user creates are +associated with their account? + +Users *own* their data when the entities that they create (events, categories, +etc) have their `user_id` associated with each new entity as a foreign key. That +would allow us to, say "get all events for a specific user". + +## Creating User Specific Data + +{{% notice blue Note "rocket" %}} +The code for this section begins with the +[auth-filter branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/auth-filter) +and ends with the +[user-data branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/user-data) +of the `CodingEventsJava` repository. +{{% /notice %}} + +### Updating the models with a `User` field + +We need to set up a **One-To-Many** relationship between the `User` model and +the different data models (`Event`, `EventCategory`, `Tag`). + +Open the `Event` model and add a `User` field to the field definitions. Call it `creator` +and give it a `@ManyToOne` annotation. + +```java + @ManyToOne + private User creator; +``` + +We'll also need to add a getter and setter for `creator`. + +```java + public User getCreator() { + return creator; + } + + public void setCreator(User creator) { + this.creator = creator; + } +``` + +Repeat the above steps to add the `creator` field/getters/setters to +`EventCategory`. + +{{% notice blue Note "rocket" %}} +The `Tag` resource will be your task to update throughout this bonus module. We +will leave hints that you should continue updating that resource but will not +explicitly include instructions on keeping the tag feature updated. + +You should update the `Tag` class as well to track `User creator`. +{{% /notice %}} + +### Saving the `User` when creating new data + +We can now store a `User` as the creator of an `Event`/`EventCategory`/ `Tag`. +Next, we need to make sure that the currently logged-in user is set as the creator +before saving new entries. Let's update the `EventController` to set +the `creator` field in new events. + +To get the currently logged-in user in `EventController`, we need references to +the `AuthenticationController` and the `HttpSession`. + +In `EventController`, add the following below your other autowired fields: + +```java + @Autowired + private AuthenticationController authController; +``` + +With that reference, we can now get the current logged-in user during the POST +handler for create. We need to add a parameter for the incoming `HttpSession` +so that we can get the currently logged in user from the `authController`. If +there are no errors in the form, we set the creator of the `newEvent` to +`currUser`. + +```java{hl_lines="3-4 10"} + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, + Errors errors, Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + return "events/create"; + } + + newEvent.setCreator(currUser); + + eventRepository.save(newEvent); + return "redirect:/events"; + } +``` + +We need to repeat the above steps for the `EventCategoryController` and `TagController`. + +### Retrieving user data from database + +In previous lessons when we created a `@ManyToOne` field, +we have included a corresponding `@OneToMany` reference in +the appropriate model --- for example, the way we define +`@OneToMany List events` in the `EventCategory` model. + +Rather than including `@OneToMany` relationships in the `User` model for events, categories +and tags, we will instead use our `CrudRepository` interfaces and define custom queries that +will achieve what we need. + +{{% notice blue Note "rocket" %}} +You can create custom queries to the database using the intuitive query builder. +We have used the built-in methods before like `findById`, `findAll`, and `save`. +We can create new queries by defining the methods in our repository interfaces. + +For example, defining an `EventRepository` method +`List findAllByCategory(EventCategory category)` gives us the ability to retrieve +all events for a given category from the database. + +More info on the query builder can be found in the [SpringDataJPA docs](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories) +{{% /notice %}} + +Let's add custom queries to the `EventRepository`, `EventCategoryRepository`, +and `TagRepository` interfaces that will allow us to retrieve all entries for a given `User`. + +In `EventRepository` add: + +```java{hl_lines="4-5"} +@Repository +public interface EventRepository extends CrudRepository { + + List findAllByCreator(User creator); + Optional findByIdAndCreator(Integer id, User creator); +} +``` + +`findAllByCreator` will retrieve the list of events for the associated user. +`findByIdAndCreator` will attempt to retrieve an event based on the event ID, +BUT it will give a null response if the corresponding event is not associated with +the `creator` argument. + +Repeat the above steps to add the corresponding methods +to the `EventCategoryRepository` and `TagRepository`. + +### Passing user data to views + +When users look at the tables for "All Events" or "All Categories", they should see +the data that they created. Similarly, when users are creating new events, they should +only see category options that they created. + +Let's update the controllers to retrieve the appropriate user data. This will involve +the `AuthenticationController`, `HttpSession`, and the repository methods we added. + +#### Updating `EventController` + +First, we'll update `displayEvents` method that handles `GET /events?categoryId=` requests. We will receive the `HttpSession` as a param, use it to get the current user with `authController`, +and finally pass the user to the `eventRepository` methods that we created. + +```java{hl_lines="2-3 7 9"} + @GetMapping + public String displayEvents(@RequestParam(required = false) Integer categoryId, Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + + if (categoryId == null) { + model.addAttribute("title", "All Events"); + model.addAttribute("events", eventRepository.findAllByCreator(currUser)); + } else { + Optional result = eventCategoryRepository.findByIdAndCreator(categoryId, currUser); + if (result.isEmpty()) { + model.addAttribute("title", "Invalid Category ID: " + categoryId); + } else { + EventCategory category = result.get(); + model.addAttribute("title", "Events in category: " + category.getName()); + model.addAttribute("events", category.getEvents()); + } + } + + return "events/index"; + } +``` + +In the `displayCreateEventForm` method, we need to pass in the user-created categories instead +of passing in all categories. Update your function like below. + +```java{hl_lines="2-3 6"} + @GetMapping("create") + public String displayCreateEventForm(Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + model.addAttribute("title", "Create Event"); + model.addAttribute(new Event()); + model.addAttribute("categories", eventCategoryRepository.findAllByCreator(currUser)); + return "events/create"; + } +``` + +Don't forget to update the same in the **error** case of the POST request handler. If +there are form errors, we want to pass the user-created categories back to the form. + +```java{hl_lines="7"} + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, + Errors errors, Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + model.addAttribute("categories", eventCategoryRepository.findAllByCreator(currUser)); + return "events/create"; + } + + newEvent.setCreator(currUser); + + eventRepository.save(newEvent); + return "redirect:/events"; + } +``` + +When we display the delete events form, we want to make sure it displays the user-created +events. Let's repeat the same procress to retrieve events for the current user in the +`displayDeleteEventsForm` method. + +```java{hl_lines="2-3 5"} + @GetMapping("delete") + public String displayDeleteEventForm(Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + model.addAttribute("title", "Delete Events"); + model.addAttribute("events", eventRepository.findAllByCreator(currUser)); + return "events/delete"; + } +``` + +Finally, we want to make sure that the event details page will show events owned by the +currently logged in user and reject any event ID's owned by other users. Once again, +we'll retrieve the current user and the event based on its ID and the current user. + +In the `displayEventDetails` method, let's add: + +```java{hl_lines="2-5"} + @GetMapping("detail") + public String displayEventDetails(@RequestParam Integer eventId, Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + + Optional result = eventRepository.findByIdAndCreator(eventId, currUser); + + if (result.isEmpty()) { + model.addAttribute("title", "Invalid Event ID: " + eventId); + } else { + Event event = result.get(); + model.addAttribute("title", event.getName() + " Details"); + model.addAttribute("event", event); + } + + return "events/detail"; + } +``` + +The rest of the function can stay the same. If a valid event ID is provided but +the current user does not own it, then the `Optional` will contain a null value. + +#### Updating `EventCategoryController` + +Let's update the `displayAllCategories` method that handles `GET +/eventCategories` requests. We can add the same `HttpSession` reference and +use `authController` to retrieve the current user. We'll pass the categories +for the current user as the model attribute. + +```java{hl_lines="2-3 5"} + @GetMapping + public String displayAllCategories(Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + model.addAttribute("title", "All Categories"); + model.addAttribute("categories", eventCategoryRepository.findAllByCreator(currUser)); + return "eventCategories/index"; + } +``` + +Luckily, the views for the `Event` and `EventCategory` resources will not need +to be updated. + +The last controller to update is the `TagController`, which we will leave for you +to complete. + +In the next lesson, we will expand the use of **Data Transfer Objects** or DTOs +to decouple our database models from our forms, and we will make use of +**Service** classes as a layer between the **Controller** and **Repository**. + diff --git a/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md new file mode 100644 index 0000000..e136131 --- /dev/null +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -0,0 +1,431 @@ +--- +title: "User Roles & Privileges" +date: 2023-11-08T23:36:10-06:00 +draft: false +weight: 3 +originalAuthor: Ben Clark # to be set by page creator +originalAuthorGitHub: brclark # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +We are going to add some additional features to Coding Events in the next three +lessons. We currently have ability for users to manage their own data. Our next +feature is to bring roles and privileges to our users. We will be able to +configure users as admin, event creator, or event attendee. + +With roles and privileges in place, we are able to restrict certain functions +to specific users. This is a common feature of many applications. Consider how +we might associate users to the roles they hold, and how we might associate +privileges to roles. + +Also, consider that your future apps may not need both roles AND privileges, +and may just require one or the other. There is flexibility in how you design +and implement future projects. + +## Adding Roles & Privileges to Users + +{{% notice blue Note "rocket" %}} +The code for this section begins with the +[add-service-dto branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/add-service-dto) +and ends with the +[user-roles-privileges branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/user-roles-privileges) +of the `CodingEventsJava` repository. +{{% /notice %}} + +This lesson describes how to add new models for `Role` and `Privilege` and +associate them with the `User` model. This lesson will *not* add role-based +functionality. That will be added in another lesson. + +Some definitions: + +- **Privilege**: access to a function or feature, such as `READ_EVENTS` or +`CREATE_EVENTS` +- **Role**: can be assigned to a user and combines multiple privileges in a +distinct role, such as `ROLE_ADMIN` or `ROLE_CREATOR` + +### `Role` & `Privilege` Models + +Our `Role` model will need a database relationship with the `Privilege` model. We +can say that a role can have many privileges, and a privilege can belong to many +roles, giving us a **Many-to-Many** relationship. + +#### Adding the `Privilege` Model + +Create a new `Privilege` in your `models` package and make this class +inherit from `AbstractEntity` and require the `@Entity` annotation. + +This class will only require a `String name` field to track what the privilege +is. Add the field, constructors (including the empty constructor), getter/setter, +and `toString` method for this class. + +In addition to this Model, we need to create some predefined privilege types +for our application, which we can do with an `enum`. We will define privileges +for being able to CRUD events and users. Some users will only have privileges +to *read events* while others will have privileges to *read and create events*. + +Create a new `enum` in `models` package named `PrivilegeType`. + +```java +public enum PrivilegeType { + READ_EVENTS, + CREATE_EVENTS, + DELETE_EVENTS, + READ_USERS, + UPDATE_USERS, + DELETE_USERS +} +``` + +#### Adding the `Role` Model + +Similar to our `Privilege` model, we need to create a `Role` model that +inherits from `AbstractEntity` and defines a field `String name`, along +with the constructors, getter/setter, and `toString` method. + +Create the `Role` class within your `models` package. + +Once you have created this base for the `Role` class, we have to define +the relationship between a `Role` object and `Privilege` object. We are +going to define a **Many-to-Many** relationship, but we are going to do +it slightly differently than we did in the previous chapter. + +Add the following field to the `Role` class after the `name` field definition. + +```java + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "roles_privileges", + joinColumns = @JoinColumn( + name = "role_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn( + name = "privilege_id", referencedColumnName = "id")) + private Collection privileges; +``` + +Here, we are defining the `@ManyToMany` relationship between roles and +privileges, and we are manually defining the *Join Table* that we want +to be created. This requires us to specify which fields in the join +table are foreign keys used to link roles to privileges. It is not +required to define the join table in this way, but it gives us an example +of how we can have more control over the database tables via our ORM +definitions. + +Add a constructor to set the `name` and a no-argument constructor as well. Also, +add a getter/setter for the `privileges` field as well. + +Similar to the `PrivilegeType` definition we added, we need a `RoleType` +enum definition to specify the types of roles our app allows. For now, +we will say that there can be *admin*, *event creator*, and *regular* users. + +Add a new `enum` definition `RoleType` to the `models` package. + +```java +public enum RoleType { + ROLE_USER("User"), + ROLE_ORGANIZER("Organizer"), + ROLE_ADMIN("Admin"); + + private final String displayName; + + RoleType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} +``` + +Admin users will be able to manage user accounts. Event creators will be able +to create new events for the event listing. Regular users will be able to read +the event listing and RSVP to the events they want to attend. + +Lastly, we need a special getter in the `Role` class that allows us to retrieve +the `RoleType` enum from a `Role` object. Add the following method to your `Role` +class: + +```java + public RoleType getType() { + return RoleType.valueOf(name); + } +``` + +#### Associating Users with Roles + +Our `User` model needs the ability to be assigned certain roles. In this regard, +we need to set up a **Many-to-Many** relationship between `User` and `Role`. + +We will make a few other additions to our `User` model as well, such as a +constraint on unique usernames, and a field to store when the user account +was created. + +First, add this `@Table` annotation above the `User` class (after `@Entity`): + +```java +@Table(uniqueConstraints = @UniqueConstraint(columnNames = "username")) +``` + +Next, we will add two new fields: + +```java + private LocalDateTime createDate; + + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable( + name="users_roles", + joinColumns = @JoinColumn( + name = "user_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn( + name = "role_id", referencedColumnName = "id")) + private Collection roles; +``` + +The `createDate` field will store info about when the user account is created. +And similar to our Many-to-Many relationship between `Role` and `Privilege`, +here we have manually set up the join table for our M2M relationship between +`User` and `Role`. + +Next, let's add a new constructor that would allow us to initialize a `User` +object and specify their roles: + +```java + public User(String username, String pwHash, Collection roles) { + this.username = username; + this.pwHash = pwHash; + this.roles = roles; + } +``` + +Our `createDate` field will be populated by a special method that runs +automatically. Add this method after the constructors: + +```java + @PrePersist + public void setUpCreateDate() { + createDate = LocalDateTime.now(); + } +``` + +Notice the `@PrePersist` annotation, which will automatically call this method +before a new `User` instance gets created in the database. + +Lastly, we need a getter/setter for our the `roles` field. Add your getter/ +setter below the other methods. + +### Adding `Role` & `Privilege` Repositories + +Now that we have models defined for our database schema, we need to define a +repository interface for each class so that we can interact with their database +entries. We will define and override some of the repository methods to give more +customized control over the database. + +First, let's create the `PrivilegeRepository` interface in the `data` package: + +```java +@Repository +public interface PrivilegeRepository extends CrudRepository { + + Privilege findByName(String name); +} +``` + +The `findByName` method creates a custom database query that allows us to +provide the name of a privilege and retrieve the `Privilege` object. + +Next, let's create the `RoleRepository` interface in the `data` package. + +```java +@Repository +public interface RoleRepository extends CrudRepository { + + Role findByName(String name); +} +``` + +### Preloading Initial Data + +Until this point, if we needed any data in our database, we had to directly +update the database or use the forms in our app to create new data. Sometimes, +we have relational data that we need preloaded into the database, and we +want to be certain it is added before our application is served to users. + +In this case, we can set up a component that will preload the database with +some specific entries *when the application boots*. This requires us to listen +for an `ApplicationReadyEvent` and trigger some writes to the database when +it occurs. + +Create a new class `InitialDataLoader` within the `data` +package. The following code block introduces some new Spring annotations: + +```java +@Component +@Transactional +public class InitialDataLoader implements ApplicationListener { + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private PrivilegeRepository privilegeRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; +} +``` + +Setting up our `InitialDataLoader` class, it has `@Component` and +`@Transactional` annotations. `@Component` tells Spring to manage this class +instance similar to `@Controller`, and it is necessary so that the class +can handle the `ApplicationListener` event. `@Transactional` is applied to the +whole class and makes sure that our method for loading data either applies +successfully or not at all. + +Next, we'll add a method for initializing data when the application is ready: + +```java + @Override + public void onApplicationEvent(final ApplicationReadyEvent event) { + Privilege createEvents = createPrivilegeIfNotFound(PrivilegeType.CREATE_EVENTS.toString()); + Privilege readEvents = createPrivilegeIfNotFound(PrivilegeType.READ_EVENTS.toString()); + Privilege deleteEvents = createPrivilegeIfNotFound(PrivilegeType.DELETE_EVENTS.toString()); + Privilege readUsers = createPrivilegeIfNotFound(PrivilegeType.READ_USERS.toString()); + Privilege updateUsers = createPrivilegeIfNotFound(PrivilegeType.UPDATE_USERS.toString()); + Privilege deleteUsers = createPrivilegeIfNotFound(PrivilegeType.DELETE_USERS.toString()); + + Role adminRole = createRoleIfNotFound(RoleType.ROLE_ADMIN.toString(), + Arrays.asList(readUsers, updateUsers, deleteUsers)); + Role organizerRole = createRoleIfNotFound(RoleType.ROLE_ORGANIZER.toString(), + Arrays.asList(createEvents, deleteEvents)); + Role userRole = createRoleIfNotFound(RoleType.ROLE_USER.toString(), + Arrays.asList(readEvents)); + + User admin = new User("admin", passwordEncoder.encode("launchcode"), + Arrays.asList(adminRole, organizerRole, userRole)); + + createUserIfNotFound(admin); + } +``` + +We will load entries for the different `Privilege` types, `Role` types, and +create a default admin user. `ROLE_ADMIN` is associated with user CRUD. +`ROLE_ORGANIZER` is associated with event creation and deletion. `ROLE_USER` +is associated with reading events. + +Finally, we need to add the methods that will create the entries if they don't +already exist in the database. Notice how the relationships are set up when +creating a `Role`, by setting the list of `Privilege` objects associated with +that `Role`. + +```java + private User createUserIfNotFound(User user) { + if (userRepository.findByUsername(user.getUsername()) == null) { + userRepository.save(user); + } + return user; + } + + private Privilege createPrivilegeIfNotFound(String name) { + Privilege privilege = privilegeRepository.findByName(name); + if (privilege == null) { + privilege = new Privilege(name); + privilegeRepository.save(privilege); + } + return privilege; + } + + private Role createRoleIfNotFound(String name, Collection privileges) { + Role role = roleRepository.findByName(name); + if (role == null) { + role = new Role(name); + role.setPrivileges(privileges); + roleRepository.save(role); + } + return role; + } +``` + +Now, when you boot your application, these entries will be added to the database +automatically the first time. Test it out. + +### Adding a `SecurityService` + +With roles and privileges added to our database, the last piece to add is +a service that can check whether the current user has a specific role or +privilege. We will not make use of this service in our application yet, but +recognize that this could be used to determine if an action can be taken in the +controller. + +Create a new package named `security` inside the `codingevents` package. Then, +create a new class `SecurityService` inside the `security` package. + +```java +@Service +public class SecurityService { + @Autowired + private UserService userService; +} +``` + +We will add two methods that allow us to check if a user has a certain privilege +or a certain role. Add the following `hasPrivilege` method to this service. + +{{% notice blue Note "rocket" %}} +The `hasPrivilege` method below uses Java streams and lambda expressions to +simplify the code for searching for a matching privilege within a user's +associated roles. Take a look at the [Java docs for Stream](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html) +for more background on this syntax. +{{% /notice %}} + +```java + public boolean hasPrivilege(String privilege) { + final User theUser = userService.getCurrentUser(); + if (theUser == null) { + return false; + } + Boolean hasPrivilege = theUser.getRoles() + .stream() + .map(Role::getPrivileges) + .flatMap(coll -> coll.stream()) + .map(Privilege::getName) + .anyMatch(p -> p.equals(privilege)); + return hasPrivilege; + } +``` + +This method will take a privilege, such as `"READ_EVENTS"`, and check if the +current user has a role with the associated privilege. For each role, we get +the privileges and gather them in a large collection, where we then check to +see if the given argument matches any in the collection. + +Similary, we will add a `hasRole` method to the service as well. + +```java + public boolean hasRole(String role) { + final User theUser = userService.getCurrentUser(); + if (theUser != null) { + return false; + } + Boolean hasRole = theUser.getRoles() + .stream() + .map(Role::getName) + .anyMatch(r -> r.equals(role)); + return hasRole; + } +``` + +This method is similar to `hasPrivilege` but simpler, as we only need to compare +the argument to the user's associated roles. + +With this service in place, we could check in each controller request handler +whether we want to allow the current user to take action. But, Spring provides +a larger framework for us to use in order to handle security, authentication, +and checking user roles & privileges. In the next lesson, we will set up that +framework and plug our existing models into it.