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

Login Refactoring and Improvement #119

Merged
merged 18 commits into from
Oct 19, 2018
Merged
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
2 changes: 1 addition & 1 deletion magpie/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
General meta information on the magpie package.
"""

__version__ = '0.7.1'
__version__ = '0.7.2'
__author__ = "Francois-Xavier Derue, Francis Charette-Migneault"
__maintainer__ = "Francis Charette-Migneault"
__email__ = 'francis.charette-migneault@crim.ca'
Expand Down
27 changes: 25 additions & 2 deletions magpie/adapter/magpieowssecurity.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import tempfile
from magpie.definitions.twitcher_definitions import *
from magpie.definitions.pyramid_definitions import *
from magpie.services import service_factory
from magpie.models import Service
from magpie.api.api_except import evaluate_call, verify_param

import requests
import logging
LOGGER = logging.getLogger("TWITCHER")

Expand All @@ -27,9 +26,33 @@ def check_request(self, request):
permission_requested = service_specific.permission_requested()

if permission_requested:
self.update_request_cookies(request)
authn_policy = request.registry.queryUtility(IAuthenticationPolicy)
authz_policy = request.registry.queryUtility(IAuthorizationPolicy)
principals = authn_policy.effective_principals(request)
has_permission = authz_policy.permits(service_specific, principals, permission_requested)
if not has_permission:
raise OWSAccessForbidden("Not authorized to access this resource.")

@staticmethod
def update_request_cookies(request):
"""
Ensure login of the user and update the request cookies if twitcher is in a special configuration.
Only update if Magpie `auth_tkt` is missing and can be retrieved from `access_token` in `Authorization` header.
Counter-validate the login procedure by calling Magpie's `/session` which should indicated a logged user.
"""
not_default = get_twitcher_configuration(request.registry.settings) != TWITCHER_CONFIGURATION_DEFAULT
if not_default and 'Authorization' in request.headers and 'auth_tkt' not in request.cookies:
ssl_verify = asbool(request.registry.settings.get('twitcher.ows_proxy_ssl_verify', True))
magpie_url = request.registry.settings.get('magpie.url')
magpie_prov = request.params.get('provider', 'WSO2')
magpie_auth = '{host}/providers/{provider}/signin'.format(host=magpie_url, provider=magpie_prov)
headers = request.headers
headers['Homepage-Route'] = '/session'
headers['Accept'] = 'application/json'
auth_resp = requests.get(magpie_auth, headers=headers, verify=ssl_verify)
if auth_resp.status_code != HTTPOk.code:
raise auth_resp.raise_for_status()
if not auth_resp.json().get('authenticated') or 'auth_tkt' not in auth_resp.request._cookies:
raise OWSAccessForbidden("Not authorized to access this resource.")
request.cookies['auth_tkt'] = auth_resp.request._cookies['auth_tkt']
1 change: 0 additions & 1 deletion magpie/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ def includeme(config):
logger.info('Adding api routes ...')

# Add all the admin ui routes
config.include('magpie.api.esgf')
config.include('magpie.api.home')
config.include('magpie.api.login')
config.include('magpie.api.management')
Expand Down
4 changes: 2 additions & 2 deletions magpie/api/api_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ def get_permission_multiformat_post_checked(request, service_resource, permissio
return perm_name


def get_value_multiformat_post_checked(request, key):
val = get_multiformat_any(request, key)
def get_value_multiformat_post_checked(request, key, default=None):
val = get_multiformat_any(request, key, default=default)
verify_param(val, notNone=True, notEmpty=True, httpError=HTTPUnprocessableEntity,
paramName=key, msgOnFail=UnprocessableEntityResponseSchema.description)
return val
Expand Down
216 changes: 203 additions & 13 deletions magpie/api/api_rest_schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from magpie.definitions.cornice_definitions import *
from magpie.definitions.pyramid_definitions import *
from magpie.constants import MAGPIE_LOGGED_USER, MAGPIE_USER_NAME_MAX_LENGTH, MAGPIE_ADMIN_PERMISSION
from magpie.constants import (
MAGPIE_LOGGED_USER,
MAGPIE_USER_NAME_MAX_LENGTH,
MAGPIE_ADMIN_PERMISSION,
MAGPIE_DEFAULT_PROVIDER
)
from magpie import __meta__
import six

Expand Down Expand Up @@ -226,6 +231,18 @@ def service_api_route_info(service_api):
ServiceResourceTypesAPI = Service(
path='/services/types/{service_type}/resources/types',
name='ServiceResourceTypes')
ProvidersAPI = Service(
path='/providers',
name='Providers')
ProviderSigninAPI = Service(
path='/providers/{provider_name}/signin',
name='ProviderSignin')
SigninAPI = Service(
path='/signin',
name='signin')
SignoutAPI = Service(
path='/signout',
name='signout')
SessionAPI = Service(
path='/session',
name='Session')
Expand All @@ -234,6 +251,13 @@ def service_api_route_info(service_api):
name='Version')


# Common path parameters
GroupNameParameter = colander.SchemaNode(colander.String(), description="Registered user group.")
UserNameParameter = colander.SchemaNode(colander.String(), description="Registered local user.")
ProviderNameParameter = colander.SchemaNode(colander.String(), description="External identity provider.",
validator=colander.OneOf([]))


class HeaderResponseSchema(colander.MappingSchema):
content_type = colander.SchemaNode(
colander.String(),
Expand Down Expand Up @@ -310,10 +334,10 @@ class ErrorVerifyParamBodySchema(colander.MappingSchema):
missing=colander.drop)


class ErrorRequestInfoBodySchema(BaseResponseBodySchema):
def __init__(self, **kw):
super(ErrorRequestInfoBodySchema, self).__init__(**kw)
assert kw.get('code') >= 400
class ErrorResponseBodySchema(BaseResponseBodySchema):
def __init__(self, code, description, **kw):
super(ErrorResponseBodySchema, self).__init__(code, description, **kw)
assert code >= 400

route_name = colander.SchemaNode(
colander.String(),
Expand All @@ -329,7 +353,7 @@ def __init__(self, **kw):
example="GET")


class InternalServerErrorResponseBodySchema(ErrorRequestInfoBodySchema):
class InternalServerErrorResponseBodySchema(ErrorResponseBodySchema):
def __init__(self, **kw):
kw['code'] = HTTPInternalServerError.code
super(InternalServerErrorResponseBodySchema, self).__init__(**kw)
Expand All @@ -353,13 +377,13 @@ class UnauthorizedResponseSchema(colander.MappingSchema):
class NotFoundResponseSchema(colander.MappingSchema):
description = "The route resource could not be found."
header = HeaderResponseSchema()
body = ErrorRequestInfoBodySchema(code=HTTPNotFound.code, description=description)
body = ErrorResponseBodySchema(code=HTTPNotFound.code, description=description)


class MethodNotAllowedResponseSchema(colander.MappingSchema):
description = "The method is not allowed for this resource."
header = HeaderResponseSchema()
body = ErrorRequestInfoBodySchema(code=HTTPMethodNotAllowed.code, description=description)
body = ErrorResponseBodySchema(code=HTTPMethodNotAllowed.code, description=description)


class UnprocessableEntityResponseSchema(colander.MappingSchema):
Expand All @@ -371,7 +395,7 @@ class UnprocessableEntityResponseSchema(colander.MappingSchema):
class InternalServerErrorResponseSchema(colander.MappingSchema):
description = "Internal Server Error. Unhandled exception occurred."
header = HeaderResponseSchema()
body = ErrorRequestInfoBodySchema(code=HTTPInternalServerError.code, description=description)
body = ErrorResponseBodySchema(code=HTTPInternalServerError.code, description=description)


class ProvidersListSchema(colander.SequenceSchema):
Expand Down Expand Up @@ -1961,6 +1985,12 @@ class GroupServicePermission_DELETE_ForbiddenResponseSchema(colander.MappingSche
body = GroupServicePermission_DELETE_ResponseBodySchema(code=HTTPForbidden.code, description=description)


class Signout_GET_OkResponseSchema(colander.MappingSchema):
description = "Sign out successful."
header = HeaderResponseSchema()
body = BaseResponseBodySchema(code=HTTPOk.code, description=description)


class GroupServicePermission_DELETE_NotFoundResponseSchema(colander.MappingSchema):
description = "Permission not found for corresponding group and resource."
header = HeaderResponseSchema()
Expand All @@ -1986,18 +2016,149 @@ class Session_GET_InternalServerErrorResponseSchema(colander.MappingSchema):
body = InternalServerErrorResponseSchema()


class ProvidersBodySchema(colander.MappingSchema):
internal = ProvidersListSchema()
external = ProvidersListSchema()


class Providers_GET_ResponseBodySchema(BaseResponseBodySchema):
provider_names = ProvidersListSchema()
internal_providers = ProvidersListSchema()
external_providers = ProvidersListSchema()
providers = ProvidersBodySchema()


class Providers_GET_OkResponseSchema(BaseResponseBodySchema):
class Providers_GET_OkResponseSchema(colander.MappingSchema):
description = "Get providers successful."
header = HeaderResponseSchema()
body = Providers_GET_ResponseBodySchema(code=HTTPOk.code, description=description)


class ProviderSignin_GET_HeaderRequestSchema(HeaderRequestSchema):
Authorization = colander.SchemaNode(
colander.String(), missing=colander.drop, example="Bearer MyF4ncy4ccEsT0k3n",
description="Access token to employ for direct signin with external provider bypassing the login procedure. "
"Access token must have been validated with the corresponding provider beforehand. "
"Supported format is 'Authorization: Bearer MyF4ncy4ccEsT0k3n'")
HomepageRoute = colander.SchemaNode(
colander.String(), missing=colander.drop, example="/session", default='Magpie UI Homepage',
description="Alternative redirection homepage after signin. "
"Must be a relative path to Magpie for security reasons.")
HomepageRoute.name = "Homepage-Route"


class ProviderSignin_GET_RequestSchema(colander.MappingSchema):
provider_name = ProviderNameParameter
header = ProviderSignin_GET_HeaderRequestSchema()


class ProviderSignin_GET_FoundResponseBodySchema(BaseResponseBodySchema):
homepage_route = colander.SchemaNode(colander.String(), description="Route to be used for following redirection.")


class ProviderSignin_GET_FoundResponseSchema(colander.MappingSchema):
description = "External login homepage route found. Temporary status before redirection to 'Homepage-Route' header."
header = HeaderResponseSchema()
body = ProviderSignin_GET_FoundResponseBodySchema(code=HTTPFound.code, description=description)


class ProviderSignin_GET_BadRequestResponseBodySchema(ErrorResponseBodySchema):
reason = colander.SchemaNode(colander.String(), description="Additional detail about the error.")


class ProviderSignin_GET_BadRequestResponseSchema(colander.MappingSchema):
description = "Incorrectly formed 'Authorization: Bearer <access_token>' header."
header = HeaderResponseSchema()
body = ProviderSignin_GET_BadRequestResponseBodySchema(code=HTTPBadRequest.code, description=description)


class ProviderSignin_GET_UnauthorizedResponseSchema(colander.MappingSchema):
description = "Unauthorized 'UserInfo' update using provided Authorization headers."
header = HeaderResponseSchema()
body = ErrorResponseBodySchema(code=HTTPUnauthorized.code, description=description)


class ProviderSignin_GET_ForbiddenResponseSchema(colander.MappingSchema):
description = "Forbidden 'Homepage-Route' host not matching Magpie refused for security reasons."
header = HeaderResponseSchema()
body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description)


class ProviderSignin_GET_NotFoundResponseBodySchema(ErrorResponseBodySchema):
param = ErrorVerifyParamBodySchema()
provider_name = colander.SchemaNode(colander.String())
providers = ProvidersListSchema()


class ProviderSignin_GET_NotFoundResponseSchema(colander.MappingSchema):
description = "Invalid `provider_name` not found within available providers."
header = HeaderResponseSchema()
body = ProviderSignin_GET_NotFoundResponseBodySchema(code=HTTPNotFound.code, description=description)


class Signin_POST_RequestBodySchema(colander.MappingSchema):
user_name = colander.SchemaNode(colander.String(), description="User name to use for sign in.")
password = colander.SchemaNode(colander.String(), description="Password to use for sign in.")
provider_name = colander.SchemaNode(colander.String(), description="Provider to use for sign in.",
default=MAGPIE_DEFAULT_PROVIDER, missing=colander.drop)


class Signin_POST_RequestSchema(colander.MappingSchema):
header = HeaderRequestSchema()
body = Signin_POST_RequestBodySchema()


class Signin_POST_OkResponseSchema(colander.MappingSchema):
description = "Login successful."
header = HeaderResponseSchema()
body = BaseResponseBodySchema(code=HTTPOk.code, description=description)


class Signin_POST_BadRequestResponseSchema(colander.MappingSchema):
description = "Could not retrieve `user_name`."
header = HeaderResponseSchema()
body = ErrorResponseBodySchema(code=HTTPBadRequest.code, description=description)


class Signin_POST_UnauthorizedResponseSchema(colander.MappingSchema):
description = "Login failure."
header = HeaderResponseSchema()
body = ErrorResponseBodySchema(code=HTTPUnauthorized.code, description=description)


class Signin_POST_ForbiddenResponseSchema(colander.MappingSchema):
description = "Could not verify `user_name`."
header = HeaderResponseSchema()
body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description)


class Signin_POST_NotAcceptableResponseSchema(colander.MappingSchema):
description = "Undefined `user_name`."
header = HeaderResponseSchema()
body = ErrorResponseBodySchema(code=HTTPNotAcceptable.code, description=description)


class Signin_POST_ConflictResponseBodySchema(ErrorResponseBodySchema):
provider_name = colander.SchemaNode(colander.String())
internal_user_name = colander.SchemaNode(colander.String())
external_user_name = colander.SchemaNode(colander.String())
external_id = colander.SchemaNode(colander.String())


class Signin_POST_ConflictResponseSchema(colander.MappingSchema):
description = "Add external user identity refused by db because it already exists."
header = HeaderResponseSchema()
body = Signin_POST_ConflictResponseBodySchema(code=HTTPConflict.code, description=description)


class Signin_POST_InternalServerErrorBodySchema(InternalServerErrorResponseBodySchema):
user_name = colander.SchemaNode(colander.String(), description="Specified user retrieved from the request.")
provider_name = colander.SchemaNode(colander.String(), description="Specified provider retrieved from the request.")


class Signin_POST_InternalServerErrorResponseSchema(colander.MappingSchema):
description = "Error occurred while signing in with external provider."
header = HeaderResponseSchema()
body = InternalServerErrorResponseBodySchema(code=HTTPNotAcceptable.code, description=description)


class Version_GET_ResponseBodySchema(BaseResponseBodySchema):
version = colander.SchemaNode(
colander.String(),
Expand Down Expand Up @@ -2428,6 +2589,35 @@ class Version_GET_OkResponseSchema(colander.MappingSchema):
'422': UnprocessableEntityResponseSchema(),
}
GroupResourcePermission_DELETE_responses = GroupServicePermission_DELETE_responses
Providers_GET_responses = {
'200': Providers_GET_OkResponseSchema(),
}
ProviderSignin_GET_responses = {
'302': ProviderSignin_GET_FoundResponseSchema(),
'400': ProviderSignin_GET_BadRequestResponseSchema(),
'401': ProviderSignin_GET_UnauthorizedResponseSchema(),
'403': ProviderSignin_GET_ForbiddenResponseSchema(),
'404': ProviderSignin_GET_NotFoundResponseSchema(),
'500': InternalServerErrorResponseSchema()
}
Signin_POST_responses = {
'200': Signin_POST_OkResponseSchema(),
'400': Signin_POST_BadRequestResponseSchema(),
'401': Signin_POST_UnauthorizedResponseSchema(),
'403': Signin_POST_ForbiddenResponseSchema(),
'404': ProviderSignin_GET_NotFoundResponseSchema(),
'406': Signin_POST_NotAcceptableResponseSchema(),
'409': Signin_POST_ConflictResponseSchema(),
'422': UnprocessableEntityResponseSchema(),
'500': Signin_POST_InternalServerErrorResponseSchema(),
}
Signout_GET_responses = {
'200': Signout_GET_OkResponseSchema(),
}
Session_GET_responses = {
'200': Session_GET_OkResponseSchema(),
'500': Session_GET_InternalServerErrorResponseSchema()
}
Version_GET_responses = {
'200': Version_GET_OkResponseSchema()
}
Expand Down
6 changes: 0 additions & 6 deletions magpie/api/esgf/__init__.py

This file was deleted.

12 changes: 5 additions & 7 deletions magpie/api/login/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
from magpie.api.api_rest_schemas import *
import logging
logger = logging.getLogger(__name__)


def includeme(config):
logger.info('Adding api login ...')
# Add all the rest api routes
config.add_route('session', '/session')
#config.add_route('signin_internal', '/signin_internal') # added via 'magpie.ini' configs
config.add_route('signin_external', '/signin_external')
config.add_route('signin', '/signin')
#config.add_route('signout', '/signout')
config.add_route('providers', '/providers')
config.add_route('external_login', 'providers/{provider_name}/signin')
config.add_route(**service_api_route_info(SessionAPI))
config.add_route(**service_api_route_info(SigninAPI))
config.add_route(**service_api_route_info(ProvidersAPI))
config.add_route(**service_api_route_info(ProviderSigninAPI))
config.scan()
Loading