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

[All] Adding overrides to user_is_authorized method #571

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions oauthenticator/bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,34 @@ def _userdata_url_default(self):
config=True, help="Automatically allow members of selected teams"
)

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""Checks if user is authorized with bitbucket OAuth.

Overrides:
- allowed_teams: Can override default self.allowed_teams

Returns: True if authorized
"""
access_token = auth_model["auth_state"]["token_response"]["access_token"]
token_type = auth_model["auth_state"]["token_response"]["token_type"]
username = auth_model["name"]

# Check if user is a member of any allowed teams.
# This check is performed here, as the check requires `access_token`.
if self.allowed_teams:
allowed_teams = overrides.pop("allowed_teams", self.allowed_teams)
if allowed_teams:
user_in_team = await self._check_membership_allowed_teams(
username, access_token, token_type
username, access_token, token_type, set(allowed_teams)
)
if not user_in_team:
self.log.warning(f"{username} not in team allowed list of users")
return False

return True

async def _check_membership_allowed_teams(self, username, access_token, token_type):
async def _check_membership_allowed_teams(
self, username, access_token, token_type, allowed_teams
):
headers = self.build_userdata_request_headers(access_token, token_type)
# We verify the team membership by calling teams endpoint.
next_page = url_concat(
Expand All @@ -74,7 +84,7 @@ async def _check_membership_allowed_teams(self, username, access_token, token_ty

user_teams = {entry["name"] for entry in resp_json["values"]}
# check if any of the organizations seen thus far are in the allowed list
if len(self.allowed_teams & user_teams) > 0:
if len(allowed_teams & user_teams) > 0:
return True
return False

Expand Down
2 changes: 1 addition & 1 deletion oauthenticator/cilogon.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def user_info_to_username(self, user_info):
)
raise web.HTTPError(500, "Failed to get username from CILogon")

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
username = auth_model["name"]
# Check if selected idp was marked as allowed
if self.allowed_idps:
Expand Down
34 changes: 23 additions & 11 deletions oauthenticator/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,33 +99,42 @@ def user_info_to_username(self, user_info):

return username

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""Checks if user is authorized with generic OAuth.

Overrides:
- allowed_groups: Can override default self.allowed_groups
- claim_groups_key: Can override default self.claim_groups_key

Returns: True if authorized
"""

claim_groups_key = overrides.pop("claim_groups_key", self.claim_groups_key)
allowed_groups = overrides.pop("allowed_groups", self.allowed_groups)
user_info = auth_model["auth_state"][self.user_auth_state_key]
if self.allowed_groups:
if allowed_groups:
self.log.info(
f"Validating if user claim groups match any of {self.allowed_groups}"
f"Validating if user claim groups match any of {allowed_groups}"
)

if callable(self.claim_groups_key):
groups = self.claim_groups_key(user_info)
if callable(claim_groups_key):
groups = claim_groups_key(user_info)
else:
try:
groups = reduce(
dict.get, self.claim_groups_key.split("."), user_info
)
groups = reduce(dict.get, claim_groups_key.split("."), user_info)
except TypeError:
# This happens if a nested key does not exist (reduce trying to call None.get)
self.log.error(
f"The key {self.claim_groups_key} does not exist in the user token, or it is set to null"
f"The key {claim_groups_key} does not exist in the user token, or it is set to null"
)
groups = None
if not groups:
self.log.error(
f"No claim groups found for user! Something wrong with the `claim_groups_key` {self.claim_groups_key}? {user_info}"
f"No claim groups found for user! Something wrong with the `claim_groups_key` {claim_groups_key}? {user_info}"
)
return False

if not self.check_user_in_groups(groups, self.allowed_groups):
if not self.check_user_in_groups(groups, allowed_groups):
return False

return True
Expand All @@ -145,6 +154,9 @@ async def update_auth_model(self, auth_model):
)
return auth_model

async def check_user_in_allowed_groups(self, user_model, allowed_groups=None):
return await super().check_user_in_allowed_groups(user_model, allowed_groups)


class LocalGenericOAuthenticator(LocalAuthenticator, GenericOAuthenticator):
"""A version that mixes in local system user creation"""
16 changes: 13 additions & 3 deletions oauthenticator/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,23 @@ def _github_client_secret_changed(self, name, old, new):
config=True,
)

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""Checks if user is authorized with github OAuth.

