From 6411efd7a3bb362b1e4eabef1fb0cef6caa16ceb Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Fri, 26 Oct 2018 18:16:01 +0200 Subject: [PATCH] Retire the vast majority of code This drops the code for the FAS auth modules for: - TurboGears1 - TurboGears2 - Django All applications should have moved over to OpenID (Connect) by now. This drops the FAS client, FAS is going away for Fedora rather soon. This drops the OpenID Base clients: for API authentication, applications need to switch to OpenID Connect. At ths moment, Bodhi is the one client I am aware of that uses this, and it will be moved to OIDC soon. Signed-off-by: Patrick Uiterwijk --- doc/CSRF.rst | 404 --------------- doc/api.rst | 42 -- doc/auth.rst | 128 ----- doc/client.rst | 301 ----------- doc/django.rst | 70 --- doc/existing.rst | 70 --- doc/faswho.rst | 63 --- doc/flask_fas.rst | 229 --------- doc/index.rst | 3 - fedora/client/__init__.py | 9 +- fedora/client/fasproxy.py | 198 -------- fedora/client/openidbaseclient.py | 370 -------------- fedora/client/openidproxyclient.py | 560 --------------------- fedora/client/proxyclient.py | 509 ------------------- fedora/django/__init__.py | 41 -- fedora/django/auth/__init__.py | 0 fedora/django/auth/backends.py | 53 -- fedora/django/auth/management/__init__.py | 26 - fedora/django/auth/middleware.py | 97 ---- fedora/django/auth/models.py | 146 ------ fedora/tg/__init__.py | 6 - fedora/tg/client.py | 11 - fedora/tg/controllers.py | 121 ----- fedora/tg/identity/__init__.py | 3 - fedora/tg/identity/jsonfasprovider1.py | 292 ----------- fedora/tg/identity/jsonfasprovider2.py | 507 ------------------- fedora/tg/identity/soprovidercsrf.py | 551 -------------------- fedora/tg/json.py | 191 ------- fedora/tg/templates/__init__.py | 8 - fedora/tg/templates/genshi/__init__.py | 83 --- fedora/tg/templates/genshi/jsglobals.html | 40 -- fedora/tg/templates/genshi/login.html | 105 ---- fedora/tg/tg1utils.py | 43 -- fedora/tg/tg2utils.py | 42 -- fedora/tg/util.py | 46 -- fedora/tg/utils.py | 435 ---------------- fedora/tg/visit/__init__.py | 3 - fedora/tg/visit/jsonfasvisit1.py | 111 ---- fedora/tg/visit/jsonfasvisit2.py | 150 ------ fedora/tg/widgets.py | 122 ----- fedora/tg2/__init__.py | 5 - fedora/tg2/templates/__init__.py | 8 - fedora/tg2/templates/genshi/__init__.py | 83 --- fedora/tg2/templates/genshi/jsglobals.html | 26 - fedora/tg2/templates/genshi/login.html | 83 --- fedora/tg2/templates/mako/__init__.py | 90 ---- fedora/tg2/templates/mako/jsglobals.mak | 22 - fedora/tg2/templates/mako/login.mak | 83 --- fedora/tg2/utils.py | 251 --------- fedora/wsgi/__init__.py | 0 fedora/wsgi/csrf.py | 307 ----------- fedora/wsgi/faswho/__init__.py | 6 - fedora/wsgi/faswho/faswhoplugin.py | 421 ---------------- setup.py | 16 +- tests/functional/test_openidbaseclient.py | 62 --- 55 files changed, 2 insertions(+), 7650 deletions(-) delete mode 100644 doc/CSRF.rst delete mode 100644 doc/auth.rst delete mode 100644 doc/client.rst delete mode 100644 doc/django.rst delete mode 100644 doc/existing.rst delete mode 100644 doc/faswho.rst delete mode 100644 doc/flask_fas.rst delete mode 100644 fedora/client/fasproxy.py delete mode 100644 fedora/client/openidbaseclient.py delete mode 100644 fedora/client/openidproxyclient.py delete mode 100644 fedora/client/proxyclient.py delete mode 100644 fedora/django/__init__.py delete mode 100644 fedora/django/auth/__init__.py delete mode 100644 fedora/django/auth/backends.py delete mode 100644 fedora/django/auth/management/__init__.py delete mode 100644 fedora/django/auth/middleware.py delete mode 100644 fedora/django/auth/models.py delete mode 100644 fedora/tg/__init__.py delete mode 100644 fedora/tg/client.py delete mode 100644 fedora/tg/controllers.py delete mode 100644 fedora/tg/identity/__init__.py delete mode 100644 fedora/tg/identity/jsonfasprovider1.py delete mode 100644 fedora/tg/identity/jsonfasprovider2.py delete mode 100644 fedora/tg/identity/soprovidercsrf.py delete mode 100644 fedora/tg/json.py delete mode 100644 fedora/tg/templates/__init__.py delete mode 100644 fedora/tg/templates/genshi/__init__.py delete mode 100644 fedora/tg/templates/genshi/jsglobals.html delete mode 100644 fedora/tg/templates/genshi/login.html delete mode 100644 fedora/tg/tg1utils.py delete mode 100644 fedora/tg/tg2utils.py delete mode 100644 fedora/tg/util.py delete mode 100644 fedora/tg/utils.py delete mode 100644 fedora/tg/visit/__init__.py delete mode 100644 fedora/tg/visit/jsonfasvisit1.py delete mode 100644 fedora/tg/visit/jsonfasvisit2.py delete mode 100644 fedora/tg/widgets.py delete mode 100644 fedora/tg2/__init__.py delete mode 100644 fedora/tg2/templates/__init__.py delete mode 100644 fedora/tg2/templates/genshi/__init__.py delete mode 100644 fedora/tg2/templates/genshi/jsglobals.html delete mode 100644 fedora/tg2/templates/genshi/login.html delete mode 100644 fedora/tg2/templates/mako/__init__.py delete mode 100644 fedora/tg2/templates/mako/jsglobals.mak delete mode 100644 fedora/tg2/templates/mako/login.mak delete mode 100644 fedora/tg2/utils.py delete mode 100644 fedora/wsgi/__init__.py delete mode 100644 fedora/wsgi/csrf.py delete mode 100644 fedora/wsgi/faswho/__init__.py delete mode 100644 fedora/wsgi/faswho/faswhoplugin.py delete mode 100644 tests/functional/test_openidbaseclient.py diff --git a/doc/CSRF.rst b/doc/CSRF.rst deleted file mode 100644 index 6323ad8c..00000000 --- a/doc/CSRF.rst +++ /dev/null @@ -1,404 +0,0 @@ -.. _CSRF-Protection: - -=============== -CSRF Protection -=============== - -:Authors: Toshio Kuratomi -:Date: 21 February 2009 -:For Version: 0.3.x - -.. currentmodule:: fedora.tg.utils - -:term:`CSRF`, Cross-Site Request Forgery is a technique where a malicious -website can gain access to a Fedora Service by hijacking a currently open -session in a user's web browser. This technique affects identification via SSL -Certificates, cookies, or anything else that the browser sends to the server -automatically when a request to that server is made. It can take place over -both GET and POST. GET requests just need to hit a vulnerable URL. POST -requests require JavaScript to construct the form that is sent to the -vulnerable server. - -.. note:: - If you just want to implement this in :ref:`Fedora-Services`, skip to the - `Summary of Changes Per App`_ section - --------------- -How CSRF Works --------------- - -1) A vulnerable web service allows users to change things on their site using - just a cookie for authentication and submission of a form or by hitting a - URL with an ``img`` tag. - -2) A malicious website is crafted that looks harmless but has JavaScript or an - ``img`` tag that sends a request to the web service with the form data or - just hits the vulnerable URL. - -3) The user goes somewhere that people who are frequently logged into the site - are at and posts something innocuous that gets people to click on the link - to the malicious website. - -4) When a user who is logged into the vulnerable website clicks on the link, - their web browser loads the page. It sees the img tag and contacts the - vulnerable website to request the listed URL *sending the user's - authentication cookie automatically*. - -5) The vulnerable server performs the action that the URL requires as the - user whose cookies were sent and the damage is done... typically without - the user knowing any better until much later. - ------------------ -How to Prevent It ------------------ - -Theory -====== - -In order to remove this problem we need to have a shared secret between the -user's browser and the web site that is only available via the http -request-response. This secret is required in order for any actions to be -performed on the site. Because the :term:`Same Origin Policy` prevents the -malicious website from reading the web page itself, a shared secret passed in -this manner will prevent the malicious site from performing any actions. -Note that this secret cannot be transferred from the user's browser to the -server via a cookie because this is something that the browser does -automatically. It can, however, be transferred from the server to the browser -via a cookie because the browser prevents scripts from other domains from -*reading* the cookies. - -Practice -======== - -The strategy we've adopted is sometimes called :term:`double submit`. Every -time we POST a form or make a GET request that requires us to be authenticated -we must also submit a token consisting of a hash of the ``tg-visit`` to show -the server that we were able to read either the cookie or the response from a -previous request. We store the token value in a GET or POST parameter named -``_csrf_token``. If the server receives the ``tg-visit`` without the -``_csrf_token`` parameter, the server renders the user anonymous until the -user clicks another link. - -.. note:: - We hash the ``tg-visit`` session to make the token because we sometimes - send the token as a parameter in GET requests so it will show up in the - servers http logs. - -Verifying the Token -------------------- - -The :mod:`~fedora.tg.identity.jsonfasprovider1` does the work of -verifying that ``_csrf_token`` has been set and that it is a valid hash of the -``tg-visit`` token belonging to the user. The sequence of events to verify a -user's identity follows this outline: - -1) If username and password given - - 1. Verify with the identity provider that the username and password match - - 1) [YES] authenticate the user - 2) [NO] user is anonymous. - -2) if tg-visit cookie present - - 1. if session_id from ``tg-visit`` is in the db and not expired and - (sha1sum(``tg-visit``) matches ``_csrf_token`` given as a (POST variable - or GET variable) - - 1) [YES] authenticate the user - 2) [NO] Take user to login page that just requires the user to click a - link. Clicking the link shows that it's not just JavaScript in the - browser attempting to load the page but an actual user. Once the link - is clicked, the user will have a ``_csrf_token``. If the link is not - clicked the user is treated as anonymous. - - 2. Verify via SSL Certificate - - 1) SSL Certificate is not revoked and able to retrieve info for the - username and (sha1sum(``tg-visit``) matches ``_csrf_token`` given as a - POST variable or GET variable) - - 1. [YES] authenticate the user - 2. [NO] Take user to login page that just requires the user to click a - link. Clicking the link shows that it's not just JavaScript in the - browser attempting to load the page but an actual user. Once the link - is clicked, the user will have a ``_csrf_token``. If the link is not - clicked the user is treated as anonymous. - -This work should mostly happen behind the scenes so the application programmer -does not need to worry about this. - -.. seealso:: The :mod:`~fedora.tg.identity.jsonfasprovider1` - documentation has more information on methods that are provided by the - identity provider in case you do need to tell what the authentication token - is and whether it is missing. - -Getting the Token into the Page -------------------------------- - -Embedding the :term:`CSRF` token into the URLs that the user can click on is -the other half of this problem. :mod:`fedora.tg.utils` provides two -functions to make this easier. - -.. autofunction:: url - -This function does everything :func:`tg.url` does in the templates. In -addition it makes sure that ``_csrf_token`` is appended to the URL. - -.. autofunction:: enable_csrf - -This function sets config values to allow ``_csrf_token`` to be passed to any -URL on the server and makes :func:`turbogears.url` point to our :func:`url` -function. Once this is run, the :func:`tg.url` function you use in the -templates will make any links that use with it contain the :term:`CSRF` -protecting token. - -Intra-Application Links -~~~~~~~~~~~~~~~~~~~~~~~ - -We support single sign-on among the web applications we've written for Fedora. -In order for this to continue working with this :term:`CSRF` protection scheme -we have to add the ``_csrf_token`` parameter to links between -:ref:`Fedora-Services`. Linking from the PackageDB to Bodhi, for instance, -needs to have the hash appended to the URL for Bodhi. Our :func:`url` -function is capable of generating these by passing the full URL into the -function like this:: - - Bodhi - -.. note:: - - You probably already use :func:`tg.url` for links within you web app to - support :attr:`server.webpath`. Adding :func:`tg.url` to external links is - new. You will likely have to update your application's templates to call - :func:`url` on these URLs. - -Logging In ----------- - -Each app's :meth:`login` controller method and templates need to be modified -in several ways. - -.. _CSRF-Login-Template: - -Templates -~~~~~~~~~ - -For the templates, python-fedora provides a set of standard templates that can -be used to add the token. - -.. automodule:: fedora.tg.templates.genshi - -Using the ```` template will give you a login form that -automatically does several things for you. - -1. The ``forward_url`` and ``previous_url`` parameters that are passed in - hidden form elements will be run through :func:`tg.url` in order to get the - ``_csrf_token`` added. -2. The page will allow "Click through validation" of a user when they have a - valid ``tg-visit`` but do not have a ``_csrf_token``. - -Here's a complete login.html from the pkgdb to show what this could look -like:: - - - - - - - - - Login to the PackageDB - - - - ${message} - - - -You should notice that this looks like a typical genshi_ template in your -project with two new features. The ```` tag in the body that's -defined in :mod:`fedora.tg.templates.genshi` is used in the body to pull in -the login formand the ```` of :file:`login.html` -uses :func:`tg.fedora_template` to load the template from python-fedora. -This function resides in :mod:`fedora.tg.utils` and is added to the -``tg`` template variable when :func:`~fedora.tg.utils.enable_csrf` is -called at startup. It does the following: - -.. currentmodule:: fedora.tg.utils - -.. autofunction:: fedora_template - -.. _genshi: http://genshi.edgewall.org -.. _`genshi match template`: http://genshi.edgewall.org/wiki/Documentation/0.5.x/xml-templates.html#py:match - -The second match template in :file:`login.html` is to help you modify the -login and logout links that appear at the top of a typical application's page. -This is an optional change that lets the links display a click-through login -link in addition to the usual login and logout. To use this, you would follow -the example to add a ``toolbar`` with the ```` into your master -template. Here's some snippets from a :file:`master.html` to illustrate:: - - - - - [...] - - [...] - - [...] - - - - - -.. warning:: - - Notice that the ```` of :file:`login.html` happens after the - ```` tag? It is important to do that because the ```` tag in - :file:`master.html` is a match template just like ````. In - genshi_, the order in which match templates are defined is significant. - -If you need to look at these templates to modify them yourself (perhaps to -port them to a different templating language) you can find them in -:file:`fedora/tg/templates/genshi/login.html` in the source tarball. - -.. _CSRF-controller-methods: - -Controllers -~~~~~~~~~~~ - -Calling :ref:`Fedora-Services` from JavaScript poses one further problem. -Sometimes the user will not have a current :term:`CSRF` token and will need to -log in. When this is done with a username and password there's no -problem because the username and password are sufficient to prove the user is -in control of the browser. However, when using an SSL Certificate, we need -the browser to log in and then use the new :term:`CSRF` token to show the user -is in control. Since JavaScript calls are most likely going to request the -information as JSON, we need to provide the token in the dict returned from -:meth:`login`. You can either modify your :meth:`login` method to do that or -use the one provided in :func:`fedora.tg.controllers.login`. - -.. note:: - - Fedora Services do not currently use certificate authentication but doing - it would not take much code. Working out the security ramifications within - Infrastructure is the main sticking point to turning this on. - -.. note:: - The `Fedora Account System `_ - has a slightly different login() method. This is because it has to handle - account expiration, mandatory password resets, and other things tied to the - status of an account. Other Fedora Services rely on FAS to do this instead - of having to handle it themselves. - -.. automodule:: fedora.tg.controllers - :members: - -AJAX -==== - -Making JavaScript calls requires that we can get the token and add it to the -URLs we request. Since JavaScript doesn't have a standard library of crypto -functions we provide the token by setting it via a template so we do not have -to calculate it on the client. This has the added bonus of propagating a -correct token even if we change the hash function later. Making use of the -token can then be done in ad hoc JavaScript code but is better handled via a -dedicated JavaScript method like the :class:`fedora.dojo.BaseClient`. - -Template --------- - -.. automodule:: fedora.tg.templates.genshi.jsglobals - -.. warning:: - Just like :file:`login.html`, the ```` tag needs to come after - the ```` tag since they're both match templates. - - -JavaScript ----------- - -The :class:`fedora.dojo.BaseClient` class has been modified to send and update -the csrf token that is provided by fedora.identity.token. It is highly -recommended that you use this library or another like it to make all calls to -the server. This keeps you from having to deal with adding the :term:`CSRF` -token to every call yourself. - -Here's a small bit of sample code:: - - - - --------------------------- -Summary of Changes Per App --------------------------- - - * On startup, run :func:`~fedora.tg.utils.enable_csrf`. This could be - done in a start-APP.py and APP.wsgi scripts. Code like this will do it:: - - from turbogears import startup - from fedora.tg.utils import enable_csrf - startup.call_on_startup.append(enable_csrf) - - * Links to other :ref:`Fedora-Services` in the templates must be run through - the :func:`tg.url` method so that the :term:`CSRF` token can be appended. - - * You must use an updated login template. Using the one provided in - python-fedora is possible by changing your login template as shown in the - :ref:`CSRF-login-template` section. - - * Optional: update the master template that shows the Login/Logout Link to - have a "Verify Login" button. You can add one to a toolbar in your - application following the instructions in the :ref:`CSRF-login-template` - section. - - * Use an updated identity provider from python-fedora. At this time, you - need python-fedora 0.3.10 or later which has a - :mod:`~fedora.tg.identity.jsonfasprovider2` and - :mod:`~fedora.tg.visit.jsonfasvisit2` that provide :term:`CSRF` protection. - The original :mod:`~fedora.tg.identity.jsonfasprovider1` is provided for - applications that have not yet started using - :func:`~fedora.tg.utils.enable_csrf` so you have to make this change in - your configuration file (:file:`APPNAME/config/app.cfg`) - - * Get the :term:`CSRF` token into your forms and URLs. The recommended way - to do this is to use :func:`tg.url` in your forms for URLs that are local - to the app or are for :ref:`Fedora-Services`. - - * Update your :meth:`login` method to make sure you're setting - ``forward_url = request.path_info`` rather than ``request.path``. One easy - way to do this is to use the :meth:`~fedora.tg.controllers.login` and - :meth:`~fedora.tg.controllers.logout` as documented in - :ref:`CSRF-controller-methods` - - * Add the token and other identity information so JavaScript can get at it. - Use the :mod:`~fedora.tg.templates.genshi.jsglobals` template to accomplish - this. - -**This one still needs to be implemented** - * AJAX calls need to be enhanced to append the CSRF token to the data. This - is best done using a JavaScript function for this like the - :class:`fedora.dojo.BaseClient` library. diff --git a/doc/api.rst b/doc/api.rst index e81d835c..ce9a83d7 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -8,48 +8,6 @@ docs into the hand created docs as we have time to integrate them. .. toctree:: :maxdepth: 2 ------- -Client ------- - -.. automodule:: fedora.client - :members: FedoraServiceError, ServerError, AuthError, AppError, - FedoraClientError, FASError, CLAError, BodhiClientException, - DictContainer - -Generic Clients -=============== - -BaseClient ----------- - -.. autoclass:: fedora.client.BaseClient - :members: - :undoc-members: - -ProxyClient ------------ - -.. autoclass:: fedora.client.ProxyClient - :members: - :undoc-members: - - -OpenIdBaseClient ----------------- - -.. autoclass:: fedora.client.OpenIdBaseClient - :members: - :undoc-members: - -.. autofunction:: fedora.client.openidbaseclient.requires_login - -OpenIdProxyClient ------------------ - -.. autoclass:: fedora.client.OpenIdProxyClient - :members: - :undoc-members: Clients for Specific Services ============================= diff --git a/doc/auth.rst b/doc/auth.rst deleted file mode 100644 index fa5baf09..00000000 --- a/doc/auth.rst +++ /dev/null @@ -1,128 +0,0 @@ -===================== -Authentication to FAS -===================== - -The :ref:`Fedora-Account-System` has a :term:`JSON` interface that we make use -of to authenticate users in our web apps. Currently, there are two modes of -operation. Some web apps have :term:`single sign-on` capability with -:ref:`FAS`. These are the :term:`TurboGears` applications that use the -:mod:`~fedora.tg.identity.jsonfasprovider`. Other apps do not have -:term:`single sign-on` but they do connect to :ref:`FAS` to verify the -username and password so changing the password in :ref:`FAS` changes it -everywhere. - -.. _jsonfas2: - -TurboGears Identity Provider 2 -============================== - -An identity provider with :term:`CSRF` protection. - -This will install as a TurboGears identity plugin. To use it, set the -following in your :file:`APPNAME/config/app.cfg` file:: - - identity.provider='jsonfas2' - visit.manager='jsonfas2' - - -.. seealso:: :ref:`CSRF-Protection` - -.. automodule:: fedora.tg.identity.jsonfasprovider2 - :members: JsonFasIdentity, JsonFasIdentityProvider - :undoc-members: - -.. automodule:: fedora.tg.visit.jsonfasvisit2 - :members: JsonFasVisitManager - :undoc-members: - -.. _jsonfas1: - -Turbogears Identity Provider 1 -============================== - -These methods are **deprecated** because they do not provide the :term:`CSRF` -protection of :ref:`jsonfas2`. Please use that identity provider instead. - -.. automodule:: fedora.tg.identity.jsonfasprovider1 - :members: JsonFasIdentity, JsonFasIdentityProvider - :undoc-members: - :deprecated: - -.. automodule:: fedora.tg.visit.jsonfasvisit1 - :members: JsonFasVisitManager - :undoc-members: - :deprecated: - -.. _djangoauth: - -Django Authentication Backend -============================= -.. toctree:: - :maxdepth: 2 - - django - - -.. _flask_fas: - -Flask Auth Plugin -================= - -.. toctree:: - :maxdepth: 2 - - flask_fas - -.. _flaskopenid: - -Flask FAS OpenId Auth Plugin -============================ - -The flask_openid provider is an alternative to the flask_fas auth plugin. It -leverages our FAS-OpenID server to do authn and authz (group memberships). -Note that not every feature is available with a generic OpenID provider -- the -plugin depends on the OpenID provider having certain extensions in order to -provide more than basic OpenID auth. - -* Any compliant OpenID server should allow you to use the basic authn features of OpenID - OpenID authentication core: http://openid.net/specs/openid-authentication-2_0.html - -* Retrieving simple information about the user such as username, human name, email - is done with sreg: http://openid.net/specs/openid-simple-registration-extension-1_0.html - which is an extension supported by many providers. - -* Advanced security features such as requiring a user to re-login to the OpenID - provider or specifying that the user login with a hardware token requires - the PAPE extension: - http://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html - -* To get groups information, the provider must implement the - https://dev.launchpad.net/OpenIDTeams extension. - - * We have extended the teams extension so you can request a team name of - ``_FAS_ALL_GROUPS_`` to retrieve all the groups that a user belongs to. - Without this addition to the teams extension you will need to manually - configure which groups you are interested in knowing about. See the - documentation for how to do so. - -* Retrieving information about whether a user has signed a CLA (For Fedora, - this is the Fedora Project Contributor Agreement). - http://fedoraproject.org/specs/open_id/cla - -If the provider you use does not support one of these extensions, the plugin -should still work but naturally, it will return empty values for the -information that the extension would have provided. - -.. toctree:: - :maxdepth: 2 - - flask_fas_openid - -.. _faswho: - -FAS Who Plugin for TurboGears2 -============================== -.. toctree:: - :maxdepth: 2 - - faswho diff --git a/doc/client.rst b/doc/client.rst deleted file mode 100644 index dea966a7..00000000 --- a/doc/client.rst +++ /dev/null @@ -1,301 +0,0 @@ -============= -Fedora Client -============= -:Authors: Toshio Kuratomi - Luke Macken -:Date: 28 May 2008 -:For Version: 0.3.x - -The client module allows you to easily code an application that talks to a -`Fedora Service`_. It handles the details of decoding the data sent from the -Service into a python data structure and raises an Exception_ if an error is -encountered. - -.. _`Fedora Service`: service.html -.. _Exception: Exceptions_ - -.. toctree:: - ----------- -BaseClient ----------- - -The :class:`~fedora.client.BaseClient` class is the basis of all your -interactions with the server. It is flexible enough to be used as is for -talking with a service but is really meant to be subclassed and have methods -written for it that do the things you specifically need to interact with the -`Fedora Service`_ you care about. Authors of a `Fedora Service`_ are -encouraged to provide their own subclasses of -:class:`~fedora.client.BaseClient` that make it easier for other people to use -a particular Service out of the box. - -Using Standalone -================ - -If you don't want to subclass, you can use :class:`~fedora.client.BaseClient` -as a utility class to talk to any `Fedora Service`_. There's three steps to -this. First you import the :class:`~fedora.client.BaseClient` and Exceptions_ -from the ``fedora.client`` module. Then you create a new -:class:`~fedora.client.BaseClient` with the URL that points to the root of the -`Fedora Service`_ you're interacting with. Finally, you retrieve data from a -method on the server. Here's some code that illustrates the process:: - - from fedora.client import BaseClient, AppError, ServerError - - client = BaseClient('https://admin.fedoraproject.org/pkgdb') - try: - collectionData = client.send_request('/collections', auth=False) - except ServerError as e: - print('%s' % e) - except AppError as e: - print('%s: %s' % (e.name, e.message)) - - for collection in collectionData['collections']: - print('%s %s' % (collection['name'], collection['version']) - -BaseClient Constructor -~~~~~~~~~~~~~~~~~~~~~~ - -In our example we only provide ``BaseClient()`` with the URL fragment it uses -as the base of all requests. There are several more optional parameters that -can be helpful. - -If you need to make an authenticated request you can specify the username and -password to use when you construct your :class:`~fedora.client.BaseClient` -using the ``username`` and ``password`` keyword arguments. If you do not use -these, authenticated requests will try to connect via a cookie that was saved -from previous runs of :class:`~fedora.client.BaseClient`. If that fails as -well, :class:`~fedora.client.BaseClient` will throw an Exception_ which you -can catch in order to prompt for a new username and password:: - - from fedora.client import BaseClient, AuthError - import getpass - MAX_RETRIES = 5 - client = BaseClient('https://admin.fedoraproject.org/pkgdb', - username='foo', password='bar') - # Note this is simplistic. It only prompts once for another password. - # Your application may want to loop through this several times. - while (count < MAX_RETRIES): - try: - collectionData = client.send_request('/collections', auth=True) - except AuthError as e: - client.password = getpass.getpass('Retype password for %s: ' % username) - else: - # data retrieved or we had an error unrelated to username/password - break - count = count + 1 - -.. warning:: - - Note that although you can set the ``username`` and ``password`` as shown - above you do have to be careful in cases where your application is - multithreaded or simply processes requests for more than one user with the - same :class:`~fedora.client.BaseClient`. In those cases, you can - accidentally overwrite the ``username`` and ``password`` between two - requests. To avoid this, make sure you instantiate a separate - :class:`~fedora.client.BaseClient` for every thread of control or for - every request you handle or use :class:`~fedora.client.ProxyClient` - instead. - -The ``useragent`` parameter is useful for identifying in log files that your -script is calling the server rather than another. The default value is -``Fedora BaseClient/VERSION`` where VERSION is the version of the -:class:`~fedora.client.BaseClient` module. If you want to override this just -give another string to this:: - - client = BaseClient('https://admin.fedoraproject.org/pkgdb', - useragent='Package Database Client/1.0') - -The ``debug`` parameter turns on a little extra output when running the -program. Set it to true if you're having trouble and want to figure out what -is happening inside of the :class:`~fedora.client.BaseClient` code. - -send_request() -~~~~~~~~~~~~~~ - -``send_request()`` is what does the heavy lifting of making a request of the -server, receiving the reply, and turning that into a python dictionary. The -usage is pretty straightforward. - -The first argument to ``send_request()`` is ``method``. It contains the name -of the method on the server. It also has any of the positional parameters -that the method expects (extra path information interpreted by the server for -those building non-`TurboGears`_ applications). - -The ``auth`` keyword argument is a boolean. If True, the session cookie for -the user is sent to the server. If this fails, the ``username`` and -``password`` are sent. If that fails, an Exception_ is raised that you can -handle in your code. - -``req_params`` contains a dictionary of additional keyword arguments for the -server method. These would be the names and values returned via a form if it -was a CGI. Note that parameters passed as extra path information should be -added to the ``method`` argument instead. - -An example:: - - import BaseClient - client = BaseClient('https://admin.fedoraproject.org/pkgdb/') - client.send_request('/package/name/python-fedora', auth=False, - req_params={'collectionVersion': '9', 'collectionName': 'Fedora'}) - -In this particular example, knowing how the server works, ``/packages/name/`` -defines the method that the server is going to invoke. ``python-fedora`` is a -positional parameter for the name of the package we're looking up. -``auth=False`` means that we'll try to look at this method without having to -authenticate. The ``req_params`` sends two additional keyword arguments: -``collectionName`` which specifies whether to filter on a single distro or -include Fedora, Fedora EPEL, Fedora OLPC, and Red Hat Linux in the output and -``collectionVersion`` which specifies which version of the distribution to -output for. - -The URL constructed by :class:`~fedora.client.BaseClient` to the server could -be expressed as[#]_:: - - https://admin.fedoraproject.org/pkgdb/package/name/python-fedora/?collectionName=Fedora&collectionVersion=9 - -In previous releases of python-fedora, there would be one further query -parameter: ``tg_format=json``. That parameter instructed the server to -return the information as JSON data instead of HTML. Although this is usually -still supported in the server, :class:`~fedora.client.BaseClient` has -deprecated this method. Servers should be configured to use an ``Accept`` -header to get this information instead. See the `JSON output`_ section of the -`Fedora Service`_ documentation for more information about the server side. - -.. _`TurboGears`: http://www.turbogears.org/ -.. _`JSON output`: service.html#selecting-json-output -.. _[#]: Note that the ``req_params`` are actually sent via ``POST`` request - rather than ``GET``. Among other things, this means that values in - ``req_params`` won't show up in apache logs. - -Subclassing -=========== - -Building a client using subclassing builds on the information you've already -seen inside of :class:`~fedora.client.BaseClient`. You might want to use this -if you want to provide a module for third parties to access a particular -`Fedora Service`_. A subclass can provide a set of standard methods for -calling the server instead of forcing the user to remember the URLs used to -access the server directly. - -Here's an example that turns the previous calls into the basis of a python API -to the `Fedora Package Database`_:: - - import getpass - import sys - from fedora.client import BaseClient, AuthError - - class MyClient(BaseClient): - def __init__(self, baseURL='https://admin.fedoraproject.org/pkgdb', - username=None, password=None, - useragent='Package Database Client/1.0', debug=None): - super(BaseClient, self).__init__(baseURL, username, password, - useragent, debug) - - def collection_list(self): - '''Return a list of collections.''' - return client.send_request('/collection') - - def package_owners(self, package, collectionName=None, - collectionVersion=None): - '''Return a mapping of release to owner for this package.''' - pkgData = client.send_request('/packages/name/%s' % (package), - {'collectionName': collectionName, - 'collectionVersion': collectionVersion}) - ownerMap = {} - for listing in pkgData['packageListings']: - ownerMap['-'.join(listing['collection']['name'], - listing['collection']['version'])] = \ - listing['owneruser'] - return ownerMap - -A few things to note: - -1) In our constructor we list a default ``baseURL`` and ``useragent``. This - is usually a good idea as we know the URL of the `Fedora Service`_ we're - connecting to and we want to know that people are using our specific API. - -2) Sometimes we'll want methods that are thin shells around the server methods - like ``collection_list()``. Other times we'll want to do more - post processing to get specific results as ``package_owners()`` does. Both - types of methods are valid if they fit the needs of your API. If you find - yourself writing more of the latter, though, you may want to consider - getting a new method implemented in the server that can return results more - appropriate to your needs as it could save processing on the server and - bandwidth downloading the data to get information that more closely matches - what you need. - -See ``pydoc fedora.client.fas2`` for a module that implements a standard -client API for the `Fedora Account System`_ - -.. _`Fedora Package Database`: https://fedorahosted.org/packagedb -.. _`Fedora Account System`: https://fedorahosted.org/fas/ - ---------------- -Handling Errors ---------------- - -:class:`~fedora.client.BaseClient` will throw a variety of errors that can be -caught to tell you what kind of error was generated. - -Exceptions -========== - -:``FedoraServiceError``: The base of all exceptions raised by - :class:`~fedora.client.BaseClient`. If your code needs to catch any of the - listed errors then you can catch that to do so. - -:``ServerError``: Raised if there's a problem communicating with the service. - For instance, if we receive an HTML response instead of JSON. - -:``AuthError``: If something happens during authentication, like an invalid - usernsme or password, ``AuthError`` will be raised. You can catch this to - prompt the user for a new usernsme. - -:``AppError``: If there is a `server side error`_ when processing a request, - the `Fedora Service`_ can alert the client of this by setting certain - flags in the response. :class:`~fedora.client.BaseClient` will see these - flags and raise an AppError. The name of the error will be stored in - AppError's ``name`` field. The error's message will be stored in - ``message``. - -.. _`server side error`: service.html#Error Handling - -Example -======= -Here's an example of the exceptions in action:: - - from fedora.client import ServerError, AuthError, AppError, BaseClient - import getpass - MAXRETRIES = 5 - - client = BaseClient('https://admin.fedoraproject.org/pkgdb') - for retry in range(0, MAXRETRIES): - try: - collectionData = client.send_request('/collections', auth=True) - except AuthError as e: - from six.moves import input - client.username = input('Username: ').strip() - client.password = getpass.getpass('Password: ') - continue - except ServerError as e: - print('Error talking to the server: %s' % e) - break - except AppError as e: - print('The server issued the following exception: %s: %s' % ( - e.name, e.message)) - - for collection in collectionData['collections']: - print('%s %s' % (collection[0]['name'], collection[0]['version'])) - ----------------- -OpenIdBaseClient ----------------- - -Applications that use OpenId to authenticate are not able to use the standard -BaseClient because the pattern of authenticating is very different. We've -written a separate client object called -:class:`~fedora.client.OpenIdBaseClient` to do this. - - - diff --git a/doc/django.rst b/doc/django.rst deleted file mode 100644 index a67ac172..00000000 --- a/doc/django.rst +++ /dev/null @@ -1,70 +0,0 @@ -==================================== -Fedora Django Authentication Backend -==================================== -:Authors: Ignacio Vazquez-Abrams -:Date: 23 Feb 2009 -:For Version: 0.3.x - -The django.auth package provides an authentication backend for Django -projects. - -.. note:: - - Django authentication does not provide :term:`single sign-on` with other - Fedora web apps. it also does not provide :term:`CSRF` protection. Look - at the way that Dango's builtin forms implement :term:`CSRF` protection - for guidance on how to protect against this sort of attack. - ------------------- -fedora.django.auth ------------------- - -As FAS users are authenticated they are added to -:class:`~fedora.django.auth.models.FasUser`. FAS groups are added to -:class:`~django.contrib.auth.models.Group` both during ``syncdb`` and when -a user is authenticated. - -Integrating into a Django Project -================================= - -Add the following lines to the project's :file:`settings.py`:: - - AUTHENTICATION_BACKENDS = ( - 'fedora.django.auth.backends.FasBackend', - ) - - FAS_USERNAME = '' - FAS_PASSWORD = '' - FAS_USERAGENT = '' - FAS_URL = '' - FAS_ADMINS = ( ... ) - -``FAS_USERNAME`` and ``FAS_PASSWORD`` are used to retrieve group -information during ``syncdb`` as well as to retrieve users via the -authentication backend. They should be set to a low-privilege account -that can read group and user information. - -``FAS_USERAGENT`` is the string used to identify yourself to the FAS -server. - -``FAS_URL`` is the base URL of the FAS server to authenticate against. - -``FAS_ADMINS`` is a tuple of usernames that you want to have superuser -rights in the Django project. - -Add ``fedora.django.auth.middleware.FasMiddleware`` to the -``MIDDLEWARE_CLASSES`` tuple, between -``django.contrib.sessions.middleware.SessionMiddleware`` and -``django.contrib.auth.middleware.AuthenticationMiddleware``. - -Additionally, set ``FAS_GENERICEMAIL`` to ``False`` in order to use the -email address specified in FAS instead of ``@fedoraproject.org``. - -Add ``fedora.django.auth`` to ``INSTALLED_APPS``. - -Finally, run ``python manage.py syncdb`` to add the models for the added app to the database. - -.. warning:: - The ``User.first_name`` and ``User.last_name`` attributes are always - empty since FAS does not have any equivalents. The ``name`` - read-only property results in a round trip to the FAS server. diff --git a/doc/existing.rst b/doc/existing.rst deleted file mode 100644 index 3c33a224..00000000 --- a/doc/existing.rst +++ /dev/null @@ -1,70 +0,0 @@ -================= -Existing Services -================= - -There are many Services in Fedora. Many of these have an interface that we -can query and get back information as :term:`JSON` data. There is -documentation here about both the services and the client modules that can -access them. - -.. _`Fedora-Account-System`: -.. _`FAS`: - ---------------------- -Fedora Account System ---------------------- - -FAS is the Fedora Account System. It holds the account data for all of our -contributors. - -.. toctree:: - :maxdepth: 2 - -.. autoclass:: fedora.client.AccountSystem - :members: - :undoc-members: - -Threadsafe Account System Access -================================ - -It is not safe to use a single instance of the -:class:`~fedora.client.AccountSystem` object in multiple threads. This is -because instance variables are used to hold some connection-specific -information (for instance, the user who is logging in). For this reason, we -also provide the :class:`fedora.client.FasProxyClient` object. - -This is especially handy when writing authn and authz adaptors that talk to -fas from a multithreaded webserver. - -.. toctree:: - :maxdepth: 2 - -.. autoclass:: fedora.client.FasProxyClient - :members: - :undoc-members: - -.. _`Bodhi`: - ------------------------- -Bodhi, the Update Server ------------------------- - -Bodhi is used to push updates from the build system to the download -repositories. It lets packagers send packages to the testing repository or to -the update repository. - -pythyon-fedora currently supports both the old Bodhi1 interface and the new -Bodhi2 interface. By using ``fedora.client.BodhiCLient``, the correct one -should be returned to you depending on what is running live on Fedora -Infrastructure servers. - -.. toctree:: - :maxdepth: 2 - -.. autoclass:: fedora.client.Bodhi2Client - :members: - :undoc-members: - -.. autoclass:: fedora.client.Bodhi1Client - :members: - :undoc-members: diff --git a/doc/faswho.rst b/doc/faswho.rst deleted file mode 100644 index ba4edc30..00000000 --- a/doc/faswho.rst +++ /dev/null @@ -1,63 +0,0 @@ -============= -FASWho Plugin -============= -:Authors: Luke Macken - Toshio Kuratomi -:Date: 3 September 2011 - -This plugin provides authentication to the Fedora Account System using the -`repoze.who` WSGI middleware. It is designed for use with :term:`TurboGears2` -but it may be used with any `repoze.who` using application. Like -:ref:`jsonfas2`, faswho has builtin :term:`CSRF` protection. This protection -is implemented as a second piece of middleware and may be used with other -`repoze.who` authentication schemes. - -------------------------------------------- -Authenticating against FAS with TurboGears2 -------------------------------------------- - -Setting up authentication against FAS in :term:`TurboGears2` is very easy. It -requires one change to be made to :file:`app/config/app_cfg.py`. This change -will take care of registering faswho as the authentication provider, enabling -:term:`CSRF` protection, switching :func:`tg.url` to use -:func:`fedora.ta2g.utils.url` instead, and allowing the `_csrf_token` -parameter to be given to any URL. - -.. autofunction:: fedora.tg2.utils.add_fas_auth_middleware - -.. autofunction:: fedora.wsgi.faswho.faswhoplugin.make_faswho_middleware - ---------------------------------------------- -Using CSRF middleware with other Auth Methods ---------------------------------------------- - -This section needs to be made clearer so that apps like mirrormanager can be -ported to use this. - -.. automodule:: fedora.wsgi.csrf -.. autoclass:: fedora.wsgi.csrf.CSRFProtectionMiddleware -.. autoclass:: fedora.wsgi.csrf.CSRFMetadataProvider - ---------- -Templates ---------- - -The :mod:`fedora.tg2.utils` module contains some templates to help you -write :term:`CSRF` aware login forms and buttons. You can use the -:func:`~fedora.tg2.utils.fedora_template` function to integrate them into your -templates: - -.. autofunction:: fedora.tg2.utils.fedora_template - -The templates themselves come in two flavors. One set for use with mako and -one set for use with genshi. - -Mako -==== - -.. automodule:: fedora.tg2.templates.mako - -Genshi -====== - -.. automodule:: fedora.tg2.templates.genshi diff --git a/doc/flask_fas.rst b/doc/flask_fas.rst deleted file mode 100644 index ba9f4bd8..00000000 --- a/doc/flask_fas.rst +++ /dev/null @@ -1,229 +0,0 @@ -===================== -FAS Flask Auth Plugin -===================== - -:Authors: Toshio Kuratomi, Ian Weller -:Date: 29 October 2012 -:For Version: 0.3.x - -The :ref:`Fedora-Account-System` has a :term:`JSON` interface that we make use -of to authenticate users in our web apps. For our :term:`Flask` applications -we have an identity provider that has :term:`single sign-on` with our -:term:`TurboGears` 1 and 2 applications. It does not protect against -:term:`CSRF` attacks in the identity layer. The flask-wtf forms package -should be used to provide that. - -------------- -Configuration -------------- - -The FAS auth plugin has several config values that can be used to control how -the auth plugin functions. You can set these in your application's config -file. - -FAS_BASE_URL - Set this to the URL of the FAS server you are authenticating against. - Default is "https://admin.fedoraproject.org/accounts/" - -FAS_USER_AGENT - User agent string to be used when connecting to FAS. You can set this to - something specific to your application to aid in debugging a connection to - the FAS server as it will show up in the FAS server's logs. Default is - "Flask-FAS/|version|" - -FAS_CHECK_CERT - When set, this will check the SSL Certificate for the FAS server to make - sure that it is who it claims to be. This is useful to set to False when - testing against a local FAS server but should always be set to True in - production. Default: True - -FAS_COOKIE_NAME - The name of the cookie used to store the session id across the Fedora - Applications that support :term:`single sign-on`. Default: "tg-visit" - -FAS_FLASK_COOKIE_REQUIRES_HTTPS - When this is set to True, the session cookie will only be returned to the - server via ssl (https). If you connect to the server via plain http, the - cookie will not be sent. This prevents sniffing of the cookie contents. - This may be set to False when testing your application but should always - be set to True in production. Default is True. - ------------------- -Sample Application ------------------- - -The following is a sample, minimal flask application that uses fas_flask for -authentication:: - - #!/usr/bin/python -tt - # Flask-FAS - A Flask extension for authorizing users with FAS - # Primary maintainer: Ian Weller - # - # Copyright (c) 2012, Red Hat, Inc. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions are met: - # - # * Redistributions of source code must retain the above copyright notice, this - # list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright notice, - # this list of conditions and the following disclaimer in the documentation - # and/or other materials provided with the distribution. - # * Neither the name of the Red Hat, Inc. nor the names of its contributors may - # be used to endorse or promote products derived from this software without - # specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - # This is a sample application. In addition to using Flask-FAS, it uses - # Flask-WTF (WTForms) to handle the login form. Use of Flask-WTF is highly - # recommended because of its CSRF checking. - - import flask - from flask.ext import wtf - from flask.ext.fas import FAS, fas_login_required - - # Set up Flask application - app = flask.Flask(__name__) - # Set up FAS extension - fas = FAS(app) - - # Application configuration - # SECRET_KEY is necessary to CSRF in WTForms. It nees to be secret to - # make the csrf tokens unguessable but if you have multiple servers behind - # a load balancer, the key needs to be the same on each. - app.config['SECRET_KEY'] = 'change me!' - # Other configuration options for Flask-FAS: - # FAS_BASE_URL: the base URL for the accounts system - # (default https://admin.fedoraproject.org/accounts/) - # FAS_CHECK_CERT: check the SSL certificate of FAS (default True) - # FAS_FLASK_COOKIE_REQUIRES_HTTPS: send the 'secure' option with - # the login cookie (default True) - # You should use these options' defaults for production applications! - app.config['FAS_BASE_URL'] = 'https://fakefas.fedoraproject.org/accounts/' - app.config['FAS_CHECK_CERT'] = False - app.config['FAS_FLASK_COOKIE_REQUIRES_HTTPS'] = False - - - # A basic login form - class LoginForm(wtf.Form): - username = wtf.TextField('Username', [wtf.validators.Required()]) - password = wtf.PasswordField('Password', [wtf.validators.Required()]) - - - # Inline templates keep this test application all in one file. Don't do this in - # a real application. Please. - TEMPLATE_START = """ -

