Table of Contents
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
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.
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); } }
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.
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).
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!
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.
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.
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... } }
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.
Complete the test code to check that the SocialDirector implementation works properly.
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.
Refactor the code that accesses the social director so that it is not duplicated in the implementation of InitStructuresNeededForUpcoming.
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?