Acitivity’s query
module has some interaction with groups in constructing and executing queries for activity pages.
The GET profile
endpoint returns an object with a groups
attribute—an Array of objects with:
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"public": {
"type": "boolean"
}
}
Annotations have a group
property, a string. Note that I believe this to be the groupid, not the name of the group.
DELETE groups/{id}/members/{user}
From the docs:
The user to remove from the group. For now, only the value
me
is supported, representing the currently-authenticated user.
Auth’s util
module has a method groupfinder
that is used to build a list of principals
for the user at hand—a list that includes possible privileged roles (admin, staff) as well as group memberships and authority associations.
NB: This groupfinder
is completely distinct from services/groupfinder.py
AFAICT. Watch out for the naming confusion.
The role
module defines Admin
and Staff
roles; this module uses the term group
but I imply that this use of group
is group
as in permission/file group, not h’s specific meaning of group.
I don’t yet have a full picture of the how all of the pieces work together in pyramid for access, authentication and authorization of (e.g. Resources, Model-defined permissions, Auth module bits, like TOKEN_POLICY, etc. all seem to be involved), but I can see in general that there is access control going on :).
There is an —add-publisher-group
command supported in the CLI (see cli/groups.py
). It relies on the group
service’s create
.
The db
’s __init__
has a method _maybe_create_world_group
which checks for the existence of the global Public group and creates it if it isn’t there.
flag_notification.py
for sending notifications to group moderators when an annotation in that group is flagged.
annotation_hidden.py
handles the formatting of hidden annotations and leaves them visible to users who are moderators of that annotation’s associated group. It contains a method _current_user_is_moderator
, but that method relies on an external moderator_check
passed to the object’s constructor. It sort of gets this from services/annotation_json_presentation.py
—it boils down to, I think, request.has_permission(‘admin’, group)
in the end.
annotation_moderation.py
also relies on an external check to be passed to it—in this case, a param has_permission
, which I assume is, once again, request.has_permission
. It performs the same check (‘admin’ permission on group).
There’s not as much to the actual groups
module as one might think. It has two main concerns:
- registering (adding) a search filter
GroupAuthFilter
which will filter out groups that the request.user isn’t qualified to see from search results. This uses thegroup
service’sgroupids_readable_by
method to get a list of, well, groupids readable by the user. - defining a schema and validation for the add-group form (user private groups in this context), presumably used by the associated view for that.
annotation.py
: Sets the default for itsgroupid
to__world__
(i.e. the global Public group)user.py
: It should be pointed out that there is no indication within theUser
model that there is any sort ofgroups
relationship—this is entirely handled by an SQLAlchemybackref
ingroup.py
group.py
: The main banana for the definition of a group, of course. Defines an enum set of “permissions” (JoinableBy, ReadableBy, WriteableBy) and builds an__acl__
(access control list) that defines permissions for thegroup
context in the pyramid app (this is whererequest.has_permission
gets its rules from).
Implemented by services/groupfinder.py
, defines an interface for retrieving a group by its publicid. See “Services”
I confess, I don’t understand the NIPSA feature (beyond it standing for “not-in-public-site-areas”) but it looks like the NIPSA flag will apply to groups created by a NIPSA’d user? (see nipsa/search.py
).
panels/navbar.py
defines the context for the navbar that is “displayed at the top of most pages”. It generates an ordered list of user groups (via request.user.groups
) to populate the groups menu, and also “groups suggestions” (I’m not sure yet where those manifest, though I can see a reference in the navbar template file at templates/panels/navbar.html.jinja2
).
There is also a small reference to groups in a “back to group page” link in back_link.py
here.
annotation_searchindex.py
has a single reference to an annotation’s group
(groupid) field.
annotation_json.py
also has that group field, but also “Converts our simple internal annotation storage format into the legacy complex permissions dict format that is still used in some places” via:
principals = security.principals_allowed_by_permission(
self.annotation_resource, 'read')
The application defines an AnnotationResource, which includes a group
property. It generates an ACL (access-control list) for a given annotation resource based in part on the principal’s permissions vis-a-vis the annotation’s group. See “Model”.
annotation.py
is a JSON schema for the annotation API. There is a group
property defined (a string; takes a group pubid). There’s also:
'permissions': {
'title': 'Permissions',
'description': 'Annotation action access control list',
'type': 'object',
'patternProperties': {
'^(admin|delete|read|update)$': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^(acct:|group:).+$',
},
}
},
Perhaps that’s the (legacy?) permissions dictionary that is referenced in presenters/annotation_json
.
n.b.: An aside: the annotation schema is the only JSON schema in schemas
. There is no schema here for groups or other resources.
config
has an ANNOTATION_MAPPING
dictionary that contains a group
property (string).
core
applies the group search filter defined in query
— it also presumably applies the GroupAuthFilter
defined in the Groups module.
parser
defines valid search params/keys, including group
query
defines a GroupFilter
group
. This provides the current “group type” definition—“private” and “publisher”—via GROUP_ACCESS_FLAGS
(the model is unaware of meaningful groupings of access settings). It also provides methods for creating a group, joining a group and leaving a group and some utility methods for obtaining a full list of groupids “readable by” a user and a full list of groupids “created” by a user.
authority_group
provides a service for listing all world-readable groups in a given authority.
groupfinder
implements the IGroupService
interface. It retrieves a group based on a string pubid. NB: Although the interface is called IGroupService
, the implemented service is groupfinder
—not the group
service which is noted above.
Other services with some groups involvement include:
auth_ticket
has a method,groups
, which returns—watch out—a list of principals for the user. That includes the groups to which they belong, but also includes admin and staff roles (if applicable) and likely does not include the global public group. In any case, this is “groups” in a different context here. See alsoAuth/util
annotation_stats
has the ability to filter annotation stats by group. It has a notion of “public”, “group” or “private” hard-coded. It queries the Annotation model filtered by group.annotation_json_presentation
: At the end of the day, this is usingrequest.has_permission
to check for group access.delete_user
: Deletes a user, removes the user as member from any group, and deletes any (private) groups created by the user if they don’t have posts from any other users.
The GET Profile
API endpoint makes use of the session
module to populate the profile
object, and, as part of that, the groups
property on the profile
object that is returned in the response.
That API endpoint is the only consumer of session.profile
in the app.
Methods within the module are responsible for generating the proper list of groups visible to the profile/user at hand (currently: always only the logged-in user or a non-auth’d user)—it is an important piece here. Relevant methods are _current_groups
and _user_groups
; the session
module is also currently responsible for the sorting of returned groups and the “model” of the returned groups (i.e. attrs).
storage.py
is a module that provides business logic for the persistence of annotations, specifically (other resources don’t use this design pattern).
There are several unique things about storage
module’s interaction with groups:
- It enforces that annotation replies are in the same group as their parent annotations
- It makes use of the
groupfinder
service to retrieveGroup
models by groupid - It checks to make sure that the current user has
write
permissions for the group in question. See “Model”.
This, the websocket/realtime API:
- Uses the
groupfinder
service to retrieve groups from pubids - Has a bit of a different tack on group permissions:
def _authorized_to_read(effective_principals, permissions):
"""Return True if the passed request is authorized to read the annotation.
If the annotation belongs to a private group, this will return False if the
authenticated user isn't a member of that group.
"""
read_permissions = permissions.get('read', [])
read_principals = translate_annotation_principals(read_permissions)
if set(read_principals).intersection(effective_principals):
return True
return False
api_moderation
: Usesrequest.has_permission
to check for group admin accessapi_flags
: uses thegroupfinder
service to retrieve an annotation’s group; sends email togroup.creator
(note: this differs from other moderation-flag related code in that it doesn’t check for admin access—it checks for the group’s creator)activity.py
: has 72 matches forgroup
, that is, groups figure in significantly (for activity/searches filtered by group, primarily). Note that it useshas_permission
to check for current user’s read access for a group as well as “joinable” access.groups.py
: View for creating a private group. Relies on thegroup
service. Note that ifgroup.joinable_by
isNone
and the user is not logged in, it raises a 404. This may not be quite the right behavior for all group types in the future. (Also: this feels like controller-level logic, not view-level?)api.py
: Note that theapi
view also contains API endpoints concerning annotations (whereas other API resources are broken out intoapi_*
modules). Relies on GroupFinder service (IGroup interface implementor).admin_groups.py
: The groups admin page shows all groups defined in the service. That means the current groups admin page onh
’s production service has, currently, 1006 pages of groups. They are not searchable. This won’t scale, of course.api_groups.py
: Endpoint for deleting a group membership (removing a user from a group). Currently only supported for currently-logged-in-user.
It used to be that we had only the Public group (anyone can read, anyone who is logged in can write) and private groups (only members can read or write, anyone with the link can join and become a member). I guess that's where the simple
"public": {"type": "boolean"}
of theGET profile
endpoint came from.Since then things have become more complicated. We've added third-party accounts and third-party groups, so the anyone can read, anyone who is logged in can write for the Public group is now anyone can read, anyone who is logged in to a
@hypothes.is
account can write, and there can be other groups such as eLife's group where it's anyone can read, anyone who is logged in to a@elifesciences.org
account can write.And of course we're now talking about implementing open groups and restricted groups, which each have their own rules.
In light of all this the simple
"public": {"type": "boolean"}
of theGET profile
endpoint may not make any sense anymore. I guess apublic: true|false
boolean is still one of the dimensions along which groups can vary, wherepublic: true
means anyone can read the group andpublic: false
means only members can read the group.I think this
public: true|false
boolean pre-dates the more fine grainedReadableBy
,JoinableBy
,WritableBy
fields now in the groups model.