Skip to content

Commit

Permalink
views: access control
Browse files Browse the repository at this point in the history
* NEW Adds customizable access control to record views. Allow
  configuring different permissions per endpoint. (reference inveniosoftware#14)

Signed-off-by: Nicolas Harraudeau <[email protected]>
  • Loading branch information
Nicolas Harraudeau committed Dec 14, 2015
1 parent 26ae5f7 commit 2a924a0
Show file tree
Hide file tree
Showing 8 changed files with 658 additions and 19 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ docs/_build/

# PyBuilder
target/

# Sqlite databases
*.db
51 changes: 42 additions & 9 deletions examples/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,35 @@

"""Minimal Flask application example for development.
For this example the access control is disabled.
Run example development server:
.. code-block:: console
$ cd examples
$ flask -a app.py db init
$ flask -a app.py db create
$ flask -a app.py load_fixture
$ flask -a app.py --debug run
"""
$ cd examples
$ flask -a app.py db init
$ flask -a app.py db create
$ flask -a app.py fixture records
$ flask -a app.py --debug run
Try to get some records
.. code-block:: console
$ curl -XGET http://localhost:5000/records/1
$ curl -XGET http://localhost:5000/records/2
$ curl -XGET http://localhost:5000/records/3
$ curl -XGET http://localhost:5000/records/4
$ curl -XGET http://localhost:5000/records/5
$ curl -XGET http://localhost:5000/records/6
$ curl -XGET http://localhost:5000/records/7
""" # noqa

from __future__ import absolute_import, print_function

import os

from flask import Flask
from flask_celeryext import FlaskCeleryExt
from flask_cli import FlaskCLI
Expand All @@ -48,13 +64,25 @@

from invenio_records_rest import InvenioRecordsREST


# create application's instance directory. Needed for this example only.
current_dir = os.path.dirname(os.path.realpath(__file__))
instance_dir = os.path.join(current_dir, 'app')
if not os.path.exists(instance_dir):
os.makedirs(instance_dir)

# Create Flask application
app = Flask(__name__)
app = Flask(__name__, instance_path=instance_dir)
app.config.update(
CELERY_ALWAYS_EAGER=True,
CELERY_CACHE_BACKEND="memory",
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
CELERY_RESULT_BACKEND="cache",
# No permission checking
RECORDS_REST_DEFAULT_CREATE_PERMISSION_FACTORY=None,
RECORDS_REST_DEFAULT_READ_PERMISSION_FACTORY=None,
RECORDS_REST_DEFAULT_UPDATE_PERMISSION_FACTORY=None,
RECORDS_REST_DEFAULT_DELETE_PERMISSION_FACTORY=None,
)
FlaskCLI(app)
FlaskCeleryExt(app)
Expand All @@ -65,8 +93,13 @@
InvenioRecordsREST(app)


@app.cli.command()
def load_fixture():
@app.cli.group()
def fixtures():
"""Command for working with test data."""


@fixtures.command()
def records():
"""Load test data fixture."""
import uuid
from invenio_records.api import Record
Expand Down
173 changes: 173 additions & 0 deletions examples/permsapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 CERN.
#
# Invenio is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307, USA.
#
# In applying this license, CERN does not
# waive the privileges and immunities granted to it by virtue of its status
# as an Intergovernmental Organization or submit itself to any jurisdiction.


"""Minimal Flask application example with access control enabled.
Run example development server:
.. code-block:: console
$ cd examples
$ flask -a permsapp.py db init
$ flask -a permsapp.py db create
$ flask -a permsapp.py fixtures records
$ flask -a permsapp.py fixtures access
$ flask -a permsapp.py --debug run
Try to get record 1:
.. code-block:: console
$ curl -XGET http://localhost:5000/records/1
Login as [email protected] (password 123456) and get record 1:
.. code-block:: console
$ curl -XPOST http://localhost:5000/login/ -d 'email=admin%40invenio-software.org&password=123456' -c mycookie
$ curl -XGET http://localhost:5000/records/1 -b mycookie
Login as [email protected] (password 123456), who has not
the permission to read record 1, and try to get record 1:
.. code-block:: console
$ curl -XPOST http://localhost:5000/login/ -d 'email=forbidden%40invenio-software.org&password=123456' -c mycookie2
$ curl -XGET http://localhost:5000/records/1 -b mycookie2
""" # noqa

from __future__ import absolute_import, print_function

import os

from flask import Flask
from flask_celeryext import FlaskCeleryExt
from flask_cli import FlaskCLI
from flask_menu import Menu
from flask_security.utils import encrypt_password
from invenio_accounts import InvenioAccounts
from invenio_accounts.views import blueprint
from invenio_db import InvenioDB, db
from invenio_pidstore import InvenioPIDStore
from invenio_records import InvenioRecords
from invenio_records.permissions import records_create_all, \
records_delete_all, records_read_all, records_update_all
from invenio_rest import InvenioREST

from invenio_access import InvenioAccess
from invenio_access.models import ActionUsers
from invenio_records_rest import InvenioRecordsREST


# create application's instance directory. Needed for this example only.
current_dir = os.path.dirname(os.path.realpath(__file__))
instance_dir = os.path.join(current_dir, 'permsapp')
if not os.path.exists(instance_dir):
os.makedirs(instance_dir)

# Create Flask application
app = Flask(__name__, instance_path=instance_dir)
app.config.update(
CELERY_ALWAYS_EAGER=True,
CELERY_CACHE_BACKEND="memory",
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
CELERY_RESULT_BACKEND="cache",
# Install Principal and Login extensions
WTF_CSRF_ENABLED=False,
ACCOUNTS_USE_CELERY=False,
SECRET_KEY='CHANGE_ME',
SECURITY_PASSWORD_SALT='CHANGE_ME_ALSO',
)
FlaskCLI(app)
FlaskCeleryExt(app)
InvenioDB(app)
InvenioREST(app)
InvenioPIDStore(app)
InvenioRecords(app)
Menu(app)
accounts = InvenioAccounts(app)
app.register_blueprint(blueprint)
InvenioAccess(app)
InvenioRecordsREST(app)

rec_uuid = 'deadbeef-1234-5678-ba11-b100dc0ffee5'


@app.cli.group()
def fixtures():
"""Command for working with test data."""


@fixtures.command()
def records():
"""Load test data fixture."""
from invenio_records.api import Record
from invenio_pidstore.models import PersistentIdentifier, PIDStatus

# Record 1 - Live record
with db.session.begin_nested():
PersistentIdentifier.create(
'recid', '1', object_type='rec', object_uuid=rec_uuid,
status=PIDStatus.REGISTERED)
Record.create({'title': 'Registered '}, id_=rec_uuid)


@fixtures.command()
def access():
"""Load access fixtures."""
admin = accounts.datastore.create_user(
email='[email protected]',
password=encrypt_password('123456'),
active=True,
)
forbidden = accounts.datastore.create_user(
email='[email protected]',
password=encrypt_password('123456'),
active=True,
)
# Give all permissions on the record to admin user
db.session.add(ActionUsers(
action=records_create_all.value,
user=admin))
db.session.add(ActionUsers(
action=records_read_all.value, argument=rec_uuid,
user=admin))
db.session.add(ActionUsers(
action=records_update_all.value, argument=rec_uuid,
user=admin))
db.session.add(ActionUsers(
action=records_delete_all.value, argument=rec_uuid,
user=admin))
# Exclude all permission on the record to the forbidden user
db.session.add(ActionUsers(
action=records_create_all.value,
user=forbidden, exclude=True))
db.session.add(ActionUsers(
action=records_read_all.value, argument=rec_uuid,
user=forbidden, exclude=True))
db.session.add(ActionUsers(
action=records_update_all.value, argument=rec_uuid,
user=forbidden, exclude=True))
db.session.add(ActionUsers(
action=records_delete_all.value, argument=rec_uuid,
user=forbidden, exclude=True))
db.session.commit()
72 changes: 71 additions & 1 deletion invenio_records_rest/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,66 @@

from __future__ import absolute_import, print_function

from werkzeug.utils import import_string, cached_property

from .views import create_blueprint


class _RecordRESTState(object):
"""Record REST state."""

def __init__(self, app):
"""Initialize state."""
self.app = app
self._read_permission_factory = None
self._create_permission_factory = None
self._update_permission_factory = None
self._delete_permission_factory = None

@cached_property
def read_permission_factory(self):
"""Load default read permission factory."""
if self._read_permission_factory is None:
imp = self.app.config[
"RECORDS_REST_DEFAULT_READ_PERMISSION_FACTORY"
]
self._read_permission_factory = import_string(imp) if imp else None
return self._read_permission_factory

@cached_property
def create_permission_factory(self):
"""Load default create permission factory."""
if self._create_permission_factory is None:
imp = self.app.config[
"RECORDS_REST_DEFAULT_CREATE_PERMISSION_FACTORY"
]
self._create_permission_factory = import_string(imp) \
if imp else None
return self._create_permission_factory

@cached_property
def update_permission_factory(self):
"""Load default update permission factory."""
if self._update_permission_factory is None:
imp = self.app.config[
"RECORDS_REST_DEFAULT_UPDATE_PERMISSION_FACTORY"
]
self._update_permission_factory = import_string(imp) \
if imp else None
return self._update_permission_factory

@cached_property
def delete_permission_factory(self):
"""Load default delete permission factory."""
if self._delete_permission_factory is None:
imp = self.app.config[
"RECORDS_REST_DEFAULT_DELETE_PERMISSION_FACTORY"
]
self._delete_permission_factory = import_string(imp) \
if imp else None
return self._delete_permission_factory


class InvenioRecordsREST(object):
"""Invenio-Records-REST extension."""

Expand All @@ -44,10 +101,23 @@ def init_app(self, app):
app.register_blueprint(
create_blueprint(app.config["RECORDS_REST_ENDPOINTS"])
)
app.extensions['invenio-records-rest'] = self
app.extensions['invenio-records-rest'] = _RecordRESTState(app)

def init_config(self, app):
"""Initialize configuration."""
app.config.setdefault(
"RECORDS_REST_DEFAULT_READ_PERMISSION_FACTORY",
"invenio_records.permissions:read_permission_factory")
app.config.setdefault(
"RECORDS_REST_DEFAULT_CREATE_PERMISSION_FACTORY",
"invenio_records.permissions:create_permission_factory")
app.config.setdefault(
"RECORDS_REST_DEFAULT_UPDATE_PERMISSION_FACTORY",
"invenio_records.permissions:update_permission_factory")
app.config.setdefault(
"RECORDS_REST_DEFAULT_DELETE_PERMISSION_FACTORY",
"invenio_records.permissions:delete_permission_factory")

# Set up API endpoints for records.
app.config.setdefault(
"RECORDS_REST_ENDPOINTS",
Expand Down
Loading

0 comments on commit 2a924a0

Please sign in to comment.