Section 18.3. Authorization

18.3. Authorization

Authentication tells you who is connecting, not whether you should accept their connection. Even if you allow someone to connect, you may not want to grant access to all the functions and resources available in an application. Authorizationcontrolling who can do whatis every bit as important as authentication in creating secure applications. We need to look at two parts of authorization in relation to FlashCom applications. The first is deciding whether a user or process will be allowed to establish a connection to an application instance. The second is controlling access to functions and resources in the instance.

Deciding if someone is allowed to connect and what he can do when you accept his connection is usually determined using one of two schemes. The first and simplest is to use application-wide, role-based authorization. Every user is assigned one or more roles within the application such as administrator, moderator, presenter, or participant. If a user doesn't have at least one role, she can't connect to an application instance. The roles assigned to each user also determine what the user can do within the application. The second and more complex way to control access is to use an access control table . Each record in the table associates a user or group of users with a resource or group of resources and defines the type of access they have to the resource. Access control recordsor something like themare required in larger, more complex applications such as a conferencing system in which access to different conference rooms must be controlled on a room-by-room basis.

18.3.1. Role-Based Authorization

It is possible for a user to have more than one role within an application. To accommodate multiple roles for each user, a user roles tables is often created within a database. For demonstration purposes, Table 18-4 and Table 18-5 show the structure of two database tables that define each role and the roles each user has.

Table 18-4. PFCS_ROLE table

Attribute name

Datatype

Notes

roleName

varchar(100)

Primary key


Table 18-5. PFCS_USERROLES table

Attribute name

Datatype

Notes

roleName

varchar(100)

Together these two attributes are the primary key of this table. The roleName attribute refers to the PFCS_ROLE table's roleName.

userName

varchar(100)

The userName attribute refers to the PFCS_USER.userName attribute.


The PFCS_ROLE table defines the allowed role names for the application that will appear in the roleName column of the PFCS_USERROLES table. The role names will depend on the roles that need to be defined for an application. The PFCS_USERROLES table defines the roles that each user plays within the application. The database does not define what the roles mean. The application code uses each user's role names to determine what resources and features to provide to them.

For example, suppose that the application is a simple conference application run by a university. Authentication might be done against the university's directory server but only people registered, presenting, or organizing the conference will have access to the application. In that case, we might define four roles and assign them to users in the database as illustrated in Table 18-6 and Table 18-7.

Table 18-6. Sample role names for a conference application

roleName

administrator

presenter

participant

cameraOperator


Notice that in Table 18-7 a single user can appear in more than one record if he has more than one role. User "blesser" is both an administrator and a presenter. User "joey" is both a cameraOperator and a presenter.

Table 18-7. Sample rows in the PFCS_USERROLES table

userName

roleName

blesser

administrator

blesser

presenter

justin

participant

joey

cameraOperator

joey

presenter

rreinhardt

presenter

peldi

participant


When a user connects to an application instance employing role-based authorization, we need to know her role(s) in order to decide if the connection should be allowed. Then we need to grant access only to the resources each user needs according to her role(s). For example, we want participants to be able to view session streams only, whereas camera operators need to be able to connect and send session streams as well as view them. Presenters may need to read and write to a personal area in a presentations directory. For example, /presentations/peldi would be user "peldi's" private area for storing and playing back streams.

So the first thing we have to do is get each user's role names from the database when he authenticates and then have a look at them. If the same database contains all the user and user role information, a simple SELECT statement can retrieve a query that can be used to build a result record to return to the application. If a separate directory system is in place, a two-step process is used. The user is authenticated against the directory service and then a SELECT is run on the database to retrieve the user's roles. In either case, the result is returned to the application server and FlashCom. There are many ways to get user and role information and return it. The sample database and code shown here is just one option.

If a Flash client is using a number of services via a Remoting gateway, the services may also have to provide different functions and information depending on the user's roles. In other words, both the application server and FlashCom will need to authenticate and get the roles for each client. Using ColdFusion, the easiest way to protect both sets of services ( components ) is to place an Application.cfm file in the same directory or in one above them in the directory tree.

Example 18-13 lists a complete Application.cfm file designed to force clients to authenticate before calling services and to establish the client's username and role. The script also allows FlashCom application instances to log in. When an application instance connects, it passes its application.name as a username and its hostIdentifier as a password. Flash clients pass the user's username and a plain text password.

