lundi 6 avril 2009

Building a Flex multi-tab CRUD application : adding security and server push

The first part of this article has described how to build the main application. The goal of this second part is to demonstrate how you can easily add security and collaborative updates to this existing application with the gdsflex 0.3 Grails plugin.

Before anything, we need to upgrade the previous project to the latest gdsflex plugin release 0.3 with :
grails install-plugin gdsflex

The usual way of adding security to a Grails application is to use the Acegi/Spring security Grails plugin. So now we have to setup this plugin.
grails install-plugin acegi

grails create-auth-domains User Role Requestmap

grails generate-manager

This will install the plugin and generate a set of gsp pages to create roles and users. Now browse http://localhost:8080/gdsexample/role/create, and create 2 roles ROLE_ADMIN and ROLE_USER. Then http://localhost:8080/gdsexample/user/create, and create 2 users admin and user, each one with one of the previously created roles. The names of the roles are important for Spring security and have to start with 'ROLE_'.

We also have to enable the Spring security integration in the GraniteDS config file:

web-app/WEB-INF/granite/granite-config.xml, uncomment or add the following line:
<security type="org.granite.messaging.service.security.SpringSecurityService"/>

Once this setup is done, we have to define a login page in the Flex application. To do this, we will simply add a ViewStack in the main MXML that will display either the login page or the application depending on the user login state. This ViewStack will be bound to the Tide Identity component that handles user authentication :

grails-app/views/GDSExample.mxml :
<?xml version="1.0" encoding="utf-8"?>

<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
layout="vertical"
backgroundGradientColors="[#0e2e7d, #6479ab]"
preinitialize="Spring.getInstance().initApplication()">

<mx:Script>
<![CDATA[
import org.granite.tide.spring.Spring;
import org.granite.tide.validators.ValidatorExceptionHandler;
import org.granite.tide.spring.Identity;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;

Spring.getInstance().addExceptionHandler(ValidatorExceptionHandler);

[Bindable] [In]
public var identity:Identity;


[Bindable]
private var message:String;

private function loginResult(event:TideResultEvent):void {
message = "";
}

private function loginFault(event:TideFaultEvent):void {
message = event.fault.faultString;
}
]]>
</mx:Script>

<mx:ViewStack id="appView" selectedIndex="{identity.loggedIn ? 1 : 0}" width="100%" height="100%">

<mx:VBox width="100%" height="100%" verticalAlign="middle" horizontalAlign="center" >
<mx:Panel title="Login"
horizontalAlign="center"
verticalGap="0" paddingTop="8" paddingBottom="8"
xmlns:mx="http://www.adobe.com/2006/mxml">

<mx:Form>
<mx:FormItem label="Username">
<mx:TextInput id="username"/>
</mx:FormItem>
<mx:FormItem label="Password">
<mx:TextInput id="password" displayAsPassword="true"
enter="identity.login(username.text, password.text, loginResult, loginFault);"/>
</mx:FormItem>
</mx:Form>

<mx:Label text="{message}"/>

<mx:Button label="Login"
click="identity.login(username.text, password.text, loginResult, loginFault);"/>
</mx:Panel>
</mx:VBox>

<mx:VBox id="mainUI" width="100%">
<mx:ApplicationControlBar id="acb" width="100%">
<mx:Label text="GraniteDS / Grails example" fontSize="18" fontWeight="bold" color="#f0f0f0"/>
<mx:Button label="Logout" click="identity.logout()"/>
</mx:ApplicationControlBar>

<BookUI id="bookUI" width="100%" height="100%"/>
</mx:VBox>

</mx:ViewStack>
</mx:Application>

As you can see, most of the code we have added is the login form. The authentication is completely handled by the Identity component. Here we make use of its two main methods login / logout and of its loggedIn property that is bindable and is here used to select which view is displayed. We also define a fault handler to display a login error message when the entered credentials are not correct.

You can check now on http://localhost:8080/gdsexample/GDSExample.swf that only authenticated people can see and change existing data.

