From dc243f0d77a9a4bfc4ca1867bf4ab8fbb13f8242 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 24 Oct 2023 09:52:50 -0500 Subject: [PATCH 01/19] Add new section to Authentication on User Owned Data Added new curriculum text for User Owned Data to the Authentication chapter. First draft so it needs to be edited. This section describes how users can own the data they create in Coding Events. TODO: - Create tutorial video - Edit this section --- .../reading/user-data/_index.md | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 content/authentication/reading/user-data/_index.md diff --git a/content/authentication/reading/user-data/_index.md b/content/authentication/reading/user-data/_index.md new file mode 100644 index 0000000..bc03cae --- /dev/null +++ b/content/authentication/reading/user-data/_index.md @@ -0,0 +1,301 @@ +--- +title: "User Owned Data" +date: 2021-10-01T09:28:27-05:00 +draft: false +weight: 6 +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 make sure that the events and categories that a user create will be +associated with their account? + +Let's associate the new data with the logged-in user using one-to-many database relationships. +We will specify that a `User` has a one-to-many relationship with the `Event` objects +that they create, along with `EventCategory` and `Tag`. This way, we can specify that +we want to load all events or save an event *for a specific user*. + +{{% 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 %}} + +## Creating User Specific Data - Video + +**TODO** + +## Creating User Specific Data - Text + +### Updating the models with a `User` field +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". 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. We will call it `creator` +as it's a reference to the user that created the event, and we will give it a `@ManyToOne` annotation +as there can be many events associated with a single creator. + +```java + @ManyToOne + private User creator; +``` + +We'll also need to add a getter and setter. Add the following to your getters +and setters in `Event` class. + +```java + public User getCreator() { + return creator; + } + + public void setCreator(User creator) { + this.creator = creator; + } +``` + +`Event` is not the only model that we want to be user-owned. Let's repeat the above steps to +add the `creator` field/getters/setters to `EventCategory` and `Tag`. + +### Saving the `User` when creating new data + +Now that we can store the associated User as the creator of an Event/Category/Tag, we need +to make sure that the current logged-in User is set as the creator before saving new entries. +Let's update the `EventController` to get the currently logged in user when creating a new +event and setting that user as the `creator`. + +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. + +```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 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`. + +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"; + } +``` + +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 use `HttpSession` and `AuthenticationController` to retrieve the current user, +and we'll retrieve 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); +``` + +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"; + } +``` + +Next, we'll update the `processCreateEventCategoryForm` method that handles +`POST /eventCategories/create` requests, by getting a reference to the current +user and setting the `creator` field in the new `eventCategory`. + +```java(hl_lines=3 10-11) + @PostMapping("create") + public String processCreateEventCategoryForm(@Valid @ModelAttribute EventCategory eventCategory, + Errors errors, Model model, HttpSession session) { + + if (errors.hasErrors()) { + model.addAttribute("title", "Create Category"); + return "eventCategories/create"; + } + + User currUser = authController.getUserFromSession(session); + eventCategory.setCreator(currUser); + + eventCategoryRepository.save(eventCategory); + return "redirect:/eventCategories"; + } +``` + +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**. + From 7204f6e7641950e3989c68bddc2e427a023a2ef0 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Thu, 26 Oct 2023 11:15:13 -0500 Subject: [PATCH 02/19] Add section for DTOs & Services to Authentication - Add content for DTOs and Services - TODO: - Add video content - Complete rest of code description --- .../reading/add-service-dto/_index.md | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 content/authentication/reading/add-service-dto/_index.md diff --git a/content/authentication/reading/add-service-dto/_index.md b/content/authentication/reading/add-service-dto/_index.md new file mode 100644 index 0000000..4e14601 --- /dev/null +++ b/content/authentication/reading/add-service-dto/_index.md @@ -0,0 +1,253 @@ +--- +title: "Data Transfer Objects & Services" +date: 2023-10-17T00:02:24-05:00 +draft: false +weight: 7 +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. +{{% /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 - Video + +**TODO** + +## Adding DTOs & Services to CodingEvents - Text + +### 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. + +Create a new package inside `codingevents` named `dto`, and move your +`RegisterFormDTO` and `LoginFormDTO` into the 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... +} +``` + +### 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: + +* `User findByUsername(String username)`: retrieves `User` from +`UserRepository` by the `username` +* `Optional findById(Integer id)`: expose `UserRepository` +functionality for possible use +* `List findAll()`: expose `UserRepository` functionality +for possible use +* `User deleteUser(Integer id)`: expose `UserRepository` functionality +for possible use +* `User save(RegisterFormDTO registration)`: validates data in +`RegisterFormDTO` and creates a new `User` in `UserRepository` +* `boolean validateUser(User user, String password)`: validate a password +by encoding it and comparing to the encoded `User` password. +* `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. + +```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 `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 {linenos=true} + 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); + + if (user.isEmpty()) { + return null; + } + + return user.get(); + } +``` + +### Adding `EventService` and `EventCategoryService` + +We will add a service "layer" to our design that will be responsible for +translating DTOs to Models and communication between the From 490198c11a15049d40828531ea2d0c7099b49d95 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 14 Nov 2023 10:18:50 -0600 Subject: [PATCH 03/19] Move PasswordEncoder to a new configuration and refactor User The PasswordEncoder is no longer stored inside the User model as a static instance. It has been moved to a configuration class and is a managed bean of the Spring framework. --- .../reading/add-service-dto/_index.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/content/authentication/reading/add-service-dto/_index.md b/content/authentication/reading/add-service-dto/_index.md index 4e14601..322ce44 100644 --- a/content/authentication/reading/add-service-dto/_index.md +++ b/content/authentication/reading/add-service-dto/_index.md @@ -143,6 +143,57 @@ public class EventCategoryDTO { } ``` +### 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 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. + +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 From 31e8cdc5a30b5c3335a937bcb7f405c71c598ee4 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Thu, 9 Nov 2023 12:07:38 -0600 Subject: [PATCH 04/19] Add text description for EventService and EventCategoryService - Added fixes for UserService description - Add descriptions for EventService and EventCategoryService - Add description to refactor controllers to use services --- .../reading/add-service-dto/_index.md | 413 +++++++++++++++++- 1 file changed, 410 insertions(+), 3 deletions(-) diff --git a/content/authentication/reading/add-service-dto/_index.md b/content/authentication/reading/add-service-dto/_index.md index 322ce44..039c0e5 100644 --- a/content/authentication/reading/add-service-dto/_index.md +++ b/content/authentication/reading/add-service-dto/_index.md @@ -247,7 +247,9 @@ 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. +`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) { @@ -270,15 +272,66 @@ Our `UserService` needs to expose some of the basic functionalities of the } ``` +#### 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); + } +} +``` + #### 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 {linenos=true} +```java public User getCurrentUser() { ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpSession session = attr.getRequest().getSession(true); @@ -301,4 +354,358 @@ Add this method to your `UserService` below the fields. ### Adding `EventService` and `EventCategoryService` We will add a service "layer" to our design that will be responsible for -translating DTOs to Models and communication between the +translating DTOs to Models and communication between the `EventController` and +`EventRepository` + +#### `EventService` + +Let's create another class in the `services` package named `EventService`. + +```java +@Service +public class EventService { + + @Autowired + private EventRepository eventRepository; + + @Autowired + private EventCategoryRepository categoryRepository; + + @Autowired + private UserService userService; + +} +``` + +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. + +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. + +```java + public List getAllEvents() { + return (List) eventRepository.findAll(); + } + + public List getAllEventsByCreator(User creator) { + return eventRepository.findAllByCreator(creator); + } + + 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 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()).get()); + + event.setCreator(userService.getCurrentUser()); + + eventRepository.save(event); + + return event; + } +``` + +#### `EventCategoryService` + +Similar to our `EventService`, we must add methods to the `EventCategoryService` +to expose functionality of the `EventCategoryRepository` to the controllers. + +```java +@Service +public class EventCategoryService { + + @Autowired + private EventCategoryRepository categoryRepository; + + @Autowired + private UserService userService; + + public List getAllCategories() { + return (List) categoryRepository.findAll(); + } + + public List getAllCategoriesByCreator(User creator) { + return categoryRepository.findAllByCreator(creator); + } + + public List getAllCategoriesByCurrentUser() { + return categoryRepository.findAllByCreator(userService.getCurrentUser()); + } + + public EventCategory getCategoryById(int id) { + return categoryRepository.findById(id).orElseThrow(ResourceNotFoundException::new); + } + + public EventCategory getCategoryByIdAndCreator(int id, User creator) { + return categoryRepository.findByIdAndCreator(id, creator).orElseThrow(ResourceNotFoundException::new); + } + + public EventCategory save(EventCategoryDTO categoryDTO) { + EventCategory category = new EventCategory(); + category.setName(categoryDTO.getName()); + category.setCreator(userService.getCurrentUser()); + + categoryRepository.save(category); + + return category; + } + +} +``` + +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 + +For now, we will continue to use `authController` to retrieve the current user +in the controller. In the next lesson, we will remove all references to +`authController` in `EventController` and `EventCategoryController`, which is +how the sequence diagram above is organized. + +#### `EventCategoryController` + +Let's start by refactoring our `EventCategoryController` to use the +`EventCategoryService` and `EventCategoryDTO`. + +Change the `EventCategoryRepository` field to be `EventCategoryService`, like below: + +```java + @Autowired + private EventCategoryService eventCategoryService; +``` + +Now, we can refactor all references to the `eventCategoryRepository` to be +`eventCategoryService` references and we can use `EventCategoryDTO` in the +create form. + +```java {hl_lines="5"} + @GetMapping + public String displayAllCategories(Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + model.addAttribute("title", "All Categories"); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCreator(currUser)); + return "eventCategories/index"; + } +``` + +```java {hl_lines="3"} + @GetMapping("create") + public String renderCreateEventCategoryForm(Model model) { + model.addAttribute("title", "Create Category"); + model.addAttribute(new EventCategoryDTO()); + return "eventCategories/create"; + } +``` + +```java {wrap="true" hl_lines="2 10"} + @PostMapping("create") + public String processCreateEventCategoryForm(@Valid @ModelAttribute EventCategoryDTO eventCategoryDto, + Errors errors, Model model, HttpSession session) { + + if (errors.hasErrors()) { + model.addAttribute("title", "Create Category"); + return "eventCategories/create"; + } + + eventCategoryService.save(eventCategoryDto); + return "redirect:/eventCategories"; + } +``` + +#### `EventController` + +Similar to above, let's start by changing the repository fields to service fields, like below: + +```java + @Autowired + private EventService eventService; + + @Autowired + private EventCategoryService eventCategoryService; +``` + +Now to update the request handlers to use `eventService` and `eventCategoryService`. +For now we will add a `try/catch` block to catch `ResourceNotFoundException` if the +category ID is invalid. + +```java {hl_lines="7 9-16"} + @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", eventService.getAllEventsByCreator(currUser)); + } else { + try { + EventCategory category = eventCategoryService.getCategoryByIdAndCreator(categoryId, currUser); + + 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"; + } +``` + +```java {hl_lines="5-6"} + @GetMapping("create") + public String displayCreateEventForm(Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + model.addAttribute("title", "Create Event"); + model.addAttribute(new EventDTO()); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCreator(currUser)); + return "events/create"; + } +``` + +```java {hl_lines="2 7 11"} + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid EventDTO newEventDto, + Errors errors, Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCreator(currUser)); + return "events/create"; + } + + eventService.save(newEventDto); + return "redirect:/events"; + } +``` + +```java {hl_lines="5 14"} + @GetMapping("delete") + public String displayDeleteEventForm(Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + model.addAttribute("title", "Delete Events"); + model.addAttribute("events", eventService.getAllEventsByCreator(currUser)); + 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"; + } +``` + +```java {hl_lines="5-12"} + @GetMapping("detail") + public String displayEventDetails(@RequestParam Integer eventId, Model model, HttpSession session) { + User currUser = authController.getUserFromSession(session); + + try { + Event event = eventService.getEventByIdAndCreator(eventId, currUser); + + model.addAttribute("title", event.getName() + " Details"); + model.addAttribute("event", event); + } catch (ResourceNotFoundException ex) { + model.addAttribute("title", "Invalid Event ID: " + eventId); + } + + return "events/detail"; + } +``` + +#### 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"} +
+
+ + +
+ +
+``` + +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. From 7bf6a317c7ff286c8e010936617376ea15485bb4 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 14 Nov 2023 10:45:06 -0600 Subject: [PATCH 05/19] Add missing methods in UserService description Description was missing explanation of `save` and `validateUser` methods. Added explanations and code snippets. --- .../reading/add-service-dto/_index.md | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/content/authentication/reading/add-service-dto/_index.md b/content/authentication/reading/add-service-dto/_index.md index 039c0e5..f55567a 100644 --- a/content/authentication/reading/add-service-dto/_index.md +++ b/content/authentication/reading/add-service-dto/_index.md @@ -173,7 +173,7 @@ public class EncoderConfig { 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 the `User` class. +that we were using in the `User` class. #### Refactoring `User` model @@ -321,7 +321,72 @@ public class ResourceNotFoundException extends RuntimeException { } ``` -#### Add `getCurrentUser()` method +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. From 7f0373c7141e9e4668a339db454211a2cda6c383 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Mon, 13 Nov 2023 14:20:58 -0600 Subject: [PATCH 06/19] Add text walkthrough for User Roles & Privileges Add text based content for how to add roles and privileges to Coding Events and associate them to users. This content does not include any changes to functionality. --- .../reading/user-roles-privileges/_index.md | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 content/authentication/reading/user-roles-privileges/_index.md diff --git a/content/authentication/reading/user-roles-privileges/_index.md b/content/authentication/reading/user-roles-privileges/_index.md new file mode 100644 index 0000000..b94da33 --- /dev/null +++ b/content/authentication/reading/user-roles-privileges/_index.md @@ -0,0 +1,250 @@ +--- +title: "User Roles & Privileges" +date: 2023-11-08T23:36:10-06:00 +draft: false +weight: 8 +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 - VIDEO + +**TODO** + +## Adding Roles & Privileges to Users - TEXT + +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, 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 a constructor, 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 getter/setter for this `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); + + @Override + void delete(Privilege privilege); +} +``` + +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); + + @Override + void delete(Role role); +} +``` + + From cad1240d8ebf6343892228f2d4e03a3b0f7b60a6 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 14 Nov 2023 12:19:12 -0600 Subject: [PATCH 07/19] Add InitialDataLoader description to User Roles & Privileges Added text description for how to preload data in to the database for user roles and privileges. --- .../reading/user-roles-privileges/_index.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/content/authentication/reading/user-roles-privileges/_index.md b/content/authentication/reading/user-roles-privileges/_index.md index b94da33..f4d1423 100644 --- a/content/authentication/reading/user-roles-privileges/_index.md +++ b/content/authentication/reading/user-roles-privileges/_index.md @@ -247,4 +247,105 @@ public interface RoleRepository extends CrudRepository { } ``` +### 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. + +```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; + } +``` From f365c4112f24618d86bd293d94129ac9e6545cd3 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 14 Nov 2023 12:34:25 -0600 Subject: [PATCH 08/19] Move User Roles & Privileges walkthrough to Authentication Next Steps Create a bonus module within "Next Steps" to house the curriculum for user roles and privileges and spring security. This is within the Authentication chapter --- content/authentication/next-steps/_index.md | 5 ++++- .../next-steps/bonus-module/_index.md | 15 +++++++++++++++ .../bonus-module}/add-service-dto/_index.md | 2 +- .../bonus-module}/user-data/_index.md | 4 ++-- .../bonus-module}/user-roles-privileges/_index.md | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 content/authentication/next-steps/bonus-module/_index.md rename content/authentication/{reading => next-steps/bonus-module}/add-service-dto/_index.md (99%) rename content/authentication/{reading => next-steps/bonus-module}/user-data/_index.md (99%) rename content/authentication/{reading => next-steps/bonus-module}/user-roles-privileges/_index.md (99%) diff --git a/content/authentication/next-steps/_index.md b/content/authentication/next-steps/_index.md index a536e77..9aaf65f 100644 --- a/content/authentication/next-steps/_index.md +++ b/content/authentication/next-steps/_index.md @@ -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 will walk through the process of basic authentication with Spring with an MVC application. + +{{% children %}} 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..bf249ef --- /dev/null +++ b/content/authentication/next-steps/bonus-module/_index.md @@ -0,0 +1,15 @@ +--- +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 +--- + +{{% children %}} diff --git a/content/authentication/reading/add-service-dto/_index.md b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md similarity index 99% rename from content/authentication/reading/add-service-dto/_index.md rename to content/authentication/next-steps/bonus-module/add-service-dto/_index.md index f55567a..25e9202 100644 --- a/content/authentication/reading/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -2,7 +2,7 @@ title: "Data Transfer Objects & Services" date: 2023-10-17T00:02:24-05:00 draft: false -weight: 7 +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 diff --git a/content/authentication/reading/user-data/_index.md b/content/authentication/next-steps/bonus-module/user-data/_index.md similarity index 99% rename from content/authentication/reading/user-data/_index.md rename to content/authentication/next-steps/bonus-module/user-data/_index.md index bc03cae..ee87a2a 100644 --- a/content/authentication/reading/user-data/_index.md +++ b/content/authentication/next-steps/bonus-module/user-data/_index.md @@ -1,8 +1,8 @@ --- title: "User Owned Data" -date: 2021-10-01T09:28:27-05:00 +date: 2023-11-14T09:28:27-05:00 draft: false -weight: 6 +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 diff --git a/content/authentication/reading/user-roles-privileges/_index.md b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md similarity index 99% rename from content/authentication/reading/user-roles-privileges/_index.md rename to content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md index f4d1423..4ffaa60 100644 --- a/content/authentication/reading/user-roles-privileges/_index.md +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -2,7 +2,7 @@ title: "User Roles & Privileges" date: 2023-11-08T23:36:10-06:00 draft: false -weight: 8 +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 From 2e5304d51f7b08f34c01a3c49ab223c928702774 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Fri, 24 Nov 2023 10:26:06 -0600 Subject: [PATCH 09/19] Add SecurityService to User Roles & Privileges Add description for initial implementation of `SecurityService` to User Roles & Privileges curriculum. --- .../user-roles-privileges/_index.md | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) 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 index 4ffaa60..c050c92 100644 --- a/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -320,7 +320,9 @@ create a default admin user. `ROLE_ADMIN` is associated with user CRUD. 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. +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) { @@ -349,3 +351,85 @@ already exist in the database. 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 here 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 AuthenticationController authController; +} +``` + +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 ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + final HttpSession session = attr.getRequest().getSession(false); + final User theUser = authController.getUserFromSession(session); + if (session != null && theUser != null) { + Boolean hasPrivilege = theUser.getRoles() + .stream() + .map(Role::getPrivileges) + .flatMap(coll -> coll.stream()) + .map(Privilege::getName) + .anyMatch(p -> p.equals(privilege)); + return hasPrivilege; + } + return false; + } +``` + +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 ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + final HttpSession session = attr.getRequest().getSession(false); + final User theUser = authController.getUserFromSession(session); + if (session != null && theUser != null) { + Boolean hasRole = theUser.getRoles() + .stream() + .map(Role::getName) + .anyMatch(r -> r.equals(role)); + return hasRole; + } + return false; + } +``` + +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. From a1c049ea366799d98b00fb1a3371b9fc7b9db3f7 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Thu, 7 Dec 2023 00:04:30 -0600 Subject: [PATCH 10/19] Add Spring Security 6 instruction set for Authentication Bonus Adds content for Spring Security bonus module: - Instructions on using spring security framework for user authentication - Instructions for spring security authorization framework using security context filters --- .../bonus-module/spring-security/_index.md | 727 ++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 content/authentication/next-steps/bonus-module/spring-security/_index.md 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..f7dec60 --- /dev/null +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -0,0 +1,727 @@ +--- +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](#). 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](#), +which provides walkthroughs on a number of Spring related features and +frameworks. +{{% /notice %}} + +## Implementing Spring Security 6 for Authentication - VIDEO + +**TODO** + +## Implementing Spring Security 6 for Authentication - TEXT + +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 + +First, it is important to 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 { align="left" zoom="false" } +classDiagram + UserDetailsService <|-- IUserService + IUserService <|.. UserService + <> UserDetailsService{ + +loadUserByUsername(String username) : UserDetails + } + <> 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.security` 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`. + +* `User findByUsername(String username);` +* `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(); + } +``` + +In the next section, our task will be to update our controllers to use the new +`UserService` and make use of the `Authentication` in the +`AuthenticationController`. + +### Refactoring Controllers & Services + +We have to update the following classes in this section: + +1. `AuthenticationController` to use `AuthenticationManager` for login +1. `SecurityService` to use `UserService` and `SecurityContextHolder` +1. `EventCategoryService` to use updated `getCurrentUser` +1. `EventCategoryController` to use updated service +1. `EventService` to use updated `getCurrentUser` +1. `EventController` to use updated service + +#### Update `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): + +```java + @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"; + } + } +``` + +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. + +#### Update `EventCategoryController` + +To update `EventCategoryController`, we want to remove the reference to +`authController` that is used to retrieve the current user. Instead we'll +make a call to a `eventCategoryService` method that assumes the current +user. + +In `EventCategoryController`, remove the field that references +`AuthenticationController`, and update the `displayAllCategories` method to +call `eventCategoryService.getAllCategoriesByCurrentUser()`, like below: + +```java + @GetMapping + public String displayAllCategories(Model model, HttpSession session) { + model.addAttribute("title", "All Categories"); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCurrentUser()); + return "eventCategories/index"; + } +``` + +#### Update `EventController` + +Our goal for `EventController` will be the same as `EventCategoryController` +--- we want to remove all references to `AuthenticationController` and the +code that was necessary to grab the current user. Instead, we will make +calls to `eventService` and `eventCategoryService` methods that assume the +current user. + +First, **remove** the `AuthenticationController` autowired field. + +Next, we need one new method in `EventCategoryService` for retrieving a +category by an id but also assuming the current user. We want to prevent +the ability for a user to look up any event category instance by id. + +Add the following method to `EventCategoryService`: + +```java + public EventCategory getCategoryByIdForCurrentUser(int id) { + return getCategoryByIdAndCreator(id, userService.getCurrentUser()); + } +``` + +Now we can update the `EventController displayEvents` method to use +`eventService` and `eventCategory` service only: + +```java + @GetMapping + public String displayEvents(@RequestParam(required = false) Integer categoryId, Model model, HttpSession session) { + 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"; + } +``` + +Update the other methods to use `eventCategoryService.getEventsByIdForCurrentId()`. +For the `displayEventDetails` method, we need to add a similar method to the +`EventService` class: + +```java + public Event getEventByIdForCurrentUser(int id) { + return getEventByIdAndCreator(id, userService.getCurrentUser()); + } +``` + +With that method, we can update `displayEventDetails` to use +`eventService.getEventByIdForCurrentUser(eventId)` like the lines below: + +```java + @GetMapping("detail") + public String displayEventDetails(@RequestParam Integer eventId, Model model, HttpSession session) { + try { + Event event = eventService.getEventByIdForCurrentUser(eventId); +``` + +### Updating Error Handling & `SecurityService` + +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

