Skip to content

Ms/user access management #138

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

Merged
merged 31 commits into from
Apr 22, 2021
Merged

Ms/user access management #138

merged 31 commits into from
Apr 22, 2021

Conversation

msokoloff1
Copy link
Contributor

@msokoloff1 msokoloff1 commented Apr 12, 2021

PLAT-1224

The purpose of this pr is to allow customers to manage organization invites and user permissions from the SDK. The following functionality was added:

  • create / update / revoke invite
  • query for active invites
  • query for remaining allowed invites to an organization
  • query for remaining available seats in an organization
  • set and update organization roles
  • assign users to projects
    • set / update / revoke project role
  • delete users from org
  • Example notebook

@msokoloff1 msokoloff1 requested a review from nmaswood April 12, 2021 17:13
Copy link

@nmaswood nmaswood left a comment

Choose a reason for hiding this comment

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

Going to take a second look tommorow when I am fresh!

I might ask Greg to also take a look, because I think experimental mode is a big change and we should be cautious in how we roll it out in terms of the API

also I think Grant/Collin might be helpful in providing some context of how the invite api ought to be used.

@msokoloff1
Copy link
Contributor Author

msokoloff1 commented Apr 14, 2021

After speaking with Grant, we decided to remove the ability to query and revoke active invites since this might behave unexpectedly. He also said we need to remove the old user_limit query in favor of the new invite_limit query. All of these requested changes have been made.

@msokoloff1 msokoloff1 requested review from gszpak and nmaswood April 14, 2021 22:59
Copy link

@nmaswood nmaswood left a comment

Choose a reason for hiding this comment

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

will continue after stand up

