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