|
|
|
To do list part IV - Security
We have our todo list application in good shape now, except there is no security yet. Let's look at what Zero provides in the realm of securing our application and modify our application from the hardcoded user johndoe to authenticating the user and controlling access.
The following concepts are covered in this tutorial:
- Security configuration
- Authentication and authorization
Security configuration
So far, we have done nothing to secure our application. Once again, we go back to the configuration file for a Zero application, zero.config, to specify how we intend to secure the to do list application and use the following steps. For this tutorial, we will use the project we created in the previous tutorial and build upon the same project to add security.
- Stop the simpletodo3 application.
- Double click zero.config.
- Add the following content to the
zero.config file:
[@include ${zero.core}/config/security/rule.config]
uri=/.*
authType=Basic
groups=[ALL_AUTHENTICATED_USERS]
- Save the
zero.config file
You are already familiar with the format used in the zero.config file to configure a Zero application. Let's look at the security configuration that we just added:
- uri
- Defines what is to be protected and defines the protected resources.
- authtype
- Can either be Basic for basic authentication or Form for form based authentication.
- groups
- Defines the groups of users who are allowed to access the protected resources.
For more information on security configuration, see the Authentication and Authorization article.
Our application is secure and we have specified that any restful resource should only be accessed by someone whose identity has been verified. How do we verify someone's identity? We need a list of users who are authorized to access the application. Let's create such a list using the following steps:
- Select the config folder.
- Click File -> New -> File and name the new file zero.users as shown on the following screen.
- Add the following content to the
zero.users file:
johndoe:bed128365216c019988915ed3add75fb:todogroup
- Save the
zero.users file.
Working with the zero.users file
This file serves as a simple user registry that contains the username, encrypted password and the group(s) that the user belongs to. Currently, we have johndoe as the user, with passw0rd as the password, who belongs to the group todogroup. You might be wondering how we got from passw0rd to bed128365216c019988915ed3add75fb? That, and more, is explained in the file based user service article. There, you will also find instructions on how to create user accounts and generate your own user registry file.
Zero supports a file based user registry as well as an LDAP based directory for storing the information about user accounts. For more information on how to use these services, see the User Service article.
Authentication and authorization
The application is only accessible by johndoe now. In our earlier tutorial, we had hardcoded the user to always be johndoe. Let's change that now using the following steps:
- Create a file under /app/resources and name it
todos.bnd
- Add the following contents to the file
todos.bnd :
owners/todos
This line allows us to specify the ID of the owner on the URI, eg /owners/johndoe/todos. With the security enabled, only the owner can manipulate his or her todo list. The security will check the authenticated user ID against this owner ID. For more information on bnd, visit the resource handling article.
- Open the
/app/resources/todos.groovy file for editing.
- Replace the content of the file with the following:
/*
* ============================================================================
* (C) Copyright IBM Corporation 2006, 2007. All Rights Reserved.
*
* You may only copy, modify, and distribute these samples internally.
* These samples have not been tested under all conditions and are provided
* to you by IBM without obligation of support of any kind.
*
* IBM PROVIDES THESE SAMPLES "AS IS" SUBJECT TO ANY STATUTORY WARRANTIES THAT
* CANNOT BE EXCLUDED. IBM MAKES NO WARRANTIES OR CONDITIONS, EITHER EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OR
* CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
* NON-INFRINGEMENT REGARDING THESE SAMPLES OR TECHNICAL SUPPORT, IF ANY.
* ============================================================================
*/
def onList() {
logger.INFO {"onList called"};
def ownerId = request.params.ownersId.get();
def owner = request.subject['remoteUser'];
logger.INFO {"authenticated remote user: $owner"};
logger.INFO {"user ID on the URL: $ownerId"};
if (!ownerId.equals(owner)) {
request.status = HttpURLConnection.HTTP_FORBIDDEN;
} else {
sendJSONData(zero.data.groovy.Manager.create('simpletodo'), owner);
}
}
def onCreate() {
logger.INFO {"onCreate called"};
def ownerId = request.params.ownersId.get();
def owner = request.subject['remoteUser'];
if (!ownerId.equals(owner)) {
request.status = HttpURLConnection.HTTP_FORBIDDEN;
} else {
def item = request.params.item.get();
def priority = request.params.priority.get();
logger.INFO {"Item to create: $item" };
if (item != null && item.size() > 0) {
def dm = zero.data.groovy.Manager.create('simpletodo');
def id = dm.insert("INSERT INTO todos (owner, item, priority, status) VALUES($owner, $item, $priority, 0)", ['id']);
request.status=HttpURLConnection.HTTP_CREATED
request.headers.out.Location=request.uri +'/'+id
sendJSONData(dm, owner);
}else {
request.status = HttpURLConnection.HTTP_NO_CONTENT;
}
}
}
def onDeleteCollection() {
logger.INFO {"onDeleteCollection not used in the app"};
}
def onRetrieve() {
logger.INFO {"onRetrieve called"};
def todosId = request.params.todosId.get();
def ownerId = request.params.ownersId.get();
def owner = request.subject['remoteUser'];
logger.INFO {"authenticated remote user: $owner"};
logger.INFO {"user ID on the URL: $ownerId"};
if (!ownerId.equals(owner)) {
request.status = HttpURLConnection.HTTP_FORBIDDEN;
} else if (todosId == null) {
request.status = HttpURLConnection.HTTP_BAD_REQUEST;
} else {
def dm = zero.data.groovy.Manager.create('simpletodo');
def data = dm.update("SELECT id, item, priority, status FROM todos WHERE owner = ? AND id = ?", ownerId, todosId);
request.view="JSON";
request.json.output = data;
zero.core.views.ViewEngine.render();
}
}
def onUpdate() {
logger.INFO {"onUpdate called"};
def todosId = request.params.todosId.get();
def ownerId = request.params.ownersId.get();
def owner = request.subject['remoteUser'];
logger.FINEST {"todosID = $todosId ownerID = $ownerId owner = $owner"};
if (!ownerId.equals(owner)) {
request.status = HttpURLConnection.HTTP_FORBIDDEN;
} else if (todosId == null) {
request.status = HttpURLConnection.HTTP_BAD_REQUEST;
} else {
def status = request.params.status.get();
def dm = zero.data.groovy.Manager.create('simpletodo');
logger.FINEST {"UPDATE todos SET status = $status WHERE owner = $owner AND id = $todosId"};
dm.update("UPDATE todos SET status = $status WHERE owner = $owner AND id = $todosId");
}
}
def onDelete() {
logger.INFO {"onDelete called"};
def todosId = request.params.todosId.get();
def ownerId = request.params.ownersId.get();
def owner = request.subject['remoteUser'];
if (!ownerId.equals(owner)) {
request.status = HttpURLConnection.HTTP_FORBIDDEN;
} else if (todosId == null) {
request.status = HttpURLConnection.HTTP_BAD_REQUEST;
} else {
def dm = zero.data.groovy.Manager.create('simpletodo');
dm.update("DELETE FROM todos WHERE owner = ? AND id = ?", [owner, todosId]);
}
}
def sendJSONData(dm, owner) {
data = dm.queryList("SELECT id, item, priority, status FROM todos WHERE owner = $owner ORDER BY priority");
logger.FINEST {"sending json data: "+data};
request.headers.out."Content-Type"="application/json"
request.view="JSON";
request.json.output = data;
render();
}
Working with the todos.groovy file
Lets talk about what changes in todos.groovy. We first get the ownerId, which is the id of the person who owns the to do list that we are trying to access, and then we get the owner, which is the user information that was provided when the user was challenged, and given the oppotunity to provide login information. If they match, we go ahead and process the request, otherwise, we set the appropriate response code. Also, we have removed the hardcoded user johndoe and updated the code to reflect the logged in user as the owner when we update the to do list.
- Open the
/app/public/todo.gt file for editing.
- Add the following contents to the todo.gt file:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!--
/*
* ============================================================================
* (C) Copyright IBM Corporation 2006, 2007. All Rights Reserved.
*
* You may only copy, modify, and distribute these samples internally.
* These samples have not been tested under all conditions and are provided
* to you by IBM without obligation of support of any kind.
*
* IBM PROVIDES THESE SAMPLES "AS IS" SUBJECT TO ANY STATUTORY WARRANTIES THAT
* CANNOT BE EXCLUDED. IBM MAKES NO WARRANTIES OR CONDITIONS, EITHER EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OR
* CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
* NON-INFRINGEMENT REGARDING THESE SAMPLES OR TECHNICAL SUPPORT, IF ANY.
* ============================================================================
*/
-->
<html>
<head>
<title>Todo List</title>
<link REL=StyleSheet HREF="todo.css" TYPE="text/css">
<script type="text/javascript"
src="<%= getRelativeUri("/dojo/dojo.js.uncompressed.js") %>"
djConfig="parseOnLoad: true, isDebug: false">
</script>
<script type="text/javascript">
dojo.require("dojo.parser");
dojo.require("dijit.TitlePane");
var owner = "<%=request.subject['remoteUser']%>"
var ownerURL = '<%= getRelativeUri("/resources/owners") %>';
function onError(response) {
alert(response);
}
function displayItems(response) {
var todolist = "";
var dellist = "";
var todolistSize = 0;
var dellistSize = 0;
for (var i=0; i<response.length; i++) {
var id = response[i].id;
var desc = response[i].item;
var priority = response[i].priority;
var status = (response[i].status == 0 ? false : true);
if (0 == priority) {
priority = "high";
} else if (1 == priority) {
priority = "medium";
} else {
priority = "low";
}
var checked = (status ? "checked" : "");
var name = "todo"+i;
var item = '<input type="checkbox" name="'+name+
'" onclick="moveTodo('+name+')" value="'+id+'"'+checked+'>' +
'<SPAN CLASS="'+priority+'">' + desc + '</SPAN> <BR>';
//if status is true (checked), the item goes to delList
//otherwise, it remains in the todo list
if (status) {
dellist += item;
dellistSize++;
} else {
todolist += item;
todolistSize++;
}
}
var todoPane=dijit.byId("todoPane");
todoPane.setTitle("Your todo items: "+todolistSize);
var delPane=dijit.byId("delPane");
delPane.setTitle("Your completed items: "+dellistSize);
document.getElementById("todoList").innerHTML = todolist;
document.getElementById("delList").innerHTML = dellist;
}
function addTodo() {
var data = {item: document.todoForm.item.value,
priority: document.todoForm.priority.selectedIndex};
var req = dojo.xhrPost({
url: ownerURL + '/' + owner + '/todos',
content: data,
sync: false
});
req.addCallbacks(getTodos, onError);
document.todoForm.reset();
}
function deleteTodos() {
var list = document.delForm;
var size = list.length - 1;
for (var i=0; i<size; i++) {
var req = dojo.xhrDelete({
url: ownerURL + '/' + owner + '/todos/' +list[i].value,
sync: true
});
}
// do this as two separate requests so that we can
// invoke the callback only when all deletes are finished
var req = dojo.xhrDelete({
url: ownerURL + '/' + owner + '/todos/' +list[size].value,
sync: false
});
req.addCallbacks(getTodos, onError);
}
function moveTodo(checkbox) {
var req = dojo.xhrPut({
url: ownerURL + '/' + owner + '/todos/'+ checkbox.value,
content: {status: (checkbox.checked? 1: 0)},
sync: false
});
req.addCallbacks(getTodos, onError);
}
function getTodos() {
var req = dojo.xhrGet({
url: ownerURL + '/' + owner + '/todos',
handleAs: 'json-comment-optional',
sync: false
});
req.addCallbacks(displayItems, onError);
}
dojo.addOnLoad(function() {
getTodos();
});
</script>
</head>
<body>
<div dojotype="dijit.TitlePane" title="Your todo items:"
id="todoPane">
<form name="todoForm">
<div id="todoList">
</div>
<br>
<input type="text" id="item"> <select name=
"priority">
<option value="1">
Priority one
</option>
<option value="2">
Priority two
</option>
<option value="3">
Priority three
</option>
</select> <input type="button" onclick="addTodo();"
value="Add">
</form>
</div>
<div dojotype="dijit.TitlePane" title=
"Your completed items:" id="delPane" open="false">
<form name="delForm">
<div id="delList">
</div>
<br>
<input type="button" onclick="deleteTodos();"
value="Delete">
</form>
</div>
</body>
</html>
Working with the todo.gt file
Let's discuss what changes we have made to the todo.gt file. First, we obtain the logged in user and store in as owner. Any time that the to do list is accessed, we need to access it using the /resources/owners//todos format. This URI format is very readable: We are accessing the todo list for a particular user, and only that user can manipulate his or her todo items. For more information, visit the resource handling article.
Running the application
To run the application, use the following steps:
- Right-click the project and click Run As -> Project Zero Application.
- Using your browser, go to the http://localhost:8080/todo.gt site.
- Provide your username and password (For this tutorial we used johndoe as the username and passw0rd as the password as shown on the following screen:
And... you are finished! You have created a rich and secure to do list Zero application that uses a database and a user registry. That was just too easy. Go ahead, check out Project Zero in depth and explore the possibilities !!!
<<< Previous
|