Book Group Management¶
Book group management lets a book superuser — a user who is allowed to manage a particular book — create groups and assign users to them, scoped to that one book. It gives book owners a self-service way to manage who can read their book without needing a global administrator for every change.
The feature is available only to users who are allowed to manage the book. Ordinary readers never see the Groups tab or the endpoints described below.
Overview¶
A book superuser can:
Create groups that are scoped to a single book.
Search for users by name or email address.
Assign found users to one of the book’s groups.
Remove members from a group.
Groups created through this feature automatically grant read access to the book, so adding a user to a group is all that is needed to let them read it.
How Groups Work¶
A few rules govern how book groups are named and managed. They keep group ids stable (so permissions survive edits) while letting titles stay readable.
You only provide a title. When creating a group you enter a short title such as
Members. The system generates the underlying group id for you.The title is prefixed with the book title. A group you title
Memberson a book titledMy bookis stored asMy book - Members.The id is generated and frozen. The id is derived from the book title and your title (for example
my-book-members). It is fixed at creation time and made unique — if that id already exists, a numeric suffix is appended (my-book-members-2, and so on). The id never changes afterwards.Renaming the book updates group titles automatically. If the book is renamed, every group title is re-prefixed with the new book title. The group id stays stable, so existing permissions and member assignments are preserved.
Scope is limited to the book. A superuser can only manage groups that were created through this feature on that book. Groups belonging to other books, and groups created by any other means, are never listed or editable here.
Groups cannot be deleted here. Deleting a group is deliberately reserved for global administrators. There is no delete-group action or endpoint in this feature (see No group deletion).
Keycloak-managed sites¶
On sites where groups are managed in Keycloak (Keycloak group sync is enabled), book groups are created in Keycloak rather than only locally:
Creating a group creates it in Keycloak using the book-prefixed title as the Keycloak group name (numbered —
My book - Members (2)— if that name is already taken). The local Plone group is the standard Keycloak-synced shadow.Assigning or removing a member propagates to the Keycloak group as well as the local group.
Renaming the book renames the Keycloak group so the titles keep following the book title.
Everything else — the Groups tab, the endpoints below, and their responses —
behaves the same. If Keycloak cannot be reached, group creation fails and
nothing is created locally (the REST API responds with 503); the superuser is
asked to try again or contact an administrator.
The Groups Tab¶
For users who are allowed to manage the book, a Groups tab appears on the
book at /<book>/book-groups. From this tab a superuser can:
Enter a title to create a new group for the book.
Search for users by name or email address.
Assign a found user to one of the book’s groups.
Remove a member from a group.
The tab is hidden from users who are not allowed to manage the book.
REST API¶
All endpoints operate on a book object and are available only to users who are allowed to manage that book.
GET @book-groups¶
Lists the groups created for this book, each with its members.
const response = await fetch('https://example.com/my-book/@book-groups', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
});
import requests
response = requests.get(
'https://example.com/my-book/@book-groups',
headers={
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
)
Response:
{
"@id": "https://example.com/my-book/@book-groups",
"items": [
{
"@id": "https://example.com/my-book/@book-groups/my-book-members",
"id": "my-book-members",
"title": "My book - Members",
"user_title": "Members",
"members": [
{
"id": "jane.doe",
"fullname": "Jane Doe",
"email": "jane@example.com"
}
]
}
],
"items_total": 1
}
The title is the full, book-prefixed group title; user_title is the plain
title the superuser entered when creating the group.
POST @book-groups¶
Creates a new group for the book. You provide only the title; the id and the
book-prefixed title are generated for you. The response is the created group
object (same shape as an entry in the list above) with an empty members list.
const response = await fetch('https://example.com/my-book/@book-groups', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({
title: 'Members',
}),
});
import requests
response = requests.post(
'https://example.com/my-book/@book-groups',
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
json={
'title': 'Members',
},
)
Response (201):
{
"@id": "https://example.com/my-book/@book-groups/my-book-members",
"id": "my-book-members",
"title": "My book - Members",
"user_title": "Members",
"members": []
}
Request Body¶
Field |
Required |
Description |
|---|---|---|
|
Yes |
The plain group title (for example |
Error Responses¶
Status |
Error message |
Cause |
|---|---|---|
400 |
|
The |
503 |
|
On a Keycloak-managed site, the group could not be created in Keycloak (e.g. Keycloak unreachable). Nothing is created locally. |
POST @book-groups/<group-id>/members¶
Adds a member to one of the book’s groups. Identify the user by their user id, or by their email address when the site manages users via an external identity provider. The response is the updated group object.
const response = await fetch(
'https://example.com/my-book/@book-groups/my-book-members/members',
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({
user: 'jane.doe',
}),
},
);
import requests
response = requests.post(
'https://example.com/my-book/@book-groups/my-book-members/members',
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
json={
'user': 'jane.doe',
},
)
Response:
{
"@id": "https://example.com/my-book/@book-groups/my-book-members",
"id": "my-book-members",
"title": "My book - Members",
"user_title": "Members",
"members": [
{
"id": "jane.doe",
"fullname": "Jane Doe",
"email": "jane@example.com"
}
]
}
Request Body¶
Field |
Required |
Description |
|---|---|---|
|
Yes |
The user id of the member to add, or their email address when users are managed via an external identity provider. |
Error Responses¶
Status |
Error message |
Cause |
|---|---|---|
404 |
|
|
400 |
(message describing the lookup) |
No user could be found for the given id or email address. |
DELETE @book-groups/<group-id>/members/<user-id>¶
Removes a member from one of the book’s groups. The response is the updated group object.
const response = await fetch(
'https://example.com/my-book/@book-groups/my-book-members/members/jane.doe',
{
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
},
);
import requests
response = requests.delete(
'https://example.com/my-book/@book-groups/my-book-members/members/jane.doe',
headers={
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
)
Response:
{
"@id": "https://example.com/my-book/@book-groups/my-book-members",
"id": "my-book-members",
"title": "My book - Members",
"user_title": "Members",
"members": []
}
Error Responses¶
Status |
Error message |
Cause |
|---|---|---|
404 |
|
|
GET @book-group-user-search¶
Searches for users to assign to a group, matching against name or email address.
Use it to build a picker for the “add member” action. The query parameter is
required; an empty query returns no items.
const response = await fetch(
'https://example.com/my-book/@book-group-user-search?query=jane',
{
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
},
);
import requests
response = requests.get(
'https://example.com/my-book/@book-group-user-search',
params={'query': 'jane'},
headers={
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
)
Response:
{
"items": [
{
"id": "jane.doe",
"fullname": "Jane Doe",
"email": "jane@example.com"
}
],
"items_total": 1
}
No group deletion¶
There is deliberately no endpoint (and no UI action) for deleting a group through this feature. A book superuser can create groups and manage their members, but removing a group entirely is reserved for global administrators.