Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable Authorization #204

Merged
merged 24 commits into from
Oct 20, 2021
Merged

Conversation

datamel
Copy link
Contributor

@datamel datamel commented Apr 13, 2021

These changes close #10
Addresses #171

Requirements check-list

Authorization

Implementation includes...

Configuration

Site config for authorization

Sets site policy for

  • limits (access server owners are permitted to give away)
  • defaults (the access users have by default (if not included in user config))

Example site config...

        "*": {                                                        #  All uiserver owners
            "*": {                                                    #  Any authenticated user
                "default": ["READ"],                         #  Has default READ access
                "limit": ["ALL"],                                #  Can be granted all permissions           
            },
            "user1": {                                             #  user1 (for all uiserver owners)
                "limit": [                                           #  can not be granted any access
                    "!ALL"
                ],
            },
          "group:group1": {"default": "ALL" }     #  any user in group1 has ALL access to all uiserver owners.
        },
        "group:group2": {                                                           #  Group of ui server owners (in system group group1)
            "*": {                                                                            #  Any authenticated user   
                "default": ["READ","CONTROL"],                             #  Has by default, READ and CONTROL access.                      
            },
        },
    }

More examples in the proposal
Reminder for reviewing, this is set to /etc/cylc/hub/config.py but can be overridden using the env variable CYLC_SITE_CONF_PATH

User config

Sets user applied permissions, supports both usernames and system groups.
This resides here: ~/.cylc/hub/config.py.

Example user config...

c.UIServer.user_authorization = {
    "user1": ["READ", "pause", "trigger", "message"], 
    "group:group1": ["ALL"],
    "user2": ["READ", "CONTROL", "!trigger", "!edit"],
    "user3": ["READ", "stop", "play"],
    "user4": ["!ALL"]                                    
}

Config Details.

  • READ, CONTROL and ALL access groups implemented. See access group table mappings.
  • Mutation level granularity has been implemented
  • See proposal for implementation details of missing default/limits
  • Negations take priority and have been implemented.

Middleware

Authorization has been implemented at GraphQL middleware level.
As of current implementation...We do not check auth for return variables, in order for this not to compromise security need confirmation that GraphQL mutations won't be nested (e.g. Can't perform a kill within a play) [Confirmed, by @dwsutherland below].

User Permissions

In line with the diagram, users and permissions are cached, reducing processing and system calls.

Note to reviewers

  • when it comes to manually test this PR, it may be best to pair review with someone, using their groups (typing groups in a terminal will reveal these). You can then configure the config to test this fully.
  • testing is thus far incomplete - I am going to look into multi user testing on GH Actions.
  • there is still tidying left for me to do (docstrings, adding tests etc) but functionality is there and, I believe, working as it should be.
  • Follow up PRs are needed, including multiple UI PRs and specific use case implementation,... I shall open these up as issues.

@codecov-commenter
Copy link

codecov-commenter commented Jul 16, 2021

Codecov Report

Merging #204 (d579666) into master (b4f6711) will increase coverage by 2.72%.
The diff coverage is 86.02%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #204      +/-   ##
==========================================
+ Coverage   75.00%   77.72%   +2.72%     
==========================================
  Files          11       12       +1     
  Lines         748     1001     +253     
  Branches      127      173      +46     
==========================================
+ Hits          561      778     +217     
- Misses        161      192      +31     
- Partials       26       31       +5     
Impacted Files Coverage Δ
cylc/uiserver/resolvers.py 33.33% <0.00%> (+0.35%) ⬆️
cylc/uiserver/authorise.py 85.46% <85.46%> (ø)
cylc/uiserver/handlers.py 86.95% <90.00%> (-1.05%) ⬇️
cylc/uiserver/app.py 87.28% <100.00%> (-0.84%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b4f6711...d579666. Read the comment docs.

Copy link
Member

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a super quick review done reading in the GitHub UI @datamel

CONTRIBUTING.md Show resolved Hide resolved
cylc/uiserver/config_defaults.py Outdated Show resolved Hide resolved
cylc/uiserver/main.py Outdated Show resolved Hide resolved
cylc/uiserver/main.py Outdated Show resolved Hide resolved
cylc/uiserver/main.py Outdated Show resolved Hide resolved
cylc/uiserver/resolvers.py Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
@kinow
Copy link
Member

kinow commented Jul 26, 2021

@datamel I've updated the issue description removing the boxes that were not necessary, and linked the issue #10? Not sure if that's the right issue, but since you were assigned I assume it was.

Copy link
Member

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@datamel checked out the branch today and remembered I hadn't used the IDE static analyzer yet. Found a few typos and unused imports. Marked them in this last review 👍

cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/tests/test_authorise.py Outdated Show resolved Hide resolved
@dwsutherland
Copy link
Member

As of current implementation...We do not check auth for return variables, in order for this not to compromise security need confirmation that GraphQL mutations won't be nested (e.g. Can't perform a kill within a play)