Comment on lines +50 to +51
raise KeyError(
f"Expected field values for {relationship.name}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Still wrapping my head around all this, but are we sure we want to raise an error here? What if we want to do lazy loading of cached relationships?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we need that in the future we can make the change. I think making this more strict is better.

Comment on lines 19 to 32
def __new__(cls, client):
if cls._instance is None:
cls._instance = super(Roles, cls).__new__(cls)
query_str = """query GetAvailableUserRolesPyApi { roles { id name } }"""
res = client.execute(query_str)
valid_roles = set()
for result in res['roles']:
_name = result['name'].upper().replace(' ', '_')
result['name'] = _name
setattr(cls._instance, _name, Role(client, result))
valid_roles.add(_name)
cls._instance.valid_roles = valid_roles
cls._instance.index = 0
return cls._instance
Copy link
Contributor

Choose a reason for hiding this comment

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

In general I'm not a huge fan of overriding __new__. Imho the code gets overly complicated. If we want to make them singletons, could we instead declare the roles as lazy initalized module vars - sth like here https://github.com/Labelbox/python-monorepo/blob/6c6bfebb68fc6f876c3abf29c9ce727a6c5744dc/services/prediction_import/prediction_import/db/client.py#L357

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a pretty standard singleton pattern. The code snippet you provided is much simpler but also because it is doing much less. This is going to basically look the same except swap new for init and add a new module level variable which feels like a lateral move rather than an improvement.

Choose a reason for hiding this comment

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

I lean more towards, structs/class should be dumb containers of data than locations of business logic.

In that sense I think you could seperate fetching GetAvailableUserRolesPyApi logic from the class __new__ and move it out into a static method or function, imho it doesn make reading the class a little trickier.

class RoleAccessor(self):
    def __init__(self):
         self.roles = get_The_roles(client)
    

or some other means to break up the logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed it to this for now.

def _get_roles(client):
query_str = """query GetAvailableUserRolesPyApi { roles { id name } }"""
if not hasattr(_get_roles, 'roles'):
_get_roles.roles = res = client.execute(query_str)
return _get_roles.roles
class Roles:
"""
Object that manages org and user roles
>>> roles = client.get_roles()
>>> roles.valid_roles # lists all valid roles
>>> roles['ADMIN'] # returns the admin Role
"""
def __init__(self, client):
res = _get_roles(client)
valid_roles = set()
for result in res['roles']:
_name = result['name'].upper().replace(' ', '_')
result['name'] = _name
setattr(self, _name, Role(client, result))
valid_roles.add(_name)
self.valid_roles = valid_roles
def __repr__(self):
return str({k: getattr(self, k) for k in self.valid_roles})
def __getitem__(self, name):
name = name.replace(' ', '_').upper()
if name not in self.valid_roles:
raise ValueError(
f"No role named {name} exists. Valid names are one of {self.valid_roles}"
)
return getattr(self, name)
def __iter__(self):
self.key_iter = iter(self.valid_roles)
return self
def __next__(self):
key = next(self.key_iter)
return getattr(self, key)
. I'm not totally clear what Nasr is suggesting so we are going to sync tomorrow.

Comment on lines 19 to 32
def __new__(cls, client):
if cls._instance is None:
cls._instance = super(Roles, cls).__new__(cls)
query_str = """query GetAvailableUserRolesPyApi { roles { id name } }"""
res = client.execute(query_str)
valid_roles = set()
for result in res['roles']:
_name = result['name'].upper().replace(' ', '_')
result['name'] = _name
setattr(cls._instance, _name, Role(client, result))
valid_roles.add(_name)
cls._instance.valid_roles = valid_roles
cls._instance.index = 0
return cls._instance

Choose a reason for hiding this comment

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

I lean more towards, structs/class should be dumb containers of data than locations of business logic.

In that sense I think you could seperate fetching GetAvailableUserRolesPyApi logic from the class __new__ and move it out into a static method or function, imho it doesn make reading the class a little trickier.

class RoleAccessor(self):
    def __init__(self):
         self.roles = get_The_roles(client)
    

or some other means to break up the logic


@beta_endpoint
def cancel_invite(client, invite_id):
"""

Choose a reason for hiding this comment

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

You could maybe turn this comments into application logic through introducing a dummy decorator that annotates like test method only or something like that.

or like throw if pytest is not running:

https://stackoverflow.com/questions/25188119/test-if-code-is-executed-from-within-a-py-test-session

Copy link
Contributor Author

@msokoloff1 msokoloff1 Apr 20, 2021

Choose a reason for hiding this comment

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

The main point of the comment is to make it so that users don't copy this into their own application. I don't think there is a risk of someone importing from the conftest outside of the tests.


@pytest.fixture
def organization(client):
# Must have at least one seat open in your org to run these tests

Choose a reason for hiding this comment

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

is this gonna make int. tests flakier?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it clears invites before and after (only @labelbox.com invites so users don't accidentally mess up anything). Our test account is only used for testing so there shouldn't be any problem here. This will only matter if you are testing locally and don't have any open seats.

@@ -208,9 +213,9 @@ def check_errors(keywords, *path):
if internal_server_error is not None:
message = internal_server_error.get("message")

if message.startswith("Syntax Error"):
if message.startswith("Syntax Error") or message.startswith(
"Invite(s) cannot be sent"):

Choose a reason for hiding this comment

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

This feels really hard-coded in a way. I would expect the basic execute function to maybe catch top level errors, but not super specific errors from particular queries.

also InvalidQueryError to me implies that the query was malformed as opposed to something about their user limits

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is because the backend just throws a 500 and there is no way to handle this without parsing the message (see the comment just above this.

Server side validation is one reason we raise this exception:

""" Indicates a malconstructed or unsupported query (either by GraphQL in
general or by Labelbox specifically). This can be the result of either client
or server side query validation. """

I don't like this pattern either but I don't think fixing this is within the scope of this delivery.

Choose a reason for hiding this comment

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

aite, one small nit is that startswith takes a tuple I believe
message.startsiwh(("message1, "message"))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

from typing import Any, Callable, Dict, List, Optional, Tuple, Type

from typing import TYPE_CHECKING
if TYPE_CHECKING:

Choose a reason for hiding this comment

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

Is adding TYPE_CHECKING check standard?

Do we add it for compatibility or performance reasons?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is to avoid circular imports. There is not really a great solution here.

return _ROLES


def format_role(name: str):

Choose a reason for hiding this comment

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

jw, do we even need to format_roles now that we are not emulating enum behaavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Idk I don't like spaces and inconsistent casing in key names. It makes it harder to use.

@msokoloff1 msokoloff1 requested a review from nmaswood April 21, 2021 17:10
Copy link

@nmaswood nmaswood left a comment

Choose a reason for hiding this comment

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

@gszpak it all looks good to me, you have any follow up thoughts?

@msokoloff1 msokoloff1 merged commit 86221cb into develop Apr 22, 2021
@msokoloff1 msokoloff1 deleted the ms/user-access-management branch April 22, 2021 11:27
msokoloff1 added a commit that referenced this pull request Sep 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants