Chapter 10. Users and Groups [DRAFT]

Table of Contents

10.1. This is a DRAFT! Give your opinion!
10.2. Creating the social director
10.2.1. The SocialDirector class
10.3. Services and the big picture
10.4. The UserManager service
10.4.1. Directories
10.4.2. UserManager
10.5. Testing the SocialDirector
10.6. Invoking the SocialDirector
10.7. Exercises

Objective: To learn how Nuxeo handles managing user accounts and groups. To gain a further understanding of utilizing the system "services" provided by Nuxeo EP.

The software isn't finished until the last user is dead. --Anonymous

10.1. This is a DRAFT! Give your opinion!

If you have any comments, questions, or general-purpose harassment you would like give us about this book, then please use the comment form at the bottom of each page! We promise that we will try to incorporate any feedback you give (minus the profanity, of course), will respond to your questions, and credit you appropriately.

10.2. Creating the social director

The role of "social director" is an informal one in most organizations; the social director is the person who has the energy and interest to devise, plan, and usually coordinate social events for a group. An example event might be, the company's annual Holiday party. Since the social director is heavily involved in most upcoming activities, it seems fair that this role have a special status with respect to our Upcoming documents. Using Eclipse and Maven as before and with this lesson's skeleton, found in the usual place on the svn server http://svn.nuxeo.org/nuxeo/sandbox/iansmith/book/project-users, create a Java class called SocialDirector in the package org.nuxeo.upcoming:

package org.nuxeo.upcoming;

import java.util.ArrayList;
import java.util.List;

import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.platform.usermanager.UserManager;
import org.nuxeo.runtime.api.Framework;

public class SocialDirector {

    public static final String SOCIAL_DIRECTOR = "socialButterflies";

    public boolean groupAlreadyExists() throws Exception{
        UserManager mgr = getUserManager();
        DocumentModel model = mgr.getGroupModel(SOCIAL_DIRECTOR);
        return model!=null;
    }


    public UserManager getUserManager() throws Exception {
        return Framework.getService(UserManager.class);
    }

    public void createGroup() throws Exception {
        UserManager mgr = getUserManager();
        //get the basic version of a group
        DocumentModel model = mgr.getBareGroupModel();
        //set some properties as if there is a document schema called "group"
        model.setProperty("group", "groupname", SOCIAL_DIRECTOR);
        model.setProperty("group", "description", "people who coordinate social events");
        model.setProperty("group", "members", new ArrayList<String>());
        //this property is a list of group members, but we want to start empty
        List<String> parentGroups = new ArrayList<String>();
        parentGroups.add("members");
        model.setProperty("group", "parentGroups", parentGroups);
        //use model to actually create the group
        mgr.createGroup(model);
    }
}

10.2.1. The SocialDirector class

As you can see from the text above, the SocialDirector class has two main methods, groupAlreadyExists and createGroup. As one would expect, these check to see if a group exists called "socialButterflies" and create that same group, respectively. Both of these methods use the UserManager to do their work, and the UserManager will be discussed in an upcoming section.

The design we are using in this lesson is to create a group denoted by the constant SOCIAL_DIRECTOR that we can add users into. This group, by the time the next few lessons are finished, will have some special rights with respect to Upcoming documents. In this sense, the "group" is really "role" but Nuxeo does not have a strong distinction between these two designations, so we will continue to use them interchangably. An example of a special privilege for those in the group SOCIAL_DIRECTOR is that they can, perhaps, modify the properties of an upcoming schema an any document in the system.

If you have forgotten how to manipulate users and groups, you may want to revisit section 3.3 or just boot up the Nuxeo server and click the "User Management" link that appears at the top of each page if you are logged in as Administrator.

10.3. Services and the big picture

