diff --git a/README.md b/README.md index ee30cc5a..275dd752 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,12 @@ This folder contains a Bottle project that will be used as demo to show how to a This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). + +#### demo_pyramid #### + +This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). '\_\_init__.py' is the main file that configures the app and its routes, 'views.py' is where all the logic and SAML handling takes place, and the templates are stored in the 'templates' folder. The 'saml' folder is the same as in the other two demos. + + #### setup.py #### Setup script is the centre of all activity in building, distributing, and installing modules. @@ -1145,3 +1151,76 @@ Once the SP is configured, the metadata of the SP is published at the /metadata ####How it works#### This demo works very similar to the flask-demo (We did it intentionally). + + +### Demo Pyramid ### + +Unlike the other two projects, you don't need a pre-existing virtualenv to get +up and running here, since Pyramid comes from the +[buildout](http://www.buildout.org/en/latest/) school of thought. + +To run the demo you need to install Pyramid, the requirements, etc.: +``` + cd demo_pyramid + python -m venv env + env/bin/pip install --upgrade pip setuptools + env/bin/pip install -e ".[testing]" +``` + +Next, edit the settings in `demo_pyramid/saml/settings.json`. (Pyramid runs on +port 6543 by default.) + +Now you can run the demo like this: +``` + env/bin/pserve development.ini +``` + +If that worked, the demo is now running at http://localhost:6543. + +####Content#### + +The Pyramid project contains: + + +* ***\_\_init__.py*** is the main Pyramid file that configures the app and its routes. + +* ***views.py*** is where all the SAML handling takes place. + +* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a layout.jinja2 template that is extended by index.jinja2 and attrs.jinja2, the templates of our simple demo that shows messages, user attributes when available and login and logout links. + +* ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). + + +####SP setup#### + +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used. + +In the views.py file we define the SAML_PATH, which will target the 'saml' folder. We require it in order to load the settings files. + +First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. + +####IdP setup#### + +Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP. + +####How it works#### + +1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). + + 2. When you click: + + 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. + + 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url. + + 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs + + 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality. + + The single log out funcionality could be tested by 2 ways. + + 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. + + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. + +Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done. diff --git a/demo_pyramid/.coveragerc b/demo_pyramid/.coveragerc new file mode 100644 index 00000000..fd429eb0 --- /dev/null +++ b/demo_pyramid/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = demo_pyramid +omit = demo_pyramid/test* diff --git a/demo_pyramid/.gitignore b/demo_pyramid/.gitignore new file mode 100644 index 00000000..bd31ad13 --- /dev/null +++ b/demo_pyramid/.gitignore @@ -0,0 +1,22 @@ +*.egg +*.egg-info +*.pyc +*$py.class +*~ +.coverage +coverage.xml +build/ +dist/ +.tox/ +nosetests.xml +env*/ +tmp/ +.cache/* +Data.fs* +*.sublime-project +*.sublime-workspace +.*.sw? +.sw? +.DS_Store +coverage +test diff --git a/demo_pyramid/CHANGES.txt b/demo_pyramid/CHANGES.txt new file mode 100644 index 00000000..14b902fd --- /dev/null +++ b/demo_pyramid/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version. diff --git a/demo_pyramid/MANIFEST.in b/demo_pyramid/MANIFEST.in new file mode 100644 index 00000000..3b3962e4 --- /dev/null +++ b/demo_pyramid/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include demo_pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/demo_pyramid/README.txt b/demo_pyramid/README.txt new file mode 100644 index 00000000..13d16ded --- /dev/null +++ b/demo_pyramid/README.txt @@ -0,0 +1,29 @@ +demo_pyramid +=============================== + +Getting Started +--------------- + +- Change directory into your newly created project. + + cd demo_pyramid + +- Create a Python virtual environment. + + python -m venv env + +- Upgrade packaging tools. + + env/bin/pip install --upgrade pip setuptools + +- Install the project in editable mode with its testing requirements. + + env/bin/pip install -e ".[testing]" + +- Run your project's tests. + + env/bin/pytest + +- Run your project. + + env/bin/pserve development.ini diff --git a/demo_pyramid/demo_pyramid/__init__.py b/demo_pyramid/demo_pyramid/__init__.py new file mode 100644 index 00000000..805e51d1 --- /dev/null +++ b/demo_pyramid/demo_pyramid/__init__.py @@ -0,0 +1,19 @@ +from pyramid.config import Configurator +from pyramid.session import SignedCookieSessionFactory + + +session_factory = SignedCookieSessionFactory('onelogindemopytoolkit') + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.set_session_factory(session_factory) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('index', '/') + config.add_route('attrs', '/attrs/') + config.add_route('metadata', '/metadata/') + config.scan() + return config.make_wsgi_app() diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json new file mode 100644 index 00000000..3115e17e --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -0,0 +1,33 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/demo_pyramid/demo_pyramid/saml/certs/README b/demo_pyramid/demo_pyramid/saml/certs/README new file mode 100644 index 00000000..03c13737 --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/certs/README @@ -0,0 +1,11 @@ +Take care of this folder that could contain private key. Be sure that this folder never is published. + +Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: + + * sp.key Private Key + * sp.crt Public cert + +Also you can use other cert to sign the metadata of the SP using the: + + * metadata.key + * metadata.crt diff --git a/demo_pyramid/demo_pyramid/saml/settings.json b/demo_pyramid/demo_pyramid/saml/settings.json new file mode 100644 index 00000000..ec40b674 --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "https:///metadata/", + "assertionConsumerService": { + "url": "https:///?acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "https:///?sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "https://app.onelogin.com/saml/metadata/", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + } +} diff --git a/demo_pyramid/demo_pyramid/static/pyramid-16x16.png b/demo_pyramid/demo_pyramid/static/pyramid-16x16.png new file mode 100644 index 00000000..97920311 Binary files /dev/null and b/demo_pyramid/demo_pyramid/static/pyramid-16x16.png differ diff --git a/demo_pyramid/demo_pyramid/static/pyramid.png b/demo_pyramid/demo_pyramid/static/pyramid.png new file mode 100644 index 00000000..4ab837be Binary files /dev/null and b/demo_pyramid/demo_pyramid/static/pyramid.png differ diff --git a/demo_pyramid/demo_pyramid/static/theme.css b/demo_pyramid/demo_pyramid/static/theme.css new file mode 100644 index 00000000..0f4b1a4d --- /dev/null +++ b/demo_pyramid/demo_pyramid/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/demo_pyramid/demo_pyramid/templates/attrs.jinja2 b/demo_pyramid/demo_pyramid/templates/attrs.jinja2 new file mode 100644 index 00000000..f83ea3be --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/attrs.jinja2 @@ -0,0 +1,31 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +{% if paint_logout %} + {% if attributes %} +

