Permissions - QutEcoacoustics/baw-server GitHub Wiki
This page is a formal specification of possible values for project permissions and authorization for the site in general. If behaviour does not match this document it should be considered a bug.
Projects are the only model/entity that are assigned permissions directly. All authorization happens via one of three paths:
- First a user's role is checked
- Then any special conditions are processed
- Finally, the permissions table is checked. All entities check authorization through relationships to projects. In most cases this means traversing the relational hierarchy up through parent nodes until the Project model is reached.
With the exceptions of the special cases in Role abilities and Special cases, all permissions are stored in the permissions table. All dynamic permissions (ones that can be changed by the web application) are stored in the permissions table.
Anonymous and logged-in user access allow for the concept of public projects: projects that users can participate in without being granted explicit access. The permissions table stores information for anonymous and logged in access. Read access for anonymous users does not imply read access for logged in users (but each of those permissions can be explicitly set).
Access specified per user via a permission can be read, write, or own. Per user access cannot be none; this is implied through the lack of a specified permission. Per-user access is standard and entries are stored in the the permissions table.
Permissions are like ACLs for the project-entity hierarchy. They are not the same as roles.
The available access levels are, in order of abilities granted are: none, read, write, own.
No access to view or modify any part of the project, or any child entity, or other secured entity.
The existence of the Project should not be available to a user with the none access level, except for enabling a user to request additional permissions. This requires the name and id attributes of the project model to be exposed publicly - this exception is listed in the special cases section, see here.
read allows all non-modifying abilities/actions to be performed on the project and all descendants. This includes viewing lists, viewing details, and viewing/downloading audio/spectrograms/annotations/comments.
write includes all the read abilities and also includes modifying actions, except for the actions listed in the own level. Allowed actions include modifying details for Sites in the Project, creating datasets/annotations/comments.
own includes all abilities in the write level and these special actions:
- Change the details of the Project
- Add, change, and remove Sites for the Project
- NB: breaking change
- Change Permissions for the Project
- NB: breaking change
- Change a project's public status (either allow anonymous or allow logged in)
- Delete the Project
Roles are special groups users can be a part of. The user's role will determine if the policies set in the permissions table are ignored or not. Roles based permissions are not dynamic and are not stored in the permissions table.
Roles are named lists of special abilities a user has access to.
The available roles are: admin, user, harvester, and guest.
The user role is the standard role. The user role has few special abilities; the bulk of authentication done for the user role is done through the permissions subsystem.
The harverster roles has special access to a small subset of abilities related to ingesting audio data.
The admin role has access to all abilities including special abilities reserved only for the admin role.
The following section will define formal specifications for the permissions table and model.
These specifications can be used for table UNIQUE constraints, model validations, and unit tests.
CREATE TABLE permissions (
id integer NOT NULL,
creator_id integer NOT NULL,
level character varying NOT NULL,
project_id integer NOT NULL,
user_id integer,
updater_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
allow_logged_in boolean DEFAULT false NOT NULL,
allow_anonymous boolean DEFAULT false NOT NULL,
CONSTRAINT permissions_exclusive_cols CHECK ((((((user_id IS NOT NULL) AND (NOT allow_logged_in)) AND (NOT allow_anonymous)) OR (((user_id IS NULL) AND allow_logged_in) AND (NOT allow_anonymous))) OR (((user_id IS NULL) AND (NOT allow_logged_in)) AND allow_anonymous)))
);
The allow_logged_in and allow_anonymous fields represent a permission that is relevant when a permission for a specific user id cannot be found.
CREATE TABLE users (
id integer NOT NULL,
user_name character varying NOT NULL,
...
roles_mask integer,
...
);
Each of these invariants should be unit-tested. Most should be part of the model validations.
¬(level = ∅) ⇔ (level IS NOT NULL)
level ∈ {own, write, read}
¬(project_id = ∅) ⇔ (project_id IS NOT NULL)
This invariant should be a uniqueness constraint applied to the permissions table.
- Let
A:=allow_anonymous - Let
L:=allow_logged_in - Let
U:=¬(user_id = ∅)
A ⊕ L ⊕ U
⇔
(A ∧ ¬L ∧ ¬U) ∨ (¬A ∧ L ∧ ¬U) ∨ (¬A ∧ ¬L ∧ U)
⇔
(A AND (NOT L) AND (NOT U)) OR ((NOT A) AND L AND (NOT U)) OR ((NOT A) AND (NOT L) AND U)
¬(roles_mask = ∅) ⇔ (roles_mask IS NOT NULL)
roles_mask ∈ {admin, user, harvester, guest}
Note: the guest value is not a valid value in database. Guest users are not stored in the database.
MARK: unsure about previous statement
This section defines which permission levels are allowed for each type of user. Types of users are:
- Let
C1:= Anonymous users ⇔(A ∧ ¬L ∧ ¬U) - Let
C2:= Any logged in user ⇔(¬A ∧ L ∧ ¬U) - Let
C3:= A specific user ⇔(¬A ∧ ¬L ∧ U)
The above definitions are taken from the 3/8 valid combinations from the truth table of Invariant C Combination of User Id, Allow Logged In, and Allow Anonymous)
Reminder: if a permission is in the permission table, it must be associated with a project_id.
Truth table: Permission levels vs types of users
| O | W | R | N (implied) | |
|---|---|---|---|---|
| C1 | F | F | T | T |
| C2 | F | T | T | T |
| C3 | T | T | T | T |
The result of this table is that 9/12 combinations are possible.
-
noneis- a valid permission level for all user types
- is not stored in the database
- is implicit if a record is not found
- Anonymous users
- can have
readaccess (only) to a specified project - do not have any write access
- can have
- Logged in users can have
writeorreadaccess (only) to a specified project - A specific user can have
own,write, orreadaccess to a specified project - Clarification: if an anonymous user has read access, it does not automatically imply a logged in user has read access - those permissions can be set separately.
- Justification: the implies part, while it makes sense, adds complexity to the rules of the system. We have a strong system already for adding both logged in and anonymous permissions, so we can just add one record for each case, without specifically programming the implied inheritance.
Based on the definitions above there are 9 basic cases permissions cases to cover.
Additional logic is included for combinations of roles and special permissions.
- IF
current_useris not defined- Create guest user and set
roles_maskto:guest - ...continue
- Create guest user and set
- IF
current_user.roles_mask == :admin- GRANT access to all
:adminrole abilities
- GRANT access to all
- IF
current_user.roles_mask == :harvester- GRANT access to all
:harvesterrole abilities
- GRANT access to all
- IF
current_user.roles_mask == :userORcurrent_user.roles_mask == :guest- IF
current_user.roles_mask == :user- GRANT access to all [
:user] role abilities
- GRANT access to all [
- GRANT access to all special cases
- EXECUTE permissions check if needed
- Determine entity's relationship to the
projectstable- NB: permissions table enties only apply to entities
- NB: all models should define their relationship to the
projectsentity unless they are covered by a special case
- Query the
permissionstable for any entries relating to theprojects determined by previous step - IF no records are found
- RETURN DENY ACCESS (permission level
:none)
- RETURN DENY ACCESS (permission level
- IF any records are found
- Choose the record with the highest permission level
- RETURN
<<highest_permission_level>>
- Determine entity's relationship to the
- IF
- ELSE DENY ACCESS
If GRANT or DENY is defined for an ability, apply authorization immediately. Note: a special case is just an ability that doesn't follow the rules for role based access or entity based access.
If a permission level is returned, apply logic from entity based access section (next section).
Entities are Ruby on Rails controllers. Controllers typically map 1:1 to an associated model.
There are seven standard actions for a controller. This section defines what the standard permission levels allow for each action.
Actions that are atypical are special cases. A list of special cases are defined in the special cases section.
Table: permission levels mapped to controller actions. T⇔GRANT, F⇔DENY
| :own | :write | :read | :none | |
|---|---|---|---|---|
index |
T | T | T | F |
show |
T | T | T | F |
new |
T | T | T | T |
create |
T | T | F | F |
update |
T | T | F | F |
destroy |
T | T | F | F |
filter |
T | T | T | F |
NB: everyone, always has access to the new action. This is because new only stubs the required fields for an entity that needs to be created. In RESTful terms, it is basically documentation. In Rails views terms, it shows an empty form - that should 403 when submitted. Just because new is accessible, it does not mean good UI has to link to it.
For each of the special cases listed below, it should be assumed that the standard entity access rules defined in the previous section are in effect, unless overridden here.
-
has :permission is used extensively below. This should be read as: the
current_userhas the:permissionpermission. -
is_guest should be read as: the
current_userhas the role:guestand is accessing the entity anonymously. -
is_implicit should be read as: the
current_userhas the role:userand has implicit (public, no permission record exists for that user/project combo) access to the entity. -
user_owned should be read as: the current user created this entity (
current_userEQUAL toentity.creator_id).- Note: this definition does not extent to
entity.updater_id
- Note: this definition does not extent to
- Projects
- GRANT
indexandshowfor ANY request but LIMIT attributes toidandname - On project creation, assign
:ownto the creator - DENY access to
updateordestroyUNLESS has:own- NB: this is breaking behaviour
- GRANT
- Permissions
- DENY access to ALL actions UNLESS has
:own- NB: breaking change
- NB: includes changing
permission.allow_anonymousorpermission.allow_logged_in
- DENY access to ALL actions UNLESS has
- Sites in Projects
- DENY access to ALL actions
- NB: essentially a private entity, no API exposed
- DENY access to ALL actions
- Sites
- IF NOT has
:own- THEN OBFUSCATE
site.latitudeandsite.longitudeand setsite.location_obfucatedto TRUE
- THEN OBFUSCATE
- ALLOW
upload_instructions,harvestIF has:own - DENY access to
create,updateordestroyUNLESS has:own- NB: this is breaking behaviour
- IF NOT has
- Audio Recordings
- DENY ALL access to
create,update, anddestroy- NB: the
harvesterrole has special access
- NB: the
- DENY ALL access to
- Audio Events
- ALLOW
index,show, andfilterfor ANY request ifaudio_event.is_referenceis TRUE- NB: reference annotations
- ALLOW
download_annotationsfor IF has:read
- ALLOW
- Audio Event Tags
- ALLOW
index,show, andfilterfor ANY request ifaudio_event_tags.audio_event.is_referenceis TRUE- NB: reference annotations
- ALLOW
- Media
- ALLOW
showfor ANY request:- IF conditions:
-
audio_event_tags.audio_event.is_referenceis TRUE - AND
audio_event_idQSP is set - AND
(audio_event WHERE audio_events.id = audio_event_id).is_referenceis TRUE
-
- Further restrictions
- Allow only 5 seconds +/-
audio_eventstart and end times
- Allow only 5 seconds +/-
- NB: reference annotations
- IF conditions:
- ALLOW
- Tags
- ???
- Tag Groups
- ???
- Audio Event Comments
- DENY access to
updateanddestroyUNLESSuser_owned
- DENY access to
- Bookmarks
- DENY access to ALL actions UNLESS
user_owned- NB: bookmarks are essentially private
- DENY access to ALL actions UNLESS
- Analysis Jobs
- DENY access to
createIFis_implicitoris_guest - DENY access to
updateANDdestroyUNLESSuser_owned
- DENY access to
- Analysis Jobs Results
- ???
- Projects Saved Searches
- DENY access to ALL actions
- NB: essentially a private entity, no API exposed
- DENY access to ALL actions
- Saved Searches
- DENY access to
updateANDdestroyUNLESSuser_owned - NB: saved searches are public - they are linked to Projects via
projects_saved_searchesand as
per standard permissions, anyone with:readcan see saved searches.
- DENY access to
- Scripts
- DENY access to
create,update, ANDdestroyactions for ANY request
- DENY access to
- Users
- DENY access to
create,updateANDdestroyactions for ANY request - UNLESS is
user_owned- Restrict access to sensitive data
- White list
- Username
- Picture
- Last seen at
- <>
- White list
- Restrict access to sensitive data
- NB: all the special cases involving creating accounts and changing a profile here are dealt with via Devise and through the
userrole
- DENY access to
This is an exhaustive list of abilities that are included in roles.
Roles are applied before permissions! Thus if any role rule matches here, the entire permission subsystem is skipped.
- GRANT access to ALL abilities (everything!)
- GRANT access to special
adminonly pages- Includes
- Resque status
- Script creation
- ...
- Includes
- GRANT access to ALL
audio_recordingabilities
- IF entity is
user_profileGRANT ACCESS (e.g.user_profileisuser_owned)
- ???