Overrides:
- allowed_organizations: Can override default self.allowed_organizations

Returns: True if authorized
"""
# Check if user is a member of any allowed organizations.
# This check is performed here, as it requires `access_token`.
access_token = auth_model["auth_state"]["token_response"]["access_token"]
token_type = auth_model["auth_state"]["token_response"]["token_type"]
if self.allowed_organizations:
for org in self.allowed_organizations:
allowed_organizations = overrides.pop(
"allowed_organizations", self.allowed_organizations
)
if allowed_organizations:
for org in allowed_organizations:
user_in_org = await self._check_membership_allowed_organizations(
org, auth_model["name"], access_token, token_type
)
Expand Down
37 changes: 28 additions & 9 deletions oauthenticator/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,16 @@ def _userdata_url_default(self):

gitlab_version = None

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""Checks if user is authorized with gitlab OAuth.

Overrides:
- allowed_project_ids: Can override default self.allowed_project_ids
- allowed_gitlab_groups: Can override default self.allowed_gitlab_groups

Returns: True if authorized
"""

access_token = auth_model["auth_state"]["token_response"]["access_token"]
user_id = auth_model["auth_state"][self.user_auth_state_key]["id"]

Expand All @@ -131,17 +140,23 @@ async def user_is_authorized(self, auth_model):
user_in_group = user_in_project = False
is_group_specified = is_project_id_specified = False

if self.allowed_gitlab_groups:
allowed_gitlab_groups = overrides.pop(
"allowed_gitlab_groups", self.allowed_gitlab_groups
)
if allowed_gitlab_groups:
is_group_specified = True
user_in_group = await self._check_membership_allowed_groups(
user_id, access_token
user_id, access_token, allowed_gitlab_groups
)

# We skip project_id check if user is in allowed group.
if self.allowed_project_ids and not user_in_group:
allowed_project_ids = overrides.pop(
"allowed_project_ids", self.allowed_project_ids
)
if allowed_project_ids and not user_in_group:
is_project_id_specified = True
user_in_project = await self._check_membership_allowed_project_ids(
user_id, access_token
user_id, access_token, allowed_project_ids
)

no_config_specified = not (is_group_specified or is_project_id_specified)
Expand Down Expand Up @@ -171,10 +186,12 @@ async def _get_gitlab_version(self, access_token):
version_ints = list(map(int, version_strings))
return version_ints

