Skip to content

Commit 6fcd908

Browse files
committed
Add Role Based Access instructions
Create user attendance relationship and UI CRUD Still need to add description to lock down app for user roles
1 parent a1c049e commit 6fcd908

1 file changed

Lines changed: 383 additions & 0 deletions

File tree

  • content/authentication/next-steps/bonus-module/role-based-access
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
---
2+
title: "Role-Based User Access"
3+
date: 2023-12-07T10:26:50-06:00
4+
draft: false
5+
weight: 5
6+
originalAuthor: Ben Clark # to be set by page creator
7+
originalAuthorGitHub: brclark # to be set by page creator
8+
reviewer: # to be set by the page reviewer
9+
reviewerGitHub: # to be set by the page reviewer
10+
lastEditor: # update any time edits are made after review
11+
lastEditorGitHub: # update any time edits are made after review
12+
lastMod: # UPDATE ANY TIME CHANGES ARE MADE
13+
---
14+
15+
In this final lesson for the Bonus Module, we will add some new features to
16+
Coding Events that make use of the roles & privileges infrastructure. We
17+
will modify some core functionality that has been there from the beginning
18+
of the project.
19+
20+
Overall, we will define the actions that two of our user types can take, the
21+
base `ROLE_USER` and the more powerful `ROLE_ORGANIZER`. `ROLE_USER` will be
22+
able to view events that have been created by `ROLE_ORGANIZER`, as well as all
23+
categories that have been created as well. `ROLE_USER` will then be able to mark
24+
themselves as interested or **attending** an event, which will store
25+
persistently in the database. `ROLE_USER` will have a limited menu bar that does
26+
not show links to
27+
28+
`ROLE_ORGANIZER` will be able to create new events and categories that are
29+
associated with their account. We will expand the access and view of the menu
30+
bar for `ROLE_ORGANIZER` so that they can see links to the routes they have
31+
access to.
32+
33+
## Adding Role-Based Features for Users - VIDEO
34+
35+
**TODO**
36+
37+
## Adding Role-Based Features for Users - TEXT
38+
39+
The first portion of this lesson will be add event attendance relationships
40+
to our models, as well as the ability to mark in login that you want to be
41+
an event organizer.
42+
43+
### Add Event Attendance Relationship
44+
45+
#### Add `User` and `Event` many-to-many relationship
46+
47+
In the `Event` model, we will add another field that stores `Collection<User>
48+
attendees` as a many-to-many relationship. Add the following field to `Event`,
49+
recognizing that we are manually specifying the join table:
50+
51+
```java
52+
@ManyToMany
53+
@JoinTable(
54+
name = "users_events",
55+
joinColumns = @JoinColumn(
56+
name = "user_id", referencedColumnName = "id"),
57+
inverseJoinColumns = @JoinColumn(
58+
name = "event_id", referencedColumnName = "id"))
59+
private Collection<User> attendees;
60+
```
61+
62+
Be sure to add a getter and a setter as well for the new field.
63+
64+
Now, in the `User` model we will add the same relationship to connect a
65+
`Collection<Event> attendingEvents` field. Add the following field to `User`:
66+
67+
```java
68+
@ManyToMany
69+
@JoinTable(
70+
name = "users_events",
71+
joinColumns = @JoinColumn(
72+
name = "event_id", referencedColumnName = "id"),
73+
inverseJoinColumns = @JoinColumn(
74+
name = "user_id", referencedColumnName = "id"))
75+
private Collection<Event> attendingEvents;
76+
```
77+
78+
Be sure to add a getter and setter for this field too.
79+
80+
#### Add Attendance CRUD logic to `EventService`
81+
82+
We can add methods to `EventService` that provide functionality for marking
83+
and removing attendance relationships between an event and a user. These
84+
methods will be used specifically in our controllers and views for displaying
85+
and adding attendance.
86+
87+
Add the following `addAttendanceForUser` method to the `EventService` class.
88+
Notice that we use the `attendees` collection to manage the relationship and
89+
add a `user` object to the collection.
90+
91+
```java
92+
public void addAttendanceForUser(Integer eventId, User user) {
93+
Event event = eventRepository.findById(eventId)
94+
.orElseThrow(ResourceNotFoundException::new);
95+
96+
if (!event.getAttendees().contains(user)) {
97+
event.getAttendees().add(user);
98+
eventRepository.save(event);
99+
}
100+
}
101+
```
102+
103+
With that in place we can make a more useful method to us ---
104+
`addAttendanceForCurrentUser`:
105+
106+
```java
107+
public void addAttendanceForCurrentUser(Integer eventId) {
108+
addAttendanceForUser(eventId, userService.getCurrentUser());
109+
}
110+
```
111+
112+
Now we have to provide the similar logic for removing a user from the attendance
113+
collection, with `removeAttendanceForUser` method:
114+
115+
```java
116+
public void removeAttendanceForUser(Integer eventId, User user) {
117+
Event event = eventRepository.findById(eventId)
118+
.orElseThrow(ResourceNotFoundException::new);
119+
120+
event.getAttendees().remove(user);
121+
eventRepository.save(event);
122+
}
123+
124+
public void removeAttendanceForCurrentUser(Integer eventId) {
125+
removeAttendanceForUser(eventId, userService.getCurrentUser());
126+
}
127+
```
128+
129+
Lastly, we will need some helpful methods for checking if the current user
130+
is marked as attending an event and for getting the events they will attend:
131+
132+
```java
133+
public boolean getUserEventAttendance(Event event) {
134+
return event.getAttendees().contains(userService.getCurrentUser());
135+
}
136+
137+
public List<Event> getAttendingEventsByCurrentUser() {
138+
return (List<Event>) userService.getCurrentUser().getAttendingEvents();
139+
}
140+
```
141+
142+
#### Add Event Attendance CRUD to `EventController`
143+
144+
We need to set up routes that will allow users to look at the events they have
145+
marked for attendance, and routes to allow for easy setting/unsetting of
146+
attendance for an event by the current user.
147+
148+
In `EventController`, let's first add a route at `GET /events/attending` that
149+
will load the table of events that the current user has rsvped to:
150+
151+
```java
152+
@GetMapping("attending")
153+
public String displayMyEvents(Model model) {
154+
model.addAttribute("events", eventService.getAttendingEventsByCurrentUser());
155+
model.addAttribute("title", "My Events");
156+
return "events/index";
157+
}
158+
```
159+
160+
Next, we need two POST request handlers at `POST /events/{id}/attending` and
161+
`POST /events/{id}/removeAttending`. These methods will assume that the current
162+
user is marking themselves for attendance to a specific event.
163+
164+
```java
165+
@PostMapping("{id}/attending")
166+
public String processUserEventAttendance(@PathVariable Integer id, Model model) {
167+
168+
eventService.addAttendanceForCurrentUser(id);
169+
170+
return "redirect:/events/detail?eventId=" + id;
171+
}
172+
173+
@PostMapping("{id}/removeAttending")
174+
public String removeUserEventAttendance(@PathVariable Integer id, Model model) {
175+
176+
eventService.removeAttendanceForCurrentUser(id);
177+
178+
return "redirect:/events/detail?eventId=" + id;
179+
}
180+
```
181+
182+
Last, we want the Event Details page to display a button to RSVP or remove attendance
183+
easily from the event. We'll pass a boolean value as a model attribute to the
184+
details template that says whether the current user is attending the event, which
185+
will help us display the correct form for changing the reservation.
186+
187+
In `displayEventDetails` method in `EventController`, add another model attribute in
188+
the `try` block:
189+
190+
```java{ hl_lines="6" }
191+
try {
192+
Event event = eventService.getEventByIdForCurrentUser(eventId);
193+
194+
model.addAttribute("title", event.getName() + " Details");
195+
model.addAttribute("event", event);
196+
model.addAttribute("userAttendance", eventService.getUserEventAttendance(event));
197+
} catch (ResourceNotFoundException ex) {
198+
```
199+
200+
#### Adding User Attendance UI in templates
201+
202+
We want to allow users to mark themselves for attendance to a coding event. We
203+
can put this functionality in multiple places. For now, we will put that choice
204+
in the event details page. We'll have a button that displays the current
205+
attendance and allows users to flip their choice.
206+
207+
In the `events/detail.html` template, let's dynamically add a button based on
208+
the `userAttendance` attribute value we passed in:
209+
210+
```html{hl_lines="5-17"}
211+
<tr>
212+
<th>Contact Email</th>
213+
<td th:text="${event.eventDetails.contactEmail}"></td>
214+
</tr>
215+
<tr>
216+
<th>
217+
Actions
218+
</th>
219+
<td>
220+
<form th:unless="${userAttendance}" method="post" th:action="@{/events/{id}/attending(id=${event.id})}">
221+
<input type="submit" class="btn btn-info btn-sm" value="RSVP">
222+
</form>
223+
<form th:if="${userAttendance}" method="post" th:action="@{/events/{id}/removeAttending(id=${event.id})}">
224+
<input type="submit" class="btn btn-outline-info btn-sm" value="I'm attending!">
225+
</form>
226+
</td>
227+
</tr>
228+
```
229+
230+
With that addition, every user should be able to mark themselves for attendance
231+
to an event.
232+
233+
#### Add `RoleRepository` Field
234+
Our `UserService` implements the `loadUserByUsername` method that is a part
235+
of the `UserDetailsService` interface. In that method, we need to properly
236+
load the *granted authorities* for that user.
237+
238+
In `UserService` add a new autowired field for the `RoleRepository`:
239+
240+
```java
241+
@Autowired
242+
private RoleRepository roleRepository;
243+
```
244+
245+
#### Update `getAuthorities` to pull roles & privileges for User
246+
247+
We want to modify `getAuthorities` to take an argument for a `Collection<Role>`
248+
object that is a list of the roles for a user. We'll refactor this method
249+
and introduce some helper methods. The result will be that `getAuthorities`
250+
returns a collection of granted authories that includes *all roles &
251+
privileges* for a given list of user roles.
252+
253+
```java
254+
private Collection<? extends GrantedAuthority> getAuthorities(
255+
Collection<Role> roles) {
256+
return getGrantedAuthorities(getPrivilegesAndRoles(roles));
257+
}
258+
259+
private List<String> getPrivilegesAndRoles(Collection<Role> roles) {
260+
}
261+
262+
private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) {
263+
}
264+
```
265+
266+
In `getPrivilegesAndRoles`, we will take a list of `Role` objects and return a
267+
list of roles and associated privileges in `String` form. We will heavily use
268+
Java `stream` and `map` methods here to translate between collections:
269+
270+
Update the `getPrivilegesAndRoles` method as below:
271+
272+
```java
273+
private List<String> getPrivilegesAndRoles(Collection<Role> roles) {
274+
List<Privilege> collection = new ArrayList<>();
275+
for (Role role : roles) {
276+
collection.addAll(role.getPrivileges());
277+
}
278+
List<String> rolesAndPrivileges = collection.stream()
279+
.map(Privilege::getName)
280+
.collect(Collectors.toList());
281+
rolesAndPrivileges.addAll(roles.stream()
282+
.map(Role::getName)
283+
.collect(Collectors.toList())
284+
);
285+
return rolesAndPrivileges;
286+
}
287+
```
288+
289+
We will use that `List<String>` object to create a `List<GrantedAuthority>`
290+
object. Update `getGrantedAuthorities` method as below:
291+
292+
```java
293+
private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) {
294+
return privileges.stream()
295+
.map(SimpleGrantedAuthority::new)
296+
.collect(Collectors.toList());
297+
}
298+
```
299+
300+
Now, the current user in session will have their roles and privileges stored in
301+
the `Authentication` object in `SecurityContext`.
302+
303+
### Add Event Organizer role capability for users
304+
305+
#### Add field to `RegisterFormDTO`
306+
307+
When a user is registering for Coding Events, they should be able to mark
308+
themselves as an event organizer, so that they can have both `ROLE_USER` and
309+
`ROLE_ORGANIZER`.
310+
311+
In `RegisterFormDTO`, add a field:
312+
313+
```java
314+
private Boolean eventOrganizer;
315+
```
316+
317+
Add a getter and a setter for this field as well.
318+
319+
#### Update `UserService save` for Event Organizer field in `RegisterFormDTO`
320+
321+
We have already added a `Boolean` to `RegisterFormDTO` that allows a user to
322+
mark themselves as an organizer. We need to make sure their assigned roles
323+
reflect that. In `UserService save` add the following update:
324+
325+
```java{ hl_lines="11-20" }
326+
public User save(RegisterFormDTO registration) {
327+
String password = registration.getPassword();
328+
String verifyPassword = registration.getVerifyPassword();
329+
if (!password.equals(verifyPassword)) {
330+
throw new UserRegistrationException("Passwords do not match");
331+
}
332+
333+
String pwHash = passwordEncoder.encode(registration.getPassword());
334+
User user = new User(registration.getUsername(), pwHash);
335+
336+
if (registration.getEventOrganizer()) {
337+
List<Role> roles = new ArrayList<>();
338+
roles.add(roleRepository.findByName(RoleType.ROLE_USER.toString()));
339+
roles.add(roleRepository.findByName(RoleType.ROLE_ORGANIZER.toString()));
340+
user.setRoles(roles);
341+
} else {
342+
user.setRoles(Collections.singletonList(
343+
roleRepository.findByName(RoleType.ROLE_USER.toString())
344+
));
345+
}
346+
347+
return userRepository.save(user);
348+
}
349+
```
350+
351+
If the user has marked themselves as an organizer, we add both `ROLE_USER` and
352+
`ROLE_ORGANIZER` to their account.
353+
354+
#### Add organizer option to register form
355+
356+
In our registration form, we need to give the option to register as an event
357+
organizer. Add the following input to the `register.html` template:
358+
359+
```html{hl_lines="6-11"}
360+
<div class="form-group">
361+
<label>Verify Password
362+
<input class="form-control" th:field="${registerFormDTO.verifyPassword}" type="password" />
363+
</label>
364+
</div>
365+
<div class="form-group">
366+
<label>
367+
<input th:field="${registerFormDTO.eventOrganizer}" type="checkbox" />
368+
I am an event organizer
369+
</label>
370+
</div>
371+
372+
<input type="submit" class="btn btn-primary" value="Register" />
373+
```
374+
375+
Now someone can successfully register as a different user type with
376+
`ROLE_ORGANIZER` set.
377+
378+
In the next section, we will add security annotations to the controllers to
379+
limit access depending on user role.
380+
381+
### Secure controllers with authorization annotations
382+
383+

0 commit comments

Comments
 (0)