User Copy with the Reseller Extension deprecated

The usercopy provisioning call copies a user along with its private data into another context, see the User Copy Tool article for what is copied and what is not. This article describes how that call behaves when the reseller extension is installed: who is allowed to copy a user, which reseller restrictions a copied user counts against, and how a copy is rejected.

Without the reseller extension nothing described here applies — the call is then reserved for the master administrator and no restrictions exist. Note that MASTER_AUTHENTICATION_DISABLED switches off both, see Prerequisites.

Motivation

Copying a user adds a user to the destination context, but it does not go through the ordinary user creation. As a consequence the reseller extension used to see neither the call nor its result:

  • No ownership was checked. The call authenticated on master level only. With the reseller extension installed, its authenticator accepts every valid subadmin on that level, and no ownership check followed. Any subadmin could therefore copy any user between arbitrary contexts, including contexts belonging to a different subadmin.
  • No restrictions were applied. Because OXUser.create was bypassed, the reseller user plug-in never ran. Copied users counted against no limit at all, so a subadmin could exceed a contracted user count by copying instead of creating.

Both are closed now: a copy is subject to the same ownership rules and the same restrictions as a user creation — provided authentication is switched on at all, see Prerequisites.

Prerequisites

  • The packages open-xchange-admin-user-copy (RMI interface and the usercopy command-line tool) and, if the SOAP endpoint is used, open-xchange-admin-soap-usercopy have to be enabled.
  • The reseller extension open-xchange-admin-reseller has to be installed and started. Enforcement depends on it: while the bundle is not up, copies run unchecked, which is reported once per node as No user-copy plug-in is registered, user copies are performed without any plug-in check.
  • Restrictions have to be initialized once via the initrestrictions command-line tool. As long as none exist, there is nothing to apply and every copy passes — without any log output. Verify with listrestrictions rather than by the absence of a warning.
  • For a subadmin to copy at all, MASTER_ACCOUNT_OVERRIDE=true has to be set in AdminDaemon.properties. This is a deployment-wide switch that defaults to false and affects every provisioning call a subadmin makes, not only this one. Without it the copy is rejected with InvalidCredentialsException, exactly as createuser would be.
  • MASTER_AUTHENTICATION_DISABLED must not be set. It makes every caller count as the master administrator, so the ownership check described below is skipped entirely and cross-owner copies become possible again — the very situation this feature closes. The switch is deployment-wide and affects all provisioning calls; it is meant for installations that place their own authentication in front of the provisioning interfaces.

Who may copy a user

The caller is authenticated on master level first, as before. In addition, a caller that is not the master administrator has to be accepted for both the source and the destination context. The rule is applied to each of the two contexts independently: a context is accepted if the caller owns it directly, or if its owner is a subadmin of the caller. The two contexts do not have to be accepted for the same reason.

Some examples, where "own" means owned by the calling subadmin itself:

Caller Source context Destination context Result
Master administrator any any allowed
Subadmin own own allowed
Subadmin own owned by a sub-subadmin allowed
Subadmin owned by a sub-subadmin owned by another sub-subadmin allowed
Subadmin own owned by an unrelated subadmin rejected
Subadmin owned by an unrelated subadmin own rejected
Subadmin without an owner any rejected
Subadmin any without an owner rejected

A context without an owner is one for which no owner is recorded. Besides contexts the master administrator created directly, this also covers contexts that existed before the reseller extension was installed and contexts whose owner mapping was removed later. They are not covered by any reseller contract, stay reserved for the master administrator, and — see below — escape the restriction check.

A rejected caller receives an InvalidCredentialsException with the message Authentication failed, and the middleware logs

Administrator <login> is not accepted as owner of both context <src> and context <dest>

The log entry deliberately states the outcome rather than a cause: besides a genuine lack of ownership it also covers a disabled MASTER_ACCOUNT_OVERRIDE and an absent authenticator plug-in.

Note that a parent reseller is accepted for contexts owned by its sub-subadmins. A parent can therefore move a user between the contexts of two different sub-resellers.

Applied restrictions

A copied user counts against the very same restrictions of the destination context as a created one:

Restriction Scope
Context.MaxUser Users in the destination context
Context.MaxUserByModuleaccess_<name> Users in the destination context with the given access combination
Subadmin.MaxOverallUser Users across all contexts of the owning subadmin
Subadmin.MaxOverallUserByModuleaccess_<name> Users across all contexts of the owning subadmin with the given access combination

The module access of the source user determines which of the by-module-access restrictions apply, since the copy keeps that access combination.

