Set Up

The solution and skeletons for this task are available to clone on this repository

git clone git@github.com:Taack/myLibrary.git
To access the skeleton for this part, go to your terminal cd into the project (myLibrary) and copy the following line.
git checkout part-3-skeleton

Or in IntelliJ you can select the wanted branch directly. Select in the top left corner main/myLibrary/remote, and you will see all the branches. Select a branch add checkout. == Introduction

In this section, we will learn about security and constraints. First, we will learn how to implement these constraints, which will allow us to restrict user inputs. Then, we will dive into security by creating two types of users: admins and borrowers, each having access to different features.

Admins will be able to:

  • add, delete and reactivate authors in the Author menu.

  • add books in the Book menu.

  • purchase and delete bookInstances in the Book menu.

  • have access to the list of users and information about each of them in a User menu.

  • approve books in the Borrowed Menu.

Borrowers will be able to:

  • see the authors in the Author menu.

  • see the books in the Book menu.

  • request a bookInstance in the Book menu.

  • return a bookInstance in the Borrowed menu.

  • see their past requests in the History menu.

Reload
Figure 1. Admins
Reload
Figure 2. Borrowers

Constraints

If an admin is approving a request, it would not make sense for them to be able to approve the book on a date before the request was made; they should not be allowed to do that. The same goes with the date the user returns the book, they should not be able to return it before the approval date. Let’s implement this in MyLibraryBorrowed.

ApprovalStatusPENDINGAPPROVEDREJECTEDMyLibraryAuthorString firstNameString lastNameDate dateOfBirthBoolean isActive = trueString toString()MyLibraryBookString titleString descriptionint numberOfPagesint count = 0int getNumberOfInstances()int getNumberOfBooksBorrowable()MyLibraryBookInstanceBoolean isActive = trueInteger serialNumberBoolean isAvailableB = trueMyLibraryBorrowedDate requestDateDate approvalDateDate returnDateApprovalStatus statusOfApproval = ApprovalStatus.PENDINGUserlistOfBooks1*listOfBookInstance1*belongsTo*1borrowHistoryOfBook1*bookInstance*1user*1
Figure 3. Class Diagram for your convenience

We need to implement constraints on our domain entities, to make sure we provide compliant and logical datas to our library.

Try to add nullable constraint on dates for borrowed, with a validator (approval date needs to occur after the requestDate, and returnDate must be after borrow appproval date)

Solution
Add the following constraints:
static constraints = {
    approvalDate nullable: true, validator: { Date val, MyLibraryBorrowed obj ->
        if (val == null) return true
        return val >= obj.requestDate
    }

    returnDate nullable: true, validator: { Date val, MyLibraryBorrowed obj ->
        if (val == null) return true
        return val >= obj.approvalDate
    }

    bookInstance nullable:false
    user nullable:false
}

For more information, refer to the Grails documentation about constraints.

Define menus

As explained earlier, there will be two new menus only available to admins. Let’s add them in MyLibraryUiService in buildMenu.

Pro Mode: Add the menus [TODO 2]

Add the following menus:
menu MyLibraryController.&listOfUsers as MC
menu MyLibraryController.&listOfRequests as MC

Set up the Users

First, we will create a method to determine what type of user is connected in MyLibraryUiService.

Pro Mode: implement isAdmin [TODO 3.1].

Solution
boolean isAdmin() {
    User currentUser = springSecurityService.currentUser as User
    return currentUser?.authorities?.any { it.authority == 'ROLE_ADMIN' }
}

Second, we will to modify the controller index to be able to determine what type of user is connected in MyLibraryController.

Solution
Modify index():
def index() {
    currentUser = springSecurityService.currentUser as User
    isAdmin = currentUser?.authorities?.any { it.authority == 'ROLE_ADMIN' }
    redirect action: 'listAuthor'
}

Modify the Builders

To accommodate the changes, we will have to modify the author, book and userBorrowed table.