Flask-FAS test app

- {% if g.fas_user %} -

Hello, {{ g.fas_user.username }} — - Log out - {% else %} -

You are not logged in — - Log in - {% endif %} - — Main page

- """ - - - @app.route('/') - def index(): - data = TEMPLATE_START - data += '

Check if you are cla+1

' % \ - flask.url_for('claplusone') - data += '

See a secret message (requires login)

' % \ - flask.url_for('secret') - return flask.render_template_string(data) - - - @app.route('/login', methods=['GET', 'POST']) - def auth_login(): - # Your application should probably do some checking to make sure the URL - # given in the next request argument is sane. (For example, having next set - # to the login page will cause a redirect loop.) Some more information: - # http://flask.pocoo.org/snippets/62/ - if 'next' in flask.request.args: - next_url = flask.request.args['next'] - else: - next_url = flask.url_for('index') - # If user is already logged in, return them to where they were last - if flask.g.fas_user: - return flask.redirect(next_url) - # Init login form - form = LoginForm() - # Init template - data = TEMPLATE_START - data += ('

Log into the ' - 'Fedora Accounts System:') - # If this is POST, process the form - if form.validate_on_submit(): - if fas.login(form.username.data, form.password.data): - # Login successful, return - return flask.redirect(next_url) - else: - # Login unsuccessful - data += '

Invalid login

' - data += """ -
- {% for field in [form.username, form.password] %} -

{{ field.label }}: {{ field|safe }}

