The OMERO security system is intended to be as transparent as possible while permitting users to configure the visibility of their data. For the user, this means that with no special actions, data and metadata created will be readable by both members of the same group and by other users, but will be writable by no one, comparable to a umask of 755 on Unix. For the developer, transparency means that there is little or no code that must be written to prevent security violations, and simple mechanisms for allowing restricted operations when the time comes.
Other links which may be of use:
Several concepts and/or components from our and other code bases play a role in OMERO security:
Now, with the concepts cleared up, we can take a look at all of the concrete source artifacts (“participants”) which are important for security.
AdminImpl.java CurrentDetails.java SecureAction.java SecuritySystem.java BasicSecuritySystem.java ACLEventListener.java EventHandler.java MergeEventListener.java OmeroInterceptor.java SessionHandler.java SecurityFilter.java EventLogListener.java EventListenersFactoryBean.java LocalAdmin.java hibernate.xml sec-system.xml services.xml
Security starts with the build system and installation. During the generation of the model (by the DSLTask), a sql script is created called “data.sql”. After ddl.sql creates the database, data.sql bootstraps the security system by creating the initial (root) experimenter, and event, and then creates the “system” group and the “user” group. It then creates a password table and sets the root password to “ome”. (It also creates all of the enumeration values, but that is unimportant for security).
Note
The password table is not mapped into Hibernate, and is only accessible via the OMERO admin interface.
To begin the runtime security process, a user logs in by providing a Login and/or a Server instance to ServiceFactory. These types are immutable and their values remain constant for the lifetime of the ServiceFactory. The user can also set the umask property on ServiceFactory_. This value is mutable and can be set at anytime.
The values are converted to java.util.Properties which are merged with the properties from the *.properties files from /etc to create the client OmeroContext (also known as the “application context”). The context contains a Principal and user credentials (password etc.) which are associated with the thread before each method execution in a specialized TargetSource. Finally, these objects are serialized to the application server along with the method arguments.
The application server first performs one query (most likely SQL) to check that the credentials match those for the given user name. A second query is executed to retrieve all roles/groups for the given user. If the roles returned are allowed to invoke the desired method, invocation continues with the queried user and roles stored in the InvocationContext.
Execution then passes to OMERO code, specifically to the interceptors and lifecycle methods defined on our session beans. This intercepting code checks the passed Principal for OMERO-specific information. If this information is available, it is passed into the SecuritySystem through the login method. Finally, execution is returned to the actual bean which can either delegate to OMERO services or perform logic themselves.
All calls to the delegates (and in the future all calls on the session beans) are also caught intercepted by Spring-configured interceptors. These guarantee that the system is always in a valid and secure state. In stack order they are:
Finally execution has reached the OMERO services and can begin to perform logic. Because of these layers, almost no special logic (other than eviction and not calling write methods from within read methods. see #223) needs to be considered. There are, however, a few special cases.
IQuery (within the application server), for example will always return a graph of active Hibernate objects. Changes to them will be persisted to the database on flush.
IUpdate, on the other hand, does contain some logic for easing persistence, though this will eventually be ported to the Hibernate event system. This includes pre-saving the newly created event and the work of UpdateFilter like reloading objects unloaded by the proxy handler (above).
Finally, IAdmin is special in that it and it alone access the non-Hibernate password data store and even access application server APIs (like JMX) in order to make authentication and authorization function properly.
Once execution has left this service layer, it enters the world of Hibernate ORM. Here we cannot actively change functionality but only provide callbacks like the OmeroInterceptor and EventListeners. The OmeroInterceptor instance registered with the Hibernate SessionFactory (via Spring) is allowed for calling back to the often mentioned SecuritySystem to determine what objects can be saved and which deleted. It also properly sets the, for a user mostly unimportant, Details object. The EventListeners are more comprehensive than the OmeroInterceptor and can influence almost every phase of the Hibernate lifecycle, specifically every method on the Session interface.
The event listeners which implement AbstractSaveEventListener (i.e. MergeEventListener, SaveOrUpdateEventListener, etc.) are responsible for reloading unloaded objects (and will hopefully take this functionality fully from IUpdate) and provide special handling for enums and other system types. There are also event listeners which are the equivalent of database triggers (pre-update, post-delete, etc.) and these are used for generating our audit log.
So much for write activities. Select queries are, as mentioned above, secured through the use of Hibernate filters which add join and where clauses dynamically to queries. For example an HQL query of the form:
select i from Image i
would be filtered so that the current user does not receive references to any objects with reduced visibility:
select i from Image i where ( current_user = :root OR i.permissions = :readable )
The actual clauses added are much more complex and are added for each joined entity type (i.e. table) which apears in a query.
select i from Image i join i.defaultPixels p
would contain the “( current_user = :root …)” clause twice.
Currently, subqueries are an issue in that the clauses do not get added to them. This may cause consternation for some particular queries.
All of this is supported by an implementation of the SecuritySystem interface which encapsulates all logic regarding security. It also hides as much as it can, and if not specifically needed should be ignored. However, before you attempt to manually check security, by all means use the security system, and for that, it may need to be acquired from the server-side OmeroContext. Currently, there is no client-side security system. See #234.
The OMERO security system and its current only implementation BasicSecuritySystem? are somewhat inert and expect well-defined and trusted (see #235) methods to invoke callbacks during the proper Hibernate phase.
When using the client library and the ServiceFactory, logging in is trivial. One need only set several System properties or place them in an omero.properties file somewhere on the classpath. Internally, Spring takes the System properties and creates an ome.system.Principal instance. This is then passed to the server on each invocation of a proxy obtained from JNDI.
Much of this infrastructure is not available to server-side code (no ome/client/spring.xml, no ServiceFactory, etc.). As such, the Principal needs to be manually created and provided to the server-side SecuritySystem.java.
Basically it amounts to this:
Principal p = new Principal( omeroUserName, omeroGroupName, omeroEventTypeValue );
securitySystem.login( p );
This must be run otherwise the EventHandler will throw a security exception.
Note
The code above is being run in a secure context (i.e. you are root). Please be careful.