No chance of nested here, we've designed the mutations as flat:

class Pause(Mutation):
    class Meta:
        description = sstrip('''
            Pause a workflow.
        ''')
        resolver = partial(mutator, command='pause')

    class Arguments:
        workflows = List(WorkflowID, required=True)

    result = GenericScalar()

So there's nowhere to nest..
In addition, all field resolving passes through the same middleware, and should share the same operation.

cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
Copy link
Member

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@datamel I finished reading the proposal, and reading the code today 🎉

Still need to play more with the UIS and authorization schema.

But one thing I couldn't find in the docs, I wonder if you could help me… I saw that we have a site and a user config. So I imagined myself as a system admin looking after a machine with several (100s?) users.

All users having ability to start their own hub (something like module load cylc or similar). Now, maybe I don't want users to start Cylc because of GraphQL queries consuming resources, or some other concern.

Maybe I could limit the module loading, but users could still use Conda to install perhaps. That got me thinking in trying the site configuration to lock my own account. i.e. set a default to !ALL or something like that.

I couldn't get that to work, and I couldn't find anything in the proposal. Is the authorization supposed to work only controlling authorization to other users?

Thanks!

@dwsutherland dwsutherland self-requested a review July 27, 2021 00:42
Copy link
Member

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a lazy-way to review it 😄

Here's my ~/.cylc/hub/config.py:

c.UIServer.ui_build_dir = '~/Development/python/workspace/cylc-ui/dist'  # path to build
c.JupyterHub.authenticator_class = 'jupyterhub.auth.DummyAuthenticator'

import pwd
def mocked_fn(any_value):
  class Obj(object):
    def __init__(self):
      self.pw_uid = 1000
      self.pw_gid = 1000
      self.pw_dir = '/home/kinow/'
      self.pw_shell = '/bin/bash'
      self.pw_gecos = any_value
      self.pw_name = any_value
      self.pw_passwd = 'x'
   
    def __repr__(self):
      return str(self.__dict__)

  return Obj()
    
pwd.getpwnam = mocked_fn

c.Authenticator.delete_invalid_users = True
c.Authenticator.admin_users = {
  'kinow'
}
c.JupyterHub.api_tokens = {
  'knockknock': 'kinow'
}
c.Application.log_level = 'DEBUG'

c.UIServer.user_authorization = {
    "cylc": ["READ", "pause", "trigger", "message"], 
    "group:group1": ["ALL"],
    "user2": ["READ", "CONTROL", "!trigger", "!edit"],
    "user3": ["READ", "stop", "play"],
    "user4": ["!ALL"]                                    
}

JupyterHub uses the pwd module to fetch the current user (we use getpass, but I think in the end both should access the same system resources and both hopefully work with LDAP/etc). Monkey-patching it and using the DummyAuthenticator with no password, that means I can log in as anything.

All the workflows are loaded from the same folder. One strange behaviour is that I can see all workflows, but upon start, the workflow disappears from the list of other users, and only my own user shows the workflow (we are probably filtering it somewhere in UIS), but that doesn't interfere with my testing here I think.

So my kinow user has:

image

And the cylc user (does not exist in my OS) has:

image

Trying to access my kinow|five from this cylc user, but no luck.

image

image

So at least I know the new authorization code is working. Now trying to get the hang of the schema and enable/disable permissions.

@kinow

This comment has been minimized.

@oliver-sanders oliver-sanders added this to the cylc-uiserver 0.6.0 milestone Jul 27, 2021
@datamel
Copy link
Contributor Author

datamel commented Aug 2, 2021

Hi @kinow,
I have a couple of suggestions to try to get this working....

  1. Change the c.UIServer.site_authorisation to c.UIServer.site_authorization

With the example above, the c.UIServer.site_authorisation would have no impact and c.UIServer.site_authorization would be empty (from the uiservers point of view) which means that you would get the 403.

Once this is fixed (apologies for the mix-up with spelling, I will post on element now I am back to get confirmation of spellings), the config you have created will still not work to allow ALL operations. This is because there is no set limit in the site config, which means that the limit falls back to the default, i.e. READ operations.

  1. Add limit to the site_authorization

In order for user cylc to be allowed ALL operations (as intended by the user config) the site config limit must match (or exceed) the user config. Limits tell the uiserver that you (user kinow) is permitted to give away operations (in this case ALL) to user cylc.

  • site config
# File: /etc/hub/config.py
c.UIServer.site_authorisation = {                        <- change this to c.UIServer.site_authorization
  "*": {
    "*": {
      "default": ["READ"]
    },
    "cylc": {  
      "default": ["READ"],
        "limit": "ALL"                                <- try adding this (need the comma on the line above too)
    }
  }
}

  • user config