- {% if field.errors %} -
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} - {% endfor %} - - {{ form.csrf_token }} -
""" - return flask.render_template_string(data, form=form) - - - @app.route('/logout') - def logout(): - if flask.g.fas_user: - fas.logout() - return flask.redirect(flask.url_for('index')) - - # This demonstrates the use of the fas_login_required decorator. The - # secret message can only be viewed by those who are logged in. - @app.route('/secret') - @fas_login_required - def secret(): - data = TEMPLATE_START + '

Be sure to drink your Ovaltine

' - return flask.render_template_string(data) - - - # This demonstrates checking for group membership inside of a function. - # The flask_fas adapter also provides a cla_plus_one_required decorator that - # can restrict a url so that you can only access it from an account that has - # cla +1. - @app.route('/claplusone') - def claplusone(): - data = TEMPLATE_START - if not flask.g.fas_user: - # Not logged in - return flask.render_template_string(data + - '

You must log in to check your cla +1 status

') - non_cla_groups = [x.name for x in flask.g.fas_user.approved_memberships - if x.group_type != 'cla'] - if len(non_cla_groups) > 0: - data += '

Your account is cla+1.

' - else: - data += '

Your account is not cla+1.

' - return flask.render_template_string(data) - - - if __name__ == '__main__': - app.run(debug=True) diff --git a/doc/index.rst b/doc/index.rst index 56046f90..a4ea5dea 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -10,9 +10,6 @@ Lesser General Public License version 2 or later. :maxdepth: 2 client - existing - service - auth javascript api diff --git a/fedora/client/__init__.py b/fedora/client/__init__.py index 89af0ec3..f63bc77f 100644 --- a/fedora/client/__init__.py +++ b/fedora/client/__init__.py @@ -154,11 +154,6 @@ def check_file_permissions(filename, allow_notexists=False): # We want people to be able to import fedora.client.*Client directly # pylint: disable-msg=W0611 -from fedora.client.proxyclient import ProxyClient -from fedora.client.fasproxy import FasProxyClient -from fedora.client.baseclient import BaseClient -from fedora.client.openidproxyclient import OpenIdProxyClient -from fedora.client.openidbaseclient import OpenIdBaseClient from fedora.client.fas2 import AccountSystem, FASError, CLAError from fedora.client.wiki import Wiki # pylint: enable-msg=W0611 @@ -166,6 +161,4 @@ def check_file_permissions(filename, allow_notexists=False): __all__ = ('FedoraServiceError', 'ServerError', 'AuthError', 'AppError', 'FedoraClientError', 'LoginRequiredError', 'DictContainer', 'FASError', 'CLAError', 'BodhiClientException', - 'ProxyClient', 'FasProxyClient', 'BaseClient', 'OpenIdProxyClient', - 'OpenIdBaseClient', 'AccountSystem', 'BodhiClient', - 'Wiki') + 'AccountSystem', 'Wiki') diff --git a/fedora/client/fasproxy.py b/fedora/client/fasproxy.py deleted file mode 100644 index 85dfc10c..00000000 --- a/fedora/client/fasproxy.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008-2009 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -'''Implement a class that sets up threadsafe communication with the Fedora - Account System - -.. moduleauthor:: Ricky Zhou -.. moduleauthor:: Toshio Kuratomi - -.. versionadded:: 0.3.17 -''' - -from fedora.client import AuthError, AppError -from fedora.client.proxyclient import ProxyClient -from fedora import __version__ - -import logging -log = logging.getLogger(__name__) - - -class FasProxyClient(ProxyClient): - '''A threadsafe client to the Fedora Account System.''' - - def __init__(self, base_url='https://admin.fedoraproject.org/accounts/', - *args, **kwargs): - '''A threadsafe client to the Fedora Account System. - - This class is optimized to proxy multiple users to the account system. - ProxyClient is designed to be threadsafe so that code can instantiate - one instance of the class and use it for multiple requests for - different users from different threads. - - If you want something that can manage a single user's connection to - the Account System then use fedora.client.AccountSystem instead. - - :kwargs base_url: Base of every URL used to contact the server. - Defaults to the Fedora Project FAS instance. - :kwargs useragent: useragent string to use. If not given, default to - "FAS Proxy Client/VERSION" - :kwarg session_name: name of the cookie to use with session handling - :kwarg debug: If True, log debug information - :kwarg insecure: If True, do not check server certificates against - their CA's. This means that man-in-the-middle attacks are - possible against the `BaseClient`. You might turn this option on - for testing against a local version of a server with a self-signed - certificate but it should be off in production. - ''' - if 'useragent' not in kwargs: - kwargs['useragent'] = 'FAS Proxy Client/%s' % __version__ - if 'session_as_cookie' in kwargs and kwargs['session_as_cookie']: - # No need to allow this in FasProxyClient as it's deprecated in - # ProxyClient - raise TypeError('FasProxyClient() got an unexpected keyword' - ' argument \'session_as_cookie\'') - kwargs['session_as_cookie'] = False - super(FasProxyClient, self).__init__(base_url, *args, **kwargs) - - def login(self, username, password): - '''Login to the Account System - - :arg username: username to send to FAS - :arg password: Password to verify the username with - :returns: a tuple of the session id FAS has associated with the user - and the user's account information. This is similar to what is - returned by - :meth:`fedora.client.proxyclient.ProxyClient.get_user_info` - :raises AuthError: if the username and password do not work - ''' - return self.send_request( - '/login', - auth_params={'username': username, 'password': password} - ) - - def logout(self, session_id): - '''Logout of the Account System - - :arg session_id: a FAS session_id to remove from FAS - ''' - self.send_request('/logout', auth_params={'session_id': session_id}) - - def refresh_session(self, session_id): - '''Try to refresh a session_id to prevent it from timing out - - :arg session_id: FAS session_id to refresh - :returns: session_id that FAS has set now - ''' - return self.send_request('', auth_params={'session_id': session_id}) - - def verify_session(self, session_id): - '''Verify that a session is active. - - :arg session_id: session_id to verify is currently associated with a - logged in user - :returns: True if the session_id is valid. False otherwise. - ''' - try: - self.send_request('/home', auth_params={'session_id': session_id}) - except AuthError: - return False - except: - raise - return True - - def verify_password(self, username, password): - '''Return whether the username and password pair are valid. - - :arg username: username to try authenticating - :arg password: password for the user - :returns: True if the username/password are valid. False otherwise. - ''' - try: - self.send_request('/home', - auth_params={'username': username, - 'password': password}) - except AuthError: - return False - except: - raise - return True - - def get_user_info(self, auth_params): - '''Retrieve information about a logged in user. - - :arg auth_params: Auth information for a particular user. For - instance, this can be a username/password pair or a session_id. - Refer to - :meth:`fedora.client.proxyclient.ProxyClient.send_request` for all - the legal values for this. - :returns: a tuple of session_id and information about the user. - :raises AuthError: if the auth_params do not give access - ''' - request = self.send_request('/user/view', auth_params=auth_params) - return (request[0], request[1]['person']) - - def person_by_id(self, person_id, auth_params): - '''Retrieve information about a particular person - - :arg auth_params: Auth information for a particular user. For - instance, this can be a username/password pair or a session_id. - Refer to - :meth:`fedora.client.proxyclient.ProxyClient.send_request` for all - the legal values for this. - :returns: a tuple of session_id and information about the user. - :raises AppError: if the server returns an exception - :raises AuthError: if the auth_params do not give access - ''' - request = self.send_request('/json/person_by_id', - req_params={'person_id': person_id}, - auth_params=auth_params) - if request[1]['success']: - # In a devel version of FAS, membership info was returned - # separately - # This has been corrected in a later version - # Can remove this code at some point - if 'approved' in request[1]: - request[1]['person']['approved_memberships'] = \ - request[1]['approved'] - if 'unapproved' in request[1]: - request[1]['person']['unapproved_memberships'] = \ - request[1]['unapproved'] - return (request[0], request[1]['person']) - else: - raise AppError(name='Generic AppError', - message=request[1]['tg_flash']) - - def group_list(self, auth_params): - '''Retrieve a list of groups - - :arg auth_params: Auth information for a particular user. For - instance, this can be a username/password pair or a session_id. - Refer to - :meth:`fedora.client.proxyclient.ProxyClient.send_request` for all - the legal values for this. - :returns: a tuple of session_id and information about groups. The - groups information is in two fields: - - :groups: contains information about each group - :memberships: contains information about which users are members - of which groups - :raises AuthError: if the auth_params do not give access - ''' - request = self.send_request('/group/list', auth_params=auth_params) - return request diff --git a/fedora/client/openidbaseclient.py b/fedora/client/openidbaseclient.py deleted file mode 100644 index 08374be3..00000000 --- a/fedora/client/openidbaseclient.py +++ /dev/null @@ -1,370 +0,0 @@ -#!/usr/bin/env python2 -tt -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013-2015 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# - -"""Base client for application relying on OpenID for authentication. - -.. moduleauthor:: Pierre-Yves Chibon -.. moduleauthor:: Toshio Kuratomi -.. moduleauthor:: Ralph Bean - -.. versionadded: 0.3.35 - -""" - -# :F0401: Unable to import : Disabled because these will either import on py3 -# or py2 not both. -# :E0611: No name $X in module: This was renamed in python3 - -import json -import logging -import os - -import lockfile -import requests -import requests.adapters -from requests.packages.urllib3.util import Retry -from six.moves.urllib.parse import urljoin - -from functools import wraps -from munch import munchify -from kitchen.text.converters import to_bytes - -from fedora import __version__ -from fedora.client import (AuthError, - LoginRequiredError, - ServerError, - UnsafeFileError, - check_file_permissions) -from fedora.client.openidproxyclient import ( - OpenIdProxyClient, absolute_url, openid_login) - -log = logging.getLogger(__name__) - -b_SESSION_DIR = os.path.join(os.path.expanduser('~'), '.fedora') -b_SESSION_FILE = os.path.join(b_SESSION_DIR, 'openidbaseclient-sessions.cache') - - -def requires_login(func): - """ - Decorator function for get or post requests requiring login. - - Decorate a controller method that requires the user to be authenticated. - Example:: - - from fedora.client.openidbaseclient import requires_login - - @requires_login - def rename_user(new_name): - user = new_name - # [...] - """ - def _decorator(request, *args, **kwargs): - """ Run the function and check if it redirected to the openid form. - Or if we got a 403 - """ - output = func(request, *args, **kwargs) - if output and \ - 'OpenID transaction in progress' in output.text: - raise LoginRequiredError( - '{0} requires a logged in user'.format(output.url)) - elif output.status_code == 403: - raise LoginRequiredError( - '{0} requires a logged in user'.format(output.url)) - return output - return wraps(func)(_decorator) - - -class OpenIdBaseClient(OpenIdProxyClient): - - """ A client for interacting with web services relying on openid auth. """ - - def __init__(self, base_url, login_url=None, useragent=None, debug=False, - insecure=False, openid_insecure=False, username=None, - cache_session=True, retries=None, timeout=None, - retry_backoff_factor=0): - """Client for interacting with web services relying on fas_openid auth. - - :arg base_url: Base of every URL used to contact the server - :kwarg login_url: The url to the login endpoint of the application. - If none are specified, it uses the default `/login`. - :kwarg useragent: Useragent string to use. If not given, default to - "Fedora OpenIdBaseClient/VERSION" - :kwarg debug: If True, log debug information - :kwarg insecure: If True, do not check server certificates against - their CA's. This means that man-in-the-middle attacks are - possible against the `BaseClient`. You might turn this option on - for testing against a local version of a server with a self-signed - certificate but it should be off in production. - :kwarg openid_insecure: If True, do not check the openid server - certificates against their CA's. This means that man-in-the- - middle attacks are possible against the `BaseClient`. You might - turn this option on for testing against a local version of a - server with a self-signed certificate but it should be off in - production. - :kwarg username: Username for establishing authenticated connections - :kwarg cache_session: If set to true, cache the user's session data on - the filesystem between runs - :kwarg retries: if we get an unknown or possibly transient error from - the server, retry this many times. Setting this to a negative - number makes it try forever. Defaults to zero, no retries. - Note that this can only be set during object initialization. - :kwarg timeout: A float describing the timeout of the connection. The - timeout only affects the connection process itself, not the - downloading of the response body. Defaults to 120 seconds. - :kwarg retry_backoff_factor: Exponential backoff factor to apply in - between retry attempts. We will sleep for: - - `{retry_backoff_factor}*(2 ^ ({number of failed retries} - 1))` - - ...seconds inbetween attempts. The backoff factor scales the rate - at which we back off. Defaults to 0 (backoff disabled). - Note that this attribute can only be set at object initialization. - """ - - # These are also needed by OpenIdProxyClient - self.useragent = useragent or 'Fedora BaseClient/%(version)s' % { - 'version': __version__} - self.base_url = base_url - self.login_url = login_url or urljoin(self.base_url, '/login') - self.debug = debug - self.insecure = insecure - self.openid_insecure = openid_insecure - self.retries = retries - self.timeout = timeout - - # These are specific to OpenIdBaseClient - self.username = username - self.cache_session = cache_session - self.cache_lock = lockfile.FileLock(b_SESSION_FILE) - - # Make sure the database for storing the session cookies exists - if cache_session: - self._initialize_session_cache() - - # python-requests session. Holds onto cookies - self._session = requests.session() - - # Also hold on to retry logic. - # http://www.coglib.com/~icordasc/blog/2014/12/retries-in-requests.html - server_errors = [500, 501, 502, 503, 504, 506, 507, 508, 509, 599] - method_whitelist = Retry.DEFAULT_METHOD_WHITELIST.union(set(['POST'])) - if retries is not None: - prefixes = ['http://', 'https://'] - for prefix in prefixes: - self._session.mount(prefix, requests.adapters.HTTPAdapter( - max_retries=Retry( - total=retries, - status_forcelist=server_errors, - backoff_factor=retry_backoff_factor, - method_whitelist=method_whitelist, - ), - )) - - # See if we have any cookies kicking around from a previous run - self._load_cookies() - - def _initialize_session_cache(self): - # Note -- fallback to returning None on any problems as this isn't - # critical. It just makes it so that we don't have to ask the user - # for their password over and over. - if not os.path.isdir(b_SESSION_DIR): - try: - os.makedirs(b_SESSION_DIR, mode=0o750) - except OSError as err: - log.warning('Unable to create {file}: {error}'.format( - file=b_SESSION_DIR, error=err)) - self.cache_session = False - return None - - @requires_login - def _authed_post(self, url, params=None, data=None, **kwargs): - """ Return the request object of a post query.""" - response = self._session.post(url, params=params, data=data, **kwargs) - return response - - @requires_login - def _authed_get(self, url, params=None, data=None, **kwargs): - """ Return the request object of a get query.""" - response = self._session.get(url, params=params, data=data, **kwargs) - return response - - @requires_login - def _authed_put(self, url, params=None, data=None, **kwargs): - """ Return the request object of a put query.""" - response = self._session.put(url, params=params, data=data, **kwargs) - return response - - @requires_login - def _authed_delete(self, url, params=None, data=None, **kwargs): - """ Return the request object of a delete query.""" - response = self._session.delete(url, params=params, data=data, **kwargs) - return response - - def send_request(self, method, auth=False, verb='POST', **kwargs): - """Make an HTTP request to a server method. - - The given method is called with any parameters set in req_params. If - auth is True, then the request is made with an authenticated session - cookie. - - :arg method: Method to call on the server. It's a url fragment that - comes after the :attr:`base_url` set in :meth:`__init__`. - :kwarg auth: If True perform auth to the server, else do not. - :kwarg req_params: Extra parameters to send to the server. - :kwarg file_params: dict of files where the key is the name of the - file field used in the remote method and the value is the local - path of the file to be uploaded. If you want to pass multiple - files to a single file field, pass the paths as a list of paths. - :kwarg verb: HTTP verb to use. GET and POST are currently supported. - POST is the default. - """ - # Decide on the set of auth cookies to use - - method = absolute_url(self.base_url, method) - - self._authed_verb_dispatcher = {(False, 'POST'): self._session.post, - (False, 'GET'): self._session.get, - (False, 'PUT'): self._session.put, - (False, 'DELETE'): self._session.delete, - (True, 'POST'): self._authed_post, - (True, 'GET'): self._authed_get, - (True, 'PUT'): self._authed_put, - (True, 'DELETE'): self._authed_delete} - - if 'timeout' not in kwargs: - kwargs['timeout'] = self.timeout - - try: - func = self._authed_verb_dispatcher[(auth, verb)] - except KeyError: - raise Exception('Unknown HTTP verb') - - try: - output = func(method, **kwargs) - except LoginRequiredError: - raise AuthError() - - try: - data = output.json() - except ValueError as e: - # The response wasn't JSON data - raise ServerError( - method, output.status_code, 'Error returned from' - ' json module while processing %(url)s: %(err)s\n%(output)s' % - { - 'url': to_bytes(method), - 'err': to_bytes(e), - 'output': to_bytes(output.text), - }) - - data = munchify(data) - - return data - - def login(self, username, password, otp=None): - """ Open a session for the user. - - Log in the user with the specified username and password - against the FAS OpenID server. - - :arg username: the FAS username of the user that wants to log in - :arg password: the FAS password of the user that wants to log in - :kwarg otp: currently unused. Eventually a way to send an otp to the - API that the API can use. - - """ - if not username: - raise AuthError("Username may not be %r at login." % username) - if not password: - raise AuthError("Password required for login.") - # It looks like we're really doing this. Let's make sure that we don't - # collide various cookies, and clear every cookie we had up until now - # for this service. - self._session.cookies.clear() - self._save_cookies() - - response = openid_login( - session=self._session, - login_url=self.login_url, - username=username, - password=password, - otp=otp, - openid_insecure=self.openid_insecure) - self._save_cookies() - return response - - @property - def session_key(self): - return "%s:%s" % (self.base_url, self.username or '') - - def has_cookies(self): - return bool(self._session.cookies) - - def _load_cookies(self): - if not self.cache_session: - return - - try: - check_file_permissions(b_SESSION_FILE, True) - except UnsafeFileError as e: - log.debug('Current sessions ignored: {}'.format(str(e))) - return - - try: - with self.cache_lock: - with open(b_SESSION_FILE, 'rb') as f: - data = json.loads(f.read().decode('utf-8')) - for key, value in data[self.session_key]: - self._session.cookies[key] = value - except KeyError: - log.debug("No pre-existing session for %s" % self.session_key) - except IOError: - # The file doesn't exist, so create it. - log.debug("Creating %s", b_SESSION_FILE) - oldmask = os.umask(0o027) - with open(b_SESSION_FILE, 'wb') as f: - f.write(json.dumps({}).encode('utf-8')) - os.umask(oldmask) - - def _save_cookies(self): - if not self.cache_session: - return - - with self.cache_lock: - try: - check_file_permissions(b_SESSION_FILE, True) - with open(b_SESSION_FILE, 'rb') as f: - data = json.loads(f.read(), encoding='utf-8') - except UnsafeFileError as e: - log.debug('Clearing sessions: {}'.format(str(e))) - os.unlink(b_SESSION_FILE) - data = {} - except Exception: - log.warn("Failed to open cookie cache before saving.") - data = {} - - oldmask = os.umask(0o027) - data[self.session_key] = self._session.cookies.items() - with open(b_SESSION_FILE, 'wb') as f: - f.write(json.dumps(data).encode('utf-8')) - os.umask(oldmask) - - -__all__ = ('OpenIdBaseClient', 'requires_login') diff --git a/fedora/client/openidproxyclient.py b/fedora/client/openidproxyclient.py deleted file mode 100644 index 55f61996..00000000 --- a/fedora/client/openidproxyclient.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/bin/env python2 -tt -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013-2014 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -"""Implement a class that sets up simple communication to a Fedora Service. - -.. moduleauthor:: Pierre-Yves Chibon -.. moduleauthor:: Toshio Kuratomi - -.. versionadded: 0.3.35 - -""" - -import copy -import logging -import re -# For handling an exception that's coming from requests: -import ssl -import time - -from six.moves import http_client as httplib -from six.moves.urllib.parse import quote, parse_qs, urljoin, urlparse - -# Hack, hack, hack around -# the horror that is logging! -# Verily, verliy, verily, verily -# We should all use something better -try: - # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, *args): - pass - -import requests - -#from munch import munchify -from kitchen.text.converters import to_bytes -# For handling an exception that's coming from requests: -import urllib3 - -from fedora import __version__ -from fedora.client import AuthError, ServerError, FedoraServiceError - -log = logging.getLogger(__name__) -log.addHandler(NullHandler()) - -OPENID_SESSION_NAME = 'FAS_OPENID' - -FEDORA_OPENID_API = 'https://id.fedoraproject.org/api/v1/' -FEDORA_OPENID_RE = re.compile(r'^http(s)?:\/\/id\.(|stg.|dev.)?fedoraproject\.org(/)?') - - -def _parse_response_history(response): - """ Retrieve the attributes from the response history. """ - data = {} - for r in response.history: - if FEDORA_OPENID_RE.match(r.url): - parsed = parse_qs(urlparse(r.url).query) - for key, value in parsed.items(): - data[key] = value[0] - break - return data - - -def openid_login(session, login_url, username, password, otp=None, - openid_insecure=False): - """ Open a session for the user. - - Log in the user with the specified username and password - against the FAS OpenID server. - - :arg session: Requests session object required to persist the cookies - that are created during redirects on the openid provider. - :arg login_url: The url to the login endpoint of the application. - :arg username: the FAS username of the user that wants to log in - :arg password: the FAS password of the user that wants to log in - :kwarg otp: currently unused. Eventually a way to send an otp to the - API that the API can use. - :kwarg openid_insecure: If True, do not check the openid server - certificates against their CA's. This means that man-in-the-middle - attacks are possible against the `BaseClient`. You might turn this - option on for testing against a local version of a server with a - self-signed certificate but it should be off in production. - - """ - # Log into the service - response = session.get( - login_url, headers={'Accept': 'application/json'}) - - try: - data = response.json() - openid_url = data.get('server_url', None) - if not FEDORA_OPENID_RE.match(openid_url): - raise FedoraServiceError( - 'Un-expected openid provider asked: %s' % openid_url) - except: - # Some consumers (like pyramid_openid) return redirects with the - # openid attributes encoded in the url - if not FEDORA_OPENID_RE.match(response.url): - raise FedoraServiceError( - 'Un-expected openid provider asked: %s' % response.url) - data = _parse_response_history(response) - - # Contact openid provider - data['username'] = username - data['password'] = password - # Let's precise to FedOAuth that we want to authenticate with FAS - data['auth_module'] = 'fedoauth.auth.fas.Auth_FAS' - data['auth_flow'] = 'fedora' - if not 'openid.mode' in data: - data['openid.mode'] = 'checkid_setup' - response = session.post( - FEDORA_OPENID_API, data=data, verify=not openid_insecure) - if not bool(response): - raise ServerError(FEDORA_OPENID_API, response.status_code, - 'Error returned from our POST to ipsilon.') - - output = response.json() - - if not output['success']: - raise AuthError(output['message']) - - response = session.post(output['response']['openid.return_to'], - data=output['response']) - - return response - - -def absolute_url(beginning, end): - """ Join two urls parts if the last part does not start with the first - part specified """ - if not end.startswith(beginning): - end = urljoin(beginning, end) - return end - - -class OpenIdProxyClient(object): - # pylint: disable-msg=R0903 - """ - A client to a Fedora Service. This class is optimized to proxy multiple - users to a service. OpenIdProxyClient is designed to be usable by code - that creates a single instance of this class and uses it in multiple - threads. However it is not completely threadsafe. See the information - on setting attributes below. - - If you want something that can manage one user's connection to a Fedora - Service, then look into using :class:`~fedora.client.OpenIdBaseClient` - instead. - - This class has several attributes. These may be changed after - instantiation. Please note, however, that changing these values when - another thread is utilizing the same instance may affect more than just - the thread that you are making the change in. (For instance, changing - the debug option could cause other threads to start logging debug - messages in the middle of a method.) - - .. attribute:: base_url - - Initial portion of the url to contact the server. It is highly - recommended not to change this value unless you know that no other - threads are accessing this :class:`OpenIdProxyClient` instance. - - .. attribute:: useragent - - Changes the useragent string that is reported to the web server. - - .. attribute:: session_name - - Name of the cookie that holds the authentication value. - - .. attribute:: debug - - If :data:`True`, then more verbose logging is performed to aid in - debugging issues. - - .. attribute:: insecure - - If :data:`True` then the connection to the server is not checked to be - sure that any SSL certificate information is valid. That means that - a remote host can lie about who it is. Useful for development but - should not be used in production code. - - .. attribute:: retries - - Setting this to a positive integer will retry failed requests to the - web server this many times. Setting to a negative integer will retry - forever. - - .. attribute:: timeout - - A float describing the timeout of the connection. The timeout only - affects the connection process itself, not the downloading of the - response body. Defaults to 120 seconds. - - """ - - def __init__(self, base_url, login_url=None, useragent=None, - session_name='session', debug=False, insecure=False, - openid_insecure=False, retries=None, timeout=None): - """Create a client configured for a particular service. - - :arg base_url: Base of every URL used to contact the server - :kwarg login_url: The url to the login endpoint of the application. - If none are specified, it uses the default `/login`. - :kwarg useragent: useragent string to use. If not given, default - to "Fedora ProxyClient/VERSION" - :kwarg session_name: name of the cookie to use with session handling - :kwarg debug: If True, log debug information - :kwarg insecure: If True, do not check server certificates against - their CA's. This means that man-in-the-middle attacks are - possible against the `BaseClient`. You might turn this option - on for testing against a local version of a server with a - self-signed certificate but it should be off in production. - :kwarg openid_insecure: If True, do not check the openid server - certificates against their CA's. This means that man-in-the- - middle attacks are possible against the `BaseClient`. You might - turn this option on for testing against a local version of a - server with a self-signed certificate but it should be off in - production. - :kwarg retries: if we get an unknown or possibly transient error - from the server, retry this many times. Setting this to a - negative number makes it try forever. Defaults to zero, no - retries. - :kwarg timeout: A float describing the timeout of the connection. - The timeout only affects the connection process itself, not the - downloading of the response body. Defaults to 120 seconds. - - """ - self.debug = debug - log.debug('proxyclient.__init__:entered') - - # When we are instantiated, go ahead and silence the python-requests - # log. It is kind of noisy in our app server logs. - if not debug: - requests_log = logging.getLogger("requests") - requests_log.setLevel(logging.WARN) - - if base_url[-1] != '/': - base_url = base_url + '/' - self.base_url = base_url - self.login_url = login_url or urljoin(self.base_url, '/login') - self.domain = urlparse(self.base_url).netloc - self.useragent = useragent or 'Fedora ProxyClient/%(version)s' % { - 'version': __version__} - self.session_name = session_name - self.insecure = insecure - self.openid_insecure = openid_insecure - - # Have to do retries and timeout default values this way as BaseClient - # sends None for these values if not overridden by the user. - if retries is None: - self.retries = 0 - else: - self.retries = retries - if timeout is None: - self.timeout = 120.0 - else: - self.timeout = timeout - log.debug('proxyclient.__init__:exited') - - def __get_debug(self): - """Return whether we have debug logging turned on. - - :Returns: True if debugging is on, False otherwise. - - """ - if log.level <= logging.DEBUG: - return True - return False - - def __set_debug(self, debug=False): - """Change debug level. - - :kwarg debug: A true value to turn debugging on, false value to turn it - off. - """ - if debug: - log.setLevel(logging.DEBUG) - else: - log.setLevel(logging.ERROR) - - debug = property(__get_debug, __set_debug, doc=""" - When True, we log extra debugging statements. When False, we only log - errors. - """) - - def login(self, username, password, otp=None): - """ Open a session for the user. - - Log in the user with the specified username and password - against the FAS OpenID server. - - :arg username: the FAS username of the user that wants to log in - :arg password: the FAS password of the user that wants to log in - :kwarg otp: currently unused. Eventually a way to send an otp to the - API that the API can use. - :return: a tuple containing both the response from the OpenID - provider and the session used to by this provider. - - """ - session = requests.session() - response = openid_login( - session=session, - login_url=self.login_url, - username=username, - password=password, - otp=otp, - openid_insecure=self.openid_insecure) - return (response, session) - - def send_request(self, method, verb='POST', req_params=None, - auth_params=None, file_params=None, retries=None, - timeout=None, headers=None): - """Make an HTTP request to a server method. - - The given method is called with any parameters set in ``req_params``. - If auth is True, then the request is made with an authenticated - session cookie. Note that path parameters should be set by adding - onto the method, not via ``req_params``. - - :arg method: Method to call on the server. It's a url fragment that - comes after the base_url set in __init__(). Note that any - parameters set as extra path information should be listed here, - not in ``req_params``. - :kwarg req_params: dict containing extra parameters to send to the - server - :kwarg auth_params: dict containing one or more means of - authenticating to the server. Valid entries in this dict are: - - :cookie: **Deprecated** Use ``session_id`` instead. If both - ``cookie`` and ``session_id`` are set, only ``session_id`` - will be used. A ``Cookie.SimpleCookie`` to send as a - session cookie to the server - :session_id: Session id to put in a cookie to construct an - identity for the server - :username: Username to send to the server - :password: Password to use with username to send to the server - :httpauth: If set to ``basic`` then use HTTP Basic Authentication - to send the username and password to the server. This may - be extended in the future to support other httpauth types - than ``basic``. - - Note that cookie can be sent alone but if one of username or - password is set the other must as well. Code can set all of - these if it wants and all of them will be sent to the server. - Be careful of sending cookies that do not match with the - username in this case as the server can decide what to do in - this case. - :kwarg file_params: dict of files where the key is the name of the - file field used in the remote method and the value is the local - path of the file to be uploaded. If you want to pass multiple - files to a single file field, pass the paths as a list of paths. - :kwarg retries: if we get an unknown or possibly transient error - from the server, retry this many times. Setting this to a - negative number makes it try forever. Default to use the - :attr:`retries` value set on the instance or in :meth:`__init__`. - :kwarg timeout: A float describing the timeout of the connection. - The timeout only affects the connection process itself, not the - downloading of the response body. Defaults to the :attr:`timeout` - value set on the instance or in :meth:`__init__`. - :kwarg headers: A dictionary containing specific headers to add to - the request made. - :returns: A tuple of session_id and data. - :rtype: tuple of session information and data from server - - """ - log.debug('openidproxyclient.send_request: entered') - - # parameter mangling - file_params = file_params or {} - - # Check whether we need to authenticate for this request - session_id = None - username = None - password = None - if auth_params: - if 'session_id' in auth_params: - session_id = auth_params['session_id'] - if 'username' in auth_params and 'password' in auth_params: - username = auth_params['username'] - password = auth_params['password'] - elif 'username' in auth_params or 'password' in auth_params: - raise AuthError( - 'username and password must both be set in auth_params' - ) - if not (session_id or username): - raise AuthError( - 'No known authentication methods specified: set ' - '"cookie" in auth_params or set both username and ' - 'password in auth_params') - - # urljoin is slightly different than os.path.join(). Make sure - # method will work with it. - method = method.lstrip('/') - # And join to make our url. - url = urljoin(self.base_url, quote(method)) - - # Set standard headers - if headers: - if not 'User-agent' in headers: - headers['User-agent'] = self.useragent - if not 'Accept' in headers: - headers['Accept'] = 'application/json' - else: - headers = { - 'User-agent': self.useragent, - 'Accept': 'application/json', - } - - # Files to upload - for field_name, local_file_name in file_params: - file_params[field_name] = open(local_file_name, 'rb') - - cookies = requests.cookies.RequestsCookieJar() - # If we have a session_id, send it - if session_id: - # Anytime the session_id exists, send it so that visit tracking - # works. Will also authenticate us if there's a need. Note - # that there's no need to set other cookie attributes because - # this is a cookie generated client-side. - cookies.set(self.session_name, session_id) - - complete_params = req_params or {} - - auth = None - if username and password: - # OpenID login - resp, session = self.login(username, password, otp=None) - cookies = session.cookies - - # If debug, give people our debug info - log.debug('Creating request %s', to_bytes(url)) - log.debug('Headers: %s', to_bytes(headers, nonstring='simplerepr')) - if self.debug and complete_params: - debug_data = copy.deepcopy(complete_params) - - if 'password' in debug_data: - debug_data['password'] = 'xxxxxxx' - - log.debug('Data: %r', debug_data) - - if retries is None: - retries = self.retries - - if timeout is None: - timeout = self.timeout - - num_tries = 0 - while True: - try: - response = session.request( - method=verb, - url=url, - data=complete_params, - cookies=cookies, - headers=headers, - auth=auth, - verify=not self.insecure, - timeout=timeout, - ) - except (requests.Timeout, requests.exceptions.SSLError) as err: - if isinstance(err, requests.exceptions.SSLError): - # And now we know how not to code a library exception - # hierarchy... We're expecting that requests is raising - # the following stupidity: - # requests.exceptions.SSLError( - # urllib3.exceptions.SSLError( - # ssl.SSLError('The read operation timed out'))) - # If we weren't interested in reraising the exception with - # full traceback we could use a try: except instead of - # this gross conditional. But we need to code defensively - # because we don't want to raise an unrelated exception - # here and if requests/urllib3 can do this sort of - # nonsense, they may change the nonsense in the future - if not (err.args and isinstance( - err.args[0], urllib3.exceptions.SSLError) - and err.args[0].args - and isinstance(err.args[0].args[0], ssl.SSLError) - and err.args[0].args[0].args - and 'timed out' in err.args[0].args[0].args[0]): - # We're only interested in timeouts here - raise - log.debug('Request timed out') - if retries < 0 or num_tries < retries: - num_tries += 1 - log.debug('Attempt #%s failed', num_tries) - time.sleep(0.5) - continue - # Fail and raise an error - # Raising our own exception protects the user from the - # implementation detail of requests vs pycurl vs urllib - raise ServerError( - url, -1, 'Request timed out after %s seconds' % timeout) - - # When the python-requests module gets a response, it attempts to - # guess the encoding using chardet (or a fork) - # That process can take an extraordinarily long time for long - # response.text strings.. upwards of 30 minutes for FAS queries to - # /accounts/user/list JSON api! Therefore, we cut that codepath - # off at the pass by assuming that the response is 'utf-8'. We can - # make that assumption because we're only interfacing with servers - # that we run (and we know that they all return responses - # encoded 'utf-8'). - response.encoding = 'utf-8' - - # Check for auth failures - # Note: old TG apps returned 403 Forbidden on authentication - # failures. - # Updated apps return 401 Unauthorized - # We need to accept both until all apps are updated to return 401. - http_status = response.status_code - if http_status in (401, 403): - # Wrong username or password - log.debug('Authentication failed logging in') - raise AuthError( - 'Unable to log into server. Invalid ' - 'authentication tokens. Send new username and password' - ) - elif http_status >= 400: - if retries < 0 or num_tries < retries: - # Retry the request - num_tries += 1 - log.debug('Attempt #%s failed', num_tries) - time.sleep(0.5) - continue - # Fail and raise an error - try: - msg = httplib.responses[http_status] - except (KeyError, AttributeError): - msg = 'Unknown HTTP Server Response' - raise ServerError(url, http_status, msg) - # Successfully returned data - break - - # In case the server returned a new session cookie to us - new_session = session.cookies.get(self.session_name, '') - - log.debug('openidproxyclient.send_request: exited') - #data = munchify(data) - return new_session, response - - -__all__ = (OpenIdProxyClient,) diff --git a/fedora/client/proxyclient.py b/fedora/client/proxyclient.py deleted file mode 100644 index 912a9e4d..00000000 --- a/fedora/client/proxyclient.py +++ /dev/null @@ -1,509 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2013 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -'''Implement a class that sets up simple communication to a Fedora Service. - -.. moduleauthor:: Luke Macken -.. moduleauthor:: Toshio Kuratomi -.. moduleauthor:: Ralph Bean -''' - -import copy -from hashlib import sha1 -import logging -# For handling an exception that's coming from requests: -import ssl -import time -import warnings - -from munch import munchify -from kitchen.text.converters import to_bytes -import requests -from six.moves import http_client as httplib -from six.moves import http_cookies as Cookie -from six.moves.urllib.parse import quote, urljoin, urlparse - -from fedora import __version__ -from fedora.client import AppError, AuthError, ServerError - -log = logging.getLogger(__name__) - - -class ProxyClient(object): - # pylint: disable-msg=R0903 - ''' - A client to a Fedora Service. This class is optimized to proxy multiple - users to a service. ProxyClient is designed to be threadsafe so that - code can instantiate one instance of the class and use it for multiple - requests for different users from different threads. - - If you want something that can manage one user's connection to a Fedora - Service, then look into using BaseClient instead. - - This class has several attributes. These may be changed after - instantiation however, please note that this class is intended to be - threadsafe. Changing these values when another thread may affect more - than just the thread that you are making the change in. (For instance, - changing the debug option could cause other threads to start logging debug - messages in the middle of a method.) - - .. attribute:: base_url - - Initial portion of the url to contact the server. It is highly - recommended not to change this value unless you know that no other - threads are accessing this :class:`ProxyClient` instance. - - .. attribute:: useragent - - Changes the useragent string that is reported to the web server. - - .. attribute:: session_name - - Name of the cookie that holds the authentication value. - - .. attribute:: session_as_cookie - - If :data:`True`, then the session information is saved locally as - a cookie. This is here for backwards compatibility. New code should - set this to :data:`False` when constructing the :class:`ProxyClient`. - - .. attribute:: debug - - If :data:`True`, then more verbose logging is performed to aid in - debugging issues. - - .. attribute:: insecure - - If :data:`True` then the connection to the server is not checked to be - sure that any SSL certificate information is valid. That means that - a remote host can lie about who it is. Useful for development but - should not be used in production code. - - .. attribute:: retries - - Setting this to a positive integer will retry failed requests to the - web server this many times. Setting to a negative integer will retry - forever. - - .. attribute:: timeout - - A float describing the timeout of the connection. The timeout only - affects the connection process itself, not the downloading of the - response body. Defaults to 120 seconds. - - .. versionchanged:: 0.3.33 - Added the timeout attribute - ''' - log = log - - def __init__(self, base_url, useragent=None, session_name='tg-visit', - session_as_cookie=True, debug=False, insecure=False, - retries=None, - timeout=None): - '''Create a client configured for a particular service. - - :arg base_url: Base of every URL used to contact the server - - :kwarg useragent: useragent string to use. If not given, default to - "Fedora ProxyClient/VERSION" - :kwarg session_name: name of the cookie to use with session handling - :kwarg session_as_cookie: If set to True, return the session as a - SimpleCookie. If False, return a session_id. This flag allows us - to maintain compatibility for the 0.3 branch. In 0.4, code will - have to deal with session_id's instead of cookies. - :kwarg debug: If True, log debug information - :kwarg insecure: If True, do not check server certificates against - their CA's. This means that man-in-the-middle attacks are - possible against the `BaseClient`. You might turn this option on - for testing against a local version of a server with a self-signed - certificate but it should be off in production. - :kwarg retries: if we get an unknown or possibly transient error from - the server, retry this many times. Setting this to a negative - number makes it try forever. Defaults to zero, no retries. - :kwarg timeout: A float describing the timeout of the connection. The - timeout only affects the connection process itself, not the - downloading of the response body. Defaults to 120 seconds. - - .. versionchanged:: 0.3.33 - Added the timeout kwarg - ''' - # Setup our logger - self._log_handler = logging.StreamHandler() - self.debug = debug - format = logging.Formatter("%(message)s") - self._log_handler.setFormatter(format) - self.log.addHandler(self._log_handler) - - # When we are instantiated, go ahead and silence the python-requests - # log. It is kind of noisy in our app server logs. - if not debug: - requests_log = logging.getLogger("requests") - requests_log.setLevel(logging.WARN) - - self.log.debug('proxyclient.__init__:entered') - if base_url[-1] != '/': - base_url = base_url + '/' - self.base_url = base_url - self.domain = urlparse(self.base_url).netloc - self.useragent = useragent or 'Fedora ProxyClient/%(version)s' % { - 'version': __version__} - self.session_name = session_name - self.session_as_cookie = session_as_cookie - if session_as_cookie: - warnings.warn( - 'Returning cookies from send_request() is' - ' deprecated and will be removed in 0.4. Please port your' - ' code to use a session_id instead by calling the ProxyClient' - ' constructor with session_as_cookie=False', - DeprecationWarning, stacklevel=2) - self.insecure = insecure - - # Have to do retries and timeout default values this way as BaseClient - # sends None for these values if not overridden by the user. - if retries is None: - self.retries = 0 - else: - self.retries = retries - if timeout is None: - self.timeout = 120.0 - else: - self.timeout = timeout - self.log.debug('proxyclient.__init__:exited') - - def __get_debug(self): - '''Return whether we have debug logging turned on. - - :Returns: True if debugging is on, False otherwise. - ''' - if self._log_handler.level <= logging.DEBUG: - return True - return False - - def __set_debug(self, debug=False): - '''Change debug level. - - :kwarg debug: A true value to turn debugging on, false value to turn it - off. - ''' - if debug: - self.log.setLevel(logging.DEBUG) - self._log_handler.setLevel(logging.DEBUG) - else: - self.log.setLevel(logging.ERROR) - self._log_handler.setLevel(logging.INFO) - - debug = property(__get_debug, __set_debug, doc=''' - When True, we log extra debugging statements. When False, we only log - errors. - ''') - - def send_request(self, method, req_params=None, auth_params=None, - file_params=None, retries=None, timeout=None): - '''Make an HTTP request to a server method. - - The given method is called with any parameters set in ``req_params``. - If auth is True, then the request is made with an authenticated session - cookie. Note that path parameters should be set by adding onto the - method, not via ``req_params``. - - :arg method: Method to call on the server. It's a url fragment that - comes after the base_url set in __init__(). Note that any - parameters set as extra path information should be listed here, - not in ``req_params``. - :kwarg req_params: dict containing extra parameters to send to the - server - :kwarg auth_params: dict containing one or more means of authenticating - to the server. Valid entries in this dict are: - - :cookie: **Deprecated** Use ``session_id`` instead. If both - ``cookie`` and ``session_id`` are set, only ``session_id`` will - be used. A ``Cookie.SimpleCookie`` to send as a session cookie - to the server - :session_id: Session id to put in a cookie to construct an identity - for the server - :username: Username to send to the server - :password: Password to use with username to send to the server - :httpauth: If set to ``basic`` then use HTTP Basic Authentication - to send the username and password to the server. This may be - extended in the future to support other httpauth types than - ``basic``. - - Note that cookie can be sent alone but if one of username or - password is set the other must as well. Code can set all of these - if it wants and all of them will be sent to the server. Be careful - of sending cookies that do not match with the username in this - case as the server can decide what to do in this case. - :kwarg file_params: dict of files where the key is the name of the - file field used in the remote method and the value is the local - path of the file to be uploaded. If you want to pass multiple - files to a single file field, pass the paths as a list of paths. - :kwarg retries: if we get an unknown or possibly transient error from - the server, retry this many times. Setting this to a negative - number makes it try forever. Default to use the :attr:`retries` - value set on the instance or in :meth:`__init__`. - :kwarg timeout: A float describing the timeout of the connection. The - timeout only affects the connection process itself, not the - downloading of the response body. Defaults to the :attr:`timeout` - value set on the instance or in :meth:`__init__`. - :returns: If ProxyClient is created with session_as_cookie=True (the - default), a tuple of session cookie and data from the server. - If ProxyClient was created with session_as_cookie=False, a tuple - of session_id and data instead. - :rtype: tuple of session information and data from server - - .. versionchanged:: 0.3.17 - No longer send tg_format=json parameter. We rely solely on the - Accept: application/json header now. - .. versionchanged:: 0.3.21 - * Return data as a Bunch instead of a DictContainer - * Add file_params to allow uploading files - .. versionchanged:: 0.3.33 - Added the timeout kwarg - ''' - self.log.debug('proxyclient.send_request: entered') - - # parameter mangling - file_params = file_params or {} - - # Check whether we need to authenticate for this request - session_id = None - username = None - password = None - if auth_params: - if 'session_id' in auth_params: - session_id = auth_params['session_id'] - elif 'cookie' in auth_params: - warnings.warn( - 'Giving a cookie to send_request() to' - ' authenticate is deprecated and will be removed in 0.4.' - ' Please port your code to use session_id instead.', - DeprecationWarning, stacklevel=2) - session_id = auth_params['cookie'].output(attrs=[], - header='').strip() - if 'username' in auth_params and 'password' in auth_params: - username = auth_params['username'] - password = auth_params['password'] - elif 'username' in auth_params or 'password' in auth_params: - raise AuthError('username and password must both be set in' - ' auth_params') - if not (session_id or username): - raise AuthError( - 'No known authentication methods' - ' specified: set "cookie" in auth_params or set both' - ' username and password in auth_params') - - # urljoin is slightly different than os.path.join(). Make sure method - # will work with it. - method = method.lstrip('/') - # And join to make our url. - url = urljoin(self.base_url, quote(method)) - - data = None # decoded JSON via json.load() - - # Set standard headers - headers = { - 'User-agent': self.useragent, - 'Accept': 'application/json', - } - - # Files to upload - for field_name, local_file_name in file_params: - file_params[field_name] = open(local_file_name, 'rb') - - cookies = requests.cookies.RequestsCookieJar() - # If we have a session_id, send it - if session_id: - # Anytime the session_id exists, send it so that visit tracking - # works. Will also authenticate us if there's a need. Note that - # there's no need to set other cookie attributes because this is a - # cookie generated client-side. - cookies.set(self.session_name, session_id) - - complete_params = req_params or {} - if session_id: - # Add the csrf protection token - token = sha1(to_bytes(session_id)) - complete_params.update({'_csrf_token': token.hexdigest()}) - - auth = None - if username and password: - if auth_params.get('httpauth', '').lower() == 'basic': - # HTTP Basic auth login - auth = (username, password) - else: - # TG login - # Adding this to the request data prevents it from being - # logged by apache. - complete_params.update({ - 'user_name': to_bytes(username), - 'password': to_bytes(password), - 'login': 'Login', - }) - - # If debug, give people our debug info - self.log.debug('Creating request %(url)s' % - {'url': to_bytes(url)}) - self.log.debug('Headers: %(header)s' % - {'header': to_bytes(headers, nonstring='simplerepr')}) - if self.debug and complete_params: - debug_data = copy.deepcopy(complete_params) - - if 'password' in debug_data: - debug_data['password'] = 'xxxxxxx' - - self.log.debug('Data: %r' % debug_data) - - if retries is None: - retries = self.retries - - if timeout is None: - timeout = self.timeout - - num_tries = 0 - while True: - try: - response = requests.post( - url, - data=complete_params, - cookies=cookies, - headers=headers, - auth=auth, - verify=not self.insecure, - timeout=timeout, - ) - except (requests.Timeout, requests.exceptions.SSLError) as e: - if isinstance(e, requests.exceptions.SSLError): - # And now we know how not to code a library exception - # hierarchy... We're expecting that requests is raising - # the following stupidity: - # requests.exceptions.SSLError( - # urllib3.exceptions.SSLError( - # ssl.SSLError('The read operation timed out'))) - # If we weren't interested in reraising the exception with - # full tracdeback we could use a try: except instead of - # this gross conditional. But we need to code defensively - # because we don't want to raise an unrelated exception - # here and if requests/urllib3 can do this sort of - # nonsense, they may change the nonsense in the future - # - # And a note on the __class__.__name__/__module__ parsing: - # On top of all the other things it does wrong, requests - # is bundling a copy of urllib3. Distros can unbundle it. - # But because of the bundling, python will think - # exceptions raised by the version downloaded by pypi - # (requests.packages.urllib3.exceptions.SSLError) are - # different than the exceptions raised by the unbundled - # distro version (urllib3.exceptions.SSLError). We could - # do a try: except trying to import both of these - # SSLErrors and then code to detect either one of them but - # the whole thing is just stupid. So we'll use a stupid - # hack of our own that (1) means we don't have to depend - # on urllib3 just for this exception and (2) is (slightly) - # less likely to break on the whims of the requests - # author. - if not (e.args - and e.args[0].__class__.__name__ == 'SSLError' - and e.args[0].__class__.__module__.endswith( - 'urllib3.exceptions') - and e.args[0].args - and isinstance(e.args[0].args[0], ssl.SSLError) - and e.args[0].args[0].args - and 'timed out' in e.args[0].args[0].args[0]): - # We're only interested in timeouts here - raise - self.log.debug('Request timed out') - if retries < 0 or num_tries < retries: - num_tries += 1 - self.log.debug( - 'Attempt #%(try)s failed' % {'try': num_tries}) - time.sleep(0.5) - continue - # Fail and raise an error - # Raising our own exception protects the user from the - # implementation detail of requests vs pycurl vs urllib - raise ServerError( - url, -1, 'Request timed out after %s seconds' % timeout) - - # When the python-requests module gets a response, it attempts to - # guess the encoding using chardet (or a fork) - # That process can take an extraordinarily long time for long - # response.text strings.. upwards of 30 minutes for FAS queries to - # /accounts/user/list JSON api! Therefore, we cut that codepath - # off at the pass by assuming that the response is 'utf-8'. We can - # make that assumption because we're only interfacing with servers - # that we run (and we know that they all return responses - # encoded 'utf-8'). - response.encoding = 'utf-8' - - # Check for auth failures - # Note: old TG apps returned 403 Forbidden on authentication - # failures. - # Updated apps return 401 Unauthorized - # We need to accept both until all apps are updated to return 401. - http_status = response.status_code - if http_status in (401, 403): - # Wrong username or password - self.log.debug('Authentication failed logging in') - raise AuthError( - 'Unable to log into server. Invalid' - ' authentication tokens. Send new username and password') - elif http_status >= 400: - if retries < 0 or num_tries < retries: - # Retry the request - num_tries += 1 - self.log.debug( - 'Attempt #%(try)s failed' % {'try': num_tries}) - time.sleep(0.5) - continue - # Fail and raise an error - try: - msg = httplib.responses[http_status] - except (KeyError, AttributeError): - msg = 'Unknown HTTP Server Response' - raise ServerError(url, http_status, msg) - # Successfully returned data - break - - # In case the server returned a new session cookie to us - new_session = response.cookies.get(self.session_name, '') - - try: - data = response.json() - except ValueError as e: - # The response wasn't JSON data - raise ServerError( - url, http_status, 'Error returned from' - ' json module while processing %(url)s: %(err)s' % - {'url': to_bytes(url), 'err': to_bytes(e)}) - - if 'exc' in data: - name = data.pop('exc') - message = data.pop('tg_flash') - raise AppError(name=name, message=message, extras=data) - - # If we need to return a cookie for deprecated code, convert it here - if self.session_as_cookie: - cookie = Cookie.SimpleCookie() - cookie[self.session_name] = new_session - new_session = cookie - - self.log.debug('proxyclient.send_request: exited') - data = munchify(data) - return new_session, data - -__all__ = (ProxyClient,) diff --git a/fedora/django/__init__.py b/fedora/django/__init__.py deleted file mode 100644 index 02b2c336..00000000 --- a/fedora/django/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009 Ignacio Vazquez-Abrams -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -.. moduleauthor:: Ignacio Vazquez-Abrams -.. moduleauthor:: Toshio Kuratomi -''' -from fedora.client import FasProxyClient - -from django.conf import settings - -connection = None - -if not connection: - connection = FasProxyClient( - base_url=settings.FAS_URL, useragent=settings.FAS_USERAGENT - ) - - -def person_by_id(userid): - sid, userinfo = connection.person_by_id( - userid, - {'username': settings.FAS_USERNAME, - 'password': settings.FAS_PASSWORD} - ) - return userinfo diff --git a/fedora/django/auth/__init__.py b/fedora/django/auth/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/fedora/django/auth/backends.py b/fedora/django/auth/backends.py deleted file mode 100644 index 92c7b5c9..00000000 --- a/fedora/django/auth/backends.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009 Ignacio Vazquez-Abrams -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -.. moduleauthor:: Ignacio Vazquez-Abrams -.. moduleauthor:: Toshio Kuratomi -''' -from fedora.client import AuthError -from fedora.django import connection, person_by_id -from fedora.django.auth.models import FasUser - -from django.contrib.auth.models import AnonymousUser -from django.contrib.auth.backends import ModelBackend - - -class FasBackend(ModelBackend): - def authenticate(self, username=None, password=None, - session_id=None): - try: - if session_id: - auth = {'session_id': session_id} - else: - auth = {'username': username, 'password': password} - session_id, userinfo = connection.get_user_info(auth_params=auth) - user = FasUser.objects.user_from_fas(userinfo) - user.session_id = session_id - if user.is_active: - return user - except AuthError: - pass - - def get_user(self, userid): - try: - userinfo = person_by_id(userid) - if userinfo: - return FasUser.objects.user_from_fas(userinfo) - except AuthError: - return AnonymousUser() diff --git a/fedora/django/auth/management/__init__.py b/fedora/django/auth/management/__init__.py deleted file mode 100644 index c023b502..00000000 --- a/fedora/django/auth/management/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009 Ignacio Vazquez-Abrams -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -.. moduleauthor:: Ignacio Vazquez-Abrams -''' -from fedora.django.auth import models - -from django.db.models.signals import post_syncdb - -post_syncdb.connect(models._syncdb_handler, sender=models) diff --git a/fedora/django/auth/middleware.py b/fedora/django/auth/middleware.py deleted file mode 100644 index ede99ec0..00000000 --- a/fedora/django/auth/middleware.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009 Ignacio Vazquez-Abrams -# Copyright (C) 2012 Red Hat, Inc -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -.. moduleauthor:: Ignacio Vazquez-Abrams -.. moduleauthor:: Toshio Kuratomi - -.. note:: Toshio only added httponly cookie support - -.. versionchanged:: 0.3.26 - Made session cookies httponly -''' -from fedora.client import AuthError - -import django -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import AnonymousUser - - -class FasMiddleware(object): - def process_request(self, request): - # Retrieve the sessionid that the user gave, associating them with the - # account system session - sid = request.COOKIES.get('tg-visit', None) - - # Check if the session is still valid - authenticated = False - if sid: - user = authenticate(session_id=sid) - if user: - try: - login(request, user) - authenticated = True - except AuthError: - pass - - if not authenticated: - # Hack around misthought out djiblits/django interaction; - # If we're logging in in django and get to here without having - # the second factor of authentication, we need to logout the - # django kept session information. Since we can't know precisely - # what private information django might be keeping we need to use - # django API to remove everything. However, djiblits requires the - # request.session.test_cookie_worked() function in order to log - # someone in later. The django logout function has removed that - # function from the session attribute so the djiblit login fails. - # - # Save the necessary pieces of the session architecture here - cookie_status = request.session.test_cookie_worked() - logout(request) - # python doesn't have closures - if cookie_status: - request.session.test_cookie_worked = lambda: True - else: - request.session.test_cookie_worked = lambda: False - request.session.delete_test_cookie = lambda: None - - def process_response(self, request, response): - if response.status_code != 301: - if isinstance(request.user, AnonymousUser): - response.set_cookie(key='tg-visit', value='', max_age=0) - if 'tg-visit' in request.session: - del request.session['tg-visit'] - else: - try: - if django.VERSION[:2] <= (1, 3): - response.set_cookie( - 'tg-visit', - request.user.session_id, max_age=1814400, - path='/', secure=True) - else: - response.set_cookie( - 'tg-visit', - request.user.session_id, max_age=1814400, - path='/', secure=True, httponly=True) - except AttributeError: - # We expect that request.user.session_id won't be set - # if the user is logging in with a non-FAS account - # (ie: Django local auth). - pass - return response diff --git a/fedora/django/auth/models.py b/fedora/django/auth/models.py deleted file mode 100644 index 3edf216c..00000000 --- a/fedora/django/auth/models.py +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009 Ignacio Vazquez-Abrams -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -.. moduleauthor:: Ignacio Vazquez-Abrams -.. moduleauthor:: Toshio Kuratomi -''' -from __future__ import print_function -from fedora.client import AuthError -from fedora.django import connection, person_by_id -from fedora import _ - -import django.contrib.auth.models as authmodels -from django.conf import settings -import six - -# Map FAS user elements to model attributes -_fasmap = { - 'id': 'id', - 'username': 'username', - 'email': 'email', -} - - -def _new_group(group): - try: - g = authmodels.Group.objects.get(id=group['id']) - except authmodels.Group.DoesNotExist: - g = authmodels.Group(id=group['id']) - g.name = group['name'] - g.save() - return g - - -def _syncdb_handler(sender, **kwargs): - # Import FAS groups - verbosity = kwargs.get('verbosity', 1) - if verbosity > 0: - print(_('Loading FAS groups...')) - try: - gl = connection.group_list({'username': settings.FAS_USERNAME, - 'password': settings.FAS_PASSWORD}) - except AuthError: - if verbosity > 0: - print(_('Unable to load FAS groups. Did you set ' - 'FAS_USERNAME and FAS_PASSWORD?')) - else: - groups = gl[1]['groups'] - for group in groups: - _new_group(group) - if verbosity > 0: - print(_('FAS groups loaded. Don\'t forget to set ' - 'FAS_USERNAME and FAS_PASSWORD to a low-privilege ' - 'account.')) - - -class FasUserManager(authmodels.UserManager): - def user_from_fas(self, user): - """ - Creates a user in the table based on the structure returned - by FAS - """ - log = open('/var/tmp/django.log', 'a') - log.write('in models user_from_fas\n') - log.close() - d = {} - for k, v in six.iteritems(_fasmap): - d[v] = user[k] - u = FasUser(**d) - u.set_unusable_password() - u.is_active = user['status'] == 'active' - admin = (user['username'] in - getattr(settings, 'FAS_ADMINS', ())) - u.is_staff = admin - u.is_superuser = admin - if getattr(settings, 'FAS_GENERICEMAIL', True): - u.email = u._get_email() - u.save() - known_groups = [] - for group in u.groups.values(): - known_groups.append(group['id']) - - fas_groups = [] - for group in user['approved_memberships']: - fas_groups.append(group['id']) - - # This user has been added to one or more FAS groups - for group in (g for g in user['approved_memberships'] - if g['id'] not in known_groups): - newgroup = _new_group(group) - u.groups.add(newgroup) - - # This user has been removed from one or more FAS groups - for gid in known_groups: - found = False - for g in user['approved_memberships']: - if g['id'] == gid: - found = True - break - if not found: - u.groups.remove(authmodels.Group.objects.get(id=gid)) - - u.save() - return u - - -class FasUser(authmodels.User): - def _get_name(self): - log = open('/var/tmp/django.log', 'a') - log.write('in models _get_name\n') - log.close() - userinfo = person_by_id(self.id) - return userinfo['human_name'] - - def _get_email(self): - log = open('/var/tmp/django.log', 'a') - log.write('in models _get_email\n') - log.close() - return '%s@fedoraproject.org' % self.username - - name = property(_get_name) - - objects = FasUserManager() - - def get_full_name(self): - log = open('/var/tmp/django.log', 'a') - log.write('in models get_full_name\n') - log.close() - if self.name: - return self.name.strip() - return self.username.strip() diff --git a/fedora/tg/__init__.py b/fedora/tg/__init__.py deleted file mode 100644 index b5287694..00000000 --- a/fedora/tg/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -''' -Functions and classes to help build a Fedora Service. -''' - -__all__ = ('client', 'json', 'tg1utils', 'tg2utils', 'widgets', - 'identity', 'utils', 'visit') diff --git a/fedora/tg/client.py b/fedora/tg/client.py deleted file mode 100644 index cdccac66..00000000 --- a/fedora/tg/client.py +++ /dev/null @@ -1,11 +0,0 @@ -'''This is for compatibility. - -The canonical location for this module from 0.3 on is fedora.client -''' -import warnings - -warnings.warn('fedora.tg.client has moved to fedora.client. This location' - ' will disappear in 0.4', DeprecationWarning, stacklevel=2) - -# pylint: disable-msg=W0401,W0614 -from fedora.client import * diff --git a/fedora/tg/controllers.py b/fedora/tg/controllers.py deleted file mode 100644 index 21d280cc..00000000 --- a/fedora/tg/controllers.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008-2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Controller functions that are standard across Fedora Applications - - -.. moduleauthor:: Toshio Kuratomi -''' -from turbogears import flash -from turbogears import identity, redirect -from cherrypy import request, response - -from fedora.tg.utils import request_format - -from turbogears.i18n.utils import get_locale -from turbogears.i18n.tg_gettext import tg_gettext - - -def f_(msg): - ''' - Translate given message from current tg locale - - Parameters - :message: text to be translated - Returns: Translated message string - ''' - return tg_gettext(msg, get_locale(), 'python-fedora') - - -def login(forward_url=None, *args, **kwargs): - '''Page to become authenticated to the Account System. - - This shows a small login box to type in your username and password - from the Fedora Account System. - - To use this, replace your current login controller method with:: - - from fedora.controllers import login as fc_login - - @expose(template='yourapp.yourlogintemplate', allow_json=True) - def login(self, forward_url=None, *args, **kwargs): - login_dict = fc_login(forward_url, args, kwargs) - # Add anything to the return dict that you need for your app - return login_dict - - :kwarg: forward_url: The url to send to once authentication succeeds - ''' - if forward_url: - if isinstance(forward_url, list): - forward_url = forward_url.pop(0) - else: - del request.params['forward_url'] - - if not identity.current.anonymous and identity.was_login_attempted() \ - and not identity.get_identity_errors(): - # User is logged in - flash(f_('Welcome, %s') % identity.current.user_name) - if request_format() == 'json': - # When called as a json method, doesn't make any sense to redirect - # to a page. Returning the logged in identity is better. - return dict(user=identity.current.user, - _csrf_token=identity.current.csrf_token) - redirect(forward_url or '/') - - if identity.was_login_attempted(): - msg = f_('The credentials you supplied were not correct or ' - 'did not grant access to this resource.') - elif identity.get_identity_errors(): - msg = f_('You must provide your credentials before accessing ' - 'this resource.') - else: - msg = f_('Please log in.') - if not forward_url: - forward_url = request.headers.get('Referer', '/') - - response.status = 403 - return dict( - logging_in=True, message=msg, - forward_url=forward_url, previous_url=request.path_info, - original_parameters=request.params - ) - - -def logout(url=None): - '''Logout from the server. - - To use this, replace your current login controller method with:: - - from fedora.controllers import logout as fc_logout - - @expose(allow_json=True) - def logout(self): - return fc_logout() - - :kwarg url: If provided, when the user logs out, they will always be taken - to this url. This defaults to taking the user to the URL they were - at when they clicked logout. - ''' - identity.current.logout() - flash(f_('You have successfully logged out.')) - if request_format() == 'json': - return dict() - if not url: - url = request.headers.get('Referer', '/') - redirect(url) diff --git a/fedora/tg/identity/__init__.py b/fedora/tg/identity/__init__.py deleted file mode 100644 index fd220400..00000000 --- a/fedora/tg/identity/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -'''TurboGears identity modules that work with Fedora.''' - -__all__ = ('jsonfasprovider',) diff --git a/fedora/tg/identity/jsonfasprovider1.py b/fedora/tg/identity/jsonfasprovider1.py deleted file mode 100644 index a62f6ff6..00000000 --- a/fedora/tg/identity/jsonfasprovider1.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2008 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -'''**Deprecated** Use jsonfasprovider2 instead a it provides CSRF protection. - -This plugin provides integration with the Fedora Account System using -:term:`JSON` calls. - - -.. moduleauthor:: Toshio Kuratomi -.. moduleauthor:: Ricky Zhou -''' - -from cherrypy import response -from turbogears import config, identity -from kitchen.text.converters import to_bytes -from kitchen.pycompat24 import sets -sets.add_builtin_set() - -from fedora.client import BaseClient, FedoraServiceError - -from fedora import __version__ - -import crypt - -import logging -log = logging.getLogger('turbogears.identity.safasprovider') - - -class JsonFasIdentity(BaseClient): - ''' - Associate an identity with a person in the auth system. - ''' - cookie_name = config.get('visit.cookie.name', 'tg-visit') - fas_url = config.get( - 'fas.url', 'https://admin.fedoraproject.org/accounts/' - ) - useragent = 'JsonFasIdentity/%s' % __version__ - cache_session = False - - def __init__(self, visit_key, user=None, username=None, password=None): - if user: - self._user = user - self._groups = frozenset( - [g['name'] for g in user['approved_memberships']] - ) - self.visit_key = visit_key - if visit_key: - # Set the cookie to the user's tg_visit key before requesting - # authentication. That way we link the two together. - session_id = visit_key - else: - session_id = None - - debug = config.get('jsonfas.debug', False) - super(JsonFasIdentity, self).__init__( - self.fas_url, - useragent=self.useragent, debug=debug, - username=username, password=password, - session_id=session_id, cache_session=self.cache_session) - - if self.debug: - import inspect - caller = inspect.getouterframes(inspect.currentframe())[1][3] - log.debug('JsonFasIdentity.__init__ caller: %s' % caller) - - response.simple_cookie[self.cookie_name] = visit_key - - # Send a request so that we associate the visit_cookie with the user - self.send_request('', auth=True) - log.debug('Leaving JsonFasIdentity.__init__') - - def send_request(self, method, req_params=None, auth=False): - ''' - Make an HTTP Request to a server method. - - We need to override the send_request provided by ``BaseClient`` to - keep the visit_key in sync. - ''' - log.debug('Entering jsonfas send_request') - if self.session_id != self.visit_key: - # When the visit_key changes (because the old key had expired or - # been deleted from the db) change the visit_key in our variables - # and the session cookie to be sent back to the client. - self.visit_key = self.session_id - response.simple_cookie[self.cookie_name] = self.visit_key - log.debug('Leaving jsonfas send_request') - return super(JsonFasIdentity, self).send_request( - method, req_params=req_params, auth=auth) - - def _get_user(self): - '''Retrieve information about the user from cache or network.''' - # pylint: disable-msg=W0704 - try: - return self._user - except AttributeError: - # User hasn't already been set - pass - # pylint: enable-msg=W0704 - - if self.debug: - import inspect - caller = inspect.getouterframes(inspect.currentframe())[1][3] - log.debug('JSONFASPROVIDER.send_request caller: %s' % caller) - - # Attempt to load the user. After this code executes, there *WILL* be - # a _user attribute, even if the value is None. - # Query the account system URL for our given user's sessionCookie - # FAS returns user and group listing - # pylint: disable-msg=W0702 - try: - data = self.send_request('user/view', auth=True) - except: - # Any errors have to result in no user being set. The rest of the - # framework doesn't know what to do otherwise. - self._user = None - return None - # pylint: enable-msg=W0702 - if not data['person']: - self._user = None - return None - self._user = data['person'] - self._groups = frozenset( - [g['name'] for g in data['person']['approved_memberships']] - ) - return self._user - user = property(_get_user) - - def _get_user_name(self): - '''Return the username for the user.''' - if self.debug: - import inspect - caller = inspect.getouterframes(inspect.currentframe())[1][3] - log.debug('JsonFasProvider._get_user_name caller: %s' % caller) - - if not self.user: - return None - return self.user['username'] - user_name = property(_get_user_name) - - def _get_anonymous(self): - '''Return True if there's no user logged in.''' - return not self.user - anonymous = property(_get_anonymous) - - def _get_display_name(self): - '''Return the user's display name.''' - if not self.user: - return None - return self.user['human_name'] - display_name = property(_get_display_name) - - def _get_groups(self): - '''Return the groups that a user is a member of.''' - try: - return self._groups - except AttributeError: - # User and groups haven't been returned. Since the json call - # returns both user and groups, this is set at user creation time. - self._groups = frozenset() - return self._groups - groups = property(_get_groups) - - def logout(self): - ''' - Remove the link between this identity and the visit. - ''' - if not self.visit_key: - return - # Call Account System Server logout method - self.send_request('logout', auth=True) - - -class JsonFasIdentityProvider(object): - ''' - IdentityProvider that authenticates users against the fedora account system - ''' - def __init__(self): - # Default encryption algorithm is to use plain text passwords - algorithm = config.get( - 'identity.saprovider.encryption_algorithm', None - ) - # pylint: disable-msg=W0212 - # TG does this so we shouldn't get rid of it. - self.encrypt_password = lambda pw: identity._encrypt_password( - algorithm, pw) - - def create_provider_model(self): - ''' - Create the database tables if they don't already exist. - ''' - # No database tables to create because the db is behind the FAS2 - # server - pass - - def validate_identity(self, user_name, password, visit_key): - ''' - Look up the identity represented by user_name and determine whether the - password is correct. - - Must return either None if the credentials weren't valid or an object - with the following properties: - - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - ''' - # pylint: disable-msg=R0201 - # TG identity providers have this method so we can't get rid of it. - try: - user = JsonFasIdentity(visit_key, - username=user_name, - password=password) - except FedoraServiceError as e: - log.warning('Error logging in %(user)s: %(error)s' % { - 'user': to_bytes(user_name), 'error': to_bytes(e)}) - return None - - return user - - def validate_password(self, user, user_name, password): - ''' - Check the supplied user_name and password against existing credentials. - Note: user_name is not used here, but is required by external - password validation schemes that might override this method. - If you use SqlAlchemyIdentityProvider, but want to check the passwords - against an external source (i.e. PAM, LDAP, Windows domain, etc), - subclass SqlAlchemyIdentityProvider, and override this method. - - :arg user: User information. Not used. - :arg user_name: Given username. - :arg password: Given, plaintext password. - :returns: True if the password matches the username. Otherwise False. - Can return False for problems within the Account System as well. - ''' - # pylint: disable-msg=W0613,R0201 - # TG identity providers take user_name in case an external provider - # needs it so we can't get rid of it. - # TG identity providers have this method so we can't get rid of it. - return user.password == crypt.crypt(password, user.password) - - def load_identity(self, visit_key): - '''Lookup the principal represented by visit_key. - - :arg visit_key: The session key for whom we're looking up an identity. - :returns: an object with the following properties: - - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - ''' - # pylint: disable-msg=R0201 - # TG identity providers have this method so we can't get rid of it. - return JsonFasIdentity(visit_key) - - def anonymous_identity(self): - ''' - Must return an object with the following properties: - - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - ''' - # pylint: disable-msg=R0201 - # TG identity providers have this method so we can't get rid of it. - return JsonFasIdentity(None) - - def authenticated_identity(self, user): - ''' - Constructs Identity object for user that has no associated visit_key. - ''' - # pylint: disable-msg=R0201 - # TG identity providers have this method so we can't get rid of it. - return JsonFasIdentity(None, user) diff --git a/fedora/tg/identity/jsonfasprovider2.py b/fedora/tg/identity/jsonfasprovider2.py deleted file mode 100644 index 0a4e48a4..00000000 --- a/fedora/tg/identity/jsonfasprovider2.py +++ /dev/null @@ -1,507 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -# Adapted from code in the TurboGears project licensed under the MIT license. -# -''' -This plugin provides authentication by integrating with the Fedora Account -System using JSON calls. - - -.. moduleauthor:: Toshio Kuratomi -.. moduleauthor:: Ricky Zhou -''' - -import crypt -from hashlib import sha1 - -import six -from turbogears import config, identity -from turbogears.identity import set_login_attempted -import cherrypy -from kitchen.pycompat24 import sets -from kitchen.text.converters import to_bytes - -sets.add_builtin_set() - -from fedora.client import ( - AccountSystem, AuthError, BaseClient, - FedoraServiceError -) - -from fedora import __version__ - -import logging -log = logging.getLogger('turbogears.identity.jsonfasprovider') - -if config.get('identity.ssl', False): - fas_user = config.get('fas.username', None) - fas_password = config.get('fas.password', None) - if not (fas_user and fas_password): - raise identity.IdentityConfigurationException( - 'Cannot enable ssl certificate auth via identity.ssl' - ' without setting fas.usernamme and fas.password for' - ' authorization') - __url = config.get('fas.url', None) - if __url: - fas = AccountSystem(__url, username=config.get('fas.username'), - password=config.get('fas.password'), retries=3) - - -class JsonFasIdentity(BaseClient): - '''Associate an identity with a person in the auth system. - ''' - cookie_name = config.get('visit.cookie.name', 'tg-visit') - fas_url = config.get( - 'fas.url', - 'https://admin.fedoraproject.org/accounts/' - ) - useragent = 'JsonFasIdentity/%s' % __version__ - cache_session = False - - def __init__(self, visit_key=None, user=None, username=None, password=None, - using_ssl=False): - # The reason we have both _retrieved_user and _user is this: - # _user is set if both the user is authenticated and a csrf_token is - # present. - # _retrieved_user actually caches the user info from the server. - # Sometimes we have to determine if a user is only lacking a token, - # then retrieved_user comes in handy. - self._retrieved_user = None - self.log = log - self.visit_key = visit_key - session_id = visit_key - self._group_ids = frozenset() - self.using_ssl = using_ssl - if user: - self._user = user - self._user_retrieved = user - self._groups = frozenset( - [g['name'] for g in user['approved_memberships']] - ) - - debug = config.get('jsonfas.debug', False) - super(JsonFasIdentity, self).__init__( - self.fas_url, - useragent=self.useragent, debug=debug, - username=username, password=password, - session_id=session_id, cache_session=self.cache_session, - retries=3 - ) - - if self.debug: - import inspect - caller = inspect.getouterframes(inspect.currentframe())[1][3] - self.log.debug('JsonFasIdentity.__init__ caller: %s' % caller) - - cherrypy.response.simple_cookie[self.cookie_name] = visit_key - - self.login(using_ssl) - self.log.debug('Leaving JsonFasIdentity.__init__') - - def send_request(self, method, req_params=None, auth=False): - '''Make an HTTP Request to a server method. - - We need to override the send_request provided by ``BaseClient`` to - keep the visit_key in sync. - ''' - self.log.debug('entering jsonfas send_request') - if self.session_id != self.visit_key: - # When the visit_key changes (because the old key had expired or - # been deleted from the db) change the visit_key in our variables - # and the session cookie to be sent back to the client. - self.visit_key = self.session_id - cherrypy.response.simple_cookie[self.cookie_name] = self.visit_key - self.log.debug('leaving jsonfas send_request') - return super(JsonFasIdentity, self).send_request( - method, - req_params=req_params, - auth=auth, - retries=3 - ) - - def __retrieve_user(self): - '''Attempt to load the user from the visit_key. - - :returns: a user or None - ''' - if self.debug: - import inspect - caller = inspect.getouterframes(inspect.currentframe())[2][3] - self.log.debug('JSONFASPROVIDER.send_request caller: %s' % caller) - - # The cached value can be in four states: - # Holds a user: we successfully retrieved it last time, return it - # Holds None: we haven't yet tried to retrieve a user, do so now - # Holds a session_id that is the same as our session_id, we - # unsuccessfully tried to retrieve a session with this id already, - # return None Holds a session_id that is different than the current - # session_id: we tried with a previous session_id; try again with the - # new one. - if self._retrieved_user: - if isinstance(self._retrieved_user, six.string_types): - if self._retrieved_user == self.session_id: - return None - else: - self._retrieved_user = None - else: - return self._retrieved_user - # I hope this is a safe place to double-check the SSL variables. - # TODO: Double check my logic with this - is it unnecessary to - # check that the username matches up? - if self.using_ssl: - if cherrypy.request.headers['X-Client-Verify'] != 'SUCCESS': - self.logout() - return None - # Retrieve the user information differently when using ssl - try: - person = fas.person_by_username(self.username, auth=True) - except Exception as e: # pylint: disable-msg=W0703 - # :W0703: Any errors have to result in no user being set. The - # rest of the framework doesn't know what to do otherwise. - self.log.warning('jsonfasprovider, ssl, returned errors' - ' from send_request: %s' % to_bytes(e)) - person = None - self._retrieved_user = person or None - return self._retrieved_user - # pylint: disable-msg=W0702 - try: - data = self.send_request('user/view', auth=True) - except AuthError as e: - # Failed to login with present credentials - self._retrieved_user = self.session_id - return None - except Exception as e: # pylint: disable-msg=W0703 - # :W0703: Any errors have to result in no user being set. The rest - # of the framework doesn't know what to do otherwise. - self.log.warning('jsonfasprovider returned errors from' - ' send_request: %s' % to_bytes(e)) - return None - # pylint: enable-msg=W0702 - - self._retrieved_user = data['person'] or None - return self._retrieved_user - - def _get_user(self): - '''Get user instance for this identity.''' - visit = self.visit_key - if not visit: - # No visit, no user - self._user = None - else: - if not (self.username and self.password): - # Unless we were given the user_name and password to login on - # this request, a CSRF token is required - if (not '_csrf_token' in cherrypy.request.params or - cherrypy.request.params['_csrf_token'] != - sha1(self.visit_key).hexdigest()): - self.log.info("Bad _csrf_token") - if '_csrf_token' in cherrypy.request.params: - self.log.info("visit: %s token: %s" % ( - self.visit_key, - cherrypy.request.params['_csrf_token'])) - else: - self.log.info('No _csrf_token present') - cherrypy.request.fas_identity_failure_reason = 'bad_csrf' - self._user = None - - # pylint: disable-msg=W0704 - try: - return self._user - except AttributeError: - # User hasn't already been set - # Attempt to load the user. After this code executes, there - # *will* be a _user attribute, even if the value is None. - self._user = self.__retrieve_user() - - if self._user: - self._groups = frozenset( - [g['name'] for g in self._user.approved_memberships] - ) - else: - self._groups = frozenset() - - # pylint: enable-msg=W0704 - return self._user - user = property(_get_user) - - def _get_token(self): - '''Get the csrf token for this identity''' - if self.visit_key: - return sha1(self.visit_key).hexdigest() - else: - return '' - csrf_token = property(_get_token) - - def _get_user_name(self): - '''Get user name of this identity.''' - if self.debug: - import inspect - caller = inspect.getouterframes(inspect.currentframe())[1][3] - self.log.debug( - 'JsonFasProvider._get_user_name caller: %s' % caller) - - if not self.user: - return None - return self.user.username - user_name = property(_get_user_name) - - ### TG: Same as TG-1.0.8 - def _get_user_id(self): - ''' - Get user id of this identity. - ''' - if not self.user: - return None - return self.user.id - user_id = property(_get_user_id) - - ### TG: Same as TG-1.0.8 - def _get_anonymous(self): - ''' - Return True if not logged in. - ''' - return not self.user - anonymous = property(_get_anonymous) - - def _get_only_token(self): - ''' - In one specific instance in the login template we need to know whether - an anonymous user is just lacking a token. - ''' - if self.__retrieve_user(): - # user is valid, just the token is missing - return True - - # Else the user still has to login - return False - only_token = property(_get_only_token) - - def _get_permissions(self): - '''Get set of permission names of this identity.''' - # pylint: disable-msg=R0201 - # :R0201: method is part of the TG Identity API - ### TG difference: No permissions in FAS - return frozenset() - permissions = property(_get_permissions) - - def _get_display_name(self): - '''Return the user's display name. - - .. warning:: - This is not a TG standard attribute. Don't use this if you want - to be compatible with other identity providers. - ''' - if not self.user: - return None - return self.user['human_name'] - display_name = property(_get_display_name) - - def _get_groups(self): - '''Return the groups that a user is a member of.''' - try: - return self._groups - except AttributeError: # pylint: disable-msg=W0704 - # :W0704: Groups haven't been computed yet - pass - if not self.user: - # User and groups haven't been returned. Since the json call - # computes both user and groups, this will now be set. - self._groups = frozenset() - return self._groups - groups = property(_get_groups) - - def _get_group_ids(self): - '''Get set of group IDs of this identity.''' - try: - return self._group_ids - except AttributeError: # pylint: disable-msg=W0704 - # :W0704: Groups haven't been computed yet - pass - if not self.groups: - self._group_ids = frozenset() - else: - self._group_ids = frozenset([g.id for g in - self._user.approved_memberships]) - return self._group_ids - group_ids = property(_get_group_ids) - - ### TG: Same as TG-1.0.8 - def _get_login_url(self): - '''Get the URL for the login page.''' - return identity.get_failure_url() - login_url = property(_get_login_url) - - def login(self, using_ssl=False): - '''Send a request so that we associate the visit_cookie with the user - - :kwarg using_ssl: Boolean that tells whether ssl was used to - authenticate - ''' - if not using_ssl: - # This is only of value if we have username and password - # which we don't if using ssl certificates - self.send_request('', auth=True) - self.using_ssl = using_ssl - - def logout(self): - ''' - Remove the link between this identity and the visit. - ''' - if not self.visit_key: - return - # Call Account System Server logout method - self.send_request('logout', auth=True) - - -class JsonFasIdentityProvider(object): - ''' - IdentityProvider that authenticates users against the fedora account system - ''' - def __init__(self): - # Default encryption algorithm is to use plain text passwords - algorithm = config.get( - 'identity.saprovider.encryption_algorithm', None) - self.log = log - # pylint: disable-msg=W0212 - # TG does this so we shouldn't get rid of it. - self.encrypt_password = lambda pw: identity._encrypt_password( - algorithm, pw) - - def create_provider_model(self): - ''' - Create the database tables if they don't already exist. - ''' - # No database tables to create because the db is behind the FAS2 - # server - pass - - def validate_identity(self, user_name, password, visit_key): - ''' - Look up the identity represented by user_name and determine whether the - password is correct. - - Must return either None if the credentials weren't valid or an object - with the following properties: - - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - - :arg user_name: user_name we're authenticating. If None, we'll try - to lookup a username from SSL variables - :arg password: password to authenticate user_name with - :arg visit_key: visit_key from the user's session - ''' - using_ssl = False - if not user_name and config.get('identity.ssl'): - if cherrypy.request.headers['X-Client-Verify'] == 'SUCCESS': - user_name = cherrypy.request.headers['X-Client-CN'] - cherrypy.request.fas_provided_username = user_name - using_ssl = True - - # pylint: disable-msg=R0201 - # TG identity providers have this method so we can't get rid of it. - try: - user = JsonFasIdentity(visit_key, username=user_name, - password=password, using_ssl=using_ssl) - except FedoraServiceError as e: - self.log.warning('Error logging in %(user)s: %(error)s' % { - 'user': to_bytes(user_name), 'error': to_bytes(e)}) - return None - - return user - - def validate_password(self, user, user_name, password): - ''' - Check the supplied user_name and password against existing credentials. - Note: user_name is not used here, but is required by external - password validation schemes that might override this method. - If you use SqlAlchemyIdentityProvider, but want to check the passwords - against an external source (i.e. PAM, LDAP, Windows domain, etc), - subclass SqlAlchemyIdentityProvider, and override this method. - - :arg user: User information. - :arg user_name: Given username. Not used. - :arg password: Given, plaintext password. - :returns: True if the password matches the username. Otherwise False. - Can return False for problems within the Account System as well. - ''' - # pylint: disable-msg=R0201,W0613 - # :R0201: TG identity providers must instantiate this method. - - # crypt.crypt(stuff, '') == '' - # Just kill any possibility of blanks. - if not user.password: - return False - if not password: - return False - - # pylint: disable-msg=W0613 - # :W0613: TG identity providers have this method - return to_bytes(user.password) == crypt.crypt( - to_bytes(password), - to_bytes(user.password) - ) - - def load_identity(self, visit_key): - '''Lookup the principal represented by visit_key. - - :arg visit_key: The session key for whom we're looking up an identity. - :return: an object with the following properties: - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - ''' - # pylint: disable-msg=R0201 - # :R0201: TG identity providers must instantiate this method. - ident = JsonFasIdentity(visit_key) - if 'csrf_login' in cherrypy.request.params: - cherrypy.request.params.pop('csrf_login') - set_login_attempted(True) - return ident - - def anonymous_identity(self): - '''Returns an anonymous user object - - :return: an object with the following properties: - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - ''' - # pylint: disable-msg=R0201 - # :R0201: TG identity providers must instantiate this method. - return JsonFasIdentity(None) - - def authenticated_identity(self, user): - ''' - Constructs Identity object for user that has no associated visit_key. - - :arg user: The user structure the identity is constructed from - :return: an object with the following properties: - :user_name: original user name - :user: a provider dependant object (TG_User or similar) - :groups: a set of group IDs - :permissions: a set of permission IDs - ''' - # pylint: disable-msg=R0201 - # :R0201: TG identity providers must instantiate this method. - return JsonFasIdentity(None, user) diff --git a/fedora/tg/identity/soprovidercsrf.py b/fedora/tg/identity/soprovidercsrf.py deleted file mode 100644 index b9c50005..00000000 --- a/fedora/tg/identity/soprovidercsrf.py +++ /dev/null @@ -1,551 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009,2013 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -# Adapted from code in the TurboGears project licensed under the MIT license. -# -''' -This plugin provides authentication from a database using sqlobject. In -addition to the standard stuff provided by the TurboGears soprovider, it -adds a csrf_token attribute that can be used to protect the server from -csrf attacks. - -.. versionadded:: 0.3.14 - - -.. moduleauthor:: Toshio Kuratomi -''' -from datetime import datetime - -try: - from hashlib import sha1 as hash_constructor -except ImportError: - from sha import new as hash_constructor - - -from sqlobject import SQLObject, SQLObjectNotFound, RelatedJoin, \ - DateTimeCol, IntCol, StringCol, UnicodeCol -from sqlobject.inheritance import InheritableSQLObject - -import warnings -import logging -log = logging.getLogger("turbogears.identity.soprovider") - -import cherrypy -import turbogears -from turbogears import identity -from turbogears.database import PackageHub -from turbogears.util import load_class -from turbogears.identity import set_login_attempted -from turbojson.jsonify import jsonify_sqlobject, jsonify - -hub = PackageHub("turbogears.identity") -__connection__ = hub - -try: - set, frozenset -except NameError: # Python 2.3 - from sets import Set as set, ImmutableSet as frozenset - - -def to_db_encoding(s, encoding): - if isinstance(s, str): - pass - elif hasattr(s, '__unicode__'): - s = unicode(s) - if isinstance(s, unicode): - s = s.encode(encoding) - return s - - -class DeprecatedAttr(object): - def __init__(self, old_name, new_name): - self.old_name = old_name - self.new_name = new_name - - def __get__(self, obj, type=None): - warnings.warn("%s has been deprecated in favour of %s" % - (self.old_name, self.new_name), DeprecationWarning) - return getattr(obj, self.new_name) - - def __set__(self, obj, value): - warnings.warn( "%s has been deprecated in favour of %s" % - (self.old_name, self.new_name), DeprecationWarning) - return setattr(obj, self.new_name, value) - - -# Global class references -- -# these will be set when the provider is initialised. -user_class = None -group_class = None -permission_class = None -visit_class = None - - -class SqlObjectCsrfIdentity(object): - """Identity that uses a model from a database (via SQLObject).""" - - def __init__(self, visit_key=None, user=None): - # The reason we have both _retrieved_user and _user is this: - # _user is set if both the user is authenticated and a csrf_token is - # present. - # _retrieved_user actually caches the user info from the server. - # Sometimes we have to determine if a user is only lacking a token, - # then retrieved_user comes in handy. - self._retrieved_user = None - self.visit_key = visit_key - if user: - self._user = user - self._user_retrieved = user - if visit_key is not None: - self.login() - - def __retrieve_user(self): - '''Attempt to load the user from the visit_key - - :returns: a user or None - ''' - if self._retrieved_user: - return self._retrieved_user - - # Retrieve the user info from the db - visit = self.visit_link - if not visit: - return None - try: - self._retrieved_user = user_class.get(visit.user_id) - except SQLObjectNotFound: - log.warning("No such user with ID: %s", visit.user_id) - self._retrieved_user = None - return self._retrieved_user - - def _get_user(self): - """Get user instance for this identity.""" - try: - return self._user - except AttributeError: - # User hasn't already been set - pass - # Attempt to load the user. After this code executes, there *will* be - # a _user attribute, even if the value is None. - visit = self.visit_link - if not visit: - # No visit, no user - self._user = None - else: - # Unless we were given the user_name and password to login on - # this request, a CSRF token is required - if (not '_csrf_token' in cherrypy.request.params or - cherrypy.request.params['_csrf_token'] != - hash_constructor(self.visit_key).hexdigest()): - log.info("Bad _csrf_token") - if '_csrf_token' in cherrypy.request.params: - log.info("visit: %s token: %s" % (self.visit_key, - cherrypy.request.params['_csrf_token'])) - else: - log.info('No _csrf_token present') - cherrypy.request.fas_identity_failure_reason = 'bad_csrf' - self._user = None - try: - return self._user - except AttributeError: - # User hasn't already been set - # Attempt to load the user. After this code executes, there - # *will* be a _user attribute, even if the value is None. - self._user = self.__retrieve_user() - return self._user - user = property(_get_user) - - def _get_token(self): - '''Get the csrf token for this identity''' - if self.visit_key: - return hash_constructor(self.visit_key).hexdigest() - else: - return '' - csrf_token = property(_get_token) - - def _get_user_name(self): - """Get user name of this identity.""" - if not self.user: - return None - return self.user.user_name - user_name = property(_get_user_name) - - def _get_user_id(self): - """Get user id of this identity.""" - if not self.user: - return None - return self.user.id - user_id = property(_get_user_id) - - def _get_anonymous(self): - """Return true if not logged in.""" - return not self.user - anonymous = property(_get_anonymous) - - def _get_only_token(self): - ''' - In one specific instance in the login template we need to know whether - an anonymous user is just lacking a token. - ''' - if self.__retrieve_user(): - # user is valid, just the token is missing - return True - - # Else the user still has to login - return False - only_token = property(_get_only_token) - - def _get_permissions(self): - """Get set of permission names of this identity.""" - try: - return self._permissions - except AttributeError: - # Permissions haven't been computed yet - pass - if not self.user: - self._permissions = frozenset() - else: - self._permissions = frozenset( - [p.permission_name for p in self.user.permissions]) - return self._permissions - permissions = property(_get_permissions) - - def _get_groups(self): - """Get set of group names of this identity.""" - try: - return self._groups - except AttributeError: - # Groups haven't been computed yet - pass - if not self.user: - self._groups = frozenset() - else: - self._groups = frozenset([g.group_name for g in self.user.groups]) - return self._groups - groups = property(_get_groups) - - def _get_group_ids(self): - """Get set of group IDs of this identity.""" - try: - return self._group_ids - except AttributeError: - # Groups haven't been computed yet - pass - if not self.user: - self._group_ids = frozenset() - else: - self._group_ids = frozenset([g.id for g in self.user.groups]) - return self._group_ids - group_ids = property(_get_group_ids) - - def _get_visit_link(self): - """Get the visit link to this identity.""" - if self.visit_key is None: - return None - try: - return visit_class.by_visit_key(self.visit_key) - except SQLObjectNotFound: - return None - visit_link = property(_get_visit_link) - - def _get_login_url(self): - """Get the URL for the login page.""" - return identity.get_failure_url() - login_url = property(_get_login_url) - - def login(self): - """Set the link between this identity and the visit.""" - visit = self.visit_link - if visit: - visit.user_id = self._user.id - else: - visit = visit_class(visit_key=self.visit_key, user_id=self._user.id) - - def logout(self): - """Remove the link between this identity and the visit.""" - visit = self.visit_link - if visit: - visit.destroySelf() - # Clear the current identity - identity.set_current_identity(SqlObjectCsrfIdentity()) - - -class SqlObjectCsrfIdentityProvider(object): - """IdentityProvider that uses a model from a database (via SQLObject).""" - - def __init__(self): - super(SqlObjectCsrfIdentityProvider, self).__init__() - get = turbogears.config.get - - global user_class, group_class, permission_class, visit_class - - user_class_path = get("identity.soprovider.model.user", - __name__ + ".TG_User") - user_class = load_class(user_class_path) - if user_class: - log.info("Succesfully loaded \"%s\"" % user_class_path) - try: - self.user_class_db_encoding = \ - user_class.sqlmeta.columns['user_name'].dbEncoding or 'UTF-8' - except (KeyError, AttributeError): - self.user_class_db_encoding = 'UTF-8' - group_class_path = get("identity.soprovider.model.group", - __name__ + ".TG_Group") - group_class = load_class(group_class_path) - if group_class: - log.info("Succesfully loaded \"%s\"" % group_class_path) - - permission_class_path = get("identity.soprovider.model.permission", - __name__ + ".TG_Permission") - permission_class = load_class(permission_class_path) - if permission_class: - log.info("Succesfully loaded \"%s\"" % permission_class_path) - - visit_class_path = get("identity.soprovider.model.visit", - __name__ + ".TG_VisitIdentity") - visit_class = load_class(visit_class_path) - if visit_class: - log.info("Succesfully loaded \"%s\"" % visit_class_path) - - # Default encryption algorithm is to use plain text passwords - algorithm = get("identity.soprovider.encryption_algorithm", None) - self.encrypt_password = lambda pw: \ - identity._encrypt_password(algorithm, pw) - - def create_provider_model(self): - """Create the database tables if they don't already exist.""" - try: - hub.begin() - user_class.createTable(ifNotExists=True) - group_class.createTable(ifNotExists=True) - permission_class.createTable(ifNotExists=True) - visit_class.createTable(ifNotExists=True) - hub.commit() - hub.end() - except KeyError: - log.warning("No database is configured:" - " SqlObjectCsrfIdentityProvider is disabled.") - return - - def validate_identity(self, user_name, password, visit_key): - """Validate the identity represented by user_name using the password. - - Must return either None if the credentials weren't valid or an object - with the following properties: - user_name: original user name - user: a provider dependant object (TG_User or similar) - groups: a set of group names - permissions: a set of permission names - - """ - try: - user_name = to_db_encoding(user_name, self.user_class_db_encoding) - user = user_class.by_user_name(user_name) - if not self.validate_password(user, user_name, password): - log.info("Passwords don't match for user: %s", user_name) - return None - log.info("Associating user (%s) with visit (%s)", - user_name, visit_key) - return SqlObjectCsrfIdentity(visit_key, user) - except SQLObjectNotFound: - log.warning("No such user: %s", user_name) - return None - - def validate_password(self, user, user_name, password): - """Check the user_name and password against existing credentials. - - Note: user_name is not used here, but is required by external - password validation schemes that might override this method. - If you use SqlObjectCsrfIdentityProvider, but want to check the passwords - against an external source (i.e. PAM, a password file, Windows domain), - subclass SqlObjectCsrfIdentityProvider, and override this method. - - """ - return user.password == self.encrypt_password(password) - - def load_identity(self, visit_key): - """Lookup the principal represented by user_name. - - Return None if there is no principal for the given user ID. - - Must return an object with the following properties: - user_name: original user name - user: a provider dependant object (TG_User or similar) - groups: a set of group names - permissions: a set of permission names - - """ - ident = SqlObjectCsrfIdentity(visit_key) - if 'csrf_login' in cherrypy.request.params: - cherrypy.request.params.pop('csrf_login') - set_login_attempted(True) - return ident - - def anonymous_identity(self): - """Return anonymous identity. - - Must return an object with the following properties: - user_name: original user name - user: a provider dependant object (TG_User or similar) - groups: a set of group names - permissions: a set of permission names - - """ - return SqlObjectCsrfIdentity() - - def authenticated_identity(self, user): - """Constructs Identity object for users with no visit_key.""" - return SqlObjectCsrfIdentity(user=user) - - -class TG_VisitIdentity(SQLObject): - """A visit to your website.""" - class sqlmeta: - table = "tg_visit_identity" - - visit_key = StringCol(length=40, alternateID=True, - alternateMethodName="by_visit_key") - user_id = IntCol() - - -class TG_Group(InheritableSQLObject): - """An ultra-simple group definition.""" - class sqlmeta: - table = "tg_group" - - group_name = UnicodeCol(length=16, alternateID=True, - alternateMethodName="by_group_name") - display_name = UnicodeCol(length=255) - created = DateTimeCol(default=datetime.now) - - # Old names - groupId = DeprecatedAttr("groupId", "group_name") - displayName = DeprecatedAttr("displayName", "display_name") - - # collection of all users belonging to this group - users = RelatedJoin("TG_User", intermediateTable="tg_user_group", - joinColumn="group_id", otherColumn="user_id") - - # collection of all permissions for this group - permissions = RelatedJoin("TG_Permission", joinColumn="group_id", - intermediateTable="tg_group_permission", - otherColumn="permission_id") - -[jsonify.when('isinstance(obj, TG_Group)')] -def jsonify_group(obj): - """Convert group to JSON.""" - result = jsonify_sqlobject(obj) - result["users"] = [u.user_name for u in obj.users] - result["permissions"] = [p.permission_name for p in obj.permissions] - return result - - -class TG_User(InheritableSQLObject): - """Reasonably basic User definition.""" - class sqlmeta: - table = "tg_user" - - user_name = UnicodeCol(length=16, alternateID=True, - alternateMethodName="by_user_name") - email_address = UnicodeCol(length=255, alternateID=True, - alternateMethodName="by_email_address") - display_name = UnicodeCol(length=255) - password = UnicodeCol(length=40) - created = DateTimeCol(default=datetime.now) - - # Old attribute names - userId = DeprecatedAttr("userId", "user_name") - emailAddress = DeprecatedAttr("emailAddress", "email_address") - displayName = DeprecatedAttr("displayName", "display_name") - - # groups this user belongs to - groups = RelatedJoin("TG_Group", intermediateTable="tg_user_group", - joinColumn="user_id", otherColumn="group_id") - - def _get_permissions(self): - perms = set() - for g in self.groups: - perms = perms | set(g.permissions) - return perms - - def _set_password(self, cleartext_password): - """Run cleartext_password through the hash algorithm before saving.""" - try: - hash = identity.current_provider.encrypt_password( - cleartext_password) - except identity.exceptions.IdentityManagementNotEnabledException: - # Creating identity provider just to encrypt password - # (so we don't reimplement the encryption step). - ip = SqlObjectCsrfIdentityProvider() - hash = ip.encrypt_password(cleartext_password) - if hash == cleartext_password: - log.info("Identity provider not enabled," - " and no encryption algorithm specified in config." - " Setting password as plaintext.") - self._SO_set_password(hash) - - def set_password_raw(self, password): - """Save the password as-is to the database.""" - self._SO_set_password(password) - -[jsonify.when('isinstance(obj, TG_User)')] -def jsonify_user(obj): - """Convert user to JSON.""" - result = jsonify_sqlobject(obj) - del result['password'] - result["groups"] = [g.group_name for g in obj.groups] - result["permissions"] = [p.permission_name for p in obj.permissions] - return result - - -class TG_Permission(InheritableSQLObject): - """Permissions for a given group.""" - class sqlmeta: - table = "tg_permission" - - permission_name = UnicodeCol(length=16, alternateID=True, - alternateMethodName="by_permission_name") - description = UnicodeCol(length=255) - - # Old attributes - permissionId = DeprecatedAttr("permissionId", "permission_name") - - groups = RelatedJoin("TG_Group", intermediateTable="tg_group_permission", - joinColumn="permission_id", otherColumn="group_id") - -[jsonify.when('isinstance(obj, TG_Permission)')] -def jsonify_permission(obj): - """Convert permissions to JSON.""" - result = jsonify_sqlobject(obj) - result["groups"] = [g.group_name for g in obj.groups] - return result - - -def encrypt_password(cleartext_password): - """Encrypt given cleartext password.""" - try: - hash = identity.current_provider.encrypt_password(cleartext_password) - except identity.exceptions.RequestRequiredException: - # Creating identity provider just to encrypt password - # (so we don't reimplement the encryption step). - ip = SqlObjectCsrfIdentityProvider() - hash = ip.encrypt_password(cleartext_password) - if hash == cleartext_password: - log.info("Identity provider not enabled, and no encryption " - "algorithm specified in config. Setting password as plaintext.") - return hash diff --git a/fedora/tg/json.py b/fedora/tg/json.py deleted file mode 100644 index c7d60ceb..00000000 --- a/fedora/tg/json.py +++ /dev/null @@ -1,191 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2008 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -JSON Helper functions. Most JSON code directly related to classes is -implemented via the __json__() methods in model.py. These methods define -methods of transforming a class into json for a few common types. - -A JSON-based API(view) for your app. Most rules would look like:: - - @jsonify.when("isinstance(obj, YourClass)") - def jsonify_yourclass(obj): - return [obj.val1, obj.val2] - -@jsonify can convert your objects to following types: -lists, dicts, numbers and strings - -.. moduleauthor:: Toshio Kuratomi -''' - -# Pylint ignored messages -# :E1101: turbogears.jsonify monkey patches some functionality in. These do -# not show up when we pylint so it thinks the members di not exist. - -import sqlalchemy -import sqlalchemy.orm -import sqlalchemy.ext.associationproxy -import sqlalchemy.engine.base -from turbojson.jsonify import jsonify - - -class SABase(object): - '''Base class for SQLAlchemy mapped objects. - - This base class makes sure we have a __json__() method on each SQLAlchemy - mapped object that knows how to: - - 1) Return json for the object. - 2) Selectively add tables pulled in from the table to the data we're - returning. - ''' - # :R0903: The SABase object just adds a __json__ method to all SA mapped - # classes so they can be serialized as json. It's used as a base class - # and that's it. - # pylint: disable-msg=R0903 - def __json__(self): - '''Transform any SA mapped class into json. - - This method takes an SA mapped class and turns the "normal" python - attributes into json. The properties (from properties in the mapper) - are also included if they have an entry in json_props. You make - use of this by setting json_props in the controller. - - Example controller:: - - john = model.Person.get_by(name='John') - # Person has a property, addresses, linking it to an Address class. - # Address has a property, phone_nums, linking it to a Phone class. - john.json_props = {'Person': ['addresses'], - 'Address': ['phone_nums']} - return dict(person=john) - - json_props is a dict that maps class names to lists of properties you - want to output. This allows you to selectively pick properties you - are interested in for one class but not another. You are responsible - for avoiding loops. ie: *don't* do this:: - - john.json_props = {'Person': ['addresses'], 'Address': ['people']} - ''' - props = {} - prop_list = {} - if hasattr(self, 'json_props'): - for base_class in self.__class__.__mro__: - # pylint: disable-msg=E1101 - if base_class.__name__ in self.json_props: - prop_list = self.json_props[base_class.__name__] - break - # pylint: enable-msg=E1101 - - # Load all the columns from the table - for column in sqlalchemy.orm.object_mapper(self).iterate_properties: - if isinstance(column, sqlalchemy.orm.properties.ColumnProperty): - props[column.key] = getattr(self, column.key) - - # Load things that are explicitly listed - for field in prop_list: - props[field] = getattr(self, field) - try: - # pylint: disable-msg=E1101 - props[field].json_props = self.json_props - except AttributeError: # pylint: disable-msg=W0704 - # :W0704: Certain types of objects are terminal and won't - # allow setting json_props - pass - - # Note: Because of the architecture of simplejson and turbojson, - # anything that inherits from a builtin list, tuple, basestring, - # or dict but needs special handling needs to be specified - # expicitly here. Using the @jsonify.when() decorator won't work. - if isinstance(props[field], - sqlalchemy.orm.collections.InstrumentedList): - props[field] = jsonify_salist(props[field]) - - return props - - -@jsonify.when('isinstance(obj, sqlalchemy.orm.query.Query)') -def jsonify_sa_select_results(obj): - '''Transform selectresults into lists. - - The one special thing is that we bind the special json_props into each - descendent. This allows us to specify a json_props on the toplevel - query result and it will pass to all of its children. - - :arg obj: sqlalchemy Query object to jsonify - :Returns: list representation of the Query with each element in it given - a json_props attributes - ''' - if 'json_props' in obj.__dict__: - for element in obj: - element.json_props = obj.json_props - return list(obj) - - -# Note: due to the way simplejson works, InstrumentedList has to be taken care -# of explicitly in SABase's __json__() method. (This is true of any object -# derived from a builtin type (list, dict, tuple, etc)) -@jsonify.when('''( - isinstance(obj, sqlalchemy.orm.collections.InstrumentedList) or - isinstance(obj, sqlalchemy.orm.attributes.InstrumentedAttribute) or - isinstance(obj, sqlalchemy.ext.associationproxy._AssociationList))''') -def jsonify_salist(obj): - '''Transform SQLAlchemy InstrumentedLists into json. - - The one special thing is that we bind the special json_props into each - descendent. This allows us to specify a json_props on the toplevel - query result and it will pass to all of its children. - - :arg obj: One of the sqlalchemy list types to jsonify - :Returns: list of jsonified elements - ''' - if 'json_props' in obj.__dict__: - for element in obj: - element.json_props = obj.json_props - return [jsonify(element) for element in obj] - - -@jsonify.when('''( - isinstance(obj, sqlalchemy.engine.ResultProxy) - )''') -def jsonify_saresult(obj): - '''Transform SQLAlchemy ResultProxy into json. - - The one special thing is that we bind the special json_props into each - descendent. This allows us to specify a json_props on the toplevel - query result and it will pass to all of its children. - - :arg obj: sqlalchemy ResultProxy to jsonify - :Returns: list of jsonified elements - ''' - if 'json_props' in obj.__dict__: - for element in obj: - element.json_props = obj.json_props - return [list(row) for row in obj] - - -@jsonify.when('''(isinstance(obj, set))''') -def jsonify_set(obj): - '''Transform a set into a list. - - simplejson doesn't handle sets natively so transform a set into a list. - - :arg obj: set to jsonify - :Returns: list representation of the set - ''' - return list(obj) diff --git a/fedora/tg/templates/__init__.py b/fedora/tg/templates/__init__.py deleted file mode 100644 index b0c67307..00000000 --- a/fedora/tg/templates/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -''' -Templates to make adding templates for some common services easier. - - -.. moduleauthor:: Toshio Kuratomi - -.. versionadded:: 0.3.10 -''' diff --git a/fedora/tg/templates/genshi/__init__.py b/fedora/tg/templates/genshi/__init__.py deleted file mode 100644 index be88e913..00000000 --- a/fedora/tg/templates/genshi/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -Genshi version of templates to make adding certain Fedora widgets easier. - --------------------------------------------- -:mod:`fedora.tg.templates.genshi.login.html` --------------------------------------------- -.. module:: fedora.tg.templates.genshi.login.html - :synopsis: Templates related to logging in and out. -.. moduleauthor:: Toshio Kuratomi -.. versionadded:: 0.3.10 - - -Include this using:: - - -.. function:: loginform([message]) - -:message: Any text or elements contained by the tag will be shown - as a message to the user. This is generally used to show status of the - last login attempt ("Please provide your credentials", "Supplied - credentials were not correct", etc) - -A match template for the main login form. This is a :term:`CSRF` token-aware -login form that will prompt for username and password when no session identity -is present and ask the user to click a link if they merely lack a token. - -Typical usage would be:: - - ${message} - -.. function:: logintoolitem(@href=URL) - -:@href: If an href attribute is present for this tag, when a user is - logged in, their username or display_name will be a link to the URL. - -A match template to add an entry to a toolbar. The entry will contain the -user's username and a logout button if the user is logged in, a verify login -button if the user has a session cookie but not a :term:`CSRF` token, or a -login button if the user doesn't have a session cookie. - -Typical usage looks like this:: - -
    - -
