diff --git a/CHANGES.rst b/CHANGES.rst index 364cf7992..780914e11 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,14 @@ Changes ******* +.. tmp "future 3.9" + +* Add missing ``WWW-Authentication`` and ``Location-When-Unauthenticated`` headers when HTTP ``Unauthorized [401]`` + response is returned (addresses `#96 `_ and + fixes `#330 `_). +* Add documentation details about ``Authentication`` and ``Authorization`` methods. + + `Unreleased `_ (latest) ------------------------------------------------------------------------------------ @@ -751,9 +759,9 @@ Features / Changes ~~~~~~~~~~~~~~~~~~~~~ * Refactoring of literal strings to corresponding ``Permission`` enum (`#167 `_). -* Change all incorrect usages of ``HTTPNotAcceptable [406]`` to ``HTTPBadRequest [400]`` +* Change all incorrect usages of HTTP ``Not Acceptable [406]`` to ``Bad Request [400]`` (`#163 `_). -* Add ``Accept`` header type checking before requests and return ``HTTPNotAcceptable [406]`` if invalid. +* Add ``Accept`` header type checking before requests and return HTTP ``Not Acceptable [406]`` if invalid. * Code formatting changes for consistency and cleanup of redundant/misguiding names (`#162 `_). * Add option ``MAGPIE_UI_ENABLED`` allowing to completely disable all ``/ui`` route (enabled by default). @@ -780,9 +788,9 @@ Features / Changes Features / Changes ~~~~~~~~~~~~~~~~~~~~~ * Logging requests and exceptions according to `MAGPIE_LOG_REQUEST` and `MAGPIE_LOG_EXCEPTION` values. -* Better handling of ``HTTPUnauthorized [401]`` and ``HTTPForbidden [403]`` according to unauthorized view +* Better handling of HTTP ``Unauthorized [401]`` and ``Forbidden [403]`` according to unauthorized view (invalid access token/headers or forbidden operation under view). -* Better handling of ``HTTPNotFound [404]`` and ``HTTPMethodNotAllowed [405]`` on invalid routes and request methods. +* Better handling of HTTP ``Not Found [404]`` and ``Method Not Allowed [405]`` on invalid routes and request methods. * Adjust ``Dockerfile`` copy order to save time if requirements did not change. `0.9.4 `_ (2019-02-19) diff --git a/docs/authentication.rst b/docs/authentication.rst new file mode 100644 index 000000000..9b758315e --- /dev/null +++ b/docs/authentication.rst @@ -0,0 +1,243 @@ +.. include:: references.rst + +.. _auth_methods: + +Authentication and Authorization Methods +========================================== + +In order to perform :term:`Authentication` in `Magpie`, multiple :term:`Providers` and methods are supported. +By default, the :term:`Internal Provider` named ``ziggurat`` is employed, which corresponds to the package used +internally to manage all `Magpie` elements. Login procedure is covered in :ref:`Authentication Requests` section. + +Supported :term:`External Providers` are presented in the table below in section :ref:`authn_providers`, although more +could be added later on. + +.. note:: + Terms :term:`Authentication` :term:`Providers`, :term:`External Providers` and :term:`External Providers` in this + chapter must not be confused with ``providers`` employed in :ref:`config_providers`. In this chapter, providers + refer to user-identity resolvers, in contrast to :term:`Service` definitions from the configuration files. + +.. _authn_requests: + +Authentication Requests +--------------------------- + +The most convenient way sign-in with `Magpie` is to employ the user interface provided on path +``{MAGPIE_URL}/ui/login``. This page will present fields that allow both :term:`Internal Provider` and +:term:`External Provider` login methods. + +Alternatively, API requests can be employed to define your own interface, or to obtain request tokens needed to +accomplish further requests interactions toward `Magpie` or obtain :term:`Authorization` from the :term:`Proxy` using +`Magpie` to enforce policies. + +Following are the supported request formats. + +.. _authn_req_method: + +Request Method +~~~~~~~~~~~~~~~ + +Both ``GET`` and ``POST`` are supported. This is in order to allow resolution of credentials for some +applications that do not correctly handle or purposely prohibit use of ``POST`` method. Also, ``GET`` helps quickly +accomplishing a login from a web browser using the ``{MAGPIE_URL}/signin`` endpoint with query parameters +(see :ref:`authn_login_query`). + +.. note:: + Whenever possible, prefer ``POST`` request with :ref:`authn_login_body` or the UI endpoint. + See also warning in :ref:`authn_login_query` for details. + + +.. _authn_login_query: + +Query Parameters +~~~~~~~~~~~~~~~~~~~~ + +This method employs the query string parameters in the URL to provide the credentials. The format is as follows. + +.. code-block:: + + GET {MAGPIE_URL}/signin?user_name=&password= + + +The response will contain :ref:`Authentication Headers` detail needed for user identification. + + +.. warning:: + Whenever possible, it is **strongly** recommended to instead use another one of the methods which offers + better support for different ``Content-Type`` responses to interact with `Magpie` as an API. + + Furthermore, using the ``POST`` method with content body and/or headers reduces risks of credential leaks that + would be visible in plain text via query parameters using ``GET`` request. Most servers and applications log path + and query parameters profusely, or even caches them, which can lead to easier identity theft or hacking of servers. + The ``GET`` method remains available for backward compatibility and quick testing purposes only. + + +.. _authn_login_body: + +Body Content +~~~~~~~~~~~~~~~~~~~ + +Body content requests allow multiple variants, based on the specified ``Content-Type`` header. +All variants employ a similar structure, but indicate the format of the body to be parsed. +By default, ``application/json`` is employed if none was specified. + +.. code-block:: + + POST {MAGPIE_URL}/signin + Headers + Content-Type: multipart/form-data; boundary= + Body + user_name: "" + password: "" + provider_name: "" # optional + + +.. code-block:: + + POST {MAGPIE_URL}/signin + Headers + Content-Type: application/x-www-form-urlencoded + Body + user_name=&password=&provider_name= + + +.. code-block:: + + POST {MAGPIE_URL}/signin + Headers + Content-Type: application/json + Body + { + "user_name": "", + "password": "", + "provider_name": "" + } + + +The response will contain :ref:`Authentication Headers` detail needed for user identification. + + +.. _authn_providers: + +Authentication Providers +--------------------------- + +For any of the :term:`Authentication` requests, omitting the ``provider_name`` identifier +(or explicitly using value ``ziggurat``) will default to employ :term:`Internal Provider` method. +This means that :term:`User` identity resolution will be attempted against locally registered users in `Magpie` +database. + +To instead use one of the :term:`External Providers`, the corresponding provider identifier must be provided within +the sign-in request contents with ``provider_name``. The value of that field must be one of the available provider in +the below table. + +Each provider has different configuration parameters as defined in `Magpie Security`_ module and use various protocols +amongst ``OpenID``, ``ESGF``-flavored ``OpenID`` and ``OAuth2``. Further :term:`External Providers` can be defined +using this module's dictionary configuration style following parameter specification of `Authomatic`_ package used for +managing this :term:`Authentication` procedure. + ++--------------------------------+-----------------------------------------------------------------------+ +| Category | Provider | ++================================+=======================================================================+ +| Open Identity (``OpenID``) | `OpenID`_ | ++--------------------------------+-----------------------------------------------------------------------+ +| *Earth System Grid Federation* | *German Climate Computing Centre* (`DKRZ`_) | +| (`ESGF`_) :sup:`(1)` | | +| +-----------------------------------------------------------------------+ +| | *French Research Institute for Environment Science* (`IPSL`_) | +| +-----------------------------------------------------------------------+ +| | *British Centre for Environmental Data Analysis* (`CEDA`_) :sup:`(2)` | +| +-----------------------------------------------------------------------+ +| | *US Lawrence Livermore National Laboratory* (`LLNL`_) :sup:`(3)` | +| +-----------------------------------------------------------------------+ +| | *Swedish Meteorological and Hydrological Institute* (`SMHI`_) | ++--------------------------------+-----------------------------------------------------------------------+ +| ``OAuth2`` | `GitHub_AuthN`_ Authentication | +| +-----------------------------------------------------------------------+ +| | `WSO2`_ Open Source Identity Server | ++--------------------------------+-----------------------------------------------------------------------+ + +| :sup:`(1)` extended variant of ``OpenID`` +| :sup:`(2)` formerly identified as *British Atmospheric Data Centre* (`BADC`_) +| :sup:`(3)` formerly identified as *Program for Climate Model Diagnosis & Intercomparison* (`PCMDI`_) + +.. note:: + Please note that due to the constantly changing nature of multiple of these external providers (APIs and moved + Websites), rarely used authentication bridges by the developers could break without prior notice. If this is the + case and you use one of the broken connectors, summit a new `issue`_. + +Using any of the :term:`External Providers` will tell `Magpie` to interrogate the configured identity URL of that +provider and use the credentials to attempt :term:`Authentication`. If successful, the response returned by that +:term:`Provider` should be parsed by `Magpie` in order to determine which corresponding local :term:`User` profile +it refers to. After validation, the :term:`Logged User` will be :term:`Authenticated` and following requests will be +applicable using the same ``Cookie`` methodology as when using normal local provider procedure. +See :ref:`Authentication Headers` for more details on that matter. + + +.. _authn_headers: + +Authentication Headers +--------------------------- + +.. versionadded:: 3.8 + + The ``WWW-Authentication`` and ``Location-When-Unauthenticated`` headers are returned whenever the + HTTP ``Unauthorized [401]`` response is the result of a request. This is done in order to help requesting + users or applications identify the endpoint where it can attempt :term:`Authentication` with credentials. + + +After execution of an :term:`Authentication` request, a ``Set-Cookie`` header with `Magpie` user identification token +named according to :ref:`config_security` should be set in the response. Web browsers and libraries for HTTP requests +handling should automatically detect that header and register the ``Cookie`` for subsequent requests. Alternatively, +the ``Cookie`` can be provided directly in the request using the following format:: + + {MAGPIE_COOKIE_NAME}=!userid_type:int; [Domain=; Path=; HttpOnly; SameSite=Lax] + + +All additional parameters are optional and can be provided to refine control of the scope the `Magpie` cookie applies +to, notably to avoid conflicts with other potential cookies employed by the request. The only mandatory parts are the +``MAGPIE_COOKIE_NAME`` value, the actual token value, and the indication ``!userid_type:int`` that tells `Magpie` the +provided token information is employed to resolve the :term:`Logged User` by ID. + +As for most of the other API request endpoints offered by `Magpie`, the ``Accept`` header can be provided to select the +format of the desired returned content. Following valid :term:`Authentication`, the body should contain a basic message +indicating as such, and returning ``OK [200]`` status. Otherwise, the appropriate HTTP error code will be returned with +a description message of the error cause. By default, header definition ``Accept: */*`` or completely omitted value for +``Accept`` will employ ``application/json`` for the returned ``Content-Type``. + + +.. _authz_headers: + +Authorization Headers +--------------------------- + +Following any successful :term:`Authentication` request as presented in the previous section, the obtained ``Cookie`` +defines which :term:`Logged User` attempts to accomplish an operation against a given protected URI. `Magpie` employs +the same ``Cookie`` both for operations provided by its API and for accessing the real :term:`Resource` protected +according to resolution of :term:`Effective Permissions` based on :term:`Applied Permissions` definitions. + +Access to Magpie Operations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the :term:`Logged User` has sufficient :term:`Permissions` + +:ref:`perm_access` + +Access to Protected Resources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When sending requests to the Policy Enforcement Point (PEP) (e.g.: `Twitcher`_ :term:`Proxy`), appropriate ``Cookie`` +headers must be defined for it to identify the :term:`Logged User` and resolve its :term:`Effective Permissions` +accordingly. Not providing those tokens will default to using ``MAGPIE_ANONYMOUS_USER``, which will result into either +one of HTTP ``Unauthorized [401]`` or ``Forbidden [403]``, depending on how the PEP interprets and returns the response +indicated by `Magpie`, unless the corresponding :term:`Resource` was allowed for :ref:`perm_public`. + +When appropriately authenticated, access to the targeted :term:`Resource` will be granted or denied depending on the +:term:`Effective Permissions` that :term:`Logged User` has for it. This decision is extensively explained in section +:ref:`perm_resolution`. + +Another alternative to obtain :term:`Authorization` (only when using the :ref:`utilities_adapter`) is +by providing the ``Authorization`` header in the request with appropriate credentials. In this situation, the adapter +will attempt a login operation inline to that original request, and if successful, will update the ``Cookie`` headers +accordingly. Although this method saves the need for the client to explicitly do an :term:`Authentication` request +toward `Magpie`'s signin path prior to :term:`Resource` access attempt, it diff --git a/docs/configuration.rst b/docs/configuration.rst index b25562da9..6024f9ea2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -25,6 +25,8 @@ the constants defined in `constants.py`_ and can be used interchangeably. Configuration Files ------------------- +.. _config_magpie_ini: + File: magpie.ini ~~~~~~~~~~~~~~~~~~~ @@ -34,6 +36,8 @@ is used by default in each tagged Docker image. If you want to provide different overridden in the Docker image using a volume mount parameter, or by specifying an alternative path through the environment variable ``MAGPIE_INI_FILE_PATH``. +.. _config_magpie_env: + File: magpie.env ~~~~~~~~~~~~~~~~~~~ @@ -50,6 +54,8 @@ variables for this file is presented in `magpie.env.example`_. When loading variables from the ``.env`` file, any conflicting environment variable will **NOT** be overridden. Therefore, only *missing but required* values will be added to the environment to ensure proper setup of `Magpie`. +.. _config_postgres_env: + File: postgres.env ~~~~~~~~~~~~~~~~~~~ @@ -58,6 +64,8 @@ employed to setup the `postgres` database connection (see ``MAGPIE_POSTGRES_ENV_ File `postgres.env.example`_ and auto-resolution of missing ``postgres.env`` is identical to ``magpie.env`` case. +.. _config_providers: + File: providers.cfg ~~~~~~~~~~~~~~~~~~~ @@ -72,6 +80,8 @@ details. Some services, such as :ref:`ServiceTHREDDS` for instance, can take additional parameters to customize some of their behaviour. Please refer to :ref:`Services` chapter for specific configuration supported. +.. _config_permissions: + File: permissions.cfg ~~~~~~~~~~~~~~~~~~~~~~ @@ -86,6 +96,8 @@ See ``MAGPIE_PERMISSIONS_CONFIG_PATH`` setting below to setup alternate referenc Please refer to the comment header of sample file `permissions.cfg`_ for specific format details as well as specific behaviour of each parameter according to encountered use cases. +.. _config_formats: + Configuration File Formats ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,6 +219,8 @@ Each webhook should define the following parameters : ``"user_name_param": "{user_name}"`` +.. _config_constants: + Settings and Constants ---------------------- @@ -218,6 +232,8 @@ Configuration variables will be used by `Magpie` on startup unless prior definit All variables (i.e.: non-``[constant]`` parameters) can also be specified by their ``magpie.[variable_name]`` setting counterpart as described at the start of the :ref:`configuration` section. +.. _config_load_settings: + Loading Settings ~~~~~~~~~~~~~~~~~ @@ -297,6 +313,8 @@ These settings can be used to specify where to find other settings through custo File path to ``postgres.env`` file with additional environment variables to configure the `postgres` connection. +.. _config_app_settings: + Application Settings ~~~~~~~~~~~~~~~~~~~~~ @@ -387,6 +405,7 @@ at the start of the :ref:`Configuration` section. generic interface items, but could be extended at a later date. The value must be one of the CSS file names located within the `themes`_ subdirectory. +.. _config_security: Security Settings ~~~~~~~~~~~~~~~~~~~~~ @@ -613,6 +632,8 @@ remain available as described at the start of the :ref:`Configuration` section. Name of the :term:`Provider` used for login. This represents the identifier that is set to define how to differentiate between a local sign-in procedure and a dispatched one some known `Authentication Providers`_. +.. _config_phoenix: + Phoenix Settings ~~~~~~~~~~~~~~~~~~~~~ @@ -648,6 +669,7 @@ Following settings provide some integration support for `Phoenix`_ in order to s Whether to push new :ref:`Service Synchronization` settings to the referenced `Phoenix`_ connection. +.. _config_twitcher: Twitcher Settings ~~~~~~~~~~~~~~~~~~~~~ @@ -704,6 +726,7 @@ employed `Twitcher`_ instance will also need to have access to `Magpie`'s databa must therefore be shared between the two services, as well as ``MAGPIE_SECRET`` value in order for successful completion of the handshake during :term:`Authentication` procedure of the request :term:`User` token. +.. _config_postgres_settings: Postgres Settings ~~~~~~~~~~~~~~~~~~~~~ @@ -789,50 +812,7 @@ configuration names are supported where mentioned. .. _SQLAlchemy Engine: https://docs.sqlalchemy.org/en/13/core/engines.html -Authentication Providers ---------------------------- - -In order to perform :term:`Authentication` in `Magpie`, multiple :term:`Providers` are supported. By default, -the :term:`Internal Provider` named ``ziggurat``, which corresponds to the package used to manage all `Magpie` elements -internally, is employed. Supported :term:`External Providers` are presented in the table below, although more could be -added later on. To signin using one of these :term:`Providers`, the corresponding identifier must be provided within -the signin request contents. - -Each as different configuration parameters as defined in `MagpieSecurity`_ and use various protocols amongst -``OpenID``, ``ESGF``-flavored ``OpenID`` and ``OAuth2``. Further :term:`External Providers` can be defined using this -module's dictionary configuration style following parameter specification of `Authomatic`_ package used for managing -this :term:`Authentication` procedure. - -+--------------------------------+-----------------------------------------------------------------------+ -| Category | Provider | -+================================+=======================================================================+ -| Open Identity (``OpenID``) | `OpenID`_ | -+--------------------------------+-----------------------------------------------------------------------+ -| *Earth System Grid Federation* | *German Climate Computing Centre* (`DKRZ`_) | -| (`ESGF`_) :sup:`(1)` | | -| +-----------------------------------------------------------------------+ -| | *French Research Institute for Environment Science* (`IPSL`_) | -| +-----------------------------------------------------------------------+ -| | *British Centre for Environmental Data Analysis* (`CEDA`_) :sup:`(2)` | -| +-----------------------------------------------------------------------+ -| | *US Lawrence Livermore National Laboratory* (`LLNL`_) :sup:`(3)` | -| +-----------------------------------------------------------------------+ -| | *Swedish Meteorological and Hydrological Institute* (`SMHI`_) | -+--------------------------------+-----------------------------------------------------------------------+ -| ``OAuth2`` | `GitHub_AuthN`_ Authentication | -| +-----------------------------------------------------------------------+ -| | `WSO2`_ Open Source Identity Server | -+--------------------------------+-----------------------------------------------------------------------+ - -| :sup:`(1)` extended variant of ``OpenID`` -| :sup:`(2)` formerly identified as *British Atmospheric Data Centre* (`BADC`_) -| :sup:`(3)` formerly identified as *Program for Climate Model Diagnosis & Intercomparison* (`PCMDI`_) - -.. note:: - Please note that due to the constantly changing nature of multiple of these external providers (APIs and moved - Websites), rarely used authentication bridges by the developers could break without prior notice. If this is the - case and you use one of the broken connectors, summit a new `issue`_. - +.. _config_auth_github: GitHub Settings ~~~~~~~~~~~~~~~~~ @@ -844,6 +824,11 @@ must be configured. These settings correspond to the values retrieved from follo Furthermore, the callback URL used for configuring the OAuth application on Github must match the running `Magpie` instance URL. For this reason, the values of ``MAGPIE_URL``, ``MAGPIE_HOST`` and ``HOSTNAME`` must be considered. +.. seealso:: + Refer to :ref:`auth_requests` and :ref:`auth_providers` for details. + +.. _config_auth_wso2: + WSO2 Settings ~~~~~~~~~~~~~~~~~ @@ -857,3 +842,6 @@ To use `WSO2`_ authentication provider, following variables must be set: To configure your `Magpie` instance as a trusted application for ``WSO2`` (and therefore retrieve values of above parameters), please refer to `WSO2_doc`_. + +.. seealso:: + Refer to :ref:`auth_requests` and :ref:`auth_providers` for details. diff --git a/docs/glossary.rst b/docs/glossary.rst index 36c806347..07b6aa186 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -15,9 +15,10 @@ Glossary ACE Access Control Entry. - Definition of an access control rule as Allow/Deny condition for a given :term:`User` or :term:`Group` to - act according to a certain :term:`Permission` name. Multiple :term:`ACE` form the effective :term:`ACL` - conditions to be evaluated to either grant or refuse access. + Definition of an access control rule (or policy) with `Allow` or `Deny` decision for a given :term:`User` or + :term:`Group` active according to a certain :term:`Permission` name and scope. Multiple :term:`ACE` form the + effective :term:`ACL` conditions to be evaluated to either grant or refuse access (i.e.: to provide + the :term:`Authorization` result based on the authenticated :term:`User`). ACL Access Control List. @@ -35,12 +36,12 @@ Glossary Authentication Process of identifying one-self using credentials in order to login into `Magpie`, or retrieving connected - session :term:`User` during an HTTP request using supported methods. + session :term:`User` during an HTTP request using supported methods. See also :ref:`auth_methods`. Authorization Process of allowing or denying access to a :term:`Resource` or :term:`Service` according to :term:`Logged User` - identified through :term:`Authentication` methods. This process typically falls into the hands of a - :term:`Proxy` application. + identified through one of the :term:`Authentication Methods`. This process typically falls into the hands of a + :term:`Proxy` application as policy enforcement point (PEP) using policy access decisions provided by `Magpie`. Context User Specific :term:`User` that is being targeted by a request from specified value for the ``{user_name}`` request @@ -122,7 +123,7 @@ Glossary Proxy Sibling service (typically `Twitcher`_) that employs `Magpie` as access management of :term:`User`, :term:`Group`, :term:`Service` and :term:`Resource` to obtain applicable sets of :term:`Permission`. - Provided these, it acts as policy enforcement point (PEP). + Provided these, it acts as Policy Enforcement Point (PEP). Public Refers to a :term:`Permission` applied on a :term:`Service` or :term:`Resource` to special elements in order diff --git a/docs/permissions.rst b/docs/permissions.rst index 7d63830e3..c897dc7b1 100644 --- a/docs/permissions.rst +++ b/docs/permissions.rst @@ -9,6 +9,9 @@ Permissions =========== +This chapter describes the various :term:`Permission` types, format representation, and usage. +For details regarding :term:`Authentication`, please refer to :ref:`auth_methods` instead. + .. _permission_types: Types of Permissions @@ -17,7 +20,7 @@ Types of Permissions Across the documentation and the code, term :term:`Permission` is often employed interchangeably to represent different and more subtle contextual functionalities. This is mostly an abuse of language, but is preserved regardless in order to maintain backward compatibility of features and API response content with older systems that could employ `Magpie`. -Therefore, care must be taken to consider under which context this term is observed to ensure correct interpretation +Therefore, care must be taken to consider under which context this term is employed to ensure correct interpretation of observed results. .. versionchanged:: 3.0 @@ -135,6 +138,7 @@ employed by `Magpie`: maybe could be combined used with group permissions and 'access permissions' do, but there is still a need to check view access dynamic group with them, might require some GroupFactory? +.. _perm_access: Route Access ------------- @@ -240,6 +244,8 @@ For all presented reasons above, it is important to distinguish between :ref:`Ac view configuration and :ref:`Applied Permissions` on resources, and they conceptually represent completely different operations, but are managed according to overlapping :term:`User` and :term:`Group` definitions. +.. _perm_public: + Public Access ------------- @@ -443,6 +449,8 @@ complementary or even contradicting :term:`Permission` entries are defined on th - |perm_example_resolve|_ +.. _perm_resolution: + Permissions Resolution ------------------------ diff --git a/docs/references.rst b/docs/references.rst index 30a10613a..23a245cf9 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -43,7 +43,7 @@ .. _magpie-cron: https://github.com/Ouranosinc/Magpie/tree/master/magpie-cron .. _magpie.env.example: https://github.com/Ouranosinc/Magpie/tree/master/env/magpie.env.example .. _magpie.ini: https://github.com/Ouranosinc/Magpie/tree/master/config/magpie.ini -.. _MagpieSecurity: https://github.com/Ouranosinc/Magpie/tree/master/magpie/security.py +.. _Magpie Security: https://github.com/Ouranosinc/Magpie/tree/master/magpie/security.py .. _permissions.cfg: https://github.com/Ouranosinc/Magpie/tree/master/config/permissions.cfg .. _postgres.env.example: https://github.com/Ouranosinc/Magpie/tree/master/env/postgres.env.example .. _providers.cfg: https://github.com/Ouranosinc/Magpie/tree/master/config/permissions.cfg diff --git a/docs/toc.rst b/docs/toc.rst index 085e9fe5c..b245e0bd2 100644 --- a/docs/toc.rst +++ b/docs/toc.rst @@ -8,6 +8,7 @@ usage installation configuration + authentication permissions services utilities diff --git a/docs/utilities.rst b/docs/utilities.rst index f2dbabcae..0930a9f56 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -82,14 +82,15 @@ Magpie Adapter: Integration with Twitcher The class :py:class:`magpie.adapter.MagpieAdapter` (`source `_) allows an easy integration with -the proxy service `Twitcher`_. This allows the user to setup a server (i.e.: using `docker-compose`_ or similar) that -can easily integrate a complete user authentication and authorization chain by having `Twitcher`_ ask `Magpie` for -the targeted service/resource access permissions via the adapter upon receiving an HTTP(S) request. +the :term:`Proxy` service `Twitcher`_. This allows the user to setup a server (i.e.: using `docker-compose`_ or similar) +that can easily integrate a complete user :term:`Authentication` and :term:`Authorization` chain by having `Twitcher`_ +ask `Magpie` for the targeted :term:`Service`/:term:`Resource` access permissions via the adapter upon receiving an +HTTP(S) request. -On each new version build of the `Magpie` docker image, a corresponding docker image is built as +On each new version build of the `Magpie` Docker image, a corresponding Docker image is built as ``pavics/twitcher:magpie-`` with pre-configured adapter within `Twitcher`_ so that both can be used together. -Furthermore, when the above docker image is used with the integrated adapter, a new HTTP ``POST`` request on route +Furthermore, when the above Docker image is used with the integrated adapter, a new HTTP ``POST`` request on route ``/verify`` is added to `Twitcher`_. This method allows to test if an authentication token cookie generated by `Magpie` (from login via API or UI) is valid and correctly interpreted by the `Twitcher`_ instance. This can be quite useful to confirm that both instances were adequately configured as both require to share the same ``magpie.secret`` configuration diff --git a/magpie/adapter/magpieowssecurity.py b/magpie/adapter/magpieowssecurity.py index 1b6bccdc8..e556b5f99 100644 --- a/magpie/adapter/magpieowssecurity.py +++ b/magpie/adapter/magpieowssecurity.py @@ -152,8 +152,9 @@ def update_request_cookies(self, request): """ Ensure login of the user and update the request cookies if Twitcher is in a special configuration. - Only update if `MAGPIE_COOKIE_NAME` is missing and is retrievable from `access_token` in `Authorization` header. - Counter-validate the login procedure by calling Magpie's `/session` which should indicated a logged user. + Only update if ``MAGPIE_COOKIE_NAME`` is missing and is retrievable from ``access_token`` field within the + ``Authorization`` header. Counter-validate the login procedure by calling Magpie's ``/session`` which should + indicate if there is a logged user. """ token_name = get_constant("MAGPIE_COOKIE_NAME", settings_container=request.registry.settings) if "Authorization" in request.headers and token_name not in request.cookies: diff --git a/magpie/api/generic.py b/magpie/api/generic.py index 38a70c4e5..f4140c552 100644 --- a/magpie/api/generic.py +++ b/magpie/api/generic.py @@ -14,10 +14,13 @@ ) from pyramid.request import Request from simplejson import JSONDecodeError +from six.moves.urllib.parse import urlparse +from magpie import __meta__ from magpie.api import exception as ax from magpie.api import schemas as s from magpie.api.requests import get_principals +from magpie.constants import get_constant from magpie.utils import ( CONTENT_TYPE_ANY, CONTENT_TYPE_HTML, @@ -26,6 +29,7 @@ SUPPORTED_ACCEPT_TYPES, get_header, get_logger, + get_magpie_url, is_magpie_ui_path ) @@ -36,7 +40,7 @@ from pyramid.registry import Registry from pyramid.response import Response - from magpie.typedefs import JSON, Str + from magpie.typedefs import HeadersType, JSON, Str LOGGER = get_logger(__name__) @@ -76,12 +80,12 @@ def not_found_or_method_not_allowed(request): def unauthorized_or_forbidden(request): # type: (Request) -> HTTPException """ - Overrides the default ``HTTPForbidden`` [403] by appropriate ``HTTPUnauthorized`` [401] when applicable. + Overrides the default HTTP ``Forbidden [403]`` by appropriate ``Unauthorized [401]`` when applicable. Unauthorized response is for restricted user access according to missing credentials and/or authorization headers. Forbidden response is for operation refused by the underlying process operations or due to insufficient permissions. - Without this fix, both situations return [403] regardless. + Without this fix, both situations return ``Forbidden [403]`` regardless. .. seealso:: - http://www.restapitutorial.com/httpstatuscodes.html @@ -89,18 +93,64 @@ def unauthorized_or_forbidden(request): In case the request references to `Magpie UI` route, it is redirected to :meth:`magpie.ui.home.HomeViews.error_view` for it to handle and display the error accordingly. """ + http_kw = None http_err = HTTPForbidden http_msg = s.HTTPForbiddenResponseSchema.description principals = get_principals(request) if Authenticated not in principals: http_err = HTTPUnauthorized http_msg = s.UnauthorizedResponseSchema.description + http_kw = {"headers": get_authenticate_headers(request)} content = get_request_info(request, default_message=http_msg) if is_magpie_ui_path(request): # need to handle 401/403 immediately otherwise target view is not even called from magpie.ui.utils import redirect_error return redirect_error(request, code=http_err.code, content=content) - return ax.raise_http(nothrow=True, http_error=http_err, detail=content["detail"], content=content) + return ax.raise_http(nothrow=True, http_error=http_err, http_kwargs=http_kw, + detail=content["detail"], content=content) + + +def get_authenticate_headers(request, error_type="invalid_token"): + # type: (Request, Str) -> Optional[HeadersType] + """ + Obtains all required headers by 401 responses based on executed :paramref:`request`. + + :param request: request that was sent to attempt authentication or access which must respond with Unauthorized. + :param error_type: additional detail of the cause of error, one of (invalid_token, invalid_token + """ + # FIXME: support other authentication methods (JWT, HTTP, Basic, Bearer Token, etc.) + # in such case, must resolve specified challenge method according to request if provided + # (https://github.com/Ouranosinc/Magpie/issues/255) + # Generic Auth: https://tools.ietf.org/html/rfc7235#section-2.1 (section for schema of 'WWW-Authenticate') + # Basic/Digest: https://tools.ietf.org/html/rfc2617 + # Bearer Token: https://tools.ietf.org/html/rfc6750 + # Cookie Token: https://tools.ietf.org/id/draft-broyer-http-cookie-auth-00.html#anchor1 + + # avoid adding headers when explicitly requested + # https://stackoverflow.com/questions/9859627 + # https://stackoverflow.com/questions/86105 + if get_header("X-Requested-With", request.headers) == "XMLHttpRequest": + return None + + # select error type: https://tools.ietf.org/html/rfc6750#section-3.1 + if error_type not in ["invalid_token", "invalid_token", "insufficient_scope"]: + error_type = "invalid_token" + cookie_name = get_constant("MAGPIE_COOKIE_NAME", request) + magpie_url = get_magpie_url(request) + signin_url = "{}{}".format(magpie_url, s.SigninAPI.path) + login_url = "{}/ui/login".format(magpie_url) + domain = urlparse(magpie_url).hostname + title = "{} Login".format(__meta__.__title__) + headers = { + # Challenge Schema: https://tools.ietf.org/html/rfc2617#section-3.2.1 (section for schema of extra params) + # WWW-Authenticate: challenge-1 [realm="<>" title="<>" params], + # challenge-2 [params], ... + "WWW-Authenticate": ("Cookie cookie-name=\"{}\" error=\"{}\" domain=\"{}\" URI=\"{}\" title=\"{}\"" + .format(cookie_name, error_type, domain, signin_url, title)), + # https://tools.ietf.org/html/rfc8053#section-4.3 + "Location-When-Unauthenticated": login_url, + } + return headers def guess_target_format(request): diff --git a/magpie/api/login/login.py b/magpie/api/login/login.py index 95a208a4d..7e7fb1254 100644 --- a/magpie/api/login/login.py +++ b/magpie/api/login/login.py @@ -103,9 +103,12 @@ def sign_in(request): pattern = ax.EMAIL_REGEX if "@" in user_name else ax.PARAM_REGEX ax.verify_param(user_name, matches=True, param_compare=pattern, param_name="user_name", http_error=HTTPUnprocessableEntity, msg_on_fail=s.UnprocessableEntityResponseSchema.description) + anonymous = get_constant("MAGPIE_ANONYMOUS_USER", request) + ax.verify_param(user_name, not_equal=True, param_compare=anonymous, param_name="user_name", + http_error=HTTPForbidden, msg_on_fail=s.Signin_POST_UnauthorizedResponseSchema.description) verify_provider(provider_name) - if provider_name in MAGPIE_INTERNAL_PROVIDERS.keys(): + if provider_name in list(MAGPIE_INTERNAL_PROVIDERS): # password can be None for external login, validate only here as needed password = ar.get_value_multiformat_body_checked(request, "password", pattern=None) # check manually to avoid inserting value in result body @@ -149,6 +152,7 @@ def login_failure(request, reason=None): .. seealso:: - :func:`sign_in` """ + http_kw = None http_err = HTTPUnauthorized if reason is None: reason = s.Signin_POST_UnauthorizedResponseSchema.description @@ -162,13 +166,16 @@ def login_failure(request, reason=None): user_name_list = ax.evaluate_call( lambda: [user.user_name for user in UserService.all(models.User, db_session=request.db)], fallback=lambda: request.db.rollback(), http_error=HTTPForbidden, - msg_on_fail=s.Signin_POST_ForbiddenResponseSchema.description) + msg_on_fail=s.Signin_POST_Internal_InternalServerErrorResponseSchema.description) if user_name in user_name_list: http_err = HTTPInternalServerError reason = s.Signin_POST_Internal_InternalServerErrorResponseSchema.description content = ag.get_request_info(request, default_message=s.Signin_POST_UnauthorizedResponseSchema.description) content.setdefault("detail", str(reason)) - ax.raise_http(http_error=http_err, content=content, detail=s.Signin_POST_UnauthorizedResponseSchema.description) + if http_err is HTTPUnauthorized: + http_kw = {"headers": ag.get_authenticate_headers(request)} + ax.raise_http(http_error=http_err, http_kwargs=http_kw, content=content, + detail=s.Signin_POST_UnauthorizedResponseSchema.description) def new_user_external(external_user_name, external_id, email, provider_name, db_session): diff --git a/magpie/api/schemas.py b/magpie/api/schemas.py index 586106b84..e2c46c777 100644 --- a/magpie/api/schemas.py +++ b/magpie/api/schemas.py @@ -2889,7 +2889,7 @@ class Signin_POST_UnauthorizedResponseSchema(BaseResponseSchemaAPI): class Signin_POST_ForbiddenResponseSchema(BaseResponseSchemaAPI): - description = "Could not verify 'user_name'." + description = "Login was refused." body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description) @@ -3605,7 +3605,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "200": Signin_POST_OkResponseSchema(), "400": Signin_POST_BadRequestResponseSchema(), # FIXME: https://github.com/Ouranosinc/Magpie/issues/359 "401": Signin_POST_UnauthorizedResponseSchema(), - "403": Signin_POST_ForbiddenResponseSchema(), # FIXME: https://github.com/Ouranosinc/Magpie/issues/359 + "403": Signin_POST_ForbiddenResponseSchema(), "404": ProviderSignin_GET_NotFoundResponseSchema(), "406": NotAcceptableResponseSchema(), "409": Signin_POST_ConflictResponseSchema(), diff --git a/magpie/utils.py b/magpie/utils.py index 18ac5f82d..05d854bbc 100644 --- a/magpie/utils.py +++ b/magpie/utils.py @@ -338,6 +338,15 @@ def patch_magpie_url(container): def get_magpie_url(container=None): # type: (Optional[AnySettingsContainer]) -> Str + """ + Obtains the configured Magpie URL entrypoint based on the various combinations of supported configuration settings. + + .. seealso:: + Documentation section :ref:`config_app_settings` for available setting combinations. + + :param container: container that provides access to application settings. + :return: resolved Magpie URL + """ if container is None: LOGGER.warning("Registry not specified, trying to find Magpie URL from environment") url = get_constant("MAGPIE_URL", raise_missing=False, raise_not_set=False, print_missing=False) @@ -368,6 +377,15 @@ def get_magpie_url(container=None): def get_phoenix_url(container=None): # type: (Optional[AnySettingsContainer]) -> Str + """ + Obtains the configured Phoenix URL entrypoint based on the various combinations of supported configuration settings. + + .. seealso:: + Documentation section :ref:`config_phoenix` for available setting combinations. + + :param container: container that provides access to application settings. + :return: resolved Phoenix URL + """ hostname = (get_constant("PHOENIX_HOST", container, raise_missing=False, raise_not_set=False) or get_constant("HOSTNAME", raise_missing=False, raise_not_set=False)) if not hostname: @@ -377,6 +395,16 @@ def get_phoenix_url(container=None): def get_twitcher_protected_service_url(magpie_service_name, hostname=None): + """ + Obtains the protected service URL behind Twitcher Proxy based on combination of supported configuration settings. + + .. seealso:: + Documentation section :ref:`config_twitcher` for available setting combinations. + + :param magpie_service_name: name of the service to employ in order to form the URL path behind the proxy. + :param hostname: override literal hostname to generate the URL instead of resolving using settings. + :return: resolved Twitcher Proxy protected service URL + """ twitcher_proxy_url = get_constant("TWITCHER_PROTECTED_URL", raise_not_set=False) if not twitcher_proxy_url: twitcher_proxy = get_constant("TWITCHER_PROTECTED_PATH", raise_not_set=False)