You have the following attributes:

+ + + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login and access again to this page +{% endif %} + +{% endblock %} diff --git a/demo_pyramid/demo_pyramid/templates/index.jinja2 b/demo_pyramid/demo_pyramid/templates/index.jinja2 new file mode 100644 index 00000000..65bcedd6 --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/index.jinja2 @@ -0,0 +1,55 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
+

Pyramid Starter project

+

Welcome to demo_pyramid, a Pyramid application generated by
Cookiecutter.

+
+ +{% if errors %} + +{% endif %} + +{% if not_auth_warn %} + +{% endif %} + +{% if success_slo %} + +{% endif %} + +{% if paint_logout %} + {% if attributes %} + + + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login Login and access to attrs page +{% endif %} + +{% endblock %} diff --git a/demo_pyramid/demo_pyramid/templates/layout.jinja2 b/demo_pyramid/demo_pyramid/templates/layout.jinja2 new file mode 100644 index 00000000..57ad87c0 --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + Cookiecutter Starter project for the Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+ +
+ +
+
+
+ + + + + + + + diff --git a/demo_pyramid/demo_pyramid/views.py b/demo_pyramid/demo_pyramid/views.py new file mode 100644 index 00000000..86814a99 --- /dev/null +++ b/demo_pyramid/demo_pyramid/views.py @@ -0,0 +1,126 @@ +import os + +from pyramid.httpexceptions import (HTTPFound, HTTPInternalServerError, HTTPOk,) +from pyramid.view import view_config + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + +SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml') + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_PATH) + return auth + + +def prepare_pyramid_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': request.server_port, + 'script_name': request.path, + 'get_data': request.GET.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'post_data': request.POST.copy(), + } + + +@view_config(route_name='index', renderer='templates/index.jinja2') +def index(request): + req = prepare_pyramid_request(request) + auth = init_saml_auth(req) + errors = [] + error_reason = "" + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + + session = request.session + + if 'sso' in request.GET: + return HTTPFound(auth.login()) + elif 'sso2' in request.GET: + return_to = '%s/attrs/' % request.host_url + return HTTPFound(auth.login(return_to)) + elif 'slo' in request.GET: + name_id = None + session_index = None + if 'samlNameId' in session: + name_id = session['samlNameId'] + if 'samlSessionIndex' in session: + session_index = session['samlSessionIndex'] + + return HTTPFound(auth.logout(name_id=name_id, session_index=session_index)) + elif 'acs' in request.GET: + auth.process_response() + errors = auth.get_errors() + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + if 'RelayState' in request.POST and self_url != request.POST['RelayState']: + return HTTPFound(auth.redirect_to(request.POST['RelayState'])) + else: + error_reason = auth.get_last_error_reason() + elif 'sls' in request.GET: + dscb = lambda: session.clear() + url = auth.process_slo(delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + return HTTPFound(url) + else: + success_slo = True + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors': errors, + 'error_reason': error_reason, + 'not_auth_warn': not_auth_warn, + 'success_slo': success_slo, + 'attributes': attributes, + 'paint_logout': paint_logout, + } + + +@view_config(route_name='attrs', renderer='templates/attrs.jinja2') +def attrs(request): + paint_logout = False + attributes = False + + session = request.session + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'paint_logout': paint_logout, + 'attributes': attributes, + } + + +@view_config(route_name='metadata', renderer='html') +def metadata(request): + req = prepare_pyramid_request(request) + auth = init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = HTTPOk(body=metadata, headers={'Content-Type': 'text/xml'}) + else: + resp = HTTPInternalServerError(body=', '.join(errors)) + return resp diff --git a/demo_pyramid/development.ini b/demo_pyramid/development.ini new file mode 100644 index 00000000..64042ee7 --- /dev/null +++ b/demo_pyramid/development.ini @@ -0,0 +1,59 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:demo_pyramid + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = 127.0.0.1:6543 [::1]:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, demo_pyramid + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_demo_pyramid] +level = DEBUG +handlers = +qualname = demo_pyramid + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/demo_pyramid/production.ini b/demo_pyramid/production.ini new file mode 100644 index 00000000..84b482ae --- /dev/null +++ b/demo_pyramid/production.ini @@ -0,0 +1,53 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:demo_pyramid + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, demo_pyramid + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_demo_pyramid] +level = WARN +handlers = +qualname = demo_pyramid + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/demo_pyramid/setup.py b/demo_pyramid/setup.py new file mode 100644 index 00000000..a30a8ddd --- /dev/null +++ b/demo_pyramid/setup.py @@ -0,0 +1,45 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + 'xmlsec', + 'isodate', + 'python-saml', +] + +setup( + name='demo_pyramid', + version='0.0', + description='demo_pyramid', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = demo_pyramid:main', + ], + }, +)