问题
I'm writing my own XWiki Authenticator (that extends XWikiAuthServiceImpl
) and therein, I want to create few groups, each with different sets of rights. How do I do it programmatically?
Example,
XWiki.MyStandardGroup
-view
,edit
,comment
XWiki.MyClassicGroup
-view
,edit
,comment
,script
XWiki.MyAdminGroup
-view
,edit
,commit
,script
,delete
,admin
Also, I create the users programmatically. How do I give different access rights to different sets of users?
On the conceptual level, how do users (with rights) work with pages (with rights)? I tried to read the following docs:
- Access Rights
- Permission types
They dont seem to explain these — or maybe, they do but written in a complex way without any concrete examples which makes it difficult to get the idea of how rights on different entities (pages, users and groups) work together. Here are some text from the Access Rights which needs example to be understood:
- When a right has been allowed at a given level, it gets implicitly denied to anyone else at the same level. This only applies to the right allowed. If only "View" is set to a user/group at this level, all other rights like "Edit" are still inherited. Using this implicit deny behavior is recommended over applying explicit denial.
What does the bold part even mean? I think the term level is used in different sense on different bullet points under the same Basic rules section.
回答1:
I feel there are three questions in this post:
- How do I create Users and Groups programatically?
- How does the Access Rights system work?
- Is there an example for the text quoted from the access rights page
First an answer to the second one.
How does the Access Rights system work - with example
There are a fixed number of rights in XWiki, like view
, edit
, etc.
Users can get these rights assigned directly to them, or they can be member of a group, and the group has these rights assigned to them. This assignment of rights can happen in different places (which are called "levels" in the documentation).
The "level" structure is as follows:
Wiki levels
First there is the main wiki (that gets pre-installed when you install the wiki). Then there might be more wikis, called "sub-wikis", which you can create manually (via the "Wikis" secton from the "Burger" menu on the top right of every wiki page). This is a simple two layer hierarchy:
main wiki (always exists)
|
|--- subwiki1
|
|--- subwiki2
|
|--- subwiki3
Subwikis cannot be nested. I am not going into details why you might want them; oen can often go without them. Users and groups can exist in the main wiki (which means their profile pages are located in the main wiki), or they can exist in subwikis (i.e. their profile pages are there.) Users and Groups from the main wiki are visible in all subwikis (and can get rights assigned to them), but not the other way round - a user located in a subwiki cannot get special rights in the main wiki (and also not in another subwiki). If such users access the main wiki, they are treated as the anonymous user. They can only log in to the subwiki.
Page levels
Second, (nearly) all data in the wiki is stored in pages. These pages are also nested, and since XWiki 7.x they can be nested arbitrarily deep. This is the other part of the "levels" structure.
For every wiki, there is a set of "top level" pages, both preinstalled and user created.
Then there are pages which are children of these "top level" pages, which in turn can have children, and so on.
As an additional complication, not all pages can have subpages. By historical convention these pages with a full name ending in WebHome
can have children pages, others can not. This is probably transparent to the user, but important for the programmer.
There is no single "root" page to start the hierarchy. As an example, for one wiki the structure might look like:
Top level Third Level
Second Level Fourth Level
Main.WebHome (preinstalled "Start" page)
|
|------ Main.Search (preinstalled search page, no subpages)
|
|------ Main.SomePage.WebHome (user created page, can have children)
Sandbox.WebHome (preinstalled playground page)
|
|------ Sandbox.TestPage1 (preinstalled demo page, no subpages)
|
|------ Sandbox.TestPage2 (preinstalled demo page, no subpages)
|
|------ Sandbox.TestPage3 (preinstalled demo page, no subpages)
|
|------ Sandbox.SomePage.WebHome (user created 2nd level page, can have children)
Documentation.WebHome (user created top level page)
|
|------ Documentation.Topic1.WebHome (user created 2nd level page, can have children)
| |
| |------ Documentation.Topic1.SubTopic1.WebHome (user created 3rd lvl page, can have children, too)
| |
| |------ Documentation.Topic1.SubTopic2.WebHome (user created 3rd lvl page, can have children, too)
| |
| |------ Documentation.Topic1.SubTopic3.WebHome (user created 3rd lvl page, can have children, too)
| | |
| | |------ Documentation.Topic1.SubTopic3.EvenMore.WebHome (user created 4th lvl page, can have children)
| |
| .
| .
| |
| |------ Documentation.Topic1.SubTopicN.WebHome (user created 3rd lvl page, can have children, too)
|
|------ Documentation.Topic2.WebHome (user created 2nd lvl page, can have children)
.
.
.
|
|------ Documentation.TopicN.WebHome (user created 2nd lvl page, can have children)
....
Granting rights
You now can grant a right to user or group on every page in this hierarchy by adding an Object of type XWiki.XWikiRights
to the page itself, specifying the list of rights to grant (confusingly stored in the attribute levels
of that object), the list of users and/or groups to grant the right to, and a allow/deny
flag ... which we will come to later. How to do that programatically is discussed in the question: Set user and group rights to document in XWiki
In that case the right is only granted for the page itself, not its subpages. If you give the right edit
on the page Main.WebHome
to the group XWiki.HomepageEditorsGroup
, then only members of this group can edit the page, but this does not affect subpages like Main.Search
or Main.SomePage.WebHome
.
That the attribute levels
here actually stores the rights is maybe confusing - again this is another historical decision. (The software is developed since 15 years or so and the developers are commited to keep backwards compatibility). What ever the attribute is named, these are rights, and not the levels the documentation talks about.
To go on with the rights management: You can also grant a right on a page and all its subpages. This only works for pages which can have subpages. Technically this is done by adding an object of type XWiki.XWikiGlobalRights
... but not to the page itself, but to a subpage named WebPreferences
. (Historical decision, again.)
So if you want to grant the view
right to the group XWiki.Topic1ViewerGroup
on the page Documentation.Topic1.WebHome
and its subpages like Documentation.Topic1.SubTopic1.WebHome
or Documentation.Topic1.SubTopic3.EvenMore.WebHome
, then you take the page Documentation.Topic1.WebPreferences
(creating it if it does not exist), and add an object of type XWiki.XWikiGlobalRights
to it, with the attributes:
level : view
groups : XWiki.Topic1ViewerGroup
allow: 1
How the rights are checked
Now the check for a specific right usually looks at a given page itself, then looks at the WebPreferences
for that page, then at the WebPreferences
of the parent page, and so on. (It is "going up the levels".)
The check stops as soon as it finds a "rights" object covering the right in question.
If no matching "rights" object has been found up to the top level page, then the wiki is checked. Rights on the wiki level are stored in the special page XWiki.XWikiPreferences
, again as objects of class XWiki.XWikiGlobalRights
.
Finally if the wiki happens to be a subwiki, the global righs on the main wiki might be consulted - again on the page names XWiki.XWikiPreferences
, but this time in the main wiki.
Example 1: check for view
right on Documentation.Topic1.SubTopic3.WebHome
Documentation.Topic1.SubTopic3.WebHome
has noXWiki.XWikiRights
- no decisionDocumentation.Topic1.SubTopic3.WebPreferences
has noXWiki.XWikiGlobalRights
- no decisionDocumentation.Topic1.WebPreferences
has anXWiki.XWikiGlobalRights
forview
- stop to make a decision- Result: if the current user is in the group
XWiki.Topic1ViewerGroup
, she/he can view the page, otherwise not
Example 2: check for edit
right on Main.WebHome
Main.WebHome
has aXWiki.XWikiRights
foredit
- stop to make a decision- Result: only users in the
XWiki.HomepageEditorsGroup
can edit, others do not
Example 3: check for edit
right on Main.SomePage.WebHome
Main.SomePage.WebHome
has noXWiki.XWikiRights
- no decisionMain.SomePage.WebPreferences
has noXWiki.XWikiGlobalRights
- no decision- up the page hierarchy:
Main.WebPreferences
has noXWiki.XWikiGlobalRights
- no decision either - (that
Main.WebHome
has aXWiki.XWikiRights
is not consulted, as the right applies only to the page itself) - up the page hierarchy: we are already at a top-level page, so go to the wiki instead
- i.e. check
XWiki.XWikiPreferences
for aXWiki.XWikiGlobalRights
foredit
- usually there is an
allow : 1
for theXWiki.XWikiAllGroup
which meansedit
is allowed for all users - if there is no such settings, and we are in a subwiki: go up the wiki hierarchy and check the
XWiki.XWikiPreferences
of the main wiki - if even there is no decision made, the
edit
right is not allowed
admin
is a special case
As a simplification for the users, but complication for the concept, the admin
right works the other way round: if the admin
right is granted on the wiki level, it is valid on all pages. Even more, it implicitely grants all the other rights, like view
and edit
. (The reason for this is that users too often locked themselves out before this special rule was introduced.)
How does the "implicit deny" work?
Now to the quote:
- When a right has been allowed at a given level, it gets implicitly denied to anyone else at the same level. This only applies to the right allowed. If only "View" is set to a user/group at this level, all other rights like "Edit" are still inherited. Using this implicit deny behavior is recommended over applying explicit denial.
I try to explain by example, too:
In the Example 1 above I wrote:
Documentation.Topic1.WebPreferences
has anXWiki.XWikiGlobalRights
forview
- stop to make a decision- Result: if the current user is in the group
XWiki.Topic1ViewerGroup
, she/he can view the page, otherwise not
Here the result is either:
- allow the user to view the page (and its sub pages), if the user is member of the
XWiki.Topic1ViewerGroup
- deny the user the right to view the page (and its sub pages), if the user is not member of the
XWiki.Topic1ViewerGroup
(i.e is "everyone else")
That is, no matter what rights the user might have otherwise - as soon as the right is set here, then only the users fulfilling the criterion in the settings are allowed to view. Everybody else is out. This is an "implicit deny".
As an alternative, assume someone has set a rights object on Sandbox.WebPreferences
(i.e. affecting the "Sandbox" and all subpages):
level : edit
groups : XWiki.Topic1ViewerGroup
allow: 1
and onSandbox.SomePage.WebHome
(i.e. affecting this sub page only):
level : edit
groups : XWiki.Topic1ViewerGroup
allow: 0
The setting allow: 0
is an "explicit deny": as soon as you are member of the XWiki.Topic1ViewerGroup
, you are not allowed to edit this page. The fact that there is an allow: 1
on a higher level in the page hierarchy (on "Sandbox" and all sub pages) does not matter, beacuse it is not on the same level.
How to do that programatically?
First, the groups should be created as "terminal" sub pages (i.e. pages not having children) in the XWiki
space, like XWiki.MyCustomGroup
. However they seem to work wherever you want to create them.
On the other hand, users must be created as pages XWiki.<LoginName>
as unfortunately there is a lot of code around that expects users to be in this location and nowhere else.
After having created the page (in the API they are called Document
), add an object of the proper class to the page, set the attributes you want and save the page.
When looking at your requirements, it does not look like you want to grant the rights to the groups in any special place in the page hierarchy; so I assume they will be set on the wiki level. Thus no need to understand all the other explanations; just grab the XWiki.XWikiPreferences
page and add the required XWiki.XWikiGlobalRights
there.
I recommend using an MandatoryDocumentInitializer for this; there is a nice example in the code base which makes sure the XWikiAllGroup
is always present.
This interface is meant to ensure that a single page is present in the wiki, but nobody keeps you from checking that other pages are set up properly, too. The only thing you need to have in mind is that the other pages are not saved automatically, but you can do that manually with the XWiki.saveDocument
method.
To create a user, there is a convenience method XWiki.createUser(String userName, Map values, XWikiContext context)
in the XWiki
class. The values
map contains the values for the attributes to be set on the new user; you can check which attributes are available on the XWiki.XWikiUsers
page in your wiki.
To create a group, you can borrow code from the example above. Note that to create a new empty group, one adds an object of type XWiki.XWikiGroups
; to add members to the group one should add one more object of type XWiki.XWikiGroups
for each user and set the member
attribute to the full name of the user (i.e. including the 'XWiki.` prefix).
So the class might start with:
@Component
@Named("XWiki.MyStandardGroup")
public class MyUserAndGroupsInitializer implements MandatoryDocumentInitializer
{
private static final String GROUP_CLASS_NAME = "XWikiGroups";
private static final String MEMBER_ATTR = "member";
private static final String RIGHTS_CLASS_NAME = "XWikiGlobalRights";
private static final String GROUPS_ATTR = "groups";
private static final String USERS_ATTR = "users";
private static final String RIGHTS_ATTR = "levels"; // ;)
private static final String ALLOW_ATTR = "allow";
@Inject
Provider<XWikiContext> contextProvider;
@Inject
org.slf4j.Logger logger;
The @Named
contains by convention the name of the page the initializer cares about. That avoids name clashes between initializers on the one hand and allows to overwrite an existing initializer for a page, if wanted. You can choose a differetn name here if you prefer.
The @Inject
ed compontents are an accessor to the current "context", which allows us to access the data in the current wiki and maintans a database connection in the background. A logger cannot hurt either.
As we need to implement the MandatoryDocumentInitializer
, we first need to tell the location of one of the pages we care about:
@Override
public EntityReference getDocumentReference()
{
return new LocalDocumentReference(XWiki.SYSTEM_SPACE, "MyStandardGroup");
}
This makes XWiki pass us in the page as a parameter in the next method; we should return true
here if that page needs to be saved afterwards. As we do e verything by ourselves, we can as well return false
always.
@Override
public boolean updateDocument(XWikiDocument document)
{
logger.info("try to create users/groups");
try {
// here create your users
// and your groups
} catch (XWikiException xe) {
// as we are not allowed to let this through:
logger.error("failed to create groups", xe);
}
return false;
}
That is it, basically. Oh, some possibly useful helpers:
Adding users is relatively easy:
private void createUser(String userFullName) throws XWikiException
{
XWikiContext context = contextProvider.get();
XWiki xwiki = context.getWiki();
Map<String,String> values = new HashMap<>();
values.put("last_name", userFullName);
values.put("password", "staple battery horses correct");
int result = xwiki.createUser(userName, values, context);
if (result > 0) {
logger.info("user [{}] created", userFullName);
} else {
logger.debug("user [{}] aleady exists", userFullName);
}
}
ok, maybe not that simple, but you can start with that one.
It is nearly the same for groups:
// pass in rights as comma separated string, e.g.: "view,comment,edit"
// members should be the full page name of the user, including the "XWiki." part
private void createGroup(String group, String rights, String... members) throws XWikiException
{
logger.info("try to create group [{}]", group);
XWikiDocument groupDoc = checkDocument(XWiki.SYSTEM_SPACE + '.' + group);
if (groupDoc.isNew()) {
addUserToGroup(groupDoc, "");
for (String member : members) {
addUserToGroup(groupDoc, member);
}
XWikiContext context = contextProvider.get();
XWiki xwiki = context.getWiki();
xwiki.saveDocument(groupDoc, "created", false, context);
logger.info("group [{}] created", group);
}
setRightsForGroup(groupDoc, rights);
}
and adding users to the group is also easy:
// return true if group needs to be saved afterwards
private boolean addUserToGroup(XWikiDocument groupDoc, String userName) throws XWikiException
{
XWikiContext context = contextProvider.get();
LocalDocumentReference groupClassReference = new LocalDocumentReference(XWiki.SYSTEM_SPACE, GROUP_CLASS_NAME);
// first check if the user is already member of the group
if (groupDoc.getXObject(groupClassReference, MEMBER_ATTR, userName, false) != null) {
// is already member, no changes necessary
logger.debug("user [{}] is already member of group [{}]", userName, groupDoc.getFullName());
return false;
}
logger.info("add user [{}] to group [{}]", userName, groupDoc.getFullName());
BaseObject newGroupEntry = groupDoc.newXObject(groupClassReference, context);
newGroupEntry.setStringValue(MEMBER_ATTR, userName);
return true;
}
... if it were not for the rights settings that I have moved into a separate helper
// set rights settings for group if it is not set yet; saves the result right away
private void setRightsForGroup(XWikiDocument groupDoc, String rights) throws XWikiException
{
XWikiContext context = contextProvider.get();
XWiki xwiki = context.getWiki();
LocalDocumentReference rightsClassReference = new LocalDocumentReference(XWiki.SYSTEM_SPACE, RIGHTS_CLASS_NAME);
String groupName = groupDoc.getFullName();
// check if the right is already set in the XWikiPreferences.
// here we need to loop over all values instead
XWikiDocument xwikiPrefDocument = xwiki.getDocument(new DocumentReference(context.getWikiId(), XWiki.SYSTEM_SPACE, "XWikiPreferences"), context);
boolean found = false;
for (BaseObject rightsSetting : xwikiPrefDocument.getXObjects(rightsClassReference)) {
if (rights.contentEquals(rightsSetting.getStringValue(RIGHTS_ATTR))
&& rightsSetting.getIntValue(ALLOW_ATTR) == 1) {
// this is the right setting!
String groups = rightsSetting.getStringValue(GROUPS_ATTR);
if (!groups.contains(groupName)) {
// our group is missing: add group and save
rightsSetting.setStringValue(GROUPS_ATTR, groups + ',' + groupName);
xwiki.saveDocument(xwikiPrefDocument, "add rights for group [" + groupName + "]", true, context);
logger.info("amended rights for group [{}]", groupName);
} else {
logger.info("rights for group [{}] already set", groupName);
}
found = true;
break;
}
}
if (!found) {
BaseObject newRightsSetting = xwikiPrefDocument.newXObject(rightsClassReference, context);
newRightsSetting.setStringValue(RIGHTS_ATTR, rights);
newRightsSetting.setIntValue(ALLOW_ATTR, 1);
newRightsSetting.setLargeStringValue(GROUPS_ATTR, groupName);
if (newRightsSetting.getIntValue(ALLOW_ATTR) != 1) {
logger.error("adding rights of class [{}] for group [{}] failed!", rightsClassReference, context);
}
xwiki.saveDocument(xwikiPrefDocument, "add rights for group [" + groupName + "]", true, context);
logger.info("added new rights for group [{}]", groupName);
}
}
I have also used a checkDocument
helper, which is basically the same as the updateDocument
in the XWikiAllGroupInitializer, except that the name is input and the tediously newly set up page is the return value.
You might want to read the Component Guide to understand how the necessary dependencies get injected. Especially you will need to add the full class name of the initializer to the src/main/resources/META-INF/components.txt
for the initializer to get activated.
Backup your database before you try this out. Except a few tries before everything is properly set up, and nothing gets saved unnecessarily on each wiki restart.
Also fiddle with the WEB-INF/classes/logback.xml
to set the level to INFO
for your package, if you want to see the log messages.
Some random other advice
Instead of managing your users programatically you might consider storing then in a LDAP Server and use this for authentication with the LDAP Authenticator. (You still need to create the groups and manage their rights, however)
While developing I found it very useful to have the Scripting Reference Documentation extension installed in my development wiki. It is no a replacement for any documentation, but being able to brwose the API Javadoc interactively somehow helps me a lot.
The Admin Tools extension has a page that shows you all rights granted in the current wiki where this extension is installed. (Go to .../xwiki/bin/view/Admin/
and click "Show Rights".)
来源:https://stackoverflow.com/questions/60794694/how-do-i-programmatically-create-new-groups-with-specific-set-of-rights-on-xwiki