Skip to content

Commit

Permalink
Merge fe8a003 into 8ffb5e4
Browse files Browse the repository at this point in the history
  • Loading branch information
MarekSuchanek committed Jul 22, 2018
2 parents 8ffb5e4 + fe8a003 commit a7dafe4
Show file tree
Hide file tree
Showing 16 changed files with 176 additions and 39 deletions.
1 change: 1 addition & 0 deletions docs/usage/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Usage
start
commands
web
privileges
3 changes: 3 additions & 0 deletions docs/usage/privileges.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Privileges and Roles
====================

28 changes: 28 additions & 0 deletions migrations/versions/13a18cc60ee5_privileges_for_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Privileges for role
Revision ID: 13a18cc60ee5
Revises: 891cf9712a20
Create Date: 2018-07-21 09:54:25.502179
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '13a18cc60ee5'
down_revision = '891cf9712a20'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('Role', sa.Column('privileges', sa.Text(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('Role', 'privileges')
# ### end Alembic commands ###
3 changes: 2 additions & 1 deletion repocribro/commands/assign_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def _assign_role(login, role_name):
role = db.session.query(Role).filter_by(name=role_name).first()
if role is None:
print('Role {} not in DB... adding'.format(role_name))
role = Role(role_name, '')
print('WARNING - created role has all privileges by default!')
role = Role(role_name, '*', '')
db.session.add(role)
user.user_account.roles.append(role)
db.session.commit()
Expand Down
32 changes: 18 additions & 14 deletions repocribro/controllers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


@admin.route('')
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def index():
"""Administration zone dashboard (GET handler)"""
ext_master = flask.current_app.container.get('ext_master')
Expand All @@ -26,7 +26,7 @@ def index():


@admin.route('/account/<login>')
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def account_detail(login):
"""Account administration (GET handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -38,7 +38,7 @@ def account_detail(login):


@admin.route('/account/<login>/ban', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def account_ban(login):
"""Ban (make inactive) account (POST handler)"""
db = flask.current_app.container.get('db')
Expand Down Expand Up @@ -66,7 +66,7 @@ def account_ban(login):


@admin.route('/account/<login>/delete', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def account_delete(login):
"""Delete account (POST handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -84,7 +84,7 @@ def account_delete(login):


@admin.route('/repository/<login>/<reponame>')
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def repo_detail(login, reponame):
"""Repository administration (GET handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -101,7 +101,7 @@ def repo_detail(login, reponame):


@admin.route('/repository/<login>/<reponame>/visibility', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def repo_visibility(login, reponame):
"""Change repository visibility (POST handler)"""
db = flask.current_app.container.get('db')
Expand Down Expand Up @@ -134,7 +134,7 @@ def repo_visibility(login, reponame):


@admin.route('/repository/<login>/<reponame>/delete', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def repo_delete(login, reponame):
"""Delete repository (POST handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -152,7 +152,7 @@ def repo_delete(login, reponame):


@admin.route('/role/<name>')
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def role_detail(name):
"""Role administration (GET handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -164,7 +164,7 @@ def role_detail(name):


@admin.route('/role/<name>/edit', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def role_edit(name):
"""Edit role (POST handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -173,12 +173,15 @@ def role_edit(name):
if role is None:
flask.abort(404)
name = flask.request.form.get('name', '')
priv = flask.request.form.get('privileges', '')
desc = flask.request.form.get('description', None)
if name == '':
flask.flash('Couldn\'t make that role...', 'warning')
return flask.redirect(flask.url_for('admin.index', tab='roles'))
# TODO: validate priv (chars, separators, etc.)
try:
role.name = name
role.privileges = priv
role.description = desc
db.session.commit()
except sqlalchemy.exc.IntegrityError as e:
Expand All @@ -191,7 +194,7 @@ def role_edit(name):


@admin.route('/role/<name>/delete', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def role_delete(name):
"""Delete role (POST handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -207,18 +210,19 @@ def role_delete(name):


@admin.route('/roles/create', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def role_create():
"""Create new role (POST handler)"""
db = flask.current_app.container.get('db')

name = flask.request.form.get('name', '')
priv = flask.request.form.get('privileges', '')
desc = flask.request.form.get('description', None)
if name == '':
flask.flash('Couldn\'t make that role...', 'warning')
return flask.redirect(flask.url_for('admin.index', tab='roles'))
try:
role = Role(name, desc)
role = Role(name, priv, desc)
db.session.add(role)
db.session.commit()
except sqlalchemy.exc.IntegrityError as e:
Expand All @@ -230,7 +234,7 @@ def role_create():


@admin.route('/role/<name>/add', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def role_assignment_add(name):
"""Assign role to user (POST handler)"""
db = flask.current_app.container.get('db')
Expand All @@ -253,7 +257,7 @@ def role_assignment_add(name):


@admin.route('/role/<name>/remove', methods=['POST'])
@permissions.admin_role.require(404)
@permissions.roles.admin.require(404)
def role_assignment_remove(name):
"""Remove assignment of role to user (POST handler)"""
db = flask.current_app.container.get('db')
Expand Down
2 changes: 2 additions & 0 deletions repocribro/controllers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import flask_login

from ..models import User, Organization, Repository
from ..security import permissions

#: Core controller blueprint
core = flask.Blueprint('core', __name__, url_prefix='')
Expand All @@ -21,6 +22,7 @@ def index():
@core.route('/search/')
@core.route('/search')
@core.route('/search/<query>')
# @permissions.actions.search.require(403)
def search(query=''):
"""Search page (GET handler)
Expand Down
12 changes: 11 additions & 1 deletion repocribro/ext_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .extending import Extension
from .extending.helpers import ViewTab, Badge
from .models import Push, Release, Repository
from .models import Push, Release, Repository, Role
from .github import GitHubAPI


Expand Down Expand Up @@ -164,6 +164,16 @@ def provide_filters():
from .filters import all_filters
return all_filters

@staticmethod
def provide_roles():
return {
'admin': Role('admin', '*', 'Service administrators'),
}

@staticmethod
def provide_actions():
return ['login', 'search']

@staticmethod
def get_gh_webhook_processors():
"""Get all GitHub webhooks processory"""
Expand Down
26 changes: 26 additions & 0 deletions repocribro/extending/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ def provide_filters():
"""
return {}

@staticmethod
def provide_roles():
"""Extension can define roles for user accounts
:return: Dictionary with name + Role entity
:rtype: dict of str: ``repocribro.models.Role``
"""
return {}

@staticmethod
def provide_actions():
"""Extension can define actions for privileges
:return: List of action names
:rtype: list of str
"""
return []

def init_models(self):
"""Hook operation for initiating the models and registering them
within db
Expand All @@ -126,6 +144,14 @@ def init_filters(self):
"""
self.register_filters_from_dict(self.provide_filters())

def init_security(self):
"""Hook operation to setup privileges (roles and actions)"""
permissions = config = self.app.container.get('permissions')
for role_name in self.provide_roles().keys():
permissions.register_role(role_name)
for action_name in self.provide_actions():
permissions.register_action(action_name)

def introduce(self):
"""Hook operation for getting short introduction of extension (mostly
for debug/log purpose)
Expand Down
22 changes: 21 additions & 1 deletion repocribro/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ def __hash__(self):
"""
return hash(self.name)

def permits(self, privilege):
privileges = self.privileges.split(':')
if privilege in privileges:
return True
if '*' in privileges: # TODO: better wildcards
return True
return False


class Anonymous(flask_login.AnonymousUserMixin):
"""Anonymous (not logged) user representation"""
Expand Down Expand Up @@ -246,6 +254,15 @@ def __repr__(self):
"""
return '<UserAccount (#{})>'.format(self.id)

def privileges(self, all_privileges=frozenset()):
privileges = set()
for priv in all_privileges:
for role in self.roles:
if role.permits(priv):
privileges.add(priv)
break
return privileges


class Role(db.Model, RoleMixin):
"""User account role in the application"""
Expand All @@ -257,13 +274,16 @@ class Role(db.Model, RoleMixin):
name = sqlalchemy.Column(sqlalchemy.String(80), unique=True)
#: Description (purpose, notes, ...) of the role
description = sqlalchemy.Column(sqlalchemy.UnicodeText)
#: Serialized list of privileges
privileges = sqlalchemy.Column(sqlalchemy.Text)
#: User accounts assigned to the role
user_accounts = sqlalchemy.orm.relationship(
'UserAccount', back_populates='roles', secondary=roles_users
)

def __init__(self, name, description):
def __init__(self, name, privileges, description):
self.name = name
self.privileges = privileges
self.description = description

def __repr__(self):
Expand Down
4 changes: 4 additions & 0 deletions repocribro/repocribro.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,13 @@ def create_app(cfg_files=['DEFAULT']):
ext_names = ext_master.call('introduce', 'unknown')
print('Loaded extensions: {}'.format(', '.join(ext_names)))

from .security import permissions
app.container.set_singleton('permissions', permissions)

ext_master.call('init_first')
ext_master.call('init_models')
ext_master.call('init_business')
ext_master.call('init_security')
ext_master.call('init_filters')
ext_master.call('init_blueprints')
ext_master.call('init_container')
Expand Down

0 comments on commit a7dafe4

Please sign in to comment.