Skip to content

Commit

Permalink
user: add publisher role
Browse files Browse the repository at this point in the history
* Adds a `publisher` role for users
* Creates user fixture with publisher role.
* Renames `superadmin` to `superuser` for consistency.
* Adds a method to retrieve organisation from current user.
* Adds a method to retrieve user record from current logged user.
* Refactors fixtures in tests for easily testing permissions.
* Removes lines and sort translations in extracted messages.

Co-Authored-by: Sébastien Délèze <[email protected]>
  • Loading branch information
Sébastien Délèze committed Jun 3, 2020
1 parent dd77731 commit 96c5792
Show file tree
Hide file tree
Showing 36 changed files with 6,069 additions and 11,069 deletions.
18 changes: 16 additions & 2 deletions data/users.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"full_name": "Jorg Mueller",
"email": "rero.sonar+superadmin@gmail.com",
"email": "rero.sonar+superuser@gmail.com",
"password": "123456",
"roles": ["superadmin"],
"roles": ["superuser"],
"birth_date": "1978-03-09",
"street": "Sonnenbergstr 39",
"postal_code": "6544",
Expand Down Expand Up @@ -41,6 +41,20 @@
"$ref": "https://sonar.ch/api/organisations/rero"
}
},
{
"full_name": "Claude Roussel",
"email": "[email protected]",
"password": "123456",
"roles": ["publisher"],
"birth_date": "1983-09-14",
"street": "Kirchstrasse 57",
"postal_code": "1660",
"city": "La Lécherette",
"phone": "+41269639084",
"organisation": {
"$ref": "https://sonar.ch/api/organisations/rero"
}
},
{
"full_name": "Jules Brochu",
"email": "[email protected]",
Expand Down
7 changes: 5 additions & 2 deletions scripts/setup
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ pipenv run invenio utils es-init --force # To take templates into account
pipenv run invenio index queue init purge

# Create admin role to restrict access
pipenv run invenio roles create superadmin
pipenv run invenio roles create superuser
pipenv run invenio roles create admin
pipenv run invenio roles create moderator
pipenv run invenio roles create publisher
pipenv run invenio roles create user
pipenv run invenio access allow superuser-access role superadmin
pipenv run invenio access allow superuser-access role superuser
pipenv run invenio access allow admin-access role superuser
pipenv run invenio access allow admin-access role admin
pipenv run invenio access allow admin-access role moderator
pipenv run invenio access allow admin-access role publisher

# Create a default location for depositing files
pipenv run invenio fixtures deposits create
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ msgid_bugs_address = [email protected]
mapping-file = babel.ini
output-file = sonar/translations/messages.pot
add-comments = NOTE
no-location = True
sort-output = True

[init_catalog]
input-file = sonar/translations/messages.pot
Expand Down
9 changes: 5 additions & 4 deletions sonar/modules/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@
from flask_bootstrap import Bootstrap
from flask_wiki import Wiki

from sonar.modules.permissions import has_admin_access, \
has_super_admin_access, has_user_access
from sonar.modules.permissions import has_admin_access, has_publisher_access, \
has_superuser_access
from sonar.modules.utils import get_switch_aai_providers

from . import config


def utility_processor():
"""Dictionary for passing data to templates."""
return dict(has_user_access=has_user_access,
return dict(
has_publisher_access=has_publisher_access,
has_admin_access=has_admin_access,
has_super_admin_access=has_super_admin_access,
has_superuser_access=has_superuser_access,
ui_version=config.SONAR_APP_UI_VERSION,
aai_providers=get_switch_aai_providers)

Expand Down
19 changes: 19 additions & 0 deletions sonar/modules/organisations/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@

from functools import partial

from werkzeug.local import LocalProxy

from sonar.modules.users.api import current_user_record

from ..api import SonarIndexer, SonarRecord, SonarSearch
from ..fetchers import id_fetcher
from ..providers import Provider
from .minters import id_minter

current_organisation = LocalProxy(
lambda: OrganisationRecord.get_organisation_by_user(current_user_record))

# provider
OrganisationProvider = type(
'OrganisationProvider',
Expand Down Expand Up @@ -55,6 +62,18 @@ class OrganisationRecord(SonarRecord):
provider = OrganisationProvider
schema = 'organisations/organisation-v1.0.0.json'

@classmethod
def get_organisation_by_user(cls, user):
"""Return organisation associated with user.
:param user: User record.
:returns: Organisation record or None.
"""
if not user or not user.get('organisation'):
return None

return cls.get_record_by_ref_link(user['organisation']['$ref'])


class OrganisationIndexer(SonarIndexer):
"""Indexing documents in Elasticsearch."""
Expand Down
27 changes: 14 additions & 13 deletions sonar/modules/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,25 @@
from invenio_records_rest.utils import check_elasticsearch

superuser_access_permission = Permission(ActionNeed('superuser-access'))
admin_access_permission = Permission(RoleNeed('moderator'), RoleNeed('admin'))
moderator_access_permission = Permission(ActionNeed('admin-access'))
user_access_permission = Permission(RoleNeed('user'),
RoleNeed('moderator'), RoleNeed('admin'))
admin_access_permission = Permission(ActionNeed('admin-access'))
publisher_access_permission = Permission(RoleNeed('publisher'),
RoleNeed('moderator'),
RoleNeed('admin'),
RoleNeed('superuser'))

# Allow access without permission check
allow_access = type('Allow', (), {'can': lambda self: True})()


def has_user_access():
"""Check if current user has at least role user.
def has_publisher_access():
"""Check if current user has at least role publisher.
This function is used in app context and can be called in all templates.
"""
if current_app.config.get('SONAR_APP_DISABLE_PERMISSION_CHECKS'):
return True

return user_access_permission.can()
return publisher_access_permission.can()


def has_admin_access():
Expand All @@ -54,10 +55,10 @@ def has_admin_access():
if current_app.config.get('SONAR_APP_DISABLE_PERMISSION_CHECKS'):
return True

return moderator_access_permission.can()
return admin_access_permission.can()


def has_super_admin_access():
def has_superuser_access():
"""Check if current user has access to super admin panel.
This function is used in app context and can be called in all templates.
Expand All @@ -83,23 +84,23 @@ def can_create_record_factory(**kwargs):
if current_app.config.get('SONAR_APP_DISABLE_PERMISSION_CHECKS'):
return allow_access

return user_access_permission
return admin_access_permission


def can_update_record_factory(**kwargs):
"""Factory to check if a record can be updated."""
if current_app.config.get('SONAR_APP_DISABLE_PERMISSION_CHECKS'):
return allow_access

return user_access_permission
return admin_access_permission


def can_delete_record_factory(**kwargs):
"""Factory to check if a record can be deleted."""
if current_app.config.get('SONAR_APP_DISABLE_PERMISSION_CHECKS'):
return allow_access

return user_access_permission
return admin_access_permission


def can_access_manage_view(func):
Expand All @@ -109,7 +110,7 @@ def decorated_view(*args, **kwargs):
if not current_user.is_authenticated:
abort(401)
else:
if has_user_access():
if has_admin_access():
return func(*args, **kwargs)

abort(403)
Expand Down
37 changes: 30 additions & 7 deletions sonar/modules/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from elasticsearch_dsl.query import Q
from flask import current_app
from flask_security import current_user
from flask_security.confirmable import confirm_user
from flask_security.recoverable import send_reset_password_instructions
from invenio_accounts.ext import hash_password
Expand All @@ -32,6 +33,9 @@
from ..minters import id_minter
from ..providers import Provider

current_user_record = LocalProxy(lambda: UserRecord.get_user_by_current_user(
current_user))

# provider
UserProvider = type('UserProvider', (Provider, ), dict(pid_type='user'))
# minter
Expand Down Expand Up @@ -77,21 +81,24 @@ class UserRecord(SonarRecord):

ROLE_USER = 'user'
ROLE_MODERATOR = 'moderator'
ROLE_PUBLISHER = 'publisher'
ROLE_ADMIN = 'admin'
ROLE_SUPERADMIN = 'superadmin'
ROLE_SUPERUSER = 'superuser'

ROLES_HIERARCHY = {
ROLE_USER: [],
ROLE_MODERATOR: [ROLE_USER],
ROLE_ADMIN: [ROLE_MODERATOR, ROLE_USER],
ROLE_SUPERADMIN: [ROLE_ADMIN, ROLE_MODERATOR, ROLE_USER],
ROLE_PUBLISHER: [ROLE_USER],
ROLE_MODERATOR: [ROLE_PUBLISHER, ROLE_USER],
ROLE_ADMIN: [ROLE_MODERATOR, ROLE_PUBLISHER, ROLE_USER],
ROLE_SUPERUSER:
[ROLE_ADMIN, ROLE_MODERATOR, ROLE_PUBLISHER, ROLE_USER],
}

minter = user_pid_minter
fetcher = user_pid_fetcher
provider = UserProvider
schema = 'users/user-v1.0.0.json'
available_roles = [ROLE_SUPERADMIN, ROLE_ADMIN, ROLE_MODERATOR, ROLE_USER]
available_roles = [ROLE_SUPERUSER, ROLE_ADMIN, ROLE_MODERATOR, ROLE_USER]

@classmethod
def create(cls,
Expand Down Expand Up @@ -195,6 +202,9 @@ def remove_roles(self):
@classmethod
def get_user_by_current_user(cls, user):
"""Get user by current logged user."""
if user.is_anonymous:
return None

return cls.get_user_by_email(email=user.email)

@classmethod
Expand Down Expand Up @@ -277,11 +287,24 @@ def is_granted(self, role_to_check):

return False

def get_all_reachable_roles(self):
"""Get list of roles depending on role hierarchy."""
roles = []
for role in self['roles']:
roles.extend(self.get_reachable_roles(role))

return list(set(roles))

@property
def is_moderator(self):
"""Check if a user a moderator."""
return self.is_granted(UserRecord.ROLE_MODERATOR)

@property
def is_publisher(self):
"""Check if a user a pulisher."""
return self.is_granted(UserRecord.ROLE_PUBLISHER)

@property
def is_user(self):
"""Check if a user a standard user."""
Expand All @@ -293,9 +316,9 @@ def is_admin(self):
return self.is_granted(UserRecord.ROLE_ADMIN)

@property
def is_super_admin(self):
def is_superuser(self):
"""Check if a user a super administrator."""
return self.is_granted(UserRecord.ROLE_SUPERADMIN)
return self.is_granted(UserRecord.ROLE_SUPERUSER)


class UserIndexer(SonarIndexer):
Expand Down
11 changes: 8 additions & 3 deletions sonar/modules/users/jsonschemas/users/user-v1.0.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,17 @@
"items": {
"type": "string",
"enum": [
"superadmin",
"superuser",
"admin",
"moderator",
"publisher",
"user"
],
"form": {
"options": [
{
"label": "role_superadmin",
"value": "superadmin"
"label": "role_superuser",
"value": "superuser"
},
{
"label": "role_admin",
Expand All @@ -113,6 +114,10 @@
"label": "role_moderator",
"value": "moderator"
},
{
"label": "role_publisher",
"value": "publisher"
},
{
"label": "role_user",
"value": "user"
Expand Down
4 changes: 2 additions & 2 deletions sonar/theme/templates/sonar/partial/dropdown_user.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ <h6 class="dropdown-header">{{current_user.email}}</h6>
{{_('Profile')}}
</a>
{% endif %}
{% if has_super_admin_access() %}
{% if has_superuser_access() %}
<a class="dropdown-item" href="{{ url_for('admin.index')}}">
{{_('Super administration')}}
</a>
{% endif %}
{% if has_user_access() %}
{% if has_admin_access() %}
<a class="dropdown-item" href="{{ url_for('sonar.manage')}}">
{{_('Administration')}}
</a>
Expand Down
3 changes: 2 additions & 1 deletion sonar/theme/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ def logged_user():

if user:
data['metadata'] = user.dumps()
data['metadata']['is_super_admin'] = user.is_super_admin
data['metadata']['is_superuser'] = user.is_superuser
data['metadata']['is_admin'] = user.is_admin
data['metadata']['is_moderator'] = user.is_moderator
data['metadata']['is_publisher'] = user.is_publisher
data['metadata']['is_user'] = user.is_user

# TODO: If an organisation is associated to user and only when running
Expand Down
Loading

0 comments on commit 96c5792

Please sign in to comment.