Restrictions of the source context are not evaluated — the copy does not add a user there.

The subadmin-level restrictions are those of the direct owner of the destination context. When a parent reseller copies into a context of one of its sub-subadmins, that sub-subadmin's limits are evaluated, not the parent's own overall limits. This matches user creation and is a property of the reseller model rather than of this call, but it is worth knowing when a contract is defined at the parent level.

If the destination context has no owner, no restriction can be attributed to a subadmin and the check is skipped entirely.

When the restrictions are evaluated

They are evaluated twice, and the difference matters:

  1. Before the copy, as a fast fail. This spares the expensive copy for an operation that is doomed anyway. It is not the binding decision — a concurrent call can invalidate its outcome the moment after it was taken.
  2. After the copy, which is the binding decision. At that point the copied user exists and is therefore counted by every concurrent call as well.

Deciding after the write is the same order that user creation uses, and it is what keeps a limit from being exceeded when several copies run at the same time: the copy that commits later necessarily sees the earlier one and is rejected.

A copy rejected by the second evaluation is removed again, and the caller is told the copy failed. The reported message is the one of the violated restriction, for example:

Maximum overall number of users reached: 8

Parallel copies

Because each copy is counted by the others once it exists, copies issued in parallel against a context close to its limit may all be rejected, even though one of them would have fit. A rejection therefore does not prove that the limit is currently reached — the call may simply have lost a race.

Copying users one after another avoids both the rejections and the wasted work of copying and removing a user again. This matters for bulk migrations in particular.

When the removal fails

Removing a rejected copy is a regular user deletion and can fail on its own, for instance if the database is unavailable at that moment. The caller then receives

The copy was rejected (<reason>), but user <id> could not be removed from context <cid> again and has to be deleted manually.

and the middleware logs an error with the same information. The left-over user is a regular user of the destination context and counts against the restrictions from then on, so it should be deleted promptly with deleteuser.

Reseller data of the copied user

Reseller data is bound to contexts and to subadmins, never to individual users — there is no per-user reseller record that could be copied. The copied user is simply subject to the restrictions and capabilities of the destination context and its owner.

Repeated attempts

If the copy fails with a database error that is worth retrying, such as a deadlock or a lock-wait timeout, it is attempted at most five times in total — the initial call plus four repeats — with a growing pause between the attempts. If all attempts fail, the call ends with

Copying user <id> from context <src> to context <dest> kept failing with a retryable error. Please try again later.

Retrying the call later is the appropriate reaction; the destination context is left without a copied user in that case.

Extending the behavior

Third-party plug-ins can take part in the copy through the interface com.openexchange.admin.plugins.OXUserCopyPluginInterface, exported by the bundle com.openexchange.admin. An implementation registered as an OSGi service is consulted for every copy:

public interface OXUserCopyPluginInterface {

    default void preCopy(Context src, Context dest, User user,
                         UserModuleAccess access, Credentials auth) throws PluginException {
        // Nothing to do
    }

    void postCopy(Context src, Context dest, User user, User copiedUser,
                  UserModuleAccess access, Credentials auth) throws PluginException;
}
  • preCopy runs before anything is written. Throwing a PluginException aborts the copy without any side effect. The method is optional because it is only a fast fail.
  • postCopy runs after the user has been copied and is the binding decision. Any verdict that has to hold under concurrency belongs here, for the reasons given above. Throwing a PluginException causes the copied user to be removed again and the call to fail — and so does any other exception escaping the method, including a RuntimeException, because a defective plug-in leaves no verdict and the copy must not stand on one. Implementations should therefore handle their own errors deliberately instead of letting them propagate.

The reseller extension implements the interface exactly this way to apply its restrictions.

Limitations

Three configurations switch enforcement off, and only one of them announces itself:

  • MASTER_AUTHENTICATION_DISABLED skips the ownership check for every caller. No log entry marks the individual copy.
  • Enforcement requires the implementing bundle to be started. While it is not, copies run unchecked; this is reported once per node, see Prerequisites.
  • Without initialized restrictions there is nothing to apply, silently. The absence of the warning above is therefore no evidence that restrictions are being enforced — check with listrestrictions.

Further limitations:

  • Contexts without a recorded owner escape the restriction check entirely.
  • A rejected copy creates and deletes a real user in the destination context. This is observable from the outside, for example in audit trails or by listeners on user deletion.
  • If several plug-ins are registered and a later one objects, the side effects of an earlier one are not compensated.
  • Should the removal of a rejected copy fail, the left-over user has to be deleted manually.