- ------------------------------------------------- -:mod:`fedora.tg.templates.genshi.jsglobals.html` ------------------------------------------------- -.. module:: fedora.tg.templates.genshi.jsglobals.html - :synopsis: Templates to get information into javascript -.. moduleauthor:: Toshio Kuratomi -.. versionadded:: 0.3.10 - - -Include this using:: - - -.. function:: jsglobals() - -A match template to add global variables to a page. Typically, you'd include -this in your :file:`master.html` template and let it be added to every other -page from there. This adds the following variables in the fedora namespace -for other scripts to access: - - :fedora.baseurl: URL fragment to prepend to any calls to the application. - In a :term:`TurboGears` application, this is the scheme, host, and - server.webpath. Example: https://admin.fedoraproject.org/pkgdb/. - This may be a relative link. - :fedora.identity.anonymous: If ``true``, there will be no other variables - in the `fedora.identity` namespace. If ``false``, these variables are - defined: - :fedora.identity.userid: Numeric, unique identifier for the user - :fedora.identity.username: Publically visible unique identifier for the - user - :fedora.identity.display_name: Common human name for the user - :fedora.identity.token: csrf token for this user's session to be added to - urls that query the server. - -Typical usage would be:: - - -''' diff --git a/fedora/tg/templates/genshi/jsglobals.html b/fedora/tg/templates/genshi/jsglobals.html deleted file mode 100644 index 68042856..00000000 --- a/fedora/tg/templates/genshi/jsglobals.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/fedora/tg/templates/genshi/login.html b/fedora/tg/templates/genshi/login.html deleted file mode 100644 index ee0f73ae..00000000 --- a/fedora/tg/templates/genshi/login.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - -
- -
-
- - - -
- -
-
- - -
  • - ${f_('Welcome')} -
    - - - - - - - - - - -
    -
  • -
  • - ${f_('You are not logged in')} -
    - -
    -
  • -
  • - ${f_('CSRF protected')} -
    - -
    -
  • -
  • -
    - -
    -
  • -
    - diff --git a/fedora/tg/tg1utils.py b/fedora/tg/tg1utils.py deleted file mode 100644 index c70e5bb4..00000000 --- a/fedora/tg/tg1utils.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Miscellaneous functions of use on a TurboGears Server. This interface is -*deprecated* - -.. versionchanged:: 0.3.25 - Deprecated - -.. moduleauthor:: Toshio Kuratomi -''' -# Pylint Messages -# :W0401: This file is for backwards compatibility. It has been renamed to -# tg/utils. We use a wildcard import just to import the names for -# functions into this namespace. -# :W0614: Ditto. -from fedora.tg.utils import * # pylint:disable-msg=W0614,W0401 - -import warnings - -warnings.warn('fedora.tg.tg1utils is deprecated. Switch to' - ' fedora.tg.utils instead. This file will disappear in 0.4', - DeprecationWarning, stacklevel=2) - -__all__ = ('add_custom_stdvars', 'absolute_url', 'enable_csrf', - 'fedora_template', 'jsonify_validation_errors', 'json_or_redirect', - 'request_format', 'tg_absolute_url', 'tg_url', 'url') diff --git a/fedora/tg/tg2utils.py b/fedora/tg/tg2utils.py deleted file mode 100644 index 9ca22536..00000000 --- a/fedora/tg/tg2utils.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Miscellaneous functions of use on a TurboGears Server. This interface is -*deprecated* - -.. versionchanged:: 0.3.25 - Deprecated - -.. moduleauthor:: Toshio Kuratomi -''' -# Pylint Messages -# :W0401: This file is for backwards compatibility. It has been renamed to -# tg2/utils. We use a wildcard import just to import the names for -# functions into this namespace. -# :W0614: Ditto. -from fedora.tg2.utils import * # pylint:disable-msg=W0614,W0401 - -import warnings - -warnings.warn('fedora.tg.tg2utils is deprecated. Switch to' - ' fedora.tg2.utils instead. This file will disappear in 0.4', - DeprecationWarning, stacklevel=2) - -__all__ = ('add_fas_auth_middleware', 'enable_csrf', 'fedora_template', - 'tg_url', 'url') diff --git a/fedora/tg/util.py b/fedora/tg/util.py deleted file mode 100644 index 20abecee..00000000 --- a/fedora/tg/util.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008-2009 Red Hat, Inc. -# Copyright (C) 2008 Ricky Zhou -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Miscellaneous functions of use on a TurboGears Server. This interface is -*deprecated* - -.. versionchanged:: 0.3.17 - Deprecated - -.. moduleauthor:: Toshio Kuratomi -.. moduleauthor:: Ricky Zhou -''' -# Pylint Messages -# :W0401: This file is for backwards compatibility. It has been renamed to -# tg1utils. We use a wildcard import just to import the names for functions -# into this namespace. -# :W0614: Ditto. -from fedora.tg.tg1utils import * # pylint:disable-msg=W0614,W0401 - -import warnings - -warnings.warn('fedora.tg.util is deprecated. Switch to one of these ' - 'instead: TG1 apps: fedora.tg.utils TG2 apps: fedora.tg2.utils.' - ' This file will disappear in 0.4', - DeprecationWarning, stacklevel=2) - -__all__ = ('add_custom_stdvars', 'enable_csrf', 'fedora_template', - 'jsonify_validation_errors', 'json_or_redirect', 'request_format', - 'tg_url', 'url') diff --git a/fedora/tg/utils.py b/fedora/tg/utils.py deleted file mode 100644 index af16ca60..00000000 --- a/fedora/tg/utils.py +++ /dev/null @@ -1,435 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008-2012 Red Hat, Inc. -# Copyright (C) 2008 Ricky Zhou -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Miscellaneous functions of use on a TurboGears Server - -.. versionchanged:: 0.3.14 - Save the original turbogears.url function as :func:`fedora.tg.util.tg_url` - -.. versionchanged:: 0.3.17 - Renamed from fedora.tg.util - -.. versionchanged:: 0.3.25 - Renamed from fedora.tg.tg1utils - -.. moduleauthor:: Toshio Kuratomi -.. moduleauthor:: Ricky Zhou -''' -from itertools import chain -import cgi -import os - -import cherrypy -from cherrypy import request -from decorator import decorator -import pkg_resources -import turbogears -from turbogears import flash, redirect, config, identity -import turbogears.util as tg_util -from turbogears.controllers import check_app_root -from turbogears.identity.exceptions import RequestRequiredException -import six -from six.moves.urllib.parse import urlencode, urlparse, urlunparse - - -# Save this for people who need the original url() function -tg_url = turbogears.url - - -def add_custom_stdvars(new_vars): - return new_vars.update({'fedora_template': fedora_template}) - - -def url(tgpath, tgparams=None, **kwargs): - '''Computes URLs. - - This is a replacement for :func:`turbogears.controllers.url` (aka - :func:`tg.url` in the template). In addition to the functionality that - :func:`tg.url` provides, it adds a token to prevent :term:`CSRF` attacks. - - :arg tgpath: a list or a string. If the path is absolute (starts - with a "/"), the :attr:`server.webpath`, :envvar:`SCRIPT_NAME` and - the approot of the application are prepended to the path. In order for - the approot to be detected properly, the root object should extend - :class:`turbogears.controllers.RootController`. - :kwarg tgparams: See param: ``kwargs`` - :kwarg kwargs: Query parameters for the URL can be passed in as a - dictionary in the second argument *or* as keyword parameters. - Values which are a list or a tuple are used to create multiple - key-value pairs. - :returns: The changed path - - .. versionadded:: 0.3.10 - Modified from turbogears.controllers.url for :ref:`CSRF-Protection` - ''' - if not isinstance(tgpath, six.string_types): - tgpath = '/'.join(list(tgpath)) - if not tgpath.startswith('/') or tgpath.startswith('//'): - # Do not allow the url() function to be used for external urls. - # This function is primarily used in redirect() calls, so this prevents - # covert redirects and thus CSRF leaking. - tgpath = '/' - if tgpath.startswith('/'): - webpath = (config.get('server.webpath') or '').rstrip('/') - if tg_util.request_available(): - check_app_root() - tgpath = request.app_root + tgpath - try: - webpath += request.wsgi_environ['SCRIPT_NAME'].rstrip('/') - except (AttributeError, KeyError): # pylint: disable-msg=W0704 - # :W0704: Lack of wsgi environ is fine... we still have - # server.webpath - pass - tgpath = webpath + tgpath - if tgparams is None: - tgparams = kwargs - else: - try: - tgparams = tgparams.copy() - tgparams.update(kwargs) - except AttributeError: - raise TypeError( - 'url() expects a dictionary for query parameters') - args = [] - # Add the _csrf_token - try: - if identity.current.csrf_token: - tgparams.update({'_csrf_token': identity.current.csrf_token}) - except RequestRequiredException: # pylint: disable-msg=W0704 - # :W0704: If we are outside of a request (called from non-controller - # methods/ templates) just don't set the _csrf_token. - pass - - # Check for query params in the current url - query_params = six.iteritems(tgparams) - scheme, netloc, path, params, query_s, fragment = urlparse(tgpath) - if query_s: - query_params = chain((p for p in cgi.parse_qsl(query_s) if p[0] != - '_csrf_token'), query_params) - - for key, value in query_params: - if value is None: - continue - if isinstance(value, (list, tuple)): - pairs = [(key, v) for v in value] - else: - pairs = [(key, value)] - for key, value in pairs: - if value is None: - continue - if isinstance(value, unicode): - value = value.encode('utf8') - args.append((key, str(value))) - query_string = urlencode(args, True) - tgpath = urlunparse((scheme, netloc, path, params, query_string, fragment)) - return tgpath - - -# this is taken from turbogears 1.1 branch -def _get_server_name(): - """Return name of the server this application runs on. - - Respects 'Host' and 'X-Forwarded-Host' header. - - See the docstring of the 'absolute_url' function for more information. - - .. note:: This comes from turbogears 1.1 branch. It is only needed for - _tg_absolute_url(). If we find that turbogears.get_server_name() - exists, we replace this function with that one. - """ - get = config.get - h = request.headers - host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host')) - if not host: - host = '%s:%s' % (get('server.socket_host', 'localhost'), - get('server.socket_port', 8080)) - return host - - -# this is taken from turbogears 1.1 branch -def tg_absolute_url(tgpath='/', params=None, **kw): - """Return absolute URL (including schema and host to this server). - - Tries to account for 'Host' header and reverse proxying - ('X-Forwarded-Host'). - - The host name is determined this way: - - * If the config setting 'tg.url_domain' is set and non-null, use this - value. - * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is - True, use the value from the 'Host' or 'X-Forwarded-Host' request header. - * Else, if config setting 'base_url_filter.on' is True and - 'base_url_filter.base_url' is non-null, use its value for the host AND - scheme part of the URL. - * As a last fallback, use the value of 'server.socket_host' and - 'server.socket_port' config settings (defaults to 'localhost:8080'). - - The URL scheme ('http' or 'http') used is determined in the following way: - - * If 'base_url_filter.base_url' is used, use the scheme from this URL. - * If there is a 'X-Use-SSL' request header, use 'https'. - * Else, if the config setting 'tg.url_scheme' is set, use its value. - * Else, use the value of 'cherrypy.request.scheme'. - - .. note:: This comes from turbogears 1.1 branch with one change: we - call tg_url() rather than turbogears.url() so that it never adds the - csrf_token - - .. versionadded:: 0.3.19 - Modified from turbogears.absolute_url() for :ref:`CSRF-Protection` - """ - get = config.get - use_xfh = get('base_url_filter.use_x_forwarded_host', False) - if request.headers.get('X-Use-SSL'): - scheme = 'https' - else: - scheme = get('tg.url_scheme') - if not scheme: - scheme = request.scheme - base_url = '%s://%s' % (scheme, _get_server_name()) - if get('base_url_filter.on', False) and not use_xfh: - base_url = get('base_url_filter.base_url').rstrip('/') - return '%s%s' % (base_url, tg_url(tgpath, params, **kw)) - - -def absolute_url(tgpath='/', params=None, **kw): - """Return absolute URL (including schema and host to this server). - - Tries to account for 'Host' header and reverse proxying - ('X-Forwarded-Host'). - - The host name is determined this way: - - * If the config setting 'tg.url_domain' is set and non-null, use this - value. - * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is - True, use the value from the 'Host' or 'X-Forwarded-Host' request header. - * Else, if config setting 'base_url_filter.on' is True and - 'base_url_filter.base_url' is non-null, use its value for the host AND - scheme part of the URL. - * As a last fallback, use the value of 'server.socket_host' and - 'server.socket_port' config settings (defaults to 'localhost:8080'). - - The URL scheme ('http' or 'http') used is determined in the following way: - - * If 'base_url_filter.base_url' is used, use the scheme from this URL. - * If there is a 'X-Use-SSL' request header, use 'https'. - * Else, if the config setting 'tg.url_scheme' is set, use its value. - * Else, use the value of 'cherrypy.request.scheme'. - - .. versionadded:: 0.3.19 - Modified from turbogears.absolute_url() for :ref:`CSRF-Protection` - """ - return url(tg_absolute_url(tgpath, params, **kw)) - - -def enable_csrf(): - '''A startup function to setup :ref:`CSRF-Protection`. - - This should be run at application startup. Code like the following in the - start-APP script or the method in :file:`commands.py` that starts it:: - - from turbogears import startup - from fedora.tg.util import enable_csrf - startup.call_on_startup.append(enable_csrf) - - If we can get the :ref:`CSRF-Protection` into upstream :term:`TurboGears`, - we might be able to remove this in the future. - - .. versionadded:: 0.3.10 - Added to enable :ref:`CSRF-Protection` - ''' - # Override the turbogears.url function with our own - # Note, this also changes turbogears.absolute_url since that calls - # turbogears.url - turbogears.url = url - turbogears.controllers.url = url - - # Ignore the _csrf_token parameter - ignore = config.get('tg.ignore_parameters', []) - if '_csrf_token' not in ignore: - ignore.append('_csrf_token') - config.update({'tg.ignore_parameters': ignore}) - - # Add a function to the template tg stdvars that looks up a template. - turbogears.view.variable_providers.append(add_custom_stdvars) - - -def request_format(): - '''Return the output format that was requested by the user. - - The user is able to specify a specific output format using either the - ``Accept:`` HTTP header or the ``tg_format`` query parameter. This - function checks both of those to determine what format the reply should - be in. - - :rtype: string - :returns: The requested format. If none was specified, 'default' is - returned - - .. versionchanged:: 0.3.17 - Return symbolic names for json, html, xhtml, and xml instead of - letting raw mime types through - ''' - output_format = cherrypy.request.params.get('tg_format', '').lower() - if not output_format: - ### TODO: Two problems with this: - # 1) TG lets this be extended via as_format and accept_format. We need - # tie into that as well somehow. - # 2) Decide whether to standardize on "json" or "application/json" - accept = tg_util.simplify_http_accept_header( - request.headers.get('Accept', 'default').lower()) - if accept in ('text/javascript', 'application/json'): - output_format = 'json' - elif accept == 'text/html': - output_format = 'html' - elif accept == 'text/plain': - output_format = 'plain' - elif accept == 'text/xhtml': - output_format = 'xhtml' - elif accept == 'text/xml': - output_format = 'xml' - else: - output_format = accept - return output_format - - -def jsonify_validation_errors(): - '''Return an error for :term:`JSON` if validation failed. - - This function checks for two things: - - 1) We're expected to return :term:`JSON` data. - 2) There were errors in the validation process. - - If both of those are true, this function constructs a response that - will return the validation error messages as :term:`JSON` data. - - All controller methods that are error_handlers need to use this:: - - @expose(template='templates.numberform') - def enter_number(self, number): - errors = fedora.tg.util.jsonify_validation_errors() - if errors: - return errors - [...] - - @expose(allow_json=True) - @error_handler(enter_number) - @validate(form=number_form) - def save(self, number): - return dict(success=True) - - :rtype: None or dict - :Returns: None if there are no validation errors or :term:`JSON` isn't - requested, otherwise a dictionary with the error that's suitable for - return from the controller. The error message is set in tg_flash - whether :term:`JSON` was requested or not. - ''' - # Check for validation errors - errors = getattr(cherrypy.request, 'validation_errors', None) - if not errors: - return None - - # Set the message for both html and json output - message = u'\n'.join([u'%s: %s' % (param, msg) for param, msg in - errors.items()]) - format = request_format() - if format in ('html', 'xhtml'): - message.translate({ord('\n'): u'
    \n'}) - flash(message) - - # If json, return additional information to make this an exception - if format == 'json': - # Note: explicit setting of tg_template is needed in TG < 1.0.4.4 - # A fix has been applied for TG-1.0.4.5 - return dict(exc='Invalid', tg_template='json') - return None - - -def json_or_redirect(forward_url): - '''If :term:`JSON` is requested, return a dict, otherwise redirect. - - This is a decorator to use with a method that returns :term:`JSON` by - default. If :term:`JSON` is requested, then it will return the dict from - the method. If :term:`JSON` is not requested, it will redirect to the - given URL. The method that is decorated should be constructed so that it - calls turbogears.flash() with a message that will be displayed on the - forward_url page. - - Use it like this:: - - import turbogears - - @json_or_redirect('http://localhost/calc/') - @expose(allow_json=True) - def divide(self, dividend, divisor): - try: - answer = dividend * 1.0 / divisor - except ZeroDivisionError: - turbogears.flash('Division by zero not allowed') - return dict(exc='ZeroDivisionError') - turbogears.flash('The quotient is %s' % answer) - return dict(quotient=answer) - - In the example, we return either an exception or an answer, using - :func:`turbogears.flash` to tell people of the result in either case. If - :term:`JSON` data is requested, the user will get back a :term:`JSON` - string with the proper information. If html is requested, we will be - redirected to 'http://localhost/calc/' where the flashed message will be - displayed. - - :arg forward_url: If :term:`JSON` was not requested, redirect to this URL - after. - - .. versionadded:: 0.3.7 - To make writing methods that use validation easier - ''' - def call(func, *args, **kwargs): - if request_format() == 'json': - return func(*args, **kwargs) - else: - func(*args, **kwargs) - raise redirect(forward_url) - return decorator(call) - -if hasattr(turbogears, 'get_server_name'): - _get_server_name = turbogears.get_server_name - - -def fedora_template(template, template_type='genshi'): - '''Function to return the path to a template. - - :arg template: filename of the template itself. Ex: login.html - :kwarg template_type: template language we need the template written in - Defaults to 'genshi' - :returns: filesystem path to the template - ''' - # :E1101: pkg_resources does have resource_filename - # pylint: disable-msg=E1101 - return pkg_resources.resource_filename( - 'fedora', os.path.join('tg', - 'templates', template_type, template)) - -__all__ = ( - 'add_custom_stdvars', 'absolute_url', 'enable_csrf', - 'fedora_template', 'jsonify_validation_errors', 'json_or_redirect', - 'request_format', 'tg_absolute_url', 'tg_url', 'url') diff --git a/fedora/tg/visit/__init__.py b/fedora/tg/visit/__init__.py deleted file mode 100644 index 23c5a354..00000000 --- a/fedora/tg/visit/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -'''TurboGears visit manager to save visit in the Fedora Account System.''' - -__all__ = ('jsonfasvisit',) diff --git a/fedora/tg/visit/jsonfasvisit1.py b/fedora/tg/visit/jsonfasvisit1.py deleted file mode 100644 index 86833d5f..00000000 --- a/fedora/tg/visit/jsonfasvisit1.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2007-2008 Red Hat, Inc. All rights reserved. -# -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -# Adapted from code in the TurboGears project licensed under the MIT license. -''' -This plugin provides integration with the Fedora Account System using JSON -calls to the account system server. - - -.. moduleauthor:: Toshio Kuratomi -''' - -from turbogears import config -from turbogears.visit.api import Visit, BaseVisitManager - -from fedora.client import FasProxyClient - -from fedora import _, __version__ - -import logging -log = logging.getLogger("turbogears.identity.savisit") - - -class JsonFasVisitManager(BaseVisitManager): - ''' - This proxies visit requests to the Account System Server running remotely. - ''' - fas_url = config.get('fas.url', 'https://admin.fedoraproject.org/accounts') - fas = None - - def __init__(self, timeout): - self.debug = config.get('jsonfas.debug', False) - if not self.fas: - self.fas = FasProxyClient( - self.fas_url, debug=self.debug, - session_name=config.get('visit.cookie.name', 'tg-visit'), - useragent='JsonFasVisitManager/%s' % __version__) - BaseVisitManager.__init__(self, timeout) - log.debug('JsonFasVisitManager.__init__: exit') - - def create_model(self): - ''' - Create the Visit table if it doesn't already exist. - - Not needed as the visit tables reside remotely in the FAS2 database. - ''' - pass - - def new_visit_with_key(self, visit_key): - ''' - Return a new Visit object with the given key. - ''' - log.debug('JsonFasVisitManager.new_visit_with_key: enter') - # Hit any URL in fas2 with the visit_key set. That will call the - # new_visit method in fas2 - # We only need to get the session cookie from this request - request_data = self.fas.refresh_session(visit_key) - session_id = request_data[0] - log.debug('JsonFasVisitManager.new_visit_with_key: exit') - return Visit(session_id, True) - - def visit_for_key(self, visit_key): - ''' - Return the visit for this key or None if the visit doesn't exist or has - expired. - ''' - log.debug('JsonFasVisitManager.visit_for_key: enter') - # Hit any URL in fas2 with the visit_key set. That will call the - # new_visit method in fas2 - # We only need to get the session cookie from this request - request_data = self.fas.refresh_session(visit_key) - session_id = request_data[0] - - # Knowing what happens in turbogears/visit/api.py when this is called, - # we can shortcircuit this step and avoid a round trip to the FAS - # server. - # if visit_key != session_id: - # # visit has expired - # return None - # # Hitting FAS has already updated the visit. - # return Visit(visit_key, False) - log.debug('JsonFasVisitManager.visit_for_key: exit') - if visit_key != session_id: - return Visit(session_id, True) - else: - return Visit(visit_key, False) - - def update_queued_visits(self, queue): - '''Update the visit information on the server''' - log.debug('JsonFasVisitManager.update_queued_visits: enter') - # Hit any URL in fas with each visit_key to update the sessions - for visit_key in queue: - log.info(_('updating visit (%s)'), visit_key) - self.fas.refresh_session(visit_key) - log.debug('JsonFasVisitManager.update_queued_visits: exit') diff --git a/fedora/tg/visit/jsonfasvisit2.py b/fedora/tg/visit/jsonfasvisit2.py deleted file mode 100644 index 8bdb3f43..00000000 --- a/fedora/tg/visit/jsonfasvisit2.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2008 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -# Adapted from code in the TurboGears project licensed under the MIT license. -# -''' -This plugin provides integration with the Fedora Account System using JSON -calls to the account system server. - - -.. moduleauthor:: Toshio Kuratomi -''' -import threading - -from turbogears import config -from turbogears.visit.api import Visit, BaseVisitManager -from kitchen.text.converters import to_bytes - -from fedora.client import FasProxyClient - -from fedora import __version__ - -import logging -log = logging.getLogger("turbogears.identity.jsonfasvisit") - - -class JsonFasVisitManager(BaseVisitManager): - ''' - This proxies visit requests to the Account System Server running remotely. - ''' - fas_url = config.get('fas.url', 'https://admin.fedoraproject.org/accounts') - fas = None - debug = config.get('jsonfas.debug', False) - error_session_id = 0 - error_session_id_lock = threading.Lock() - - def __init__(self, timeout): - self.log = log - if not self.fas: - JsonFasVisitManager.fas = FasProxyClient( - self.fas_url, - debug=self.debug, - session_name=config.get('visit.cookie.name', 'tg-visit'), - useragent='JsonFasVisitManager/%s' % __version__, - retries=3 - ) - BaseVisitManager.__init__(self, timeout) - self.log.debug('JsonFasVisitManager.__init__: exit') - - def create_model(self): - ''' - Create the Visit table if it doesn't already exist. - - Not needed as the visit tables reside remotely in the FAS2 database. - ''' - pass - - def new_visit_with_key(self, visit_key): - ''' - Return a new Visit object with the given key. - ''' - self.log.debug('JsonFasVisitManager.new_visit_with_key: enter') - # Hit any URL in fas2 with the visit_key set. That will call the - # new_visit method in fas2 - # We only need to get the session cookie from this request - try: - request_data = self.fas.refresh_session(visit_key) - except Exception: - # HACK -- if we get an error from calling FAS, we still need to - # return something to the application - try: - JsonFasVisitManager.error_session_id_lock.acquire() - session_id = str(JsonFasVisitManager.error_session_id) - JsonFasVisitManager.error_session_id += 1 - finally: - JsonFasVisitManager.error_session_id_lock.release() - else: - session_id = request_data[0] - self.log.debug('JsonFasVisitManager.new_visit_with_key: exit') - return Visit(session_id, True) - - def visit_for_key(self, visit_key): - ''' - Return the visit for this key or None if the visit doesn't exist or has - expired. - ''' - self.log.debug('JsonFasVisitManager.visit_for_key: enter') - # Hit any URL in fas2 with the visit_key set. That will call the - # new_visit method in fas2 - # We only need to get the session cookie from this request - try: - request_data = self.fas.refresh_session(visit_key) - except Exception: - # HACK -- if we get an error from calling FAS, we still need to - # return something to the application - try: - JsonFasVisitManager.error_session_id_lock.acquire() - session_id = str(JsonFasVisitManager.error_session_id) - JsonFasVisitManager.error_session_id += 1 - finally: - JsonFasVisitManager.error_session_id_lock.release() - else: - session_id = request_data[0] - - # Knowing what happens in turbogears/visit/api.py when this is called, - # we can shortcircuit this step and avoid a round trip to the FAS - # server. - # if visit_key != session_id: - # # visit has expired - # return None - # # Hitting FAS has already updated the visit. - # return Visit(visit_key, False) - self.log.debug('JsonFasVisitManager.visit_for_key: exit') - if visit_key != session_id: - return Visit(session_id, True) - else: - return Visit(visit_key, False) - - def update_queued_visits(self, queue): - '''Update the visit information on the server''' - self.log.debug( - 'JsonFasVisitManager.update_queued_visits: %s' % len(queue)) - # Hit any URL in fas with each visit_key to update the sessions - for visit_key in queue: - self.log.info('updating visit (%s)', to_bytes(visit_key)) - try: - self.fas.refresh_session(visit_key) - except Exception: - # If fas returns an error, push the visit back onto the queue - try: - self.lock.acquire() - self.queue[visit_key] = queue[visit_key] - finally: - self.lock.release() - self.log.debug('JsonFasVisitManager.update_queued_visitsr exit') diff --git a/fedora/tg/widgets.py b/fedora/tg/widgets.py deleted file mode 100644 index c2f54db9..00000000 --- a/fedora/tg/widgets.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Proof-of-concept Fedora TurboGears widgets - -.. moduleauthor:: Luke Macken -''' - -import re -import feedparser -try: - import simplejson as json -except ImportError: - from . import json as json - -from bugzilla import Bugzilla -from six.moves.urllib.request import urlopen -from turbogears.widgets import Widget - - -class FedoraPeopleWidget(Widget): - '''Widget to display the Fedora People RSS Feed. - ''' - template = """ - - - - - -
    ${entry['title']}
    - """ - params = ["widget_id", "entries"] - - def __init__(self, widget_id=None): - self.widget_id = widget_id - self.entries = [] - regex = re.compile('') - feed = feedparser.parse('http://planet.fedoraproject.org/rss20.xml') - for entry in feed['entries']: - self.entries.append({ - 'link': entry['link'], - 'title': entry['title'], - 'image': regex.match(entry['summary']).group(1) - }) - - def __json__(self): - return {'id': self.widget_id, 'entries': self.entries} - - -class FedoraMaintainerWidget(Widget): - '''Widget to show the packages a maintainer owns. - ''' - url = "https://admin.fedoraproject.org/pkgdb/package/rpms/${pkg['name']}/" - template = """ - - - - -
    ${pkg['name']}
    - """ % url - params = ["widget_id", "packages"] - - def __init__(self, username, widget_id=None): - self.widget_id = widget_id - page = urlopen( - 'https://admin.fedoraproject.org/pkgdb/' - 'api/packager/package/%s/' % username - ) - page = json.load(page) - self.packages = page['point of contact'] + page['co-maintained'] - - def __json__(self): - return {'id': self.widget_id, 'packages': self.packages} - - -class BugzillaWidget(Widget): - '''Widget to show the stream of bugs submitted against a package.''' - template = """ - - - - -
    - ${bug.bug_id} ${bug.short_short_desc} -
    - """ - params = ["widget_id", "bugs"] - - def __init__(self, email, widget_id=None): - self.widget_id = widget_id - bugzilla = Bugzilla(url='https://bugzilla.redhat.com/xmlrpc.cgi') - # pylint: disable-msg=E1101 - # :E1101: Bugzilla class monkey patches itself with methods like - # query. - self.bugs = bugzilla.query({ - 'product': 'Fedora', - 'email1': email, - 'emailassigned_to1': True - })[:5] - # pylint: enable-msg=E1101 - - def __json__(self): - return {'id': self.widget_id, 'bugs': self.bugs} diff --git a/fedora/tg2/__init__.py b/fedora/tg2/__init__.py deleted file mode 100644 index f00fa0cf..00000000 --- a/fedora/tg2/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -''' -Functions and classes to help build a Fedora Service. -''' - -__all__ = ('templates', 'utils') diff --git a/fedora/tg2/templates/__init__.py b/fedora/tg2/templates/__init__.py deleted file mode 100644 index 6b190310..00000000 --- a/fedora/tg2/templates/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -''' -Templates to make adding templates for some common services easier. - - -.. moduleauthor:: Toshio Kuratomi - -.. versionadded:: 0.3.25 -''' diff --git a/fedora/tg2/templates/genshi/__init__.py b/fedora/tg2/templates/genshi/__init__.py deleted file mode 100644 index 5bc6b0f5..00000000 --- a/fedora/tg2/templates/genshi/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -Genshi version of templates to make adding certain Fedora widgets easier. - ---------------------------------------------- -:mod:`fedora.tg2.templates.genshi.login.html` ---------------------------------------------- -.. module:: fedora.tg2.templates.genshi.login.html - :synopsis: Templates related to logging in and out. -.. moduleauthor:: Toshio Kuratomi -.. versionadded:: 0.3.26 - - -Include this using:: - - -.. function:: loginform([message]) - -:message: Any text or elements contained by the tag will be shown - as a message to the user. This is generally used to show status of the - last login attempt ("Please provide your credentials", "Supplied - credentials were not correct", etc) - -A match template for the main login form. This is a :term:`CSRF` token-aware -login form that will prompt for username and password when no session identity -is present and ask the user to click a link if they merely lack a token. - -Typical usage would be:: - - ${message} - -.. function:: logintoolitem(@href=URL) - -:@href: If an href attribute is present for this tag, when a user is - logged in, their username or display_name will be a link to the URL. - -A match template to add an entry to a toolbar. The entry will contain the -user's username and a logout button if the user is logged in, a verify login -button if the user has a session cookie but not a :term:`CSRF` token, or a -login button if the user doesn't have a session cookie. - -Typical usage looks like this:: - -
      - -
    - -------------------------------------------------- -:mod:`fedora.tg2.templates.genshi.jsglobals.html` -------------------------------------------------- -.. module:: fedora.tg2.templates.genshi.jsglobals.html - :synopsis: Templates to get information into javascript -.. moduleauthor:: Toshio Kuratomi -.. versionadded:: 0.3.26 - - -Include this using:: - - -.. function:: jsglobals() - -A match template to add global variables to a page. Typically, you'd include -this in your :file:`master.html` template and let it be added to every other -page from there. This adds the following variables in the fedora namespace -for other scripts to access: - - :fedora.baseurl: URL fragment to prepend to any calls to the application. - In a :term:`TurboGears` application, this is the scheme, host, and - server.webpath. Example: https://admin.fedoraproject.org/pkgdb/. - This may be a relative link. - :fedora.identity.anonymous: If ``true``, there will be no other variables - in the `fedora.identity` namespace. If ``false``, these variables are - defined: - :fedora.identity.userid: Numeric, unique identifier for the user - :fedora.identity.username: Publically visible unique identifier for the - user - :fedora.identity.display_name: Common human name for the user - :fedora.identity.token: csrf token for this user's session to be added to - urls that query the server. - -Typical usage would be:: - - -''' diff --git a/fedora/tg2/templates/genshi/jsglobals.html b/fedora/tg2/templates/genshi/jsglobals.html deleted file mode 100644 index bdebaddb..00000000 --- a/fedora/tg2/templates/genshi/jsglobals.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/fedora/tg2/templates/genshi/login.html b/fedora/tg2/templates/genshi/login.html deleted file mode 100644 index 33689b5d..00000000 --- a/fedora/tg2/templates/genshi/login.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - -
    -
  • - ${_('Welcome')} -
    - - - - - - - - -
    -
  • -
  • - ${_('You are not logged in')} -
    - -
    -
  • -
  • - ${_('CSRF protected')} -
    - -
    -
  • -
  • -
    - -
    -
  • -
    -
    - diff --git a/fedora/tg2/templates/mako/__init__.py b/fedora/tg2/templates/mako/__init__.py deleted file mode 100644 index 759ca5da..00000000 --- a/fedora/tg2/templates/mako/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -Mako version of templates to make adding certain Fedora widgets easier. - ------------------------------------------- -:mod:`fedora.tg2.templates.mako.login.mak` ------------------------------------------- -.. module:: fedora.tg2.templates.mako.login.mak - :synopsis: Templates related to logging in and out. -.. moduleauthor:: Toshio Kuratomi -.. versionadded:: 0.3.25 - - -Include this using:: - - <%namespace name="fedora" file="${context['fedora_template']('login.mak')}" /> - -.. function:: loginform(message='') - -:kwarg message: Any text or elements contained by the tag will be shown - as a message to the user. This is generally used to show status of the - last login attempt ("Please provide your credentials", "Supplied - credentials were not correct", etc) - -A function for generating the main login form. This is a :term:`CSRF` -token-aware login form that will prompt for username and password when no -session identity is present and ask the user to click a link if they merely -lack a token. - -Typical usage, given the above import of the :file:`login.mak` template would -be:: - - ${fedora.loginform()} - - -.. function:: logintoolitem(href=None) - -:kwarg href: If an href is given, when a user is logged in, their username or - display_name will be a link to the URL. - -This function creates an entry into a toolbar for logging into a web app. -The entry will contain the user's username and a logout button if the user is -logged in, a verify login button if the user has a session cookie but not -a :term:`CSRF` token, or a login button if the user doesn't have a session -cookie. - -Typical usage looks like this:: - -
      - ${fedora.logintoolitem(href=tg.url('/users/info'))} -
    - ----------------------------------------------- -:mod:`fedora.tg2.templates.mako.jsglobals.mak` ----------------------------------------------- -.. module:: fedora.tg2.templates.mako.jsglobals.mak - :synopsis: Templates to get information into javascript -.. moduleauthor:: Toshio Kuratomi -.. versionadded:: 0.3.25 - - -Include this using:: - - <%namespace name="jsglobals" file="${context['fedora_template']('jsglobals.mak')}" /> - -.. function:: jsglobals() - -A function to add global variables to a page. Typically, you'd include -this in your :file:`master.mak` template and let the javascript variables -defined there propogate to every page on your site (since they all should -inherit from :file:`master.mak`). This adds the following variables in the -fedora namespace for other scripts to access: - - :fedora.baseurl: URL fragment to prepend to any calls to the application. - In a :term:`TurboGears` application, this is the scheme, host, and - server.webpath. Example: https://admin.fedoraproject.org/pkgdb/. - This may be a relative link. - :fedora.identity.anonymous: If ``true``, there will be no other variables - in the `fedora.identity` namespace. If ``false``, these variables are - defined: - :fedora.identity.userid: Numeric, unique identifier for the user - :fedora.identity.username: Publically visible unique identifier for the - user - :fedora.identity.display_name: Common human name for the user - :fedora.identity.token: csrf token for this user's session to be added to - urls that query the server. - -Typical usage would be:: - - ${jsglobals.jsglobals()} -''' diff --git a/fedora/tg2/templates/mako/jsglobals.mak b/fedora/tg2/templates/mako/jsglobals.mak deleted file mode 100644 index 7fdf44a3..00000000 --- a/fedora/tg2/templates/mako/jsglobals.mak +++ /dev/null @@ -1,22 +0,0 @@ -<%def name="jsglobals()"> - - % if request.identity: - - % endif - diff --git a/fedora/tg2/templates/mako/login.mak b/fedora/tg2/templates/mako/login.mak deleted file mode 100644 index 40ad66c8..00000000 --- a/fedora/tg2/templates/mako/login.mak +++ /dev/null @@ -1,83 +0,0 @@ -<%def name="loginform(message='')"> - - - -<%def name="logintoolitem(href)"> -% if request.identity: -
  • - ${_('Welcome')} - % if href: - - % if hasattr(request.identity['user'], 'display_name'): - ${request.identity['user'].display_name} - % else: - ${request.identity['user'].user_name} - % endif - - % else: - % if hasattr(request.identity['user'], 'display_name'): - ${request.identity['user'].display_name} - % else: - ${request.identity['user'].user_name} - % endif - % endif -
  • - -% elif not request.identity and not request.environ.get('CSRF_AUTH_SESSION_ID'): - ## If not logged in and no sign that we just lack a csrf token, offer login -
  • - ${_('You are not logged in')} -
    - -
    -
  • -% elif not request.identity: - ## Only CSRF_token missing -
  • - ${_('CSRF protected')} - ## Just go back to the present page using tg.url() to append the _csrf_token -
    - -
    -
  • -% endif -% if request.identity or request.environ.get('CSRF_AUTH_SESSION_ID'): -
  • -
    - -
    -
  • -% endif - diff --git a/fedora/tg2/utils.py b/fedora/tg2/utils.py deleted file mode 100644 index d89a2dc6..00000000 --- a/fedora/tg2/utils.py +++ /dev/null @@ -1,251 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Miscellaneous functions of use on a TurboGears 2 Server - -.. versionadded:: 0.3.17 -.. versionchanged:: 0.3.25 - Moved from fedora.tg.tg2utils - Modified fedora_template to allow dotted lookup - -.. moduleauthor:: Toshio Kuratomi -''' - -from copy import copy -from hashlib import sha1 -import logging -import os - -from munch import Munch -from kitchen.text.converters import to_unicode -import pkg_resources -from repoze.what.plugins.pylonshq import booleanize_predicates -import tg -from tg import config - -from fedora.wsgi.faswho import make_faswho_middleware -from fedora.urlutils import update_qs - -tg_url = tg.url - -### FIXME: Need jsonify_validation_errors, json_or_redirect, request_format -# To have all of the functions that exist for TG1 - - -def url(*args, **kwargs): - '''Compute URL - - This is a replacement for :func:`tg.controllers.url` (aka :func:`tg.url` - in the template). In addition to the functionality that :func:`tg.url` - provides, it adds a token to prevent :term:`CSRF` attacks. - - The arguments and return value are the same as for :func:`tg.url` - - The original :func:`tg.url` is accessible as - :func:`fedora.tg2.utils.tg_url` - - .. versionadded:: 0.3.17 - Added to enable :ref:`CSRF-Protection` for TG2 - ''' - new_url = tg_url(*args, **kwargs) - - # Set the current _csrf_token on the url. It will overwrite any current - # _csrf_token - csrf_token = None - identity = tg.request.environ.get('repoze.who.identity') - if identity: - csrf_token = identity.get('_csrf_token', None) - else: - session_id = tg.request.environ.get('CSRF_AUTH_SESSION_ID') - if session_id: - csrf_token = sha1(session_id).hexdigest() - if csrf_token: - new_url = update_qs(new_url, {'_csrf_token': csrf_token}, - overwrite=True) - return new_url - - -def fedora_template(template, template_type='mako', dotted_lookup=True): - '''Function to return the path to a template. - - :arg template: filename of the template itself. Ex: login.mak - :kwarg template_type: template language we need the template written in - Defaults to 'mako' - :kwarg dotted_lookup: if True, return the resource as a dotted module path - If False, return the resource as a filename. Default: True - :returns: filesystem path or dotted module path equivalent to the template - - .. versionchanged:: 0.3.25 - Added dotted_lookup - Made this work with tg2 - ''' - # :E1101: pkg_resources does have resource_filename - # pylint: disable-msg=E1101 - resource = pkg_resources.resource_filename( - 'fedora', os.path.join('tg2', - 'templates', template_type, template) - ) - - # pylint: enable-msg=E1101 - - if dotted_lookup: - # Find the location of the base resource (fedora) - base = pkg_resources.resource_filename('fedora', '') - if resource.startswith(base): - # subtract that from the resource - resource = resource[len(base):] - if resource[0] == '/': - resource = 'fedora%s' % resource - else: - resource = 'fedora/%s' % resource - # Strip the filename extension - resource = os.path.splitext(resource)[0] - - # Turn '/' into '.' - resource = to_unicode(resource) - resource = resource.translate({ord(u'/'): u'.'}) - - return resource - - -def add_fas_auth_middleware(self, app, *args): - ''' Add our FAS authentication middleware. - - This is a convenience method that sets up the FAS authentication - middleware. It needs to be used in :file:`app/config/app_cfg.py` like - this: - - .. code-block:: diff - - from myapp import model - from myapp.lib import app_globals, helpers. - - -base_config = AppConfig() - +from fedora.tg2.utils import add_fas_auth_middleware - + - +class MyAppConfig(AppConfig): - + add_auth_middleware = add_fas_auth_middleware - + - +base_config = MyAppConfig() - + - base_config.renderers = [] - - base_config.package = myapp - - The configuration of the faswho middleware is done via attributes on - MyAppConfig. For instance, to change the base url for the FAS server to - be authenticated against, set the connection to insecure for testing, and - the url of the login form, do this:: - - from munch import Munch - class MyAppConfig(AppConfig): - fas_auth = Munch( - fas_url='https://fakefas.fedoraproject.org/accounts', - insecure=True, login_form_url='/alternate/login') - add_auth_middleware = add_fas_auth_middleware - - The complete set of values that can be set in :attr:`fas_auth` is the same - as the parameters that can be passed to - :func:`fedora.wsgi.faswho.faswhoplugin.make_faswho_middleware` - ''' - # Set up csrf protection - enable_csrf() - - booleanize_predicates() - - if not hasattr(self, 'fas_auth'): - self.fas_auth = Munch() - - # Configuring auth logging: - if 'log_stream' not in self.fas_auth: - self.fas_auth['log_stream'] = logging.getLogger('auth') - - # Pull in some of the default auth arguments - auth_args = copy(self.fas_auth) - - app = make_faswho_middleware(app, **auth_args) - return app - - -def enable_csrf(): - '''A startup function to setup :ref:`CSRF-Protection`. - - This should be run at application startup. It does three things: - - 1) overrides the :func:`tg.url` function with - :func:`fedora.tg2.utils.url` so that :term:`CSRF` tokens are - appended to URLs. - 2) tells the TG2 app to ignore `_csrf_token`. This lets the app know not - to throw an error if the variable is passed through to the app. - 3) adds :func:`fedora.tg.fedora_template` function to the template - variables. This lets us xi:include templates from python-fedora in - our templates. - - Presently, this is run when the faswho middleware is invoked. See the - documentation for :func:`fedora.tg.tgutils.add_fas_auth_middleware` for - how to enable that. - - .. note:: - - The following code is broken at least as late as - :term:`TurboGears`-2.0.3. We need to have something similar in order - to use CSRF protection with applications that do not always want to - rely on FAS to authenticate. - - .. seealso:: http://trac.turbogears.org/ticket/2432 - - To run this at application startup, add - code like the following to :file:`MYAPP/config/app_config.py`:: - - from fedora.tg2.utils import enable_csrf - base_config.call_on_startup = [enable_csrf] - - If we can get the :ref:`CSRF-Protection` into upstream :term:`TurboGears`, - we might be able to remove this in the future. - - .. versionadded:: 0.3.17 - Added to enable :ref:`CSRF-Protection` - ''' - # Override the tg.url function with our own - tg.url = url - tg.controllers.url = url - try: - # TG-2.1+ - tg.controllers.util.url = url - except AttributeError: - # TG-2.0.x - pass - - # Ignore the _csrf_token parameter - ignore = config.get('ignore_parameters', []) - if '_csrf_token' not in ignore: - ignore.append('_csrf_token') - config['ignore_parameters'] = ignore - - # Add a function to the template tg stdvars that looks up a template. - var_provider = config.get('variable_provider', None) - if var_provider: - config['variable_provider'] = lambda: \ - var_provider().update({'fedora_template': fedora_template}) - else: - config['variable_provider'] = lambda: {'fedora_template': - fedora_template} - -__all__ = ('add_fas_auth_middleware', 'enable_csrf', 'fedora_template', - 'tg_url', 'url') diff --git a/fedora/wsgi/__init__.py b/fedora/wsgi/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/fedora/wsgi/csrf.py b/fedora/wsgi/csrf.py deleted file mode 100644 index e9e9011c..00000000 --- a/fedora/wsgi/csrf.py +++ /dev/null @@ -1,307 +0,0 @@ -# -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008-2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -Cross-site Request Forgery Protection. - -http://en.wikipedia.org/wiki/Cross-site_request_forgery - - -.. moduleauthor:: John (J5) Palmieri -.. moduleauthor:: Luke Macken - -.. versionadded:: 0.3.17 -''' - -from hashlib import sha1 -import logging - -from munch import Munch -from kitchen.text.converters import to_bytes -from webob import Request -try: - # webob > 1.0 - from webob.headers import ResponseHeaders -except ImportError: - # webob < 1.0 - from webob.headerdict import HeaderDict as ResponseHeaders -from paste.httpexceptions import HTTPFound -from paste.response import replace_header -from repoze.who.interfaces import IMetadataProvider -from zope.interface import implements - -from fedora.urlutils import update_qs - -log = logging.getLogger(__name__) - - -class CSRFProtectionMiddleware(object): - ''' - CSRF Protection WSGI Middleware. - - A layer of WSGI middleware that is responsible for making sure - authenticated requests originated from the user inside of the app's domain - and not a malicious website. - - This middleware works with the :mod:`repoze.who` middleware, and requires - that it is placed below :mod:`repoze.who` in the WSGI stack, - since it relies upon ``repoze.who.identity`` to exist in the environ before - it is called. - - To utilize this middleware, you can just add it to your WSGI stack below - the :mod:`repoze.who` middleware. Here is an example of utilizing the - `CSRFProtectionMiddleware` within a TurboGears2 application. - In your ``project/config/middleware.py``, you would wrap your main - application with the `CSRFProtectionMiddleware`, like so: - - .. code-block:: python - - from fedora.wsgi.csrf import CSRFProtectionMiddleware - def make_app(global_conf, full_stack=True, **app_conf): - app = make_base_app(global_conf, wrap_app=CSRFProtectionMiddleware, - full_stack=full_stack, **app_conf) - - You then need to add the CSRF token to every url that you need to be - authenticated for. When used with TurboGears2, an overridden version of - :func:`tg.url` is provided. You can use it directly by calling:: - - from fedora.tg2.utils import url - [...] - url = url('/authentication_needed') - - An easier and more portable way to use that is from within TG2 to set this - up is to use :func:`fedora.tg2.utils.enable_csrf` when you setup your - application. This function will monkeypatch TurboGears2's :func:`tg.url` - so that it adds a csrf token to urls. This way, you can keep the same - code in your templates and controller methods whether or not you configure - the CSRF middleware to provide you with protection via - :func:`~fedora.tg2.utils.enable_csrf`. - ''' - - def __init__(self, application, csrf_token_id='_csrf_token', - clear_env='repoze.who.identity repoze.what.credentials', - token_env='CSRF_TOKEN', auth_state='CSRF_AUTH_STATE'): - ''' - Initialize the CSRF Protection WSGI Middleware. - - :csrf_token_id: The name of the CSRF token variable - :clear_env: Variables to clear out of the `environ` on invalid token - :token_env: The name of the token variable in the environ - :auth_state: The environ key that will be set when we are logging in - ''' - log.info('Creating CSRFProtectionMiddleware') - self.application = application - self.csrf_token_id = csrf_token_id - self.clear_env = clear_env.split() - self.token_env = token_env - self.auth_state = auth_state - - def _clean_environ(self, environ): - ''' Delete the ``keys`` from the supplied ``environ`` ''' - log.debug('clean_environ(%s)' % to_bytes(self.clear_env)) - for key in self.clear_env: - if key in environ: - log.debug('Deleting %(key)s from environ' % - {'key': to_bytes(key)}) - del(environ[key]) - - def __call__(self, environ, start_response): - ''' - This method is called for each request. It looks for a user-supplied - CSRF token in the GET/POST parameters, and compares it to the token - attached to ``environ['repoze.who.identity']['_csrf_token']``. If it - does not match, or if a token is not provided, it will remove the - user from the ``environ``, based on the ``clear_env`` setting. - ''' - request = Request(environ) - log.debug('CSRFProtectionMiddleware(%(r_path)s)' % - {'r_path': to_bytes(request.path)}) - - token = environ.get('repoze.who.identity', {}).get(self.csrf_token_id) - csrf_token = environ.get(self.token_env) - - if token and csrf_token and token == csrf_token: - log.debug('User supplied CSRF token matches environ!') - else: - if not environ.get(self.auth_state): - log.debug('Clearing identity') - self._clean_environ(environ) - if 'repoze.who.identity' not in environ: - environ['repoze.who.identity'] = Munch() - if 'repoze.who.logins' not in environ: - # For compatibility with friendlyform - environ['repoze.who.logins'] = 0 - if csrf_token: - log.warning('Invalid CSRF token. User supplied' - ' (%(u_token)s) does not match what\'s in our' - ' environ (%(e_token)s)' % - {'u_token': to_bytes(csrf_token), - 'e_token': to_bytes(token)}) - - response = request.get_response(self.application) - - if environ.get(self.auth_state): - log.debug('CSRF_AUTH_STATE; rewriting headers') - token = environ.get('repoze.who.identity', {})\ - .get(self.csrf_token_id) - - loc = update_qs( - response.location, {self.csrf_token_id: str(token)}) - response.location = loc - log.debug('response.location = %(r_loc)s' % - {'r_loc': to_bytes(response.location)}) - environ[self.auth_state] = None - - return response(environ, start_response) - - -class CSRFMetadataProvider(object): - ''' - Repoze.who CSRF Metadata Provider Plugin. - - This metadata provider is called with an authenticated users identity - automatically by repoze.who. It will then take the SHA1 hash of the - users session cookie, and set it as the CSRF token in - ``environ['repoze.who.identity']['_csrf_token']``. - - This plugin will also set ``CSRF_AUTH_STATE`` in the environ if the user - has just authenticated during this request. - - To enable this plugin in a TurboGears2 application, you can - add the following to your ``project/config/app_cfg.py`` - - .. code-block:: python - - from fedora.wsgi.csrf import CSRFMetadataProvider - base_config.sa_auth.mdproviders = [('csrfmd', CSRFMetadataProvider())] - - Note: If you use the faswho plugin, this is turned on automatically. - ''' - implements(IMetadataProvider) - - def __init__(self, csrf_token_id='_csrf_token', session_cookie='tg-visit', - clear_env='repoze.who.identity repoze.what.credentials', - login_handler='/post_login', token_env='CSRF_TOKEN', - auth_session_id='CSRF_AUTH_SESSION_ID', - auth_state='CSRF_AUTH_STATE'): - ''' - Create the CSRF Metadata Provider Plugin. - - :kwarg csrf_token_id: The name of the CSRF token variable. The - identity will contain an entry with this as key and the - computed csrf_token as the value. - :kwarg session_cookie: The name of the session cookie - :kwarg login_handler: The path to the login handler, used to determine - if the user logged in during this request - :kwarg token_env: The name of the token variable in the environ. - The environ will contain the token from the request - :kwarg auth_session_id: The environ key containing an optional - session id - :kwarg auth_state: The environ key that indicates when we are - logging in - ''' - self.csrf_token_id = csrf_token_id - self.session_cookie = session_cookie - self.clear_env = clear_env - self.login_handler = login_handler - self.token_env = token_env - self.auth_session_id = auth_session_id - self.auth_state = auth_state - - def strip_script(self, environ, path): - # Strips the script portion of a url path so the middleware works even - # when mounted under a path other than root - if path.startswith('/') and 'SCRIPT_NAME' in environ: - prefix = environ.get('SCRIPT_NAME') - if prefix.endswith('/'): - prefix = prefix[:-1] - - if path.startswith(prefix): - path = path[len(prefix):] - - return path - - def add_metadata(self, environ, identity): - request = Request(environ) - log.debug('CSRFMetadataProvider.add_metadata(%(r_path)s)' - % {'r_path': to_bytes(request.path)}) - - session_id = environ.get(self.auth_session_id) - if not session_id: - session_id = request.cookies.get(self.session_cookie) - log.debug('session_id = %(s_id)r' % {'s_id': - to_bytes(session_id)}) - - if session_id and session_id != 'Set-Cookie:': - environ[self.auth_session_id] = session_id - token = sha1(session_id).hexdigest() - identity.update({self.csrf_token_id: token}) - log.debug('Identity updated with CSRF token') - path = self.strip_script(environ, request.path) - if path == self.login_handler: - log.debug('Setting CSRF_AUTH_STATE') - environ[self.auth_state] = True - environ[self.token_env] = token - else: - environ[self.token_env] = self.extract_csrf_token(request) - - app = environ.get('repoze.who.application') - if app: - # This occurs during login in some application configurations - if isinstance(app, HTTPFound) and environ.get(self.auth_state): - log.debug('Got HTTPFound(302) from' - ' repoze.who.application') - # What possessed people to make this a string or - # a function? - location = app.location - if hasattr(location, '__call__'): - location = location() - loc = update_qs(location, {self.csrf_token_id: - str(token)}) - - headers = app.headers.items() - replace_header(headers, 'location', loc) - app.headers = ResponseHeaders(headers) - log.debug('Altered headers: %(headers)s' % { - 'headers': to_bytes(app.headers)}) - else: - log.warning('Invalid session cookie %(s_id)r, not setting CSRF' - ' token!' % {'s_id': to_bytes(session_id)}) - - def extract_csrf_token(self, request): - '''Extract and remove the CSRF token from a given - :class:`webob.Request` - ''' - csrf_token = None - - if self.csrf_token_id in request.GET: - log.debug("%(token)s in GET" % {'token': - to_bytes(self.csrf_token_id)}) - csrf_token = request.GET[self.csrf_token_id] - del(request.GET[self.csrf_token_id]) - request.query_string = '&'.join(['%s=%s' % (k, v) for k, v in - request.GET.items()]) - - if self.csrf_token_id in request.POST: - log.debug("%(token)s in POST" % {'token': - to_bytes(self.csrf_token_id)}) - csrf_token = request.POST[self.csrf_token_id] - del(request.POST[self.csrf_token_id]) - - return csrf_token diff --git a/fedora/wsgi/faswho/__init__.py b/fedora/wsgi/faswho/__init__.py deleted file mode 100644 index 4f8e6877..00000000 --- a/fedora/wsgi/faswho/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from fedora.wsgi.faswho.faswhoplugin import ( - FASWhoPlugin, - make_faswho_middleware -) - -__all__ = (FASWhoPlugin, make_faswho_middleware) diff --git a/fedora/wsgi/faswho/faswhoplugin.py b/fedora/wsgi/faswho/faswhoplugin.py deleted file mode 100644 index 6ddb7b6d..00000000 --- a/fedora/wsgi/faswho/faswhoplugin.py +++ /dev/null @@ -1,421 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2008-2011 Red Hat, Inc. -# This file is part of python-fedora -# -# python-fedora is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# python-fedora is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with python-fedora; if not, see -# -''' -repoze.who plugin to authenticate against hte Fedora Account System - -.. moduleauthor:: John (J5) Palmieri -.. moduleauthor:: Luke Macken -.. moduleauthor:: Toshio Kuratomi - -.. versionadded:: 0.3.17 -.. versionchanged:: 0.3.26 - - Added secure and httponly as optional attributes to the session cookie - - Removed too-aggressive caching (wouldn't detect logout from another app) - - Added ability to authenticate and request a page in one request -''' -import os -import sys -import logging - -import pkg_resources - -from beaker.cache import Cache -from munch import Munch -from kitchen.text.converters import to_bytes, exception_to_bytes -from paste.httpexceptions import HTTPFound -from repoze.who.middleware import PluggableAuthenticationMiddleware -from repoze.who.classifiers import default_request_classifier -from repoze.who.classifiers import default_challenge_decider -from repoze.who.interfaces import IChallenger, IIdentifier -from repoze.who.plugins.basicauth import BasicAuthPlugin -from repoze.who.plugins.friendlyform import FriendlyFormPlugin -from paste.request import parse_dict_querystring, parse_formvars -from six.moves.urllib.parse import quote_plus -import webob - -from fedora.client import AuthError -from fedora.client.fasproxy import FasProxyClient -from fedora.wsgi.csrf import CSRFMetadataProvider, CSRFProtectionMiddleware - -log = logging.getLogger(__name__) - -FAS_URL = 'https://admin.fedoraproject.org/accounts/' -FAS_CACHE_TIMEOUT = 900 # 15 minutes (FAS visits timeout after 20) - -fas_cache = Cache('fas_repozewho_cache', type='memory') - - -def fas_request_classifier(environ): - classifier = default_request_classifier(environ) - if classifier == 'browser': - request = webob.Request(environ) - if not request.accept.best_match( - ['application/xhtml+xml', 'text/html']): - classifier = 'app' - return classifier - - -def make_faswho_middleware( - app, log_stream=None, - login_handler='/login_handler', - login_form_url='/login', - logout_handler='/logout_handler', - post_login_url='/post_login', post_logout_url=None, fas_url=FAS_URL, - insecure=False, ssl_cookie=True, httponly=True): - ''' - :arg app: WSGI app that is being wrapped - :kwarg log_stream: :class:`logging.Logger` to log auth messages - :kwarg login_handler: URL where the login form is submitted - :kwarg login_form_url: URL where the login form is displayed - :kwarg logout_handler: URL where the logout form is submitted - :kwarg post_login_url: URL to redirect the user to after login - :kwarg post_logout_url: URL to redirect the user to after logout - :kwarg fas_url: Base URL to the FAS server - :kwarg insecure: Allow connecting to a fas server without checking the - server's SSL certificate. Opens you up to MITM attacks but can be - useful when testing. *Do not enable this in production* - :kwarg ssl_cookie: If :data:`True` (default), tell the browser to only - send the session cookie back over https. - :kwarg httponly: If :data:`True` (default), tell the browser that the - session cookie should only be read for sending to a server, not for - access by JavaScript or other clientside technology. This prevents - using the session cookie to pass information to JavaScript clients but - also prevents XSS attacks from stealing the session cookie - information. - ''' - - # Because of the way we override values (via a dict in AppConfig), we - # need to make this a keyword arg and then check it here to make it act - # like a positional arg. - if not log_stream: - raise TypeError( - 'log_stream must be set when calling make_fasauth_middleware()') - - faswho = FASWhoPlugin(fas_url, insecure=insecure, ssl_cookie=ssl_cookie, - httponly=httponly) - csrf_mdprovider = CSRFMetadataProvider() - - form = FriendlyFormPlugin(login_form_url, - login_handler, - post_login_url, - logout_handler, - post_logout_url, - rememberer_name='fasident', - charset='utf-8') - - form.classifications = {IIdentifier: ['browser'], - IChallenger: ['browser']} # only for browser - - basicauth = BasicAuthPlugin('repoze.who') - - identifiers = [ - ('form', form), - ('fasident', faswho), - ('basicauth', basicauth) - ] - authenticators = [('fasauth', faswho)] - challengers = [('form', form), ('basicauth', basicauth)] - mdproviders = [('fasmd', faswho), ('csrfmd', csrf_mdprovider)] - - if os.environ.get('FAS_WHO_LOG'): - log_stream = sys.stdout - - app = CSRFProtectionMiddleware(app) - app = PluggableAuthenticationMiddleware( - app, - identifiers, - authenticators, - challengers, - mdproviders, - fas_request_classifier, - default_challenge_decider, - log_stream=log_stream, - ) - - return app - - -class FASWhoPlugin(object): - - def __init__(self, url, insecure=False, session_cookie='tg-visit', - ssl_cookie=True, httponly=True): - self.url = url - self.insecure = insecure - self.fas = FasProxyClient(url, insecure=insecure) - self.session_cookie = session_cookie - self.ssl_cookie = ssl_cookie - self.httponly = httponly - self._session_cache = {} - self._metadata_plugins = [] - - for entry in pkg_resources.iter_entry_points( - 'fas.repoze.who.metadata_plugins'): - self._metadata_plugins.append(entry.load()) - - def _retrieve_user_info(self, environ, auth_params=None): - ''' Retrieve information from fas and cache the results. - - We need to retrieve the user fresh every time because we need to - know that the password hasn't changed or the session_id hasn't - been invalidated by the user logging out. - ''' - if not auth_params: - return None - - user_data = self.fas.get_user_info(auth_params) - - if not user_data: - self.forget(environ, None) - return None - if isinstance(user_data, tuple): - user_data = list(user_data) - - # Set session_id in here so it can be found by other plugins - user_data[1]['session_id'] = user_data[0] - - # we don't define permissions since we don't have any peruser data - # though other services may wish to add another metadata plugin to do - # so - if not 'permissions' in user_data[1]: - user_data[1]['permissions'] = set() - - # we keep the approved_memberships list because there is also an - # unapproved_membership field. The groups field is for repoze.who - # group checking and may include other types of groups besides - # memberships in the future (such as special fedora community groups) - - groups = set() - for g in user_data[1]['approved_memberships']: - groups.add(g['name']) - - user_data[1]['groups'] = groups - # If we have information on the user, cache it for later - fas_cache.set_value(user_data[1]['username'], user_data, - expiretime=FAS_CACHE_TIMEOUT) - return user_data - - def identify(self, environ): - '''Extract information to identify a user - - Retrieve either a username and password or a session_id that can be - passed on to FAS to authenticate the user. - ''' - log.info('in identify()') - - # friendlyform compat - if not 'repoze.who.logins' in environ: - environ['repoze.who.logins'] = 0 - - req = webob.Request(environ, charset='utf-8') - cookie = req.cookies.get(self.session_cookie) - - # This is compatible with TG1 and it gives us a way to authenticate - # a user without making two requests - query = req.GET - form = Munch(req.POST) - form.update(query) - if form.get('login', None) == 'Login' and \ - 'user_name' in form and \ - 'password' in form: - identity = { - 'login': form['user_name'], - 'password': form['password'] - } - keys = ('login', 'password', 'user_name') - for k in keys: - if k in req.GET: - del(req.GET[k]) - if k in req.POST: - del(req.POST[k]) - return identity - - if cookie is None: - return None - - log.info('Request identify for cookie %(cookie)s' % - {'cookie': to_bytes(cookie)}) - try: - user_data = self._retrieve_user_info( - environ, - auth_params={'session_id': cookie}) - except Exception as e: # pylint:disable-msg=W0703 - # For any exceptions, returning None means we failed to identify - log.warning(e) - return None - - if not user_data: - return None - - # Preauthenticated - identity = {'repoze.who.userid': user_data[1]['username'], - 'login': user_data[1]['username'], - 'password': user_data[1]['password']} - return identity - - def remember(self, environ, identity): - log.info('In remember()') - result = [] - - user_data = fas_cache.get_value(identity['login']) - try: - session_id = user_data[0] - except Exception: - return None - - set_cookie = ['%s=%s; Path=/;' % (self.session_cookie, session_id)] - if self.ssl_cookie: - set_cookie.append('Secure') - if self.httponly: - set_cookie.append('HttpOnly') - set_cookie = '; '.join(set_cookie) - result.append(('Set-Cookie', set_cookie)) - return result - - def forget(self, environ, identity): - log.info('In forget()') - # return a expires Set-Cookie header - - user_data = fas_cache.get_value(identity['login']) - try: - session_id = user_data[0] - except Exception: - return None - - log.info('Forgetting login data for cookie %(s_id)s' % - {'s_id': to_bytes(session_id)}) - - self.fas.logout(session_id) - - result = [] - fas_cache.remove_value(key=identity['login']) - expired = '%s=\'\'; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT'\ - % self.session_cookie - result.append(('Set-Cookie', expired)) - return result - - # IAuthenticatorPlugin - def authenticate(self, environ, identity): - log.info('In authenticate()') - - def set_error(msg): - log.info(msg) - err = 1 - environ['FAS_AUTH_ERROR'] = err - # HTTPForbidden ? - err_app = HTTPFound(err_goto + '?' + - 'came_from=' + quote_plus(came_from)) - environ['repoze.who.application'] = err_app - - err_goto = '/login' - default_came_from = '/' - if 'SCRIPT_NAME' in environ: - sn = environ['SCRIPT_NAME'] - err_goto = sn + err_goto - default_came_from = sn + default_came_from - - query = parse_dict_querystring(environ) - form = parse_formvars(environ) - form.update(query) - came_from = form.get('came_from', default_came_from) - - try: - auth_params = {'username': identity['login'], - 'password': identity['password']} - except KeyError: - try: - auth_params = {'session_id': identity['session_id']} - except: - # On error we return None which means that auth failed - set_error('Parameters for authenticating not found') - return None - - try: - user_data = self._retrieve_user_info(environ, auth_params) - except AuthError as e: - set_error('Authentication failed: %s' % exception_to_bytes(e)) - log.warning(e) - return None - except Exception as e: - set_error('Unknown auth failure: %s' % exception_to_bytes(e)) - return None - - if user_data: - try: - del user_data[1]['password'] - environ['CSRF_AUTH_SESSION_ID'] = user_data[0] - return user_data[1]['username'] - except ValueError: - set_error('user information from fas not in expected format!') - return None - except Exception: - pass - set_error('An unknown error happened when trying to log you in.' - ' Please try again.') - return None - - def add_metadata(self, environ, identity): - log.info('In add_metadata') - - if identity.get('error'): - log.info('Error exists in session, no need to set metadata') - return 'error' - - plugin_user_info = {} - for plugin in self._metadata_plugins: - plugin(plugin_user_info) - identity.update(plugin_user_info) - del plugin_user_info - - user = identity.get('repoze.who.userid') - (session_id, user_info) = fas_cache.get_value( - key=user, - expiretime=FAS_CACHE_TIMEOUT) - - #### FIXME: Deprecate this line!!! - # If we make a new version of fas.who middleware, get rid of saving - # user information directly into identity. Instead, save it into - # user, as is done below - identity.update(user_info) - - identity['userdata'] = user_info - identity['user'] = Munch() - identity['user'].created = user_info['creation'] - identity['user'].display_name = user_info['human_name'] - identity['user'].email_address = user_info['email'] - identity['user'].groups = user_info['groups'] - identity['user'].password = None - identity['user'].permissions = user_info['permissions'] - identity['user'].user_id = user_info['id'] - identity['user'].user_name = user_info['username'] - identity['groups'] = user_info['groups'] - identity['permissions'] = user_info['permissions'] - - if 'repoze.what.credentials' not in environ: - environ['repoze.what.credentials'] = {} - - environ['repoze.what.credentials']['groups'] = user_info['groups'] - permissions = user_info['permissions'] - environ['repoze.what.credentials']['permissions'] = permissions - - # Adding the userid: - userid = identity['repoze.who.userid'] - environ['repoze.what.credentials']['repoze.what.userid'] = userid - - def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, id(self)) diff --git a/setup.py b/setup.py index ee388be9..e5e361c4 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ keywords='Fedora Python Webservices', url=URL, packages=find_packages(), - py_modules=['flask_fas', 'flask_fas_openid'], + py_modules=['flask_fas_openid'], include_package_data=True, # non-setuptools package. When everything we care about uses # python-2.5 distutils we can add these: @@ -31,7 +31,6 @@ 'openidc-client', ], extras_require={ - 'tg': ['TurboGears >= 1.0.4', 'SQLAlchemy', 'decorator'], 'wsgi': ['repoze.who', 'Beaker', 'Paste'], 'flask': [ 'Flask', 'Flask_WTF', 'python-openid', 'python-openid-teams', @@ -39,27 +38,14 @@ ], }, entry_points={ - 'turbogears.identity.provider': ( - 'jsonfas = fedora.tg.identity.jsonfasprovider1:JsonFasIdentityProvider [tg]', - 'jsonfas2 = fedora.tg.identity.jsonfasprovider2:JsonFasIdentityProvider [tg]', - 'sqlobjectcsrf = fedora.tg.identity.soprovidercsrf:SqlObjectCsrfIdentityProvider [tg]'), - 'turbogears.visit.manager': ( - 'jsonfas = fedora.tg.visit.jsonfasvisit1:JsonFasVisitManager [tg]', - 'jsonfas2 = fedora.tg.visit.jsonfasvisit2:JsonFasVisitManager [tg]' - ), }, message_extractors={ 'fedora': [ ('**.py', 'python', None), - ('tg2/templates/mako/**.mak', 'mako', None), - ('tg2/templates/genshi/**.html', 'mako', None), - ('tg/templates/genshi/**.html', 'genshi', None), ], }, classifiers=[ 'Development Status :: 4 - Beta', - 'Framework :: TurboGears', - 'Framework :: Django', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', diff --git a/tests/functional/test_openidbaseclient.py b/tests/functional/test_openidbaseclient.py deleted file mode 100644 index 0ee37c90..00000000 --- a/tests/functional/test_openidbaseclient.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -""" Test the OpenIdBaseClient. """ - - -import os -import shutil -import tempfile -import unittest - -from .functional_test_utils import networked - -from fedora.client import FedoraServiceError -from fedora.client.openidbaseclient import OpenIdBaseClient - -BASE_URL = 'http://127.0.0.1:5000' -BASE_URL = 'http://209.132.184.188' -LOGIN_URL = '{}/login/'.format(BASE_URL) - -class OpenIdBaseClientTest(unittest.TestCase): - - """Test the OpenId Base Client.""" - - def setUp(self): - self.client = OpenIdBaseClient(BASE_URL) - self.session_file = os.path.expanduser( - '~/.fedora/baseclient-sessions.sqlite') - try: - self.backup_dir = tempfile.mkdtemp(dir='~/.fedora/') - except OSError: - self.backup_dir = tempfile.mkdtemp() - - self.saved_session_file = os.path.join( - self.backup_dir, 'baseclient-sessions.sqlite.bak') - self.clear_cookies() - - def tearDown(self): - self.restore_cookies() - shutil.rmtree(self.backup_dir) - - def clear_cookies(self): - try: - shutil.move(self.session_file, self.saved_session_file) - except IOError as e: - # No previous sessionfile is fine - if e.errno != 2: - raise - # Sentinel to say that there was no previous session_file - self.saved_session_file = None - - def restore_cookies(self): - if self.saved_session_file: - shutil.move(self.saved_session_file, self.session_file) - - @networked - def test_no_openid_session(self): - """Raise FedoraServiceError for no session on service or openid server.""" - self.assertRaises(FedoraServiceError, self.client.login, 'username', 'password') - - @networked - def test_no_service_session(self): - """Open a service session when we have an openid session.""" - pass