To further refine the security of the application, we could decide that only administrators are allowed to delete books in our database. This can be done server-side with Spring security, but here we'll just see how to modify the UI according to the user access rights. This is not enabled by default in the plugin, we just have to change the plugin settings, and add a GraniteDSConfig.groovy in grails-app/conf:

grails-app/conf/GraniteDSConfig.groovy :
graniteConfig {

springSecurityAuthorizationEnabled = true

}

Then we have to make a few changes in the MXML UI to hide the Delete button when the user is not an administrator:

grails-app/views/BookEdit.mxml :

import org.granite.tide.spring.Identity;

[Bindable] [In]
public var identity:Identity;


<mx:HBox>
<mx:Button label="Save" click="save()"/>
<mx:Button label="Delete"
enabled="{!create}"
visible="{identity.ifAllGranted('ROLE_ADMIN')}"
click="remove()"/>
<mx:Button label="Cancel" click="cancel()"/>
</mx:HBox>

You can check that if you don't log in with a user having the ROLE_ADMIN role, you don't have the Delete button. You should of course also enable server-side Spring security (for example with the @Secured annotation) and define a AccessDeniedExceptionHandler that would catch all security errors to display a uniform message (see the documentation or GDS/Tide/Spring example for more information).
The Identity component provides 4 bindable boolean methods ifAllGranted, ifNotGranted, ifAnyGranted and hasPermission corresponding to the JSP tags available in the Spring security tag library. The hasPermission is not supported right now by the Grails plugin but it depends on the configuration of Spring security ACLs that itself would probably require a complete article.

To finish this post, we will add real-time multiuser updates by adding the Gravity push functionality to our application. First we have to enable these functionalities in the plugin settings:

grails-app/conf/GraniteDSConfig.groovy :
graniteConfig {

springSecurityAuthorizationEnabled = true

gravityEnabled = true

dataDispatchEnabled = false
}

Then we have to define a Gravity channel and topic in the services-config.xml Flex configuration:

web-app/WEB-INF/flex/services-config.xml:
<services-config>

<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">

<destination id="spring">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideSpringFactory</factory>
<persistenceManagerBeanName>tidePersistenceManager</persistenceManagerBeanName>
</properties>
</destination>
</service>

<service id="gravity-service"
class="flex.messaging.services.MessagingService"
messageTypes="flex.messaging.messages.AsyncMessage">
<adapters>
<adapter-definition id="simple" class="org.granite.gravity.adapters.SimpleServiceAdapter"/>
</adapters>

<destination id="bookManagerTopic">
<properties>
<session-selector>true</session-selector>
</properties>
<channels>
<channel ref="my-gravityamf"/>
</channels>
<adapter ref="simple"/>
</destination>
</service>
</services>

<factories>
<factory id="tideSpringFactory" class="org.granite.tide.spring.SpringServiceFactory">
</factory>
</factories>

<!--
! Declares my-graniteamf channel.
!-->
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>

<channel-definition id="my-gravityamf" class="org.granite.gravity.channels.GravityChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/gravityamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>

</services-config>

Here we have just added a new channel-definition, gravity-service and topic destination in the configuration, now we have to tell the Flex client and the Grails components to send/receive the data updates on this topic. The server part is relatively simple, we just have to annotate our controllers/services with the DataEnabled annotation:
@DataEnabled(topic="bookManagerTopic", params=ObserveAllPublishAll.class, publish=DataEnabled.PublishMode.ON_SUCCESS)

This annotation specifies the topic and has two other important parameters:

- 'params' defines a publishing selection class. To keep things simple here, we will just write a very simple selection class that allow everyone to observer and publish all updates. For some reason, using Groovy classes in annotations triggers an infinite loop in the Grails compilation, so we will just write a Java class this time:

src/java/ObserveAllPublishAll.java
import org.granite.tide.data.DataObserveParams;
import org.granite.tide.data.DataPublishParams;
import org.granite.tide.data.DataTopicParams;

public class ObserveAllPublishAll implements DataTopicParams {

public void observes(DataObserveParams params) {
// Define key/value pairs that the client is able to observe
// Will be used to construct the topic selector for the client calling the component
}

public void publishes(DataPublishParams params, Object entity) {
// Define key/value pairs to add in the published message headers
// Will be used to match against the topic selector of the subscribed clients
}
}

- 'publish' defines the publishing mode. ON_SUCCESS indicates that all remote calls on annotated components that do not trigger exceptions will dispatch the corresponding entity updates. In general you will want to have more control on the dispatching and use the MANUAL mode to be sure that updates are dispatched only on transaction success and include the dispatch in a Grails interceptor. We could also have used a transactional JMS topic to have a real transactional behaviour. Let's keep it simple for this example.

On Flex side, you then have to add the data observer component in the main MXML:

grails-app/views/GDSExample.mxml :
            import org.granite.tide.data.DataObserver;

// Register a data observer component with the name of the topic
Spring.getInstance().addComponent("bookManagerTopic", DataObserver);
// Binds the subscribe and unsubscribe methods of the component to the application login/logout events
Spring.getInstance().addEventObserver("org.granite.tide.login", "bookManagerTopic", "subscribe");
Spring.getInstance().addEventObserver("org.granite.tide.logout", "bookManagerTopic", "unsubscribe");

[In]
public var bookManagerTopic:DataObserver;

An interesting thing here is how you can bind a Tide event to any Tide component method, allowing for a complete decoupling between the component and the application.

Now if you open two browsers on the Flex application (in different http sessions, so for example you can try with one Firefox and one Internet Explorer, or better on two different machines), you can check that updates saved on one browser are (almost) immediately visible on the other.

If you look at the console, you will most likely see a lot of error stack traces concerning some RetryRequest error. This has something to do with some Grails filter logging the Jetty continuation retry exception. That's not extremely important, and it will be addressed in a later release. These configuration aspects of Gravity and other advanced features will also be improved to avoid having many places where to change configuration and centralize most of it in the GraniteDSConfig.groovy file.


Now we've seen a simple use of most features of GraniteDS. The complete project is available [here|http://www.graniteds.org/confluence/download/attachments/9371677/gdsexample-0.3.zip] and can be unzipped after having created the project and installed the various plugins (also remember to create roles and user otherwise you won't be able to log in).
We could have implemented the server side with any other Java server technology supported out-of-the-box by GDS/Tide (JBoss Seam, Spring, EJB3) with minimal change on the Flex side, but the Grails/GraniteDS combination seems a good choice to quickly develop Flex applications with a Java server backend.

7 commentaires:

Rob a dit…

The BookUI referenced in GDSExample.mxml is missing so the example does not compile. Could you please add this to the blog.

Rob a dit…

OK, I found the previous blog that provides the missing code.

Rob a dit…

Where is the Java Docs for org.granite.tide.data.DataEnabled ?

Roger a dit…

Hello, i am trying to understand how gravity works since i have no previous experience with data push, i am working on top of this example and the GAE examples as well. I am particularly interested in filtering updates for observers, for example, let's say that only users with a certain role can see a certain record, instead of publishing to all clients i would like to know how to filter the updates so only users with a given role will see the update, in this post you mention the ObserveAllPublishAll.java class but the methods are not implemented, how can i implement the filter logic in these methods?

Thanks.

Jean-brice a dit…

hi, i need an example with more details about Manual publishing.
I did research on granite website but this too vague about manual publishing and interceptor.
I don't now how use the interceptor with the observer.
Is there anybody who can help me?

kwame a dit…

Very nice all these, but can somebody tell me how to integrate all these in Flexbuilder 3?
I tried to import a Grails/gdsflex in eclipse and enabled the Flex nature; but the project has been flaged as errors.
I urgently need help .
many Thanks,
Kwame.

lala a dit…

Thanks for your post and welcome to check: here
.