Example 18-13. An Application.cfm that authenticates FlashCom and Flash client requests to the services it protects
 <cfsetting showdebugoutput="no" /> <cfapplication name="Conference" /> <cfset dbName="pfcs" /> <cfset ipList=StructNew( ) /> <cfset ipList["127.0.0.1"] = "k38djskl3o9" /> <cfset ipList["172.28.253.102"] = "cmvhw1s0lv" /> <cfset ipList["192.168.0.1"] = "0kn59sxnd" /> <cflogin>   <cfif not isDefined("cflogin")>     <cfabort showerror="Invalid credentials." />   </cfif>   //Deal with Flash Communication Server instance connections   <cfif (FindNoCase("FlashCom", CGI.HTTP_USER_AGENT) eq 1)>     <cfif (ipList[CGI.REMOTE_ADDR] neq cflogin.password)>       <cfabort showerror="Invalid credentials.." />     <cfelse>       <cfloginuser name="#cflogin.name#" password="FlashCom" roles="FlashCom"/>     </cfif>   //Deal with Flash client connections   <cfelse>   <cftry>     <cfquery name="authenticate" datasource="#dbName#">       SELECT u.userName, ur.roleName       FROM PFCS_USER u, PFCS_USERROLES ur       WHERE       u.userName = <cfqueryparam value="#cflogin.name#">       AND password = <cfqueryparam value="#Hash(cflogin.password)#">       AND u.userName = ur.userName     </cfquery>     <cfif authenticate.recordCount eq 0>       <cfabort showerror="Invalid credentials..." />     </cfif>     <cfset roles="" />     <cfloop query="authenticate" >       <cfset roles = ListAppend(roles, "#authenticate.roleName#") />     </cfloop>     <cfloginuser name="#cflogin.name#"                  password="#cflogin.password#"                  roles="#roles#" />   <cfcatch type="Any">     <cfthrow message = "Invalid credentials...." >   </cfcatch>   </cftry>   </cfif> </cflogin> 

When a Flash client attempts to access a service protected by this Application.cfm file, it must pass the username and password using NetConnect.setCredentials( ) . For example, using Flash Remoting for AS 2.0, a service proxy is created:

 authService = new Service(   "https://flash-communications.net/flashservices/gateway",   null,   "pfcs.ch18.conference.user",   null,   null); 

When the user logs in, the NetConnection's credentials are set, and then the service's getTicket( ) method is called:

 authService.connection.setCredentials(pfcs.user.userName, pfcs.user.password); var temp_pc:PendingCall = authService.getTicket({userName:pfcs.user.userName}); temp_pc.responder = new RelayResponder(this,"onAuthResult","onAuthFault"); 

The request to call a function on pfcs.ch18.conference.user is intercepted by the Application.cfm script, and the credentials set in the client are available as properties in the <cflogin> struct. The SELECT statement looks them up in the PFCS_USER table but also retrieves the user's roles, if any, from the PFCS_USERROLES table:

 SELECT u.userName, ur.roleName FROM PFCS_USER u, PFCS_USERROLES ur WHERE u.userName = <cfqueryparam value="#cflogin.name#"> AND password = <cfqueryparam value="#Hash(cflogin.password)#"> AND u.userName = ur.userName 

If there are no PFCS_USERROLES rows for the user, no records are returned and the client will not be allowed to log in. ColdFusion stores user roles in a comma-delimited string (also called a list in ColdFusion), so a <cfloop> tag is used to loop through all the records returned by the query and build up the string:

 <cfset roles="" /> <cfloop query="authenticate" >   <cfset roles = ListAppend(roles, "#authenticate.roleName#") /> </cfloop> 

When the roles are collected in a string, the <cflogin> tag is used to log the client in to the ColdFusion application and the service's function will be called:

 <cfloginuser name="#cflogin.name#" password="#cflogin.password#" roles="#roles#" /> 

If the login does not succeed, the service's function will not be called. The getTicket( ) function does not need to authenticate the client. It only has to create a ticket and store it in the database. When the application instance needs to check the ticket, it calls the enhanced checkTicket( ) function shown in Example 18-14 (compare it with the original in Example 18-4). To stop Flash clients that have logged in from calling checkTicket( ) , hostIdentifier and agent checks are still done.

