Skip to content

Commit

Permalink
Merge pull request #204 from datamel/authorisation-initial
Browse files Browse the repository at this point in the history
Configurable Authorization
  • Loading branch information
hjoliver committed Oct 20, 2021
2 parents ec66bf0 + d579666 commit 763e14c
Show file tree
Hide file tree
Showing 13 changed files with 1,265 additions and 68 deletions.
1 change: 1 addition & 0 deletions .mailmap
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ Bruno Kinoshita <kinow@users.noreply.github.com>
David Matthews <david.matthews@metoffice.gov.uk>
Hilary Oliver <hilary.j.oliver@gmail.com>
Sadie Bartholomew <30274190+sadielbartholomew@users.noreply.github.com>
Mel Hall <37735232+datamel@users.noreply.github.com> Melanie Hall
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Multi-user functionality implemented.

### Enhancements

[#204](https://github.com/cylc/cylc-uiserver/pull/**204**) -
Implementation of configurable multi user authorisation.

[#230](https://github.com/cylc/cylc-uiserver/pull/230) -
Convert the UI Server to a jupyter_server extension.

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ below.
- Stephen McVeigh
- Ronnie Dutta
- David Matthews
- Mel Hall
<!-- end-shortlog -->

(All contributors are identifiable with email addresses in the git version
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ Currently the UI Server accepts these configurations:
* `c.CylcUIServer.ui_build_dir`
* `c.CylcUIServer.ui_version`
* `c.CylcUIServer.scan_iterval`
* `c.CylcUIServer.site_authorization`
* `c.CylcUIServer.user_authorization`

See the `cylc.uiserver.app.UIServer` file for details.

Expand Down
161 changes: 151 additions & 10 deletions cylc/uiserver/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import getpass
import os
from pathlib import Path, PurePath
import sys
Expand All @@ -23,6 +24,7 @@
from tornado import ioloop
from tornado.web import RedirectHandler
from traitlets import (
Dict,
Float,
TraitError,
TraitType,
Expand All @@ -38,10 +40,13 @@
from cylc.flow.network.graphql import (
CylcGraphQLBackend, IgnoreFieldMiddleware
)

from cylc.uiserver import (
__file__ as uis_pkg,
)
from cylc.uiserver.authorise import (
Authorization,
AuthorizationMiddleware
)
from cylc.uiserver.data_store_mgr import DataStoreMgr
from cylc.uiserver.handlers import (
CylcStaticHandler,
Expand Down Expand Up @@ -107,6 +112,103 @@ class CylcUIServer(ExtensionApp):
]
)
)
# TODO: Add a link to the access group table mappings in cylc documentation
AUTH_DESCRIPTION = '''
Authorization can be granted at operation (mutation) level, i.e.
specifically grant user access to execute Cylc commands, e.g.
``play``, ``pause``, ``edit``, ``trigger`` etc. For your
convenience, these operations have been mapped to access groups
``READ``, ``CONTROL`` and ``ALL``.
To remove permissions, prepend the access group or operation with
``!``.
Permissions are additive but negated permissions take precedence
above additions e.g. ``CONTROL, !stop`` will permit all operations
in the ``CONTROL`` group except for ``stop``.
.. note::
Any authorization permissions granted to a user will be
applied to all workflows.
'''

site_authorization = Dict(
config=True,
help='''
Dictionary containing site limits and defaults for authorization.
This configuration should be placed only in the site set
configuration file and not the user configuration file (use
``c.CylcUIServer.user_authorization`` for user defined
authorization).
If this configuration is empty, site authorization defaults to no
configurable authorization and users will be unable to set any
authorization.
''' + AUTH_DESCRIPTION + '''
.. rubric:: Example Configuration:
.. code-block:: python
c.CylcUIServer.site_authorization = {
"*": { # For all ui-server owners,
"*": { # Any authenticated user
"default": "READ", # Will have default read-only access
},
"user1": { # user1
"default": ["!ALL"], # No privileges for all ui-server
# owners.
}, # No limit set, so all ui-server owners
}, # limit is also "!ALL" for user1
"server_owner_1": { # For specific UI Server owner,
"group:group_a": { # Any user who is a member of group_a
"default": "READ", # Will have default read-only access
"limit": ["ALL", "!play"], # server_owner_1 is able to
}, # grant All privileges, except play.
},
"group:grp_of_svr_owners": { # Group of owners of UI Servers
"group:group_b": {
"limit": [ # can grant groupB users up to READ and
"READ", # CONTROL privileges, without stop and
"CONTROL", # kill
"!stop",
"!kill", # No default, so default is no access
],
},
},
}
''')

user_authorization = Dict(
config=True,
help='''
Dictionary containing authorized users and permission levels for
authorization.
Use this setting to share control of your workflows
with other users.
Note that you are only permitted to give away permissions up to
your limit for each user, as defined in the site_authorization
configuration.
''' + AUTH_DESCRIPTION + '''
Example configuration, residing in
``~/.cylc/hub/jupyter_config.py``:
.. code-block:: python
c.CylcUIServer.user_authorization = {
"*": ["READ"], # any authenticated user has READ access
"group:group2": ["ALL"], # Any user in system group2 has access
# to all operations
"userA": ["ALL", "!stop"], # userA has ALL operations, not stop
}
'''
)

ui_path = PathType(
config=False,
Expand Down Expand Up @@ -194,6 +296,14 @@ def _check_ui_build_dir_exists(self, proposed):
return proposed['value']
raise TraitError(f'ui_build_dir does not exist: {proposed["value"]}')

@validate('site_authorization')
def _check_site_auth_dict_correct_format(self, proposed):
# TODO: More advanced auth dict validating
if isinstance(proposed['value'], dict):
return proposed['value']
raise TraitError(
f'Error in site authorization config: {proposed["value"]}')

@staticmethod
def _list_ui_versions(path: Path) -> List[str]:
"""Return a list of UI build versions detected in self.ui_path."""
Expand Down Expand Up @@ -248,12 +358,6 @@ def __init__(self, *args, **kwargs):
log=self.log,
workflows_mgr=self.workflows_mgr,
)
self.subscription_server = TornadoSubscriptionServer(
schema,
backend=CylcGraphQLBackend(),
middleware=[IgnoreFieldMiddleware],
)

ioloop.IOLoop.current().add_callback(
self.workflows_mgr.update
)
Expand Down Expand Up @@ -282,16 +386,27 @@ def initialize_settings(self):
).start()

def initialize_handlers(self):
self.authobj = self.set_auth()
self.set_sub_server()

self.handlers.extend([
('cylc/version', CylcVersionHandler),
(
'cylc/version',
CylcVersionHandler,
{'auth': self.authobj}
),
(
'cylc/graphql',
UIServerGraphQLHandler,
{
'schema': schema,
'resolvers': self.resolvers,
'backend': CylcGraphQLBackend(),
'middleware': [IgnoreFieldMiddleware],
'middleware': [
AuthorizationMiddleware,
IgnoreFieldMiddleware
],
'auth': self.authobj,
}
),
(
Expand All @@ -301,8 +416,12 @@ def initialize_handlers(self):
'schema': schema,
'resolvers': self.resolvers,
'backend': CylcGraphQLBackend(),
'middleware': [IgnoreFieldMiddleware],
'middleware': [
AuthorizationMiddleware,
IgnoreFieldMiddleware
],
'batch': True,
'auth': self.authobj,
}
),
(
Expand All @@ -316,6 +435,7 @@ def initialize_handlers(self):
(
'cylc/userprofile',
UserProfileHandler,
{'auth': self.authobj}
),
(
'cylc/(.*)?',
Expand All @@ -335,6 +455,27 @@ def initialize_handlers(self):
)
])

def set_sub_server(self):
self.subscription_server = TornadoSubscriptionServer(
schema,
backend=CylcGraphQLBackend(),
middleware=[
IgnoreFieldMiddleware,
AuthorizationMiddleware,
],
auth=self.authobj
)

def set_auth(self):
"""Create authorization object.
One for the lifetime of the UIServer.
"""
return Authorization(
getpass.getuser(),
self.config.CylcUIServer.user_authorization,
self.config.CylcUIServer.site_authorization
)

def initialize_templates(self):
"""Change the jinja templating environment."""

Expand Down
Loading

0 comments on commit 763e14c

Please sign in to comment.