# File: /home/kinow/.cylc/hub/config.py
c.UIServer.user_authorization = {
    "cylc": ["ALL"]                                 
}

Fingers crossed this will be good to go now.

If these suggestions fail, I can give you a call on element this evening/your morning to see if I can get you up and running with this.

Copy link
Member

@hjoliver hjoliver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works great 🎉 No major issues found. A few minor code suggestions, and some questions (next) for possible follow-up.

cylc/uiserver/handlers.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
cylc/uiserver/authorise.py Outdated Show resolved Hide resolved
)
# If not explicit permissions for access user in owner conf then revert
# to site defaults
if len(user_conf_permitted_ops) == 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd better support implicit as well as explicit negation, because it's likely to be assumed by users, and we do for partial cases, right?:

'x' : ['op1', 'op2']   # allow only 'op1' and 'op2'

'y': ['op1']  # allow only 'op1'

# therefore:
'z': [ ]  # allow no access

@hjoliver
Copy link
Member

hjoliver commented Oct 19, 2021

A couple of more general issues, possibly for follow-up:

  • does it make sense to be able to grant control access without read access?

  • I wasted a bunch of time due to typos in my user config file (user error 😬 ) which results in user config getting silently ignored. And which, depending on site config, can result in granting more than the intended level of access to others. Is there anything we can do about that, or is it a feature of Jupyter config handling?

  • If so .. perhaps we could have the UI display authorization settings to the owner (and maybe to the accessing user, or is that a security no-no?) ... so it can be seen whether or not the auth config was used

@hjoliver hjoliver closed this Oct 19, 2021
@hjoliver hjoliver reopened this Oct 19, 2021
@hjoliver
Copy link
Member

(Yikes, lost control of the keyboard and closed the issue!)

@hjoliver
Copy link
Member

(I should probably spend more time looking at this yet, but it's taken a while for me to get up to ~speed again on the UIS, figure out how to diagnose what it's doing etc.).

@oliver-sanders
Copy link
Member

does it make sense to be able to grant control access without read access?

Not really.

I wasted a bunch of time due to typos in my user config file (user error 😬 ) which results in user config getting silently ignored.

Jupyter Server loads the config so there isn't much we can do, however, if you look at the UIS log you should see a warning for each mistyped config (providing you got the CylcUIServer namespace right).

If so .. perhaps we could have the UI display authorization settings to the owner (and maybe to the accessing user, or is that a security no-no?)

The UIS logs the permissions of the authenticated user when they first connect. There's kinda an interface for viewing permissions vie the user-profile. On Mel's UI branch this is used to disable mutation buttons.

@datamel
Copy link
Contributor Author

datamel commented Oct 19, 2021

Thanks so much for the review @hjoliver, I will implement the changes today.

  • does it make sense to be able to grant control access without read access?

The reason we are keeping them separate is due to negations. If you did !CONTROL, that should not remove read access and so, by logic CONTROL should not add read access. I think having the addition doing something different to the negation would be unexpected for users. I appreciate that we could go either way on this one though, maybe worth a discussion at VC this week?

  • I wasted a bunch of time due to typos in my user config file (user error 😬 ) which results in user config getting silently ignored. And which, depending on site config, can result in granting more than the intended level of access to others. Is there anything we can do about that, or is it a feature of Jupyter config handling?

Yes, typos could potentially be a bit frustrating for users. To add to what Oliver has said, I have also opened an issue to add more detailed validating of the config. Ideally this would flag typos in operation name.

datamel and others added 3 commits October 19, 2021 10:38
Co-authored-by: Hilary James Oliver <hilary.j.oliver@gmail.com>
Co-authored-by: Hilary James Oliver <hilary.j.oliver@gmail.com>
Co-authored-by: Hilary James Oliver <hilary.j.oliver@gmail.com>
@hjoliver
Copy link
Member

@datamel note I commited a small syntax fix to your branch to make the tests pass (I had ommited a comma in a code change suggestion of mine that you commited, I think).

@datamel
Copy link
Contributor Author

datamel commented Oct 19, 2021

@datamel note I commited a small syntax fix to your branch to make the tests pass (I had ommited a comma in a code change suggestion of mine that you commited, I think).

Grand thanks! I'm just about to push a change. I spotted a problem with the permissions being passed between uiserver and ui, the format of some of the mutations were not matching up, hard to spot, since I hadn't really tested things like Set Verbosity. There is a bit of a format change between cylc-flow, cylc-uisever and ui... they use mix of kebab case, snake case and camel case which makes it easy for mismatch.

Copy link
Member

@hjoliver hjoliver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, this can go in now 🎉 I'll post another issue for minor follow-up questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Define how the authorization in the UI server will work with other components
7 participants