In this part of the workshop we want to add our customized authorization rules for our application.
As a result of the previous workshop steps we now have authentication for all our web endpoints (including the actuator endpoints) and we can log in using our own users. But here security does not stop.
We know who is using our application (authentication) but we do not have control over what this user is allowed to do in our application (authorization).
As a best practice the authorization should always be implemented on different layers like the web and method layer. This way the authorization still prohibits access even if a user manages to bypass the web url based authorization filter by playing around with manipulated URL's.
Authorization Rules
Our required authorization rule matrix looks like this:
URL
Http method
Restricted
Roles with access
/.css,/.jpg,/*.ico,...
All
No
--
/books
GET
Yes
Authenticated
/books
POST,PUT,DELETE
Yes
LIBRARY_CURATOR
/books/{id}/borrow
POST
Yes
LIBRARY_USER
/books/{id}/return
POST
Yes
LIBRARY_USER
/users
GET,POST,PUT,DELETE
Yes
LIBRARY_ADMIN
/actuator/*
GET
Yes
LIBRARY_ADMIN
Web Layer Authorizations
All the web layer authorization rules are configured in the WebSecurityConfiguration class by adding more authorization rules:
By adding the annotation @EnableGlobalMethodSecurity(prePostEnabled = true) the authorization on method layer is enabled. The web layer authorization rules are added as combinations of mvcMatchers() and hasRole() statements.
Method Layer Authorization
We continue with authorization on the method layer by adding the rules to our business service classes BookService and UserService. To achieve this we use the @PreAuthorize annotations provided by spring security. Same as other spring annotations (e.g. @Transactional) you can put @PreAuthorize annotations on global class level or on method level.
Depending on your authorization model you may use @PreAuthorize to authorize using static roles or to authorize using dynamic expressions (usually if you have roles with permissions).
If you want to have a permission based authorization you can use the predefined interface PermissionEvaluator inside the @PreAuthorize annotations like this:
In the workshop due to time constraints we have to keep things simple so we just use static roles. Here it is done for the all operations of the book service.
packagecom.example.libraryserver.book.service;importcom.example.libraryserver.book.data.Book;importcom.example.libraryserver.book.data.BookRepository;importcom.example.libraryserver.security.AuthenticatedUser;importcom.example.libraryserver.user.data.UserRepository;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.util.IdGenerator;importjava.util.List;importjava.util.Optional;importjava.util.UUID;@Service@PreAuthorize("isAuthenticated()")@Transactional(readOnly =true)publicclassBookService {privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(BookService.class);privatefinalBookRepository bookRepository;privatefinalUserRepository userRepository;privatefinalIdGenerator idGenerator;publicBookService(BookRepository bookRepository,UserRepository userRepository,IdGenerator idGenerator) {this.bookRepository= bookRepository;this.userRepository= userRepository;this.idGenerator= idGenerator; }publicOptional<Book> findOneByIdentifier(UUID identifier) {LOGGER.trace("find book for identifier {}", identifier);returnbookRepository.findOneByIdentifier(identifier); }publicList<Book> findAll() {LOGGER.trace("find all books");returnbookRepository.findAll(); } @PreAuthorize("hasRole('LIBRARY_CURATOR')") @TransactionalpublicBooksave(Book book) {LOGGER.trace("Save book {}", book);if (book.getIdentifier() ==null) {book.setIdentifier(idGenerator.generateId()); }returnbookRepository.save(book); } @PreAuthorize("hasRole('LIBRARY_USER')") @TransactionalpublicOptional<Book> borrowForUser(UUID bookIdentifier,UUID userIdentifier,AuthenticatedUser authenticatedUser) {LOGGER.trace("borrow book with identifier {} for user with identifier {}", bookIdentifier, userIdentifier);return bookRepository.findOneByIdentifier(bookIdentifier).filter( b ->b.getBorrowedByUser() ==null&& authenticatedUser !=null&&userIdentifier.equals(authenticatedUser.getIdentifier())).flatMap( b -> userRepository.findOneByIdentifier(userIdentifier).map( u -> {b.setBorrowedByUser(u);Book borrowedBook =bookRepository.save(b);LOGGER.info("Borrowed book {} for user {}", borrowedBook, u);returnOptional.of(borrowedBook); }).orElse(Optional.empty())); } @PreAuthorize("hasRole('LIBRARY_USER')") @TransactionalpublicOptional<Book> returnForUser(UUID bookIdentifier,UUID userIdentifier,AuthenticatedUser authenticatedUser) {LOGGER.trace("return book with identifier {} of user with identifier {}", bookIdentifier, userIdentifier);return bookRepository.findOneByIdentifier(bookIdentifier).filter( b ->b.getBorrowedByUser() !=null&& authenticatedUser !=null&&b.getBorrowedByUser().getIdentifier().equals(userIdentifier)&&b.getBorrowedByUser().getIdentifier().equals(authenticatedUser.getIdentifier())).flatMap( b -> userRepository.findOneByIdentifier(userIdentifier).map( u -> {b.setBorrowedByUser(null);Book returnedBook =bookRepository.save(b);LOGGER.info("Returned book {} for user {}", returnedBook, u);returnOptional.of(returnedBook); }).orElse(Optional.empty())); } @PreAuthorize("hasRole('LIBRARY_CURATOR')") @TransactionalpublicbooleandeleteOneByIdentifier(UUID bookIdentifier) {LOGGER.trace("delete book with identifier {}", bookIdentifier);return bookRepository.findOneByIdentifier(bookIdentifier).map( b -> {bookRepository.delete(b);returntrue; }).orElse(false); }}
BookService.java
And now we add it the same way for the all operations of the user service.
packagecom.example.libraryserver.user.service;importcom.example.libraryserver.user.data.User;importcom.example.libraryserver.user.data.UserRepository;importorg.owasp.security.logging.SecurityMarkers;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.util.IdGenerator;importjava.util.List;importjava.util.Optional;importjava.util.UUID;@Service@Transactional(readOnly =true)publicclassUserService {privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(UserService.class);privatefinalUserRepository userRepository;privatefinalIdGenerator idGenerator;publicUserService(UserRepository userRepository,IdGenerator idGenerator) {this.userRepository= userRepository;this.idGenerator= idGenerator; } @PreAuthorize("hasRole('LIBRARY_ADMIN')")publicOptional<User> findOneByIdentifier(UUID identifier) {returnuserRepository.findOneByIdentifier(identifier); }publicOptional<User> findOneByEmail(String email) {returnuserRepository.findOneByEmail(email); } @PreAuthorize("hasRole('LIBRARY_ADMIN')")publicList<User> findAll() {LOGGER.trace("find all users");returnuserRepository.findAll(); } @PreAuthorize("hasRole('LIBRARY_ADMIN')") @TransactionalpublicUsersave(User user) {LOGGER.info(SecurityMarkers.CONFIDENTIAL,"save user with password={}",user.getPassword());LOGGER.trace("save user {}", user);if (user.getIdentifier() ==null) {user.setIdentifier(idGenerator.generateId()); }returnuserRepository.save(user); } @PreAuthorize("hasRole('LIBRARY_ADMIN')") @TransactionalpublicbooleandeleteOneIdentifier(UUID userIdentifier) {LOGGER.trace("delete user with identifier {}", userIdentifier);return userRepository.findOneByIdentifier(userIdentifier).map( u -> {userRepository.delete(u);returntrue; }).orElse(false); }}
UserService.java
In this workshop lab we added the authorization to web and method layers. So now for particular RESTful endpoints access is only permitted to users with special roles.
NOTE: You find the completed code in project lab4/library-server-complete.
But how do you know that you have implemented all the authorization rules and did not leave a big security leak for your RESTful API? Or you may change some authorizations later by accident?
To be on a safer side here you need automatic testing. Yes, this can also be done for security! We will see how this works in the next workshop lab.