async def _check_membership_allowed_groups(self, user_id, access_token):
async def _check_membership_allowed_groups(
self, user_id, access_token, allowed_gitlab_groups
):
headers = _api_headers(access_token)
# Check if user is a member of any group in the allowed list
for group in map(url_escape, self.allowed_gitlab_groups):
for group in map(url_escape, allowed_gitlab_groups):
url = "%s/groups/%s/members/%s%d" % (
self.gitlab_api,
quote(group, safe=''),
Expand All @@ -192,10 +209,12 @@ async def _check_membership_allowed_groups(self, user_id, access_token):
return True # user _is_ in group
return False

async def _check_membership_allowed_project_ids(self, user_id, access_token):
async def _check_membership_allowed_project_ids(
self, user_id, access_token, allowed_project_ids
):
headers = _api_headers(access_token)
# Check if user has developer access to any project in the allowed list
for project in self.allowed_project_ids:
for project in allowed_project_ids:
url = "%s/projects/%s/members/%s%d" % (
self.gitlab_api,
project,
Expand Down
17 changes: 14 additions & 3 deletions oauthenticator/globus.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,25 @@ async def get_users_groups_ids(self, tokens):

return user_group_ids

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""Checks if user is authorized with globus OAuth.

Overrides:
- allowed_globus_groups: Can override default self.allowed_globus_groups

Returns: True if authorized
"""

tokens = self.get_globus_tokens(auth_model["auth_state"]["token_response"])

if self.allowed_globus_groups or self.admin_globus_groups:
allowed_globus_groups = overrides.pop(
"allowed_globus_groups", self.allowed_globus_groups
)
if allowed_globus_groups or self.admin_globus_groups:
# If any of these configurations are set, user must be in the allowed or admin Globus Group
user_group_ids = await self.get_users_groups_ids(tokens)
if not self.check_user_in_groups(
user_group_ids, self.allowed_globus_groups
user_group_ids, set(allowed_globus_groups)
):
if not self.check_user_in_groups(
user_group_ids, self.admin_globus_groups
Expand Down
19 changes: 18 additions & 1 deletion oauthenticator/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,14 @@ def _cast_hosted_domain(self, proposal):
def _username_claim_default(self):
return 'email'

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""Checks if user is authorized with google OAuth.

Overrides:
- allowed_google_groups: Can override default self.allowed_google_groups

Returns: True if authorized
"""
user_email = auth_model["auth_state"][self.user_auth_state_key]['email']
user_email_domain = user_email.split('@')[1]

Expand All @@ -142,6 +149,16 @@ async def user_is_authorized(self, auth_model):
raise HTTPError(
403, f"Google account domain @{user_email_domain} not authorized."
)

allowed_google_groups = overrides.pop("allowed_google_groups", None)
if allowed_google_groups:
# Only check if the override is passed so original authentication logic is not altered
return (
await self._add_google_groups_info(
auth_model, google_groups=allowed_google_groups
)
) is not None

return True

async def update_auth_model(self, auth_model, google_groups=None):
Expand Down
5 changes: 3 additions & 2 deletions oauthenticator/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,11 @@ async def update_auth_model(self, auth_model, **kwargs):
"""
return auth_model

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""
Checks if the user that is authenticating should be authorized or not and False otherwise.
Should be overridden with any relevant logic specific to each oauthenticator.
Should be overridden with any relevant logic specific to each oauthenticator. Overrides can be
passed to check user authorization against different conditions.

Returns True by default.

Expand Down
10 changes: 7 additions & 3 deletions oauthenticator/openshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,21 @@ async def update_auth_model(self, auth_model):

return auth_model

async def user_is_authorized(self, auth_model):
async def user_is_authorized(self, auth_model, **overrides):
"""
Use the group info stored on the OpenShift User object to determine if a user
is authorized to login.

Overrides:
- allowed_groups: Can override default self.allowed_groups
"""
user_groups = set(auth_model['auth_state']['openshift_user']['groups'])
username = auth_model['name']
allowed_groups = overrides.pop("allowed_groups", self.allowed_groups)

if self.allowed_groups or self.admin_groups:
if allowed_groups or self.admin_groups:
msg = f"username:{username} User not in any of the allowed/admin groups"
if not self.user_in_groups(user_groups, self.allowed_groups):
if not self.user_in_groups(user_groups, allowed_groups):
if not self.user_in_groups(user_groups, self.admin_groups):
self.log.warning(msg)
return False
Expand Down
17 changes: 17 additions & 0 deletions oauthenticator/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,20 @@ def client(io_loop, request):
c = AsyncHTTPClient()
assert isinstance(c, MockAsyncHTTPClient)
return c


@fixture
def get_auth_model():
async def mock_auth_model(authenticator, handler):
access_token_params = authenticator.build_access_tokens_request_params(
handler, None
)
token_info = await authenticator.get_token_info(handler, access_token_params)
user_info = await authenticator.token_to_user(token_info)
username = authenticator.user_info_to_username(user_info)
return {
"name": username,
"auth_state": authenticator.build_auth_state_dict(token_info, user_info),
}

return mock_auth_model
30 changes: 30 additions & 0 deletions oauthenticator/tests/test_bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,33 @@ def test_deprecated_config(caplog):

assert authenticator.allowed_teams == {"red"}
assert authenticator.allowed_users == {"blue"}


async def test_bitbucket_user_is_authorized_overrides(bitbucket_client, get_auth_model):
client = bitbucket_client
authenticator = BitbucketOAuthenticator()
authenticator.allowed_teams = ['blue', 'red']

teams = {
'red': ['grif', 'simmons', 'donut', 'sarge', 'lopez'],
'blue': ['tucker', 'caboose', 'burns', 'sheila', 'texas'],
}

def list_teams(request):
token = request.headers['Authorization'].split(None, 1)[1]
username = client.access_tokens[token]['username']
values = []
for team, members in teams.items():
if username in members:
values.append({'name': team})
return {'values': values}

client.hosts['api.bitbucket.org'].append(('/2.0/workspaces', list_teams))

handler = client.handler_for_user(user_model('caboose'))
auth_model = await get_auth_model(authenticator, handler)

is_authorized = await authenticator.user_is_authorized(
auth_model, allowed_teams=['red'] # caboose is not red
)
assert not is_authorized
Loading