Example 18-14. The checkTicket( ) function, enhanced to support roles
 <cffunction name="checkTicket" access="remote" returnType="struct" output="false">   <cfargument name="ticketID" type="string" required="true">   <cfargument name="ticket" type="string" required="true">   <cfargument name="ip" type="string" required="true">   <cfargument name="hostIdentifier" type="string" required="true">   <cfset result=StructNew( ) />   <cfset result.success = 0 />   <cfif (FindNoCase("FlashCom", CGI.HTTP_USER_AGENT) neq 1)>     <cfset result.error="user agent error." >     <cfreturn result />   </cfif>   <cfset ipList=StructNew( )>   <cfset ipList["127.0.0.1"] = "k38djskl3o9">   <cfset ipList["172.28.253.102"] = "cmvhw1s0lv">   <cfset ipList["192.168.0.1"] = "0kn59sxnd">   <cfif (ipList[CGI.REMOTE_ADDR] neq hostIdentifier)>     <cfset result.error="host identifier error." >     <cfreturn result />   </cfif>   <cftry>    <cfset rightNow = CreateODBCDateTime( Now( ) ) >     <cfquery datasource="#dbName#" name="userTicketQuery">       SELECT u.userName, u.firstName, u.lastName, u.email, ur.roleName       FROM PFCS_USER u, PFCS_TICKET t, PFCS_USERROLES ur       WHERE t.ticketID = <cfqueryparam cfsqltype="cf_sql_varchar"                                        value="#ticketID#">       AND t.ticket = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticket#">       AND t.ip = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ip#">       AND (#rightNow# BETWEEN t.issueDateTime AND t.staleDateTime)       AND u.userName = t.userName       AND u.userName = ur.userName     </cfquery>     <cfif userTicketQuery.RecordCount eq 0>       <cfset result.error="record of zero." >       <cfreturn result />     </cfif>     <cfquery datasource="#dbName#" name="deleteTicket">       DELETE FROM PFCS_TICKET       WHERE ticketID = <cfqueryparam cfsqltype="cf_sql_varchar"                                      value="#ticketID#">       AND ticket = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticket#">     </cfquery>     <cfset result.success = 1 />     <cfset result.userName = userTicketQuery.userName />     <cfset result.firstName = userTicketQuery.firstName />     <cfset result.lastName = userTicketQuery.lastName />     <cfset result.email = userTicketQuery.email />     <cfset result.roles=StructNew( ) />     <cfloop query="userTicketQuery">       <cfset result.roles["#userTicketQuery.roleName#"] = true />     </cfloop>     <cfcatch type="Any">       //Uncomment the next line for debugging purposes       <cfset result.error = cfcatch.detail />       <cfreturn result />     </cfcatch>   </cftry>   <cfreturn result /> </cffunction> <cffunction name="silentLogout"             access="remote" returnType="string" output="false" >   <cflogout  />   <cfreturn "OK" /> </cffunction> <cffunction name="logout" access="remote" returnType="string" output="false" >   <cflogout  />   <cfreturn "OK" /> </cffunction> 

The code performs a join on three tables to get the user's personal information, ticket information, and role names in one SELECT statement. In ActionScript, it is more convenient to have each role as a property of an object, so the code creates a struct with a property for each role within the result struct:

 <cfset result.roles=StructNew(  ) /> <cfloop query="userTicketQuery">   <cfset result.roles["#userTicketQuery.roleName#"] = true /> </cfloop> 

Finally, Example 18-15 shows the onAuthenticate( ) method that is called when a client uses a ticket to connect to an application instance. Compare it to the original in Example 18-5.