Solution
Modify buildAuthorTable TODO 4.1.1–4.1.4:
boolean isAdmin = isAdmin() //TODO 4.1.1
if(!isSelect && isAdmin) { // TODO 4.1.2 & TODO 4.1.4
if(!isAdmin) {filter.addFilter(buildIsActiveAuthorFilter(author))} //TODO 4.1.3
Modify buildBookTable TODO 4.2.1–4.2.8:
boolean isAdmin = isAdmin() //TODO 4.2.1
if(isAdmin) column {label "Number of instances "} // TODO 4.2.2
if(isAdmin) column {label "Modify number of Book Instances"} // TODO 4.2.3
if (!isAdmin) label "Request Form" // TODO 4.2.4

// TODO 4.2.5
if(!isAdmin) {
    MyLibraryBookInstance bookInstance = new MyLibraryBookInstance()
    filter.addFilter(new FilterExpression(true, Operator.EQ, book.listOfBookInstance_,bookInstance.isAvailableB_))
}

if (isAdmin) {rowColumn {rowField bookIterator.numberOfInstances_}} // TODO 4.2.6

// TODO 4.2.7 & 4.2.8
if(isAdmin) {
    rowColumn {
        rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&selectBookInstance as MC, bookIterator.id
        rowAction ActionIcon.ADD * IconStyle.SCALE_DOWN, MyLibraryController.&purchaseBook as MC, bookIterator.id
    }
} else {
    rowColumn {
        rowAction ActionIcon.CREATE * IconStyle.SCALE_DOWN, MyLibraryController.&requestBookInstance as MC, bookIterator.id
    }
}

We need to add a showUser variable, which allows showing borrow records for that specific user.

Solution
Modify buildUserBorrowsTable TODO 4.3.1–4.3.7:
Boolean isAdmin = isAdmin() // TODO 4.3.1
if (isCurrently  && !isAdmin) column {label "Return Book"} // TODO 4.3.2

if(isAdmin) {
    column { label borrowed.user_ }
    if (!showUser) {
        label "Approve Book"
    }
}

User currentUser = springSecurityService.currentUser as User
if (showUser) {
    currentUser = showUser
}

if(!isUser || showUser) {
    filter.addFilter(new FilterExpression(currentUser, Operator.EQ, borrowed.user_))
}

if (isCurrently && !isAdmin) {  // TODO 4.3.6

if(isAdmin) {
    rowColumn { rowField borrowedIterator.user.username_ }
    if (!showUser) {
        rowColumn {
            rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&approveBook as MC, borrowedIterator.id
        }
    }
}

Now that we have added the necessary elements to display all the requests of all the users. Let’s render this table.

Solution
def listOfRequests() {
    UiTableSpecifier tableUserBorrowsSpecifier = myLibraryUiService.buildUserBorrowsTable(true, null, true)
    UiFilterSpecifier filterUserBorrowsSpecifier = myLibraryUiService.buildUserBorrowsFilter()

    taackUiService.show(new UiBlockSpecifier().ui {
        tableFilter filterUserBorrowsSpecifier, tableUserBorrowsSpecifier
    }, myLibraryUiService.buildMenu())
}

User Menu

As explained earlier, we also want to have a table displaying all the users for admins. Let’s start by implementing the buildUsersTable (you can use the author table as a base exemple to implement that.

Solution
UiTableSpecifier buildUsersTable() {
    UiTableSpecifier buildUsersSpecifier = new UiTableSpecifier()
    User user = new User()

    buildUsersSpecifier.ui {
        header {
            label user.username_
            label "Authorities"
        }

        TaackFilter taackFilter = taackFilterService.getBuilder(User)
                .setSortOrder(TaackFilter.Order.ASC, user.username_)
                .setMaxNumberOfLine(10).build()

        iterate taackFilter, {User userIterator ->
            rowColumn {
                rowAction ActionIcon.SHOW * IconStyle.SCALE_DOWN, MyLibraryController.&showUser as MC, userIterator.id
                rowField userIterator.username_
            }
            rowField userIterator.authorities_
        }
    }
}

We can then render and serve it on a page from your controller

Solution
def listOfUsers() {
    UiTableSpecifier tableUsersSpecifier = myLibraryUiService.buildUsersTable()

    taackUiService.show(new UiBlockSpecifier().ui {
        table tableUsersSpecifier
    }, myLibraryUiService.buildMenu())
}

Let’s implement a showUser and buildUserShow methods in the controller and the UI service. Go to MyLibraryUiService

UIService
UiShowSpecifier buildUserShow(User user) {
    UiShowSpecifier userShowSpecifier = new UiShowSpecifier()

    userShowSpecifier.ui(user, {
        fieldLabeled user.username_
        fieldLabeled user.firstName_
        fieldLabeled user.lastName_
        fieldLabeled user.authorities_
    })
}
Controller
def showUser(User user) {
    UiShowSpecifier showSpec = myLibraryUiService.buildUserShow(user)
    UiTableSpecifier userBorrowsSpecifier = myLibraryUiService.buildUserBorrowsTable(false, user)
    UiFilterSpecifier userBorrowsFilterSpecifier = myLibraryUiService.buildUserBorrowsFilter(user)
    UiTableSpecifier userBorrowsCurrentlySpecifier = myLibraryUiService.buildUserBorrowsTable(true, user)

    taackUiService.show(new UiBlockSpecifier().ui {
        modal {
            show showSpec
            tableFilter userBorrowsFilterSpecifier, userBorrowsSpecifier
            tableFilter userBorrowsFilterSpecifier, userBorrowsCurrentlySpecifier
        }
    })

}

Security Part 1

Great, we now have everything we need. We just have to add the layer of security. One of the great things about taack is how simple this is. Just by adding @Secured(['ROLE_ADMIN']) above a method, you are protecting the method from any user that does not have the ROLE_ADMIN. @Secured(['ROLE_ADMIN']) will not only hide the icon but also make the method inaccessible via direct link.

Let’s go ahead and add @Secured(['ROLE_ADMIN']) and @Secured(['ROLE_BORROWER']) to the necessary methods. [TODO 6.1] Hint: there is a total of 11 SECURED to add in MyLibraryController.

In larger projects, adding conditional statements to hide buttons or columns can become very complex. This is why we recommend trying to have the same number of rows on display for all the pages/modals using the same builder. To hide buttons in this case, you can use another technique.

Security Part 2

You can create a file where you will specify the methods that have to be secured as well as the conditions in which some methods have to be secured.

Navigate to intranet/app/myLibrary/grails-app/services/my/library/MyLibrarySecurityService.groovy that has been created for you.

Most of the methods have been implemented for you. Read the documentation in the file for more information.

Using the same idea as returnSecurityClosure (which checks if the ApprovalStatus is Approved to allow the user to see the button to return a book) you will implement approvedSecurityClosure.

Pro Mode: implement approvedSecurityClosure [TODO 6.2]

SecurityService
private static boolean approvedSecurityClosure(Long id, Map p) {
    MyLibraryBorrowed borrowed = MyLibraryBorrowed.get(id)
    return (borrowed.statusOfApproval != ApprovalStatus.APPROVED)
}
Add the following lines of code in init:
TaackUiEnablerService.securityClosure(
    this.&approvedSecurityClosure,
    MyLibraryController.&approveBook as MethodClosure)

Congratulations! You have finished implementing the project.

In the next part, you are going to learn how to create graphs.