+
+``` + +#### Updating `SecurityService` + +Our last miscellaneous task is to update the `SecurityService` class to use +the Security framework. We do not currently use this service for any +functionality but its methods can be used for role authorization if needed. + +Our goal is to remove the use of `AuthenticationController` and instead use the +`SecurityContext` and `UserService` for our needs. Replace the +`AuthenticationController` field with `UserService`: + +```java + @Autowired + private UserService userService; +``` + +The `hasPrivilege` method will be completely refactored, so you can delete the +body of the method. We will use the `SecurityContextHolder` to retrieve any +currently stored `Authentication` object and use that to get the current `User` +and their privileges: + +```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; + } +``` + +Update the `hasRole` method similarly: + +```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; + } +``` + +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. + + From 3ff0e31ee068bbc3fdb6e36779a6d33683e15561 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Fri, 8 Dec 2023 17:47:41 -0600 Subject: [PATCH 11/19] Add Role Based Access instructions Create user attendance relationship and UI CRUD Add `PreAuthorize` annotation to controllers to restrict access based on role Update navigation to reflect the current user's role --- .../bonus-module/role-based-access/_index.md | 584 ++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 content/authentication/next-steps/bonus-module/role-based-access/_index.md 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..744e3fb --- /dev/null +++ b/content/authentication/next-steps/bonus-module/role-based-access/_index.md @@ -0,0 +1,584 @@ +--- +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 - VIDEO + +**TODO** + +## Adding Role-Based Features for Users - TEXT + +The first portion of this lesson will be 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 rsvped 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 someone can successfully register as a different user type with +`ROLE_ORGANIZER` 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: + +```java{hl_lines="1"} + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @GetMapping("create") + public String displayCreateEventForm(Model model) { +``` + +```java{hl_lines="1"} + @PreAuthorize("hasRole('ROLE_ORGANIZER')") + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid EventDTO newEventDto, + Errors errors, Model model) { +``` + +```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) { +``` + +### 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 either use: + +* `sec:authorize="isAuthenticated()"` +* `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. + +**TODO** last instruction about Add Tag in `details.html` From 1688ff552d7bdaf9021da038159f45b094eba58a Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 12 Dec 2023 09:42:58 -0600 Subject: [PATCH 12/19] Fixes part 1 - going throuh the User Owned Data section Fix mermaid issues and clean up grammar/spelling --- .../bonus-module/add-service-dto/_index.md | 4 +- .../bonus-module/user-data/_index.md | 74 ++++++++----------- 2 files changed, 32 insertions(+), 46 deletions(-) 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 index 25e9202..e9c593b 100644 --- a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -101,8 +101,8 @@ 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. -Create a new package inside `codingevents` named `dto`, and move your -`RegisterFormDTO` and `LoginFormDTO` into the package. +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. diff --git a/content/authentication/next-steps/bonus-module/user-data/_index.md b/content/authentication/next-steps/bonus-module/user-data/_index.md index ee87a2a..ef019e0 100644 --- a/content/authentication/next-steps/bonus-module/user-data/_index.md +++ b/content/authentication/next-steps/bonus-module/user-data/_index.md @@ -34,6 +34,7 @@ The code for this section begins with the [auth-filter branch](https://github.co ## Creating User Specific Data - Text ### Updating the models with a `User` field + 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". We need to set up a **One-To-Many** relationship @@ -61,18 +62,26 @@ and setters in `Event` class. } ``` -`Event` is not the only model that we want to be user-owned. Let's repeat the above steps to -add the `creator` field/getters/setters to `EventCategory` and `Tag`. +`Event` is not the only model that we want to be user-owned. **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 -Now that we can store the associated User as the creator of an Event/Category/Tag, we need -to make sure that the current logged-in User is set as the creator before saving new entries. -Let's update the `EventController` to get the currently logged in user when creating a new -event and setting that user as the `creator`. +We can store an associated User as the creator of an Event/Category/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` first to set +the `User creator` field in new events. -To get the currently logged-in user in `EventController`, we need references to the -`AuthenticationController` and the `HttpSession`. +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: @@ -81,10 +90,13 @@ In `EventController`, add the following below your other autowired fields: private AuthenticationController authController; ``` -With that reference, we can now get the current logged-in user during the POST handler for -create. +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) +```java{hl_lines="3-4 10"} @PostMapping("create") public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, Errors errors, Model model, HttpSession session) { @@ -101,10 +113,6 @@ 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`. - We need to repeat the above steps for the `EventCategoryController` and `TagController`. ### Retrieving user data from database @@ -135,7 +143,7 @@ and `TagRepository` interfaces that will allow us to retrieve all entries for a In `EventRepository` add: -```java(hl_lines=4-5) +```java{hl_lines="4-5"} @Repository public interface EventRepository extends CrudRepository { @@ -166,7 +174,7 @@ the `AuthenticationController`, `HttpSession`, and the repository methods we add 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) +```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); @@ -192,7 +200,7 @@ and finally pass the user to the `eventRepository` methods that we created. 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) +```java{hl_lines="2-3 6"} @GetMapping("create") public String displayCreateEventForm(Model model, HttpSession session) { User currUser = authController.getUserFromSession(session); @@ -206,7 +214,7 @@ of passing in all categories. Update your function like below. 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) +```java{hl_lines="7"} @PostMapping("create") public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, Errors errors, Model model, HttpSession session) { @@ -222,7 +230,7 @@ When we display the delete events form, we want to make sure it displays the use events. Let's repeat the same procress to retrieve events for the current user in the `displayDeleteEventsForm` method. -```java(hl_lines=2-3 5) +```java{hl_lines="2-3 5"} @GetMapping("delete") public String displayDeleteEventForm(Model model, HttpSession session) { User currUser = authController.getUserFromSession(session); @@ -239,7 +247,7 @@ and we'll retrieve the event based on its ID and the current user. In the `displayEventDetails` method, let's add: -```java(hl_lines=2-5) +```java{hl_lines="2-5"} @GetMapping("detail") public String displayEventDetails(@RequestParam Integer eventId, Model model, HttpSession session) { User currUser = authController.getUserFromSession(session); @@ -257,7 +265,7 @@ Let's update the `displayAllCategories` method that handles `GET 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) +```java{hl_lines="2-3 5"} @GetMapping public String displayAllCategories(Model model, HttpSession session) { User currUser = authController.getUserFromSession(session); @@ -267,28 +275,6 @@ for the current user as the model attribute. } ``` -Next, we'll update the `processCreateEventCategoryForm` method that handles -`POST /eventCategories/create` requests, by getting a reference to the current -user and setting the `creator` field in the new `eventCategory`. - -```java(hl_lines=3 10-11) - @PostMapping("create") - public String processCreateEventCategoryForm(@Valid @ModelAttribute EventCategory eventCategory, - Errors errors, Model model, HttpSession session) { - - if (errors.hasErrors()) { - model.addAttribute("title", "Create Category"); - return "eventCategories/create"; - } - - User currUser = authController.getUserFromSession(session); - eventCategory.setCreator(currUser); - - eventCategoryRepository.save(eventCategory); - return "redirect:/eventCategories"; - } -``` - Luckily, the views for the `Event` and `EventCategory` resources will not need to be updated. From 31af88b157a01bb6ea46c262a9d0cdb0331f1ce1 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 12 Dec 2023 17:36:09 -0600 Subject: [PATCH 13/19] Edit the add-service-dto section with some updates from review Clean up some of the code based on review and errors Change the description to force the reader to implement more of `EventCategory` on their own. Add instructions for `AuthenticationController` updates to use `UserService` --- .../bonus-module/add-service-dto/_index.md | 327 +++++++++++------- 1 file changed, 194 insertions(+), 133 deletions(-) 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 index e9c593b..6fa8656 100644 --- a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -188,7 +188,8 @@ 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. +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 @@ -408,11 +409,101 @@ Add this method to your `UserService` below the fields. Optional user = findById(userId); - if (user.isEmpty()) { + 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 user.get(); + 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:"; } ``` @@ -426,6 +517,10 @@ translating DTOs to Models and communication between the `EventController` and 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 { @@ -442,13 +537,11 @@ public class 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. - 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. +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() { @@ -459,6 +552,10 @@ methods to filter events by user. return eventRepository.findAllByCreator(creator); } + public List getAllEventsByCurrentUser() { + return getAllEventsByCreator(userService.getCurrentUser()); + } + public Event getEventById(int id) { return eventRepository.findById(id).orElseThrow(ResourceNotFoundException::new); } @@ -467,6 +564,10 @@ methods to filter events by user. 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); } @@ -481,10 +582,12 @@ takes in an `EventDTO` object and will translate it to our `Event` and Event event = new Event(); event.setName(eventDTO.getName()); - EventDetails details = new EventDetails(eventDTO.getDescription(), eventDTO.getContactEmail()); + EventDetails details = new EventDetails(eventDTO.getDescription(), + eventDTO.getContactEmail()); event.setEventDetails(details); - event.setEventCategory(categoryRepository.findById(eventDTO.getCategoryId()).get()); + event.setEventCategory(categoryRepository.findById(eventDTO.getCategoryId()) + .orElse(null)); event.setCreator(userService.getCurrentUser()); @@ -494,117 +597,43 @@ takes in an `EventDTO` object and will translate it to our `Event` and } ``` -#### `EventCategoryService` +#### Add `EventCategoryService` Similar to our `EventService`, we must add methods to the `EventCategoryService` to expose functionality of the `EventCategoryRepository` to the controllers. -```java -@Service -public class EventCategoryService { - - @Autowired - private EventCategoryRepository categoryRepository; - - @Autowired - private UserService userService; - - public List getAllCategories() { - return (List) categoryRepository.findAll(); - } +We will only need `EventCategoryRepository` and `UserService` autowired fields +in this class. - public List getAllCategoriesByCreator(User creator) { - return categoryRepository.findAllByCreator(creator); - } - - public List getAllCategoriesByCurrentUser() { - return categoryRepository.findAllByCreator(userService.getCurrentUser()); - } +Write the following methods, using the previous section on `EventService` as a +guide: - public EventCategory getCategoryById(int id) { - return categoryRepository.findById(id).orElseThrow(ResourceNotFoundException::new); - } - - public EventCategory getCategoryByIdAndCreator(int id, User creator) { - return categoryRepository.findByIdAndCreator(id, creator).orElseThrow(ResourceNotFoundException::new); - } - - public EventCategory save(EventCategoryDTO categoryDTO) { - EventCategory category = new EventCategory(); - category.setName(categoryDTO.getName()); - category.setCreator(userService.getCurrentUser()); - - categoryRepository.save(category); - - return category; - } - -} -``` +* `List getAllCategories()` +* `List getAllCategoriesByCreator(User creator)` +* `List getAllCategoriesByCurrentUser()` +* `EventCategory getCategoryById(int id)` +* `EventCategory getCategoryByIdAndCreator(int id, User creator)` +* `EventCategory getCategoryByIdForCurrentUser(int id)` +* `EventCategory save(EventCategoryDTO categoryDTO)` 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 -For now, we will continue to use `authController` to retrieve the current user -in the controller. In the next lesson, we will remove all references to -`authController` in `EventController` and `EventCategoryController`, which is -how the sequence diagram above is organized. - -#### `EventCategoryController` - -Let's start by refactoring our `EventCategoryController` to use the -`EventCategoryService` and `EventCategoryDTO`. - -Change the `EventCategoryRepository` field to be `EventCategoryService`, like below: - -```java - @Autowired - private EventCategoryService eventCategoryService; -``` - -Now, we can refactor all references to the `eventCategoryRepository` to be -`eventCategoryService` references and we can use `EventCategoryDTO` in the -create form. - -```java {hl_lines="5"} - @GetMapping - public String displayAllCategories(Model model, HttpSession session) { - User currUser = authController.getUserFromSession(session); - model.addAttribute("title", "All Categories"); - model.addAttribute("categories", eventCategoryService.getAllCategoriesByCreator(currUser)); - return "eventCategories/index"; - } -``` - -```java {hl_lines="3"} - @GetMapping("create") - public String renderCreateEventCategoryForm(Model model) { - model.addAttribute("title", "Create Category"); - model.addAttribute(new EventCategoryDTO()); - return "eventCategories/create"; - } -``` - -```java {wrap="true" hl_lines="2 10"} - @PostMapping("create") - public String processCreateEventCategoryForm(@Valid @ModelAttribute EventCategoryDTO eventCategoryDto, - Errors errors, Model model, HttpSession session) { - - if (errors.hasErrors()) { - model.addAttribute("title", "Create Category"); - return "eventCategories/create"; - } - - eventCategoryService.save(eventCategoryDto); - return "redirect:/eventCategories"; - } -``` +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` -Similar to above, let's start by changing the repository fields to service fields, like below: +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 @@ -614,21 +643,24 @@ Similar to above, let's start by changing the repository fields to service field private EventCategoryService eventCategoryService; ``` -Now to update the request handlers to use `eventService` and `eventCategoryService`. -For now we will add a `try/catch` block to catch `ResourceNotFoundException` if the -category ID is invalid. +Now, we have to to update the request handlers to use `eventService` and +`eventCategoryService`. -```java {hl_lines="7 9-16"} - @GetMapping - public String displayEvents(@RequestParam(required = false) Integer categoryId, Model model, HttpSession session) { - User currUser = authController.getUserFromSession(session); +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.getAllEventsByCreator(currUser)); + model.addAttribute("events", eventService.getAllEventsByCurrentUser()); } else { try { - EventCategory category = eventCategoryService.getCategoryByIdAndCreator(categoryId, currUser); + EventCategory category = eventCategoryService.getCategoryByIdForCurrentUser(categoryId); model.addAttribute("title", "Events in category: " + category.getName()); model.addAttribute("events", category.getEvents()); @@ -641,39 +673,52 @@ category ID is invalid. } ``` -```java {hl_lines="5-6"} +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, HttpSession session) { - User currUser = authController.getUserFromSession(session); + public String displayCreateEventForm(Model model) { model.addAttribute("title", "Create Event"); model.addAttribute(new EventDTO()); - model.addAttribute("categories", eventCategoryService.getAllCategoriesByCreator(currUser)); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCurrentUser()); return "events/create"; } ``` -```java {hl_lines="2 7 11"} +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, HttpSession session) { - User currUser = authController.getUserFromSession(session); + public String processCreateEventForm(@ModelAttribute @Valid EventDTO newEventDTO, + Errors errors, Model model) { if(errors.hasErrors()) { model.addAttribute("title", "Create Event"); - model.addAttribute("categories", eventCategoryService.getAllCategoriesByCreator(currUser)); + model.addAttribute("categories", eventCategoryService.getAllCategoriesByCurrentUser()); return "events/create"; } - eventService.save(newEventDto); + 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, HttpSession session) { - User currUser = authController.getUserFromSession(session); + public String displayDeleteEventForm(Model model) { model.addAttribute("title", "Delete Events"); - model.addAttribute("events", eventService.getAllEventsByCreator(currUser)); + model.addAttribute("events", eventService.getAllEventsByCurrentUser()); return "events/delete"; } @@ -690,13 +735,15 @@ category ID is invalid. } ``` -```java {hl_lines="5-12"} - @GetMapping("detail") - public String displayEventDetails(@RequestParam Integer eventId, Model model, HttpSession session) { - User currUser = authController.getUserFromSession(session); +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.getEventByIdAndCreator(eventId, currUser); + Event event = eventService.getEventByIdForCurrentUser(eventId); model.addAttribute("title", event.getName() + " Details"); model.addAttribute("event", event); @@ -708,6 +755,20 @@ category ID is invalid. } ``` +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. + #### Updating Views to use DTOs Now that we have our controllers updated to use services, we have to update From 18a3484b1bf0c92aa0f90d3de38e6b00f584b80a Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Wed, 13 Dec 2023 15:43:32 -0600 Subject: [PATCH 14/19] Fixes from user-roles-privileges proofread Some wording updates --- .../bonus-module/user-roles-privileges/_index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index c050c92..d45b5cf 100644 --- a/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -55,8 +55,8 @@ 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, constructor, getter/setter, and `toString` method for this -class. +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 @@ -80,7 +80,7 @@ public enum PrivilegeType { Similar to our `Privilege` model, we need to create a `Role` model that inherits from `AbstractEntity` and defines a field `String name`, along -with a constructor, getter/setter, and `toString` method. +with the constructors, getter/setter, and `toString` method. Create the `Role` class within your `models` package. @@ -357,7 +357,7 @@ automatically the first time. Test it out. ### Adding a `SecurityService` -With roles and privileges added to our database, the last piece to add here is +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 From d8280a1c7a813c0b55e197104ab26a72354a7c0c Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Thu, 14 Dec 2023 09:40:58 -0600 Subject: [PATCH 15/19] Spring Security fixes from proofread Update the mermaid chart for class diagram Fix small mistakes in wording or missing variables/method names --- .../bonus-module/spring-security/_index.md | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/content/authentication/next-steps/bonus-module/spring-security/_index.md b/content/authentication/next-steps/bonus-module/spring-security/_index.md index f7dec60..808990b 100644 --- a/content/authentication/next-steps/bonus-module/spring-security/_index.md +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -90,33 +90,33 @@ 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 { align="left" zoom="false" } +```mermaid{ zoom="false" } classDiagram - UserDetailsService <|-- IUserService - IUserService <|.. UserService - <> UserDetailsService{ - +loadUserByUsername(String username) : UserDetails + UserDetailsService <|-- IUserService : extends + IUserService <|.. UserService : implements + class UserDetailsService{ + loadUserByUsername(String username) UserDetails } - <> IUserService{ - +findByUsername(String username) : User - +save(RegisterFormDTO registration) : User - +getCurrentUser() : User - +findById(Integer id) : Optional~User~ - +findAll() : List~User~ - +deleteUser(Integer id) : User + 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 + 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.security` package named +Create a new *interface* in the `codingevents.services` package named `IUserService`. This interface should **extend** the `UserDetailsService` interface, which @@ -427,12 +427,14 @@ Lastly, we are going to update the `processLoginForm` to use the 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): +in case of errors). Also, add the parameter for `HttpServletResponse` to the +method. -```java +```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()) { @@ -444,7 +446,7 @@ in case of errors): 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) +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 @@ -546,7 +548,9 @@ Now we can update the `EventController displayEvents` method to use } ``` -Update the other methods to use `eventCategoryService.getEventsByIdForCurrentId()`. +Update the other methods to use +`eventCategoryService.getAllCategoriesByCurrentUser()` and +`eventService.getAllEventsByCurrentUser()` functions. For the `displayEventDetails` method, we need to add a similar method to the `EventService` class: From 4561a4a669b07db3830e2730fcd9c96eb318d0f4 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Thu, 21 Dec 2023 11:04:13 -0600 Subject: [PATCH 16/19] Add fixes from peer review Clean up language and grammar in the lessons. Implement some fixes to code snippets. --- content/authentication/next-steps/_index.md | 2 +- .../bonus-module/add-service-dto/_index.md | 4 +- .../bonus-module/role-based-access/_index.md | 60 +++++++++++++++---- .../bonus-module/spring-security/_index.md | 21 ++++--- .../bonus-module/user-data/_index.md | 57 +++++++++++------- 5 files changed, 97 insertions(+), 47 deletions(-) diff --git a/content/authentication/next-steps/_index.md b/content/authentication/next-steps/_index.md index 9aaf65f..397ba3b 100644 --- a/content/authentication/next-steps/_index.md +++ b/content/authentication/next-steps/_index.md @@ -19,6 +19,6 @@ Now that you have successfully added authentication to an existing project you c 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. +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 %}} 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 index 6fa8656..ed4e77e 100644 --- a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -767,7 +767,9 @@ 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. +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 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 index 744e3fb..e5f1051 100644 --- a/content/authentication/next-steps/bonus-module/role-based-access/_index.md +++ b/content/authentication/next-steps/bonus-module/role-based-access/_index.md @@ -36,7 +36,7 @@ access to. ## Adding Role-Based Features for Users - TEXT -The first portion of this lesson will be add event attendance relationships +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. @@ -146,7 +146,7 @@ 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 rsvped to: +will load the table of events that the current user has RSVP'd to: ```java @GetMapping("attending") @@ -242,7 +242,7 @@ In `UserService` add a new autowired field for the `RoleRepository`: private RoleRepository roleRepository; ``` -#### Update `getAuthorities` to pull roles & privileges for User +#### 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 @@ -372,8 +372,8 @@ organizer. Add the following input to the `register.html` template: ``` -Now someone can successfully register as a different user type with -`ROLE_ORGANIZER` set. +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. @@ -492,19 +492,36 @@ the following method: ``` For the rest of CRUD, add the same `@PreAuthorize` annotation so that only -`ROLE_ORGANIZER` can create and delete events: +`ROLE_ORGANIZER` can create and delete events. -```java{hl_lines="1"} +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"} +```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"} @@ -519,6 +536,27 @@ For the rest of CRUD, add the same `@PreAuthorize` annotation so that only 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"; + } +``` + ### Update Navigation by Role Depending on which role is logged-in, we can selectively display certain content @@ -527,10 +565,10 @@ 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 either use: +We will use one of the following: -* `sec:authorize="isAuthenticated()"` -* `sec:authorize="hasRole('ROLE_ORGANIZER')"` +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. diff --git a/content/authentication/next-steps/bonus-module/spring-security/_index.md b/content/authentication/next-steps/bonus-module/spring-security/_index.md index 808990b..72c1cf3 100644 --- a/content/authentication/next-steps/bonus-module/spring-security/_index.md +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -36,7 +36,7 @@ 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](#), +[Baeldung blog](https://www.baeldung.com/spring-security-authentication-and-registration), which provides walkthroughs on a number of Spring related features and frameworks. {{% /notice %}} @@ -73,7 +73,7 @@ request filtering ### Authentication in Spring Security 6 -First, it is important to take a look at the [Spring Security Authentication +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. @@ -125,8 +125,8 @@ 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`. -* `User findByUsername(String username);` -* `User save(RegisterFormDTO registration);` +1. `User findByUsername(String username);` +1. `User save(RegisterFormDTO registration);` All together, our new interface should look like this: @@ -268,7 +268,7 @@ 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 +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 @@ -504,16 +504,15 @@ call `eventCategoryService.getAllCategoriesByCurrentUser()`, like below: #### Update `EventController` -Our goal for `EventController` will be the same as `EventCategoryController` ---- we want to remove all references to `AuthenticationController` and the -code that was necessary to grab the current user. Instead, we will make -calls to `eventService` and `eventCategoryService` methods that assume the -current user. +Our goal for `EventController` will be the same as `EventCategoryController`: +to remove all references to `AuthenticationController` and the code to grab the +current user. Instead, we will make calls to `eventService` and +`eventCategoryService` methods that assume the current user. First, **remove** the `AuthenticationController` autowired field. Next, we need one new method in `EventCategoryService` for retrieving a -category by an id but also assuming the current user. We want to prevent +category that the current user created by the category id. We want to prevent the ability for a user to look up any event category instance by id. Add the following method to `EventCategoryService`: diff --git a/content/authentication/next-steps/bonus-module/user-data/_index.md b/content/authentication/next-steps/bonus-module/user-data/_index.md index ef019e0..3683916 100644 --- a/content/authentication/next-steps/bonus-module/user-data/_index.md +++ b/content/authentication/next-steps/bonus-module/user-data/_index.md @@ -15,13 +15,12 @@ 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 make sure that the events and categories that a user create will be +can we ensure that the events and categories that a user creates are associated with their account? -Let's associate the new data with the logged-in user using one-to-many database relationships. -We will specify that a `User` has a one-to-many relationship with the `Event` objects -that they create, along with `EventCategory` and `Tag`. This way, we can specify that -we want to load all events or save an event *for a specific user*. +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". {{% 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. @@ -35,22 +34,18 @@ The code for this section begins with the [auth-filter branch](https://github.co ### Updating the models with a `User` field -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". We need to set up a **One-To-Many** relationship -between the `User` model and the different data models (`Event`, `EventCategory`, `Tag`). +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. We will call it `creator` -as it's a reference to the user that created the event, and we will give it a `@ManyToOne` annotation -as there can be many events associated with a single creator. +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. Add the following to your getters -and setters in `Event` class. +We'll also need to add a getter and setter for `creator`. ```java public User getCreator() { @@ -62,8 +57,8 @@ and setters in `Event` class. } ``` -`Event` is not the only model that we want to be user-owned. **Repeat the above -steps** to add the `creator` field/getters/setters to `EventCategory`. +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 @@ -75,10 +70,10 @@ You should update the `Tag` class as well to track `User creator`. ### Saving the `User` when creating new data -We can store an associated User as the creator of an Event/Category/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` first to set -the `User creator` field in new events. +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`. @@ -224,6 +219,12 @@ there are form errors, we want to pass the user-created categories back to the f 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 @@ -242,8 +243,7 @@ events. Let's repeat the same procress to retrieve events for the current user i 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 use `HttpSession` and `AuthenticationController` to retrieve the current user, -and we'll retrieve the event based on its ID and the current user. +we'll retrieve the current user and the event based on its ID and the current user. In the `displayEventDetails` method, let's add: @@ -253,6 +253,17 @@ In the `displayEventDetails` method, let's add: 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 @@ -266,7 +277,7 @@ 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 + @GetMapping public String displayAllCategories(Model model, HttpSession session) { User currUser = authController.getUserFromSession(session); model.addAttribute("title", "All Categories"); From 6425520cb6e78dfb790b2a4bbbb5f03b1d394aae Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Fri, 29 Dec 2023 21:56:26 -0600 Subject: [PATCH 17/19] Updates from testing and tutorial making Add numbered lists instead of unordered lists. Remove unneeded sections based on code refactor --- .../bonus-module/add-service-dto/_index.md | 37 ++-- .../bonus-module/spring-security/_index.md | 159 +----------------- .../user-roles-privileges/_index.md | 24 +-- 3 files changed, 30 insertions(+), 190 deletions(-) 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 index ed4e77e..76b8f3a 100644 --- a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -35,6 +35,9 @@ called **Services** to manage the interactions between the **Controllers** and t 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" } @@ -229,19 +232,19 @@ public class UserService { Our `UserService` is going to expose a few methods: -* `User findByUsername(String username)`: retrieves `User` from +1. `User findByUsername(String username)`: retrieves `User` from `UserRepository` by the `username` -* `Optional findById(Integer id)`: expose `UserRepository` +1. `Optional findById(Integer id)`: expose `UserRepository` functionality for possible use -* `List findAll()`: expose `UserRepository` functionality +1. `List findAll()`: expose `UserRepository` functionality for possible use -* `User deleteUser(Integer id)`: expose `UserRepository` functionality +1. `User deleteUser(Integer id)`: expose `UserRepository` functionality for possible use -* `User save(RegisterFormDTO registration)`: validates data in +1. `User save(RegisterFormDTO registration)`: validates data in `RegisterFormDTO` and creates a new `User` in `UserRepository` -* `boolean validateUser(User user, String password)`: validate a password +1. `boolean validateUser(User user, String password)`: validate a password by encoding it and comparing to the encoded `User` password. -* `User getCurrentUser()`: retrieves currently logged in `User` from +1. `User getCurrentUser()`: retrieves currently logged in `User` from the `user` attribute in `HttpSession` which is loaded from the current request context @@ -509,9 +512,9 @@ Last, we need to update `processLoginForm` to use `userService` and the new ### Adding `EventService` and `EventCategoryService` -We will add a service "layer" to our design that will be responsible for -translating DTOs to Models and communication between the `EventController` and -`EventRepository` +The `EventService` and `EventCategoryService` will be responsible for +translating `EventDTO` and `EventCategoryDTO` to model objects and +communication between the `EventController` and `EventRepository`. #### `EventService` @@ -608,13 +611,13 @@ in this class. Write the following methods, using the previous section on `EventService` as a guide: -* `List getAllCategories()` -* `List getAllCategoriesByCreator(User creator)` -* `List getAllCategoriesByCurrentUser()` -* `EventCategory getCategoryById(int id)` -* `EventCategory getCategoryByIdAndCreator(int id, User creator)` -* `EventCategory getCategoryByIdForCurrentUser(int id)` -* `EventCategory save(EventCategoryDTO categoryDTO)` +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)` Now that our service layer is added, we can refactor our controllers to use them and our form views to use DTOs. diff --git a/content/authentication/next-steps/bonus-module/spring-security/_index.md b/content/authentication/next-steps/bonus-module/spring-security/_index.md index 72c1cf3..b5ac490 100644 --- a/content/authentication/next-steps/bonus-module/spring-security/_index.md +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -384,22 +384,7 @@ Add this last method to the `WebSecurity` class: } ``` -In the next section, our task will be to update our controllers to use the new -`UserService` and make use of the `Authentication` in the -`AuthenticationController`. - -### Refactoring Controllers & Services - -We have to update the following classes in this section: - -1. `AuthenticationController` to use `AuthenticationManager` for login -1. `SecurityService` to use `UserService` and `SecurityContextHolder` -1. `EventCategoryService` to use updated `getCurrentUser` -1. `EventCategoryController` to use updated service -1. `EventService` to use updated `getCurrentUser` -1. `EventController` to use updated service - -#### Update `AuthenticationController` +### Refactoring `AuthenticationController` Recall that we have plugged in to the Spring Security framework by using `UserDetailsService` interface and relying on `Authentication` objects @@ -482,94 +467,7 @@ included the `/logout` route as part of our security filter setup in `WebSecurity`. Navigating to `/logout` will invalidate the security session automatically. -#### Update `EventCategoryController` - -To update `EventCategoryController`, we want to remove the reference to -`authController` that is used to retrieve the current user. Instead we'll -make a call to a `eventCategoryService` method that assumes the current -user. - -In `EventCategoryController`, remove the field that references -`AuthenticationController`, and update the `displayAllCategories` method to -call `eventCategoryService.getAllCategoriesByCurrentUser()`, like below: - -```java - @GetMapping - public String displayAllCategories(Model model, HttpSession session) { - model.addAttribute("title", "All Categories"); - model.addAttribute("categories", eventCategoryService.getAllCategoriesByCurrentUser()); - return "eventCategories/index"; - } -``` - -#### Update `EventController` - -Our goal for `EventController` will be the same as `EventCategoryController`: -to remove all references to `AuthenticationController` and the code to grab the -current user. Instead, we will make calls to `eventService` and -`eventCategoryService` methods that assume the current user. - -First, **remove** the `AuthenticationController` autowired field. - -Next, we need one new method in `EventCategoryService` for retrieving a -category that the current user created by the category id. We want to prevent -the ability for a user to look up any event category instance by id. - -Add the following method to `EventCategoryService`: - -```java - public EventCategory getCategoryByIdForCurrentUser(int id) { - return getCategoryByIdAndCreator(id, userService.getCurrentUser()); - } -``` - -Now we can update the `EventController displayEvents` method to use -`eventService` and `eventCategory` service only: - -```java - @GetMapping - public String displayEvents(@RequestParam(required = false) Integer categoryId, Model model, HttpSession session) { - 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"; - } -``` - -Update the other methods to use -`eventCategoryService.getAllCategoriesByCurrentUser()` and -`eventService.getAllEventsByCurrentUser()` functions. -For the `displayEventDetails` method, we need to add a similar method to the -`EventService` class: - -```java - public Event getEventByIdForCurrentUser(int id) { - return getEventByIdAndCreator(id, userService.getCurrentUser()); - } -``` - -With that method, we can update `displayEventDetails` to use -`eventService.getEventByIdForCurrentUser(eventId)` like the lines below: - -```java - @GetMapping("detail") - public String displayEventDetails(@RequestParam Integer eventId, Model model, HttpSession session) { - try { - Event event = eventService.getEventByIdForCurrentUser(eventId); -``` - -### Updating Error Handling & `SecurityService` +### 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 @@ -667,59 +565,6 @@ Create `404.html` that has the following message: ``` -#### Updating `SecurityService` - -Our last miscellaneous task is to update the `SecurityService` class to use -the Security framework. We do not currently use this service for any -functionality but its methods can be used for role authorization if needed. - -Our goal is to remove the use of `AuthenticationController` and instead use the -`SecurityContext` and `UserService` for our needs. Replace the -`AuthenticationController` field with `UserService`: - -```java - @Autowired - private UserService userService; -``` - -The `hasPrivilege` method will be completely refactored, so you can delete the -body of the method. We will use the `SecurityContextHolder` to retrieve any -currently stored `Authentication` object and use that to get the current `User` -and their privileges: - -```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; - } -``` - -Update the `hasRole` method similarly: - -```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; - } -``` - 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. 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 index d45b5cf..1aa775f 100644 --- a/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -110,7 +110,8 @@ 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 getter/setter for this `privileges` field as well. +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, @@ -226,9 +227,6 @@ First, let's create the `PrivilegeRepository` interface in the `data` package: public interface PrivilegeRepository extends CrudRepository { Privilege findByName(String name); - - @Override - void delete(Privilege privilege); } ``` @@ -240,10 +238,8 @@ Next, let's create the `RoleRepository` interface in the `data` package. ```java @Repository public interface RoleRepository extends CrudRepository { - Role findByName(String name); - @Override - void delete(Role role); + Role findByName(String name); } ``` @@ -370,7 +366,7 @@ create a new class `SecurityService` inside the `security` package. @Service public class SecurityService { @Autowired - private AuthenticationController authController; + private UserService userService; } ``` @@ -386,10 +382,8 @@ for more background on this syntax. ```java public boolean hasPrivilege(String privilege) { - final ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - final HttpSession session = attr.getRequest().getSession(false); - final User theUser = authController.getUserFromSession(session); - if (session != null && theUser != null) { + final User theUser = userService.getCurrentUser(); + if (theUser != null) { Boolean hasPrivilege = theUser.getRoles() .stream() .map(Role::getPrivileges) @@ -411,10 +405,8 @@ Similary, we will add a `hasRole` method to the service as well. ```java public boolean hasRole(String role) { - final ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - final HttpSession session = attr.getRequest().getSession(false); - final User theUser = authController.getUserFromSession(session); - if (session != null && theUser != null) { + final User theUser = userService.getCurrentUser(); + if (theUser != null) { Boolean hasRole = theUser.getRoles() .stream() .map(Role::getName) From 11c3dd675c87495387dfaf3da147d0868bc680c4 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Tue, 9 Jan 2024 10:46:08 -0600 Subject: [PATCH 18/19] Clean up intro to bonus module, Tag instructions, and code fixes Add intro description to the Bonus Module Leave hints about implementing the `Tag` class changes in the instructions Update code to match latest version of project --- content/authentication/next-steps/_index.md | 6 +-- .../next-steps/bonus-module/_index.md | 10 +++++ .../bonus-module/add-service-dto/_index.md | 13 ++++++- .../bonus-module/role-based-access/_index.md | 15 +++++++- .../bonus-module/spring-security/_index.md | 12 +++++- .../bonus-module/user-data/_index.md | 12 ++++-- .../user-roles-privileges/_index.md | 38 +++++++++++-------- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/content/authentication/next-steps/_index.md b/content/authentication/next-steps/_index.md index 397ba3b..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 --- @@ -21,4 +21,4 @@ Now that you have successfully added authentication to an existing project you c 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 walk through the process of basic authentication with Spring with an MVC application. -{{% children %}} +{{% children style="h4" %}} diff --git a/content/authentication/next-steps/bonus-module/_index.md b/content/authentication/next-steps/bonus-module/_index.md index bf249ef..e13a06f 100644 --- a/content/authentication/next-steps/bonus-module/_index.md +++ b/content/authentication/next-steps/bonus-module/_index.md @@ -12,4 +12,14 @@ 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 index 76b8f3a..50e2d56 100644 --- a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -96,6 +96,12 @@ sequenceDiagram **TODO** +{{% 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 DTOs & Services to CodingEvents - Text ### Adding `EventDTO` & `EventCategoryDTO` @@ -146,6 +152,8 @@ public class EventCategoryDTO { } ``` +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 @@ -619,6 +627,8 @@ guide: 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. @@ -834,7 +844,8 @@ Lastly, we will update `eventCategories/create.html` and use the ``` -Our updates should be complete. There should be no change in functionality +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 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 index e5f1051..3821d55 100644 --- a/content/authentication/next-steps/bonus-module/role-based-access/_index.md +++ b/content/authentication/next-steps/bonus-module/role-based-access/_index.md @@ -34,6 +34,12 @@ access to. **TODO** +{{% 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 %}} + ## Adding Role-Based Features for Users - TEXT The first portion of this lesson will add event attendance relationships @@ -557,6 +563,13 @@ user: } ``` + +{{% 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 @@ -618,5 +631,3 @@ 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. - -**TODO** last instruction about Add Tag in `details.html` diff --git a/content/authentication/next-steps/bonus-module/spring-security/_index.md b/content/authentication/next-steps/bonus-module/spring-security/_index.md index b5ac490..6ffcbbe 100644 --- a/content/authentication/next-steps/bonus-module/spring-security/_index.md +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -31,7 +31,9 @@ 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](#). This framework has +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. @@ -45,6 +47,14 @@ frameworks. **TODO** +{{% 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 %}} + ## Implementing Spring Security 6 for Authentication - TEXT To begin, we need to make sure that our Gradle project includes the necessary diff --git a/content/authentication/next-steps/bonus-module/user-data/_index.md b/content/authentication/next-steps/bonus-module/user-data/_index.md index 3683916..b81aa11 100644 --- a/content/authentication/next-steps/bonus-module/user-data/_index.md +++ b/content/authentication/next-steps/bonus-module/user-data/_index.md @@ -22,14 +22,18 @@ 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". -{{% 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 %}} - ## Creating User Specific Data - Video **TODO** +{{% 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 %}} + ## Creating User Specific Data - Text ### Updating the models with a `User` field 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 index 1aa775f..ba6c1fc 100644 --- a/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -30,6 +30,14 @@ and implement future projects. **TODO** +{{% 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 %}} + ## Adding Roles & Privileges to Users - TEXT This lesson describes how to add new models for `Role` and `Privilege` and @@ -383,16 +391,16 @@ for more background on this syntax. ```java public boolean hasPrivilege(String privilege) { final User theUser = userService.getCurrentUser(); - if (theUser != null) { - Boolean hasPrivilege = theUser.getRoles() - .stream() - .map(Role::getPrivileges) - .flatMap(coll -> coll.stream()) - .map(Privilege::getName) - .anyMatch(p -> p.equals(privilege)); - return hasPrivilege; + if (theUser == null) { + return false; } - return false; + Boolean hasPrivilege = theUser.getRoles() + .stream() + .map(Role::getPrivileges) + .flatMap(coll -> coll.stream()) + .map(Privilege::getName) + .anyMatch(p -> p.equals(privilege)); + return hasPrivilege; } ``` @@ -407,13 +415,13 @@ Similary, we will add a `hasRole` method to the service as well. public boolean hasRole(String role) { final User theUser = userService.getCurrentUser(); if (theUser != null) { - Boolean hasRole = theUser.getRoles() - .stream() - .map(Role::getName) - .anyMatch(r -> r.equals(role)); - return hasRole; + return false; } - return false; + Boolean hasRole = theUser.getRoles() + .stream() + .map(Role::getName) + .anyMatch(r -> r.equals(role)); + return hasRole; } ``` From 01748af3a9846853ebc6eda0d2fffbe7ec1ef466 Mon Sep 17 00:00:00 2001 From: Ben Clark Date: Wed, 24 Jan 2024 14:33:52 -0600 Subject: [PATCH 19/19] Remove mentions of video lessons from spring security bonus module This leaves the bonus module as a strictly text-based walkthrough --- .../next-steps/bonus-module/add-service-dto/_index.md | 6 +----- .../next-steps/bonus-module/role-based-access/_index.md | 6 +----- .../next-steps/bonus-module/spring-security/_index.md | 6 +----- .../next-steps/bonus-module/user-data/_index.md | 6 +----- .../next-steps/bonus-module/user-roles-privileges/_index.md | 6 +----- 5 files changed, 5 insertions(+), 25 deletions(-) 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 index 50e2d56..b42bb4e 100644 --- a/content/authentication/next-steps/bonus-module/add-service-dto/_index.md +++ b/content/authentication/next-steps/bonus-module/add-service-dto/_index.md @@ -92,9 +92,7 @@ sequenceDiagram E->>User: redirect: /events ``` -## Adding DTOs & Services to CodingEvents - Video - -**TODO** +## 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) @@ -102,8 +100,6 @@ and ends with the [add-service-dto branch](https://github.com/LaunchCodeEducatio of the `CodingEventsJava` repository. {{% /notice %}} -## Adding DTOs & Services to CodingEvents - Text - ### 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 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 index 3821d55..37fc2da 100644 --- a/content/authentication/next-steps/bonus-module/role-based-access/_index.md +++ b/content/authentication/next-steps/bonus-module/role-based-access/_index.md @@ -30,9 +30,7 @@ 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 - VIDEO - -**TODO** +## 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) @@ -40,8 +38,6 @@ and ends with the [role-based-access branch](https://github.com/LaunchCodeEducat of the `CodingEventsJava` repository. {{% /notice %}} -## Adding Role-Based Features for Users - TEXT - 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. diff --git a/content/authentication/next-steps/bonus-module/spring-security/_index.md b/content/authentication/next-steps/bonus-module/spring-security/_index.md index 6ffcbbe..3e01bd5 100644 --- a/content/authentication/next-steps/bonus-module/spring-security/_index.md +++ b/content/authentication/next-steps/bonus-module/spring-security/_index.md @@ -43,9 +43,7 @@ which provides walkthroughs on a number of Spring related features and frameworks. {{% /notice %}} -## Implementing Spring Security 6 for Authentication - VIDEO - -**TODO** +## Implementing Spring Security 6 for Authentication {{% notice blue Note "rocket" %}} The code for this section begins with the @@ -55,8 +53,6 @@ and ends with the of the `CodingEventsJava` repository. {{% /notice %}} -## Implementing Spring Security 6 for Authentication - TEXT - To begin, we need to make sure that our Gradle project includes the necessary dependencies for Spring Security 6. diff --git a/content/authentication/next-steps/bonus-module/user-data/_index.md b/content/authentication/next-steps/bonus-module/user-data/_index.md index b81aa11..c81da1f 100644 --- a/content/authentication/next-steps/bonus-module/user-data/_index.md +++ b/content/authentication/next-steps/bonus-module/user-data/_index.md @@ -22,9 +22,7 @@ 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 - Video - -**TODO** +## Creating User Specific Data {{% notice blue Note "rocket" %}} The code for this section begins with the @@ -34,8 +32,6 @@ and ends with the of the `CodingEventsJava` repository. {{% /notice %}} -## Creating User Specific Data - Text - ### Updating the models with a `User` field We need to set up a **One-To-Many** relationship between the `User` model and 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 index ba6c1fc..e136131 100644 --- a/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md +++ b/content/authentication/next-steps/bonus-module/user-roles-privileges/_index.md @@ -26,9 +26,7 @@ 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 - VIDEO - -**TODO** +## Adding Roles & Privileges to Users {{% notice blue Note "rocket" %}} The code for this section begins with the @@ -38,8 +36,6 @@ and ends with the of the `CodingEventsJava` repository. {{% /notice %}} -## Adding Roles & Privileges to Users - TEXT - 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.