Example 18-15. The onAuthenticate( ) method, enhanced to support roles
 application.onAuthenticate = function (client, result) {   // Make sure the authenticated user has not already logged in.   if (pfcs.clients[result.username]) {     application.rejectConnection(client, {msg:"You are already logged in."});     return;   }   // Exclude client's with no roles from connecting.   var noRoles = true;   for (var p in result.ROLES) {     noRoles = false;     break;   }   if (noRoles) {     application.rejectConnection(client, {msg:"Access Denied."});     return;   }   // Set up the client's user record.   client.pfcs = {user:{}};   var user = client.pfcs.user;   user.userName = result.USERNAME;   user.firstName = result.FIRSTNAME;   user.lastname = result.LASTNAME;   user.email = result.EMAIL;   user.roles = result.ROLES;   // Store the client in the   pfcs.clients   object by   userName   .   pfcs.clients[user.userName] = client;   // Control access and bandwidth based on one and only one of the   // user's roles.   client.readAccess = "";   // Deny all readAccess to start.   client.writeAccess = "";  // Deny all writeAccess to start.   if (user.roles["administrator"]) {     client.readAccess = "/";      client.writeAccess = "/";   }   else if (user.roles["presenter"]) {     client.readAccess = "/sessions;/presentations/" + user.userName;     client.writeAccess = "/presentations/" + user.userName;     client.setBandwidthLimit(250000, 100000);   }   else if (user.roles["cameraOperator"]) {     client.readAccess = "/sessions";     client.writeAccess = "/sessions";     client.setBandwidthLimit(250000, 500000);   }   else if (user.roles["participant"]) {     client.readAccess = "/sessions";     client.setBandwidthLimit(250000, 8000);   }   // Accept the client's connection request.   application.acceptConnection(client); }; 

Much of the code should look familiar. The result object that includes the user's personal information is passed into onAuthenticate( ) . The result object also contains a roles object that can be checked for certain roles. For example, if the user has the role "administrator", full access to read and write any resource within the instance is granted, and no bandwidth limits are adjusted:

 if (user.roles["administrator"]) {   client.readAccess = "/";   client.writeAccess = "/"; } 

On the other hand, if the user role is "participant", then no write access is provided at all and the client-to-server bandwidth limit is reduced to constrain how much data the client can send to the server:

 else if (user.roles["participant"]) {   client.readAccess = "/sessions";   client.setBandwidthLimit(250000, 8000); } 

Functions within an application instance may have to check if a user has a certain role before performing certain tasks . In Example 18-15, the roles object is stored in the client object as the client.pfcs.user.roles object. The role can be looked up directly, or a simple global function can be used to find out if a client has a certain role:

 function clientInRole(client, role) {   if (client.pfcs.user.roles[role]) {     return true;   }   return false; } 

18.3.1.1 Component-level authorization

In component-based applications, each component is responsible for managing its own streams, shared objects, and remote methods . When a client is added to or connects to a component, the component should look at the user's roles and decide what resources to make available to the client. The best way to control access is to deny all access to each client when it attempts to connect to the application and then have each component grant access only to resources within its own namespace. For example, after turning off read and write access, the client's readAccess and writeAccess strings will be empty. If a TextChat component allows read and write access to some of its resources, it will add paths to them. A SharedText component will do the same. As each component grants access, the readAccess and writeAccess properties of the client will grow into long semicolon-delimited strings.

Table 18-8 shows the readAccess and writeAccess strings a user with an administrator role might have in an application. For readability, line breaks have been added after each semicolon. Table 18-9 shows the access strings for someone with the role of participant.

Table 18-8. Access strings for an administrator

Client property

Value

readAccess

pfcs/PeopleGrid/main;

pfcs/SharedText/main;

pfcs/TextChat/main/currentSession;

pfcs/TextChat/main/sessionArchive

writeAccess

pfcs/PeopleGrid/main


Table 18-9. Access strings for a participant

Client property

Value

readAccess

pfcs/PeopleGrid/main;

pfcs/SharedText/main;

pfcs/TextChat/main/currentSession

writeAccess

(None)


Note that even the administrator does not have root-level access. Each part of each access string gives access either to a component's unique namespace or to an area within it. For example, both users have read access to the pfcs.PeopleGrid.main URI containing the people shared object. However, no one has access to the pfcs/TextChat/main URI. The participant has access to the pfcs/TextChat/main/currentSession URI, and the administrator has access to both the pfcs/TextChat/main/currentSession and pfcs/TextChat/main/sessionArchive URIs. The administrator has write access to one URI and the participant has no write accessfor participants, writeAccess is an empty string.

In other words, everyone can read the people shared object so that the client-side PeopleGrid component can display its contents. However, no client can update the people shared object directly. The server-side PeopleGrid component is responsible for updating its shared object. Even a malicious user or attacker with a hacked (modified) Flash client cannot update the shared object directly. Similarly, every TextChat user has read access to the pfcs/TextChat/main/currentSession URI so that her client can receive messages from the pfcs/TextChat/main/currentSession/messages shared object. TextChat messages are sent to the server using NetConnection.call( ) and are checked before being sent to all clients.

18.3.1.2 Implementing component-level authorization

To illustrate one of many ways to implement component-level authorization, I'll use the components first introduced in Chapter 13 and the simple PFCS component framework introduced in Chapter 15. The server-side object part of each component is based on the PFCSComponent class. Example 18-16 shows the methods related to controlling read and write access to streams and shared objects.

Example 18-16. PFCSComponent access control methods
 PFCSComponent.prototype.addAccessPath = function (client, accessStringName, path) {   var pathArray = this.getAccessArray(client, accessStringName);   pathArray.push(path);   client[accessStringName] = pathArray.join(";"); }; PFCSComponent.prototype.removeAccessPath = function (client,                                                      accessStringName, path) {   var pathArray = this.getAccessArray(client, accessStringName);   var len = pathArray.length;   for (var i = 0; i < len; i++) {     if (pathArray[i] == path) {       pathArray.splice(i, 1);    // Remove the element       break;     }   }   client[accessStringName] = pathArray.join(";"); }; PFCSComponent.prototype.getAccessArray = function (client, accessStringName) {   var accessString = client[accessStringName];   if (accessString.length > 0)     return accessString.split(";");   else     return []; }; 

The access control methods do the work of adding or deleting a string from either the client.readAccess or client.writeAccess string. Now, let's have a look at how a PeopleGrid component makes use of the addAccessPath( ) and removeAccessPath( ) methods. Since the class is small, Example 18-17 is a complete listing.

Example 18-17. The server-side PeopleGrid class
 function PeopleGrid (name) {   PFCSComponent.apply(this, [name]);   this.so = SharedObject.get(this.path + "/people"); } PeopleGrid.prototype = new PFCSComponent( ); PeopleGrid.prototype.constructor = PeopleGrid; PeopleGrid.prototype.className = "PeopleGrid"; PeopleGrid.prototype.addClient = function (client) {   var user = pfcs.getUser(client);   this.so.setProperty(user.userName, user);   // Let all clients read the shared object.   this.addAccessPath(client, "readAccess", this.path);   // Only administrators can write to the shared object.   if (pfcs.clientInRole(client, "administrator")) {      this.addAccessPath(client, "writeAccess", this.path);   } }; PeopleGrid.prototype.removeClient = function (client) {   var user = pfcs.getUser(client);   this.so.setProperty(user.userName, null);   // Remove all the access strings that were added for this component.   this.removeAccessPath(client, "readAccess", this.path);   if (pfcs.clientInRole(client, "administrator")) {     this.removeAccessPath(client, "writeAccess", this.path);   } }; PeopleGrid.prototype.close = function ( ) {   this.so.close( ); }; 

When a client is registered with a component on the server, the component's addClient( ) method is called just as illustrated in Chapter 13 and Chapter 15. In Example 18-17, the component gets information from the PFCS framework and the user's roles and then calls its own addAccessPath( ) method to add access strings as illustrated in Table 18-8 and Table 18-9. When the client is removed from the chat, the removeClient( ) method is called and the component uses the removeAccessPath( ) method to deny access to the client.

18.3.1.3 Granting temporary access

In some cases, you may want to grant only temporary access to a resource. For example, in the SharedText example in Chapter 13, a voluntary locking system was used to control which client could update the component's shared object. One way to enforce locking, rather than having it based on cooperating clients, would be to abandon having each client update the shared object directly. Every client would use NetConnection.call( ) to request updates. If the server received an update request from a client that did not have the shared text area locked, the server would ignore the update request.

In many cases, using NetConnection.call( ) to send data that is checked over by the server is the best and most secure approach. However, if the client is able to add, delete, and update items, then it is possible to grant temporary access to each client without imposing the potential performance penalty of handling all the data in Server-Side ActionScript.

Example 18-18 shows the server-side SharedText.edit( ) method. It is very similar to the one described in Chapter 13 with the difference that the addAccessPath( ) and removeAccessPath( ) methods are used to provide one, and only one, client with write access to the shared object.

Example 18-18. The SharedText.edit( ) method with access control
 SharedText.prototype.edit  = function (userName) {   var client = pfcs.getClient(userName);   var currentUser = this.so.getProperty("currentUser");   if (userName == currentUser) {     this.so.setProperty("currentUser", "");     this.removeAccessPath(client, "writeAccess", this.path);   }   else if (currentUser != "")     return;   else {     this.addAccessPath(client, "writeAccess", this.path);     this.so.setProperty("currentUser", userName);   } }; 

18.3.2. Role-Based Remote Method Access

Any method attached to the server-side client object or Client.prototype is a potential security problem because it can be called directly by the Flash movie using NetConnection.call( ) . For example, it might seem like a good design idea to attach, to a client, a User object that includes all the methods an application needs for manipulating user information:

 client.user = new User(result); 

However, there is always the danger that the Flash client will call a method that it shouldn't on the client object. For example, if the User object has a method to promote the user to another role, you wouldn't want a malicious user to promote himself to administrator using a hacked Flash client:

 nc.call("user.promoteRole", null, "administrator"); 

Even when you do want a Flash client to be able to call a method on the server-side client object, you must make sure a malicious user cannot do more than he should be allowed to. For example, in a chat application, you don't want the user to be able to use a hacked Flash client to pretend to be someone else during a chat session.

A good way to minimize security threats with remote methods is to attach only proxy methods to each server-side client object individually when the client connects, based on the client's user roles. Each proxy method calls a component method and passes on information about the client. The component method validates the information and then acts on it.

In component-based applications, each component should be responsible for attaching to the client object a proxy object that contains methods for only that component. That way, each component has its own object on the client object, which it can manage without interfering with other components. The addObjectPath( ) method of the server-side PFCSComponent class is designed to add a proxy object to the client based on the unique path of each component. For example, a SharedText component named main would use the addObjectPath( ) method to add the pfcs.SharedText.main object path to the client object. The client.pfcs.SharedText.main object is the SharedText component's proxy object. When the client no longer needs the SharedText component, the component can use the removeObjectPath( ) method to delete the proxy object from client . Example 18-19 shows both the addClient( ) and removeClient( ) methods of a SharedText component, which is based on PFCSComponent and therefore inherits the addObjectPath( ) and removeObjectPath( ) methods.

Example 18-19. The server-side addClient( ) and removeClient( ) methods of the SharedText component
 SharedText.prototype.addClient = function (client) {   // Add remote methods to the client.   this.addObjectPath(client);   // Any client can ask to edit the messages shared object.   var thisComponent = this;   client.pfcs[this.className][this.name].edit = function ( ) {     thisComponent.edit(pfcs.getUserName(client));   };   // Only some clients can delete the current user on the messages   // shared object.   if (pfcs.clientInRoles(client, "administrator", "moderator", "presenter")) {     client.pfcs[this.className][this.name].unlock = function ( ) {       thisComponent.unlock(pfcs.getUserName(client));     };   }   // Any client can read the messages shared object.   this.addAccessPath(client, "readAccess", this.path);   // Write access is granted only when a client has access permission. }; SharedText.prototype.removeClient = function (client) {   // Remove remote methods.   this.removeObjectPath(client);   // Remove access.   this.removeAccessPath(client, "readAccess", this.path);   this.removeAccessPath(client, "writeAccess", this.path);   // Release user's lock on the messages shared object stored in   this.so   .   var currentUser = this.so.getProperty("currentUser");   if (client.pfcs.user.userName == currentUser) {     this.so.setProperty("currentUser", "");   } }; 

In the preceding addClient( ) method, every client is provided with an edit( ) method so that it can request access to the shared text area:

 var thisComponent = this; client.pfcs[this.className][this.name].edit = function (  ) {   thisComponent.edit(pfcs.getUserName(client)); }; 

The method itself is only a proxy for the real method of the component. All it does is call the component's edit( ) method, pass it the username retrieved from the global pfcs object. If the user is in certain roles, the unlock( ) method is also added to the client:

 if (pfcs.clientInRoles(client, "administrator", "moderator", "presenter")) {   client.pfcs[this.className][this.name].unlock = function (  ) {     thisComponent.unlock(pfcs.getUserName(client));   }; } 

The unlock( ) method is designed to solve a problem you may have noticed with the SharedText component presented in Chapter 13. A user is supposed to click the Edit button to stop anyone else from using the shared text area while she prepares to update it. A malicious user could simply lock the component and refuse to release it for the entire session. One solution is to provide trusted users, such as administrators or moderators, the ability to unlock the shared text area, overriding other users if necessary. Example 18-20 shows the server-side unlock( ) method.

Example 18-20. The unlock( ) method of the server-side SharedText component
 SharedText.prototype.unlock  = function (userName) {   var currentUser = this.so.getProperty("currentUser");   if (currentUser) {     this.removeAccessPath(pfcs.getClient(currentUser), "writeAccess", this.path);     this.so.setProperty("currentUser", "");   } }; 

On the client, only an administrator or other privileged user will see an Unlock button, which can be used to release control of the shared text area. Here is the code in the client-side SharedText component that displays the Unlock button to privileged users only:

 if (_global.pfcs.inRoles("administrator", "moderator", "presenter")) {   createObject("Button", "unlockButton", depth++);   unlockButton.label = "Unlock";   unlockButton.setSize(80, 22);   unlockButton.addEventListener("click", this); } 

The important point is that even if an attacker hacks the code in a .swf to show the hidden button and clicks it, the unlock( ) method on the server cannot be called by non-privileged users.

18.3.3. Fine-Grained Access Controls

Sometimes URI-level access controls are not fine-grained enough for an application. In a typical scenario, a number of streams will have been saved in one directory on the server, and you want to provide access to only some of them. Fortunately, the server-side Stream object can be used to create a stream at an accessible URI and play a stream in an inaccessible URI. The Stream object's get( ) and play( ) methods are all that are needed:

 ns = Stream.get("public/broadcast"); ns.play("private/announcement27"); 

Now, any connected client that does not have read access to the private directory but does have read access to the public directory can play the stream this way:

 ns = new NetStream(nc); ns.play("public/broadcast"); 

Sometimes, we want to make the stream available to only one user and not broadcast it to everyone. In that case, we can give each client read access to his own directory based on his username or user ID and use the server-side Stream object to play the stream for one client only:

 ns = Stream.get("users/blesser/announcement"); ns.play("private/announcement27"); 

Only a client with read access to the users/blesser directory will be able to subscribe to the stream.

A similar mechanism for providing access to individual shared objects in the same directory is not available. However, shared objects are less likely to require individual access controls. When write access to individual shared objects or shared object slots must be controlled, remote methods can be used to control updates on the server. However, individual shared object and stream-level access controls would be a welcome enhancement to a future version of FlashCom.

As a practical application of using the Stream object for fine-grained access control, we'll improve on the TextChat component introduced in Chapter 13. The TextChat component records every text message to a stream. All the messages of each chat session are recorded in a separate stream file.

When a user connects, the client has to show each user any messages she missed earlier in the current session. To establish a little more context, the component also shows the messages from the previous session. Since the messages are stored in streams, the client creates a NetConnection object and plays the last session and current session's streams to get the messages. In the previous version of TextChat, all the other streams are still there in a directory with read access. Anyone who can connect to the application canwith the right .swf play any chat session stream. If it is important to protect the stream archives so that only the last and current stream can be played , we need to reorganize the directory structure of the older TextChat component so that there is a readable URI space for TextChat users and another one that is not readable. The two URIs we'll use are:

 pfcs/TextChat/main/currentSession pfcs/TextChat/main/sessionArchive 

where main is the name of a TextChat instance. In the addClient( ) method, we control access to the two namespaces this way:

 this.archivePath = this.path + "/sessionArchive"; this.currentPath = this.path + "/currentSession"; this.addAccessPath(client, "readAccess", this.currentPath); if (pfcs.clientInRoles(client, "administrator", "moderator", "presenter")) {   this.addAccessPath(client, "readAccess", this.archivePath); } 

Now that we have protected the archive, we need to make the last and current session streams available. When a client needs to play the two streams, it calls a remote method named getHistory( ) on the component. Example 18-21 lists the server-side getHistory( ) method from the TextChat.asc file.

Example 18-21. The server-side TextChat.getHistory( ) method
 TextChat.prototype.getHistory = function (client) {   var proxy = client.pfcs[this.className][this.name];   // Stream path the user can read.   var ns = Stream.get(this.currentPath + "/" + pfcs.getUserName(client));   proxy.ns = ns;   ns.onStatus = function (info) {     if (info.code == "NetStream.Play.Stop") {       this.send("endHistory");       delete proxy.ns;     }   };   // Play streams the user could not otherwise read.   var lastSession = this.sessions_so.getProperty("lastSession");   if (lastSession) {     lastSession = this.archivePath + "/" + lastSession;     ns.play(lastSession, 0, -1, 2);   }   var currentSession = this.sessions_so.getProperty("currentSession");   if (currentSession) {     currentSession = this.archivePath + "/" + currentSession;     ns.play(currentSession, 0, -1, 2);   } }; 

The Stream object ns plays the recorded streams as a new live stream. Since each client needs to play the stream from the beginning, the component publishes it to a different URI for each client. The URI is the current path with the user's username tacked on the end:

 var ns = Stream.get(this.currentPath + "/" + pfcs.getUserName(client)); 

When the server-side ns.onStatus( ) method receives an information object with a code value of "NetStream.Play.Stop" it sends a message to indicate that the stream is over:

 this.send("endHistory"); 

Example 18-22 shows the client-side code from the TextChat component that sets up a NetStream object, calls the server-side getHistory( ) method, and plays the stream. That work can't begin until a connected NetConnection is available, so we perform the operation in the set nc( ) method (by which time the connection is available).

Example 18-22. The client-side TextChat component setter method for nc
 public function set nc (nc:NetConnection) {   __nc = nc;   var currentPath = path + "currentSession";   // Set up the NetStream so we can play back the last session's history.   history_ns = new NetStream(nc);   history_ns.owner = this;   history_ns.setBufferTime(10);   history_ns.endHistory = function ( ) {     this.owner.showHistory( );   };   history_ns.showMessage = function (msg, userName, dateTime) {     this.owner.accumulateHistory(msg, userName, dateTime);   };   history_ns.play(currentPath + "/" + _global.pfcs.getUserName( ), -2, -1, 3);   // Set up the shared object that will send out all new messages.   messages_so = SharedObjectFactory.getRemote(currentPath + "/messages", nc.uri);   messages_so.addRemoteMethod("showMessage");   messages_so.addEventListener("showMessage", this);   // Wait until the   messages   shared object is sync'ed before   // we ask for the history.   messages_so.addEventListener("onFirstSync", this);   messages_so.connect(nc); } function onFirstSync (ev) {   __nc.call(path + "getHistory", null); } 

Using proxies to provide fine-grained access control to streams has one drawback. Recorded streams are played as though they are live. So, for example, it is not possible to seek within the stream using the client's NetStream object. If it is necessary to seek within a stream, the client must call server-side methods to have the server-side Stream play the stream from the correct point.

18.3.4. Access Control Tables

Even moderately complex applications often need something more than application-wide user roles to control access to instances and their resources. For example, a multiroom conferencing system needs to allow some users into some rooms and not others. In one room, a user may have the role of moderator and in another that of participant. In other words, we need to associate a user role with a resource such as a room. A common and powerful approach is to use access control tables . Each entry in an access control table should have three parts: the name of the resource, the access rule describing what can and cannot be done with the resource, and the ID of a user or user group that is being granted access.

As a practical example, we imagine a course seminar system. One seminar room is provided for each course section. Users in a course section could be given access to seminar rooms as shown in Table 18-10. The resourceName column represents the application instance name of each seminar room.

Table 18-10. The PFCS_ACCESSCONTROL access control table

groupName

roleName

resourceName

CPS001Section001

participant

seminar/room1

CPS001Section002

participant

seminar/room2

CPS001Professor

presenter

seminar/room1

CPS001Professor

presenter

seminar/room2

CPS001Assistant

moderator

seminar/room1

CPS001Assistant

moderator

seminar/room2


When user groups are used in an access control table, a user group table must be available to associate users with those groups. Table 18-11 shows some sample records from a user groups table.

Table 18-11. The PFCS_USERGROUPS table with sample entries

userName

groupName

blesser

CPS001Section001

peldi

CPS001Professor

peldi

CPS003Assistant

joey

CPS001Professor

justin

CPS001Section002

dave

CPS004Professor

rreinhardt

CPS001Section001


When someone logs into our web-based seminar application, a simple database search can retrieve the seminar rooms to which the user is entitled to connect:

 SELECT ac.resourceName, ac.roleName FROM PFCS_ACCESSCONTROL ac, PFCS_USERGROUPS ug WHERE ug.groupName = ac.groupName AND ug.userName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#userName#"> 

Similarly, to check whether a user is entitled to a resource when he tries to connect to a seminar room, the seminar instance can authenticate and get the user's role using a three-way join on the user, user groups, and access control tables. The resource name can be the instance name in the application.name property:

 SELECT u.firstName, u.lastName, u.email, ac.roleName FROM PFCS_ACCESSCONTROL ac, PFCS_USERGROUPS ug WHERE ug.groupName = ac.groupName ug.userName = u.userName AND ug.userName = <cfqueryparam value="#userName#"> AND u.password = <cfqueryparam value="#password#"> AND ac.resourceName = <cfqueryparam value="#resourceName#"> 

Given the data in Table 18-11, if user "dave" tries to connect to the CPS001Section001 instance, his connection will be refused . If user "blesser" attempts to connect to the same room instance, he will have the participant role, whereas user "peldi" will have the presenter role.

18.3.5. Anonymous Access Restrictions

Anonymous users present special problems. Users who are not forced to log in should never be allowed to write anything to the server or communicate directly with other users. They should be provided only controlled, read-only access to the resources you want them to have. Even then, some special precautions are necessary to avoid denial-of-service (DoS) attacks. Anonymous users are especially challenging for a number of reasons. There are always people who, if they believe they cannot be identified, will persistently harass and drive away other users, cheat at online games , try to disable your server, and post hate propaganda, spam, or pornography.

Only marginally better are users who can acquire a free account using nothing more than an email address (especially a bogus email address). They are virtually anonymous, as it is extremely difficult if not impossible , short of taking expensive legal action, to identify a user. If someone breaks your acceptable use rules, you can disable his account, but he can just sign up for another one using another free email account. You should be extremely cautious about providing write access of any kind or the ability to interact with other users to users with free self-registering accounts. Consider, as a warning, the experience of popular bloggers who allowed anonymous readers to post comments. Many have been so inundated with spam that, after trying various filtering options, they have simply given up accepting comments. Text comments are generally very small, so accepting thousands of them over a day or two may not slow down a web server.

Accepting video and audio streams is quite different from accepting text messages. Video and audio can consume huge amounts of bandwidth and disk space, and the server has to do a lot more work to manage streams. Don't accept video and audio streams from anonymous users unless it is in a very controlled environment, such as a kiosk at a museum or trade show. Even at a trade show, you should consider requiring the user to swipe his magnetic trade show badge, if applicable , to gain access.




Programming Flash Communication Server
Programming Flash Communication Server
ISBN: 0596005040
EAN: 2147483647
Year: 2003
Pages: 203

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net