As you saw in the previous lesson's tests and are now seeing in this lesson's implementation, services are a key ingredient to programming with Nuxeo. There are a multitude of services that are available: so far we have used the EventService, EventProducer, and now the UserService. The complete set of services visible to your program varies somewhat based on which OSGi bundles you have installed/deployed into your Nuxeo system. If you have not deployed the bundle LeftHandedMonkeyWrench, then it is likely that the call Framework.getService(LeftHandedMonkeyWrench.class) will fail - by returning null - at run-time. It is important that you think about the classes you have visible to you in Eclipse at compile time - such as LeftHandedMonkeyWrench - and their relationship to deployed bundle. As a general rule, if your code fails at run-time to find a Service you expected to be present, it is likely that your MANIFEST.MF is missing a dependency. Why? The fact that Nuxeo's Framework could not find a service means that a bundle was not present you needed, but this should never happen if you have written the appropriate Nuxeo-Require lines in your manifest! The case that, unfortunately, causes this situation somewhat often is when you forgot the Nuxeo-Require line in your manifest, but got "lucky" (unlucky?) and your code worked on a particular Nuxeo installation because that bundle was already deployed, maybe be someone else's code. Since test code tends to run in a version of Nuxeo that has few bundles deployed (to speed test execution) it is a good idea to see what bundles you need to run your test code - denoted by deployBundle() in the JUnit test case - and make sure that your manifest declares at least those bundles as required.

Returning to the "big picture" theme that was discussed earlier, the Nuxeo UI and default server functionality have "no magic." In other words, they are implemented the same way that your code is; even the Nuxeo server developers have to be concerned about which services they use and which bundles are deployed! Going the other direction, in a future lesson you will develop your own service and allow it to be found and utilized by components outside your own. This process, naturally, would mean that other users could write a dependency on your bundle (!), then access your service with Framework.getService(YourCode.class). The only part of the Nuxeo system that is implemented "with magic" is the Nuxeo runtime system since it is used to provide the OSGi-like infrastructure that both your code and the Nuxeo server depend on. Nuxeo is just software, it cannot be "turtles all the way down" (with apologies to Bertrand Russel or someone else http://en.wikipedia.org/wiki/Turtles_all_the_way_down).

10.4. The UserManager service

To motivate the use of the UserManager service we are going to partially explain how users and groups work in Nuxeo. This explanation is not really intended to be exactly accurate, but rather to give you a flavor of what complexity the UserManager is hiding from you!

10.4.1. Directories

Directories are a Nuxeo 5 concept that, sadly, is not particularly related to the more common notion of a "directory" in a filesystem although both contain structured content. In Nuxeo, a directory is a collection records that describe a user or a group. The particular fields that describe the directory can vary quite dramatically between Nuxeo installations. Some nuxeo installations will want to be "stand alone" and simply have someone who maintains a text file. Other installations may want to piggyback on their existing user account configuration, say for their database like Postgres or MySQL. Finally, you may have organizations with highly-developed user management regimes that use Open LDAP or microsoft domains (which are also based on LDAP). In all of these cases, the data kept about a user will vary quite a bit although all are likely to include a user name of some kind - some Microsoft installations use Last Name, First Name rather than a shortened form as most other systems do - and some way of authenticating the user. Some organizations maintain building maps such that a user account includes an office name or perhaps a phone number while other user directories allow free form text in some fields so the location of a user may be "out to lunch."

Groups are even more problematic because these are often maintained by administrators of other systems - databases or LDAP servers - that are loathe to change just because a new Nuxeo server has been installed. Again, a nuxeo server must be able to handle groups defined again in text files, in databases (with varying configurations in terms of security, database design, table design, etc), or in LDAP services that are difficult to change. There may be an arbitrary number of both groups and users, and in some situations, Nuxeo must even take the "users database" from two different sources and merge these together to form the set of valid Nuxeo users! This capability is particularly critical in situations where the administration of the Nuxeo server must be done by someone who is not listed as an "Administrator" from the standpoint of the organization that maintains the "normal" set of user accounts.

The Nuxeo directory mechanism is used in a number of situations in the system in addition to managing Users and Groups. For more information on this powerful tool, please see chapter 18 of the Nuxeo Book.

10.4.2. UserManager

We hope that the "scary" description in the previous subsection convinced you that there is a need for a higher-level API for manipulating Users and Groups. The User Manager provides access to functionality with calls such as areUsersReadOnly(), getUsersInGroup(String), and checkUsernamePassword(String, String), the last of which allows you validate a users credentials. If you have an instance of the type UserManager in a Java file and type control-space after typing the dot, you can see all the methods the UserManager exposes, as is shown here:

As we explained in the previous section about directories, there are many possible fields that might be available to you with respect to a particular User or Group. To ease this burden, the UserManager presents this variability in a form you have already seen - using a Schema. Since schemas may have arbitrarily complex structures, it is convenient to re-use this notion when describing users or groups. Although it may seem strange to describe a user with a document, this is the way it is exposed via the UserManager.

The methods getBareUser and getBareGroup will hand you back a DocumentModel that represents an simple abstraction of a schema for users or groups. This version prevents you needing to walk through all the fields exposed by the particular installation in favor of a small set of well-understood properties. You can fill in the fields just as you did when you created a new document in the last lesson's tests, and then ask the UserManager to create the User or Group that you have defined (as we did in the listing above).

In the case of the "bare" version of users that you can use if you want to do simple operations, here is the XML schema definition:

<?xml version="1.0"?>

<xs:schema targetNamespace="http://www.nuxeo.org/ecm/schemas/user"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:nxs="http://www.nuxeo.org/ecm/schemas/user">

  <xs:include schemaLocation="base.xsd" />

  <xs:element name="username" type="xs:string" />
  <xs:element name="password" type="xs:string" />
  <xs:element name="firstName" type="xs:string" />
  <xs:element name="lastName" type="xs:string" />
  <xs:element name="company" type="xs:string" />
  <xs:element name="email" type="xs:string" />

  <!-- inverse reference -->
  <xs:element name="groups" type="nxs:stringList" />

</xs:schema>

there is a similar simplification for the schema that describes a group:

<?xml version="1.0"?>
<xs:schema targetNamespace="http://www.nuxeo.org/ecm/schemas/group"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:nxs="http://www.nuxeo.org/ecm/schemas/group">

  <xs:include schemaLocation="base.xsd" />

  <xs:element name="groupname" type="xs:string" />
  <xs:element name="description" type="xs:string" />

  <!-- references -->
  <xs:element name="members" type="nxs:stringList" />
  <xs:element name="subGroups" type="nxs:stringList" />

  <!-- inverse reference -->
  <xs:element name="parentGroups" type="nxs:stringList" />

</xs:schema>

Both of the schemas above use a type that is defined by Nuxeo, the nxs:stringlist which is (suprise!) a list of strings. In the case of the members field of the group schema, the list of strings represents a list of usernames of the members of that group.

If you want to manipulate all the fields of a user -- perhaps to change their room assignment in organizations that have this field -- you will need to use the API of the DocumentModel returned from the UserManager's getUserModel function. With this you can inquire about all the fields available, but it is up to you to interpret their semantics.

10.5. Testing the SocialDirector

You will notice that we have included a new base class that you should use for testing the SocialDirector, UserManagerTestBase. This base class understands the details the dependencies of the UserManager. It calls the deployBundle function that is available to Nuxeo tests, plus a few others, to make sure the dependencies are available. This new class has been included because the UserManager cannot function without something that describes the particular directory (in the sense of section 4.1) structure being used. We have supplied you with a some sample configurations that will allow the tests to run with the user manager. In effect, we must define a simple "world" for users and groups so the UserManager can provide access to it. These files can be found src/test/resources/testconf and may be of interest to readers that are interested in how to construct more complex test setups.

Testing the SocialDirector is a straightforward process. In the next listing, we have provided you with all the code necessary to run the test. However, some of the comments are labelled "SKETCH" and these suggest how to construct the test yourself. If we provided you with all the details of that part of the test, what we would do about exercise number 1?

package org.nuxeo.upcoming.test;

import org.nuxeo.ecm.core.repository.jcr.testing.RepositoryOSGITestCase;
import org.nuxeo.upcoming.SocialDirector;

public class SocialDirectorTest extends UserManagerTestBase {

    public void testGroupBasics() throws Exception {
        // send our upcoming bundle to the infrastructure
        deployBundle("org.nuxeo.book.upcoming");

        // SKETCH: object we are testing is social director

        // SKETCH: sanity check that we can access the usermanager

        // SKETCH: not much to do because the object is created as a side
        // SKETCH: effect of loading the bundle...
    }

 
}

10.6. Invoking the SocialDirector

In the previous lesson, we have insured that our document type, Upcoming, was created before any pesky users could actually manipulate the Nuxeo server and create a document. We would dearly love to use this same mechanism to insure that if our bundle is installed, the group "socialButterflies" is present on the server. Oh, if only life were that simple.

There are two OSGi-related problems that we have to overcome to have our group "created" at the time, roughly, that our bundle is activated. These affect how we write DocumentCreationListener.activate(). The first, and most important, problem is that OSGi notion of dependencies that we have so carefully explained to you so far, was a bit naive. One thinks of "requires" as something means that if component A "requires" component B then when component A runs, component B can be used. It is more correct to say that OSGi guarantees that B will be visible to A, but it says nothing about whether B is ready to be used. Depending on timing and load on the server running the code, B may still be initializing when A tries to call it! In our case, the social director plays the role of A and the directories that make up the definition of users and groups act as B. So, we are forced to register a listener that waits for a "Framework" Event. This listener waits, specifically, for the Framework.START event; this event indicates that not only are we finished loading all the components but this system is actually ready to do real work. Thus, we can make API calls on the user manager without fear after this event has been received.

What makes this problem so troublesome is that the "framework" here is actually OSGi-supplied, not Nuxeo. This means that the Framework.STARTED event is not well coordinated with Nuxeo. This leads to our second problem; we have to deal with a couple Nuxeo-related issues ourselves since Nuxeo is unaware of the "start" event. One complication is the test code -- entirely supplied by Nuxeo -- does not generate this event so we have to detect when we are running in a test and avoid waiting for the event that will never come! Worse yet, when we are running in the real Nuxeo server, we have to change the class loader to one that is sure to work with Nuxeo components or they may not be available. It is possible that would work, but would depend on exactly what class loader is selected at the time of the event's delivery, so we played it safe.

After all this, the activate() method has now become either just a call to the initStructuresNeededForUpcoming() method - when we are running in a test - or something that creates a listener that calls initStructuresNeededForUpcoming() when it receives the signal that everything is loaded and ready - in the server case.

Here is the snippet of code we would like to run. This is the code that "does the work" when the program starts up although as we just explained it can be a bit tricky to decide when this should be executed! The full code for the tests are in the lesson skeleton.

        // make sure we have a social director group
        try {
            SocialDirector director = new SocialDirector();
            if (!director.groupAlreadyExists()) {
                director.createGroup();
            } 
        } catch (Exception e) {
            throw new ClientException(e);
        }

The careful reader will notice that this now introduces a dependency on SocialDirector from the event handler object. (Did you catch that?) Thus, you need to change the base class of the test of the event handler test to be UserManagerTestBase so the relevant bundles are deployed before the test runs. Both of the test classes are now child classes of UserManagerTestBase! You may find it interesting and informative to see what happens if you do not do this, as well.

At this point, if you go through the usual deployment process, you should be able to see your class in the list of groups that users can be added to. You can see the "Manage Users" link that is highlighted in the screen shot below; click that link to see the page shown in the darker part of the image and you can add users to the socialButterflies group.

10.7. Exercises

  1. Complete the test code to check that the SocialDirector implementation works properly.

  2. If you have a copy of the Nuxeo source or can download it, examine the directory src/main/resources in the package nuxeo-platform-directory-sql. This is the source code of the bundle org.nuxeo.ecm.directory.sql referred to by the file default-sql-directories-bundle.xml that is in your server's config subdirectory below the nuxeo.ear deployment. There are a number of files in that directory show how Nuxeo's default groups get created. Figure out how to change the default Administrator password and deploy that change.

  3. Refactor the code that accesses the social director so that it is not duplicated in the implementation of InitStructuresNeededForUpcoming.

  4. Can you use an xml file with a contribution to an extension point to create a listener for the Framework.STARTED event? Why or why not?