diff --git a/playbooks/groups/fedocal.yml b/playbooks/groups/fedocal.yml new file mode 100644 index 0000000000..c68f48ac0c --- /dev/null +++ b/playbooks/groups/fedocal.yml @@ -0,0 +1,91 @@ +# create a new fedocal server +# NOTE: should be used with --limit most of the time +# NOTE: make sure there is room/space for this server on the vmhost +# NOTE: most of these vars come from group_vars/fedocal* or from hostvars + +- name: make fedocal + hosts: fedocal-stg + #;fedocal + user: root + gather_facts: False + + vars_files: + - /srv/web/infra/ansible/vars/global.yml + - ${private}/vars.yml + - ${vars}/${ansible_distribution}.yml + + tasks: + - include: $tasks/virt_instance_create.yml + - include: $tasks/accelerate_prep.yml + + handlers: + - include: $handlers/restart_services.yml + +- name: make the box be real + hosts: fedocal-stg + #;fedocal + user: root + gather_facts: True + accelerate: True + + vars_files: + - /srv/web/infra/ansible/vars/global.yml + - ${private}/vars.yml + - ${vars}/${ansible_distribution}.yml + + roles: + - /srv/web/infra/ansible/roles/base + - /srv/web/infra/ansible/roles/rkhunter + - /srv/web/infra/ansible/roles/denyhosts + - /srv/web/infra/ansible/roles/nagios_client + - /srv/web/infra/ansible/roles/fas_client + + tasks: + - include: $tasks/hosts.yml + - include: $tasks/yumrepos.yml + - include: $tasks/2fa_client.yml + - include: $tasks/motd.yml + - include: $tasks/sudo.yml + - include: $tasks/openvpn_client.yml + when: env != "staging" + - include: $tasks/apache.yml + - include: $tasks/mod_wsgi.yml + + handlers: + - include: $handlers/restart_services.yml + +- name: set up fedmsg + hosts: fedocal-stg + #;fedocal + user: root + gather_facts: True + accelerate: True + + vars_files: + - /srv/web/infra/ansible/vars/global.yml + - ${private}/vars.yml + - ${vars}/${ansible_distribution}.yml + + roles: + - /srv/web/infra/ansible/roles/fedmsg_base + + handlers: + - include: $handlers/restart_services.yml + +- name: deploy fedocal itself + hosts: fedocal-stg + #;fedocal + user: root + gather_facts: True + accelerate: True + + vars_files: + - /srv/web/infra/ansible/vars/global.yml + - ${private}/vars.yml + - ${vars}/${ansible_distribution}.yml + + roles: + - /srv/web/infra/ansible/roles/fedocal + + handlers: + - include: $handlers/restart_services.yml diff --git a/roles/fedocal/tasks/main.yml b/roles/fedocal/tasks/main.yml new file mode 100644 index 0000000000..6ad1efdbd1 --- /dev/null +++ b/roles/fedocal/tasks/main.yml @@ -0,0 +1,64 @@ +--- +# Configuration for the fedocal webapp + +- name: clean yum metadata + command: yum clean all + tags: + - packages + +- name: install needed packages + yum: pkg=$item state=installed + with_items: + - fedocal + - python-psycopg2 + - python-openid-cla + - python-openid-teams + - python-memcached + tags: + - packages + +- name: copy sundry fedocal configuration + template: src={{ item.file }} + dest={{ item.location }}/{{ item.dest }} + owner=apache group=apache mode=0600 + with_items: + - { file: fedocal_admin.cfg, location: /etc/fedocal, dest: fedocal.cfg } + tags: + - config + notify: + - restart apache + +- name: create the database scheme + command: /usr/bin/python2 /usr/share/fedocal/fedocal_createdb.py + environment: + FEDOCAL_CONFIG: /etc/fedocal/fedocal.cfg + +- name: replace the fedocal configuration file by the one with the normal user + template: src={{ item.file }} + dest={{ item.location }}/{{ item.file }} + owner=apache group=apache mode=0600 + with_items: + - { file: fedocal.cfg, location: /etc/fedocal } + - { file: fedocal.conf, location: /etc/httpd/conf.d } + - { file: fedocal.wsgi, location: /usr/share/fedocal } + tags: + - config + notify: + - restart apache + +- name: set sebooleans so fedocal can talk to the db + action: seboolean name=httpd_can_network_connect_db + state=true + persistent=true + + +- name: hotfix python-fedora-flask to include latest flask_fas_openid + template: src={{ item.file }} + dest={{ item.location }}/{{ item.file }} + owner=apache group=apache mode=0600 + with_items: + - { file: flask_fas_openid.py, location: /usr/lib/python2.6/site-packages/ } + tags: + - config + notify: + - restart apache diff --git a/roles/fedocal/templates/flask_fas_openid.py b/roles/fedocal/templates/flask_fas_openid.py new file mode 100644 index 0000000000..b951fb5883 --- /dev/null +++ b/roles/fedocal/templates/flask_fas_openid.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# Flask-FAS-OpenID - A Flask extension for authorizing users with FAS-OpenID +# +# Primary maintainer: Patrick Uiterwijk +# +# Copyright (c) 2013, Patrick Uiterwijk +# 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 + +''' +FAS-OpenID authentication plugin for the flask web framework + +.. moduleauthor:: Patrick Uiterwijk + +..versionadded:: 0.3.33 +''' +from functools import wraps + +from bunch import Bunch +import flask +try: + from flask import _app_ctx_stack as stack +except ImportError: + from flask import _request_ctx_stack as stack + +import openid +from openid.consumer import consumer +from openid.fetchers import setDefaultFetcher, Urllib2Fetcher +from openid.extensions import pape, sreg +from openid_cla import cla +from openid_teams import teams + +from fedora import __version__ + +class FAS(object): + + def __init__(self, app=None): + self.app = app + if self.app is not None: + self._init_app(app) + + def _init_app(self, app): + app.config.setdefault('FAS_OPENID_ENDPOINT', + 'http://id.fedoraproject.org/') + app.config.setdefault('FAS_OPENID_CHECK_CERT', True) + + if not self.app.config['FAS_OPENID_CHECK_CERT']: + setDefaultFetcher(Urllib2Fetcher()) + + @app.route('/_flask_fas_openid_handler/', methods=['GET', 'POST']) + def flask_fas_openid_handler(): + return self._handle_openid_request() + + app.before_request(self._check_session) + + def _handle_openid_request(self): + return_url = flask.session['FLASK_FAS_OPENID_RETURN_URL'] + cancel_url = flask.session['FLASK_FAS_OPENID_CANCEL_URL'] + base_url = self.normalize_url(flask.request.base_url) + oidconsumer = consumer.Consumer(flask.session, None) + info = oidconsumer.complete(flask.request.values, base_url) + display_identifier = info.getDisplayIdentifier() + + if info.status == consumer.FAILURE and display_identifier: + return 'FAILURE. display_identifier: %s' % display_identifier + elif info.status == consumer.CANCEL: + if cancel_url: + return flask.redirect(cancel_url) + return 'OpenID request was cancelled' + elif info.status == consumer.SUCCESS: + sreg_resp = sreg.SRegResponse.fromSuccessResponse(info) + pape_resp = pape.Response.fromSuccessResponse(info) + teams_resp = teams.TeamsResponse.fromSuccessResponse(info) + cla_resp = cla.CLAResponse.fromSuccessResponse(info) + user = {'fullname': '', 'username': '', 'email': '', 'timezone': '', 'cla_done': False, 'groups': []} + if not sreg_resp: + # If we have no basic info, be gone with them! + return flask.redirect(cancel_url) + user['username'] = sreg_resp.get('nickname') + user['fullname'] = sreg_resp.get('fullname') + user['email'] = sreg_resp.get('email') + user['timezone'] = sreg_resp.get('timezone') + if cla_resp: + user['cla_done'] = cla.CLA_URI_FEDORA_DONE in cla_resp.clas + if teams_resp: + user['groups'] = frozenset(teams_resp.teams) # The groups do not contain the cla_ groups + flask.session['FLASK_FAS_OPENID_USER'] = user + flask.session.modified = True + return flask.redirect(return_url) + else: + return 'Strange state: %s' % info.status + + def _check_session(self): + if not 'FLASK_FAS_OPENID_USER' in flask.session or flask.session['FLASK_FAS_OPENID_USER'] is None: + flask.g.fas_user = None + else: + user = flask.session['FLASK_FAS_OPENID_USER'] + # Add approved_memberships to provide backwards compatibility + # New applications should only use g.fas_user.groups + user['approved_memberships'] = [] + for group in user['groups']: + membership = dict() + membership['name'] = group + user['approved_memberships'].append(Bunch.fromDict(membership)) + flask.g.fas_user = Bunch.fromDict(user) + flask.g.fas_session_id = 0 + + def login(self, username=None, password=None, return_url=None, + cancel_url=None, groups=['_FAS_ALL_GROUPS_']): + """Tries to log in a user. + + Sets the user information on :attr:`flask.g.fas_user`. + Will set 0 to :attr:`flask.g.fas_session_id, for compatibility + with flask_fas. + + :arg username: Not used, but accepted for compatibility with the + flask_fas module + :arg password: Not used, but accepted for compatibility with the + flask_fas module + :arg return_url: The URL to forward the user to after login + :arg groups: A string or a list of group the user should belong to + to be authentified. + :returns: True if the user was succesfully authenticated. + :raises: Might raise an redirect to the OpenID endpoint + """ + if return_url is None: + if 'next' in flask.request.args.values(): + return_url = flask.request.args.values['next'] + else: + return_url = flask.request.url + oidconsumer = consumer.Consumer(flask.session, None) + try: + request = oidconsumer.begin(self.app.config['FAS_OPENID_ENDPOINT']) + except consumer.DiscoveryFailure, exc: + # VERY strange, as this means it could not discover an OpenID endpoint at FAS_OPENID_ENDPOINT + return 'discoveryfailure' + if request is None: + # Also very strange, as this means the discovered OpenID endpoint is no OpenID endpoint + return 'no-request' + + if isinstance(groups, basestring): + groups = [groups] + + request.addExtension(sreg.SRegRequest(required=['nickname', 'fullname', 'email', 'timezone'])) + request.addExtension(pape.Request([])) + request.addExtension(teams.TeamsRequest(requested=groups)) + request.addExtension(cla.CLARequest(requested=[cla.CLA_URI_FEDORA_DONE])) + + trust_root = self.normalize_url(flask.request.url_root) + return_to = trust_root + '_flask_fas_openid_handler/' + + flask.session['FLASK_FAS_OPENID_RETURN_URL'] = return_url + flask.session['FLASK_FAS_OPENID_CANCEL_URL'] = cancel_url + if request.shouldSendRedirect(): + redirect_url = request.redirectURL(trust_root, return_to, False) + return flask.redirect(redirect_url) + else: + return request.htmlMarkup(trust_root, return_to, form_tag_attrs={'id': 'openid_message'}, immediate=False) + + def logout(self): + '''Logout the user associated with this session + ''' + flask.session['FLASK_FAS_OPENID_USER'] = None + flask.g.fas_session_id = None + flask.g.fas_user = None + flask.session.modified = True + + def normalize_url(self, url): + ''' Replace the scheme prefix of a url with our preferred scheme. + ''' + scheme = self.app.config['PREFERRED_URL_SCHEME'] + scheme_index = url.index('://') + return scheme + url[scheme_index:] + + +# This is a decorator we can use with any HTTP method (except login, obviously) +# to require a login. +# If the user is not logged in, it will redirect them to the login form. +# http://flask.pocoo.org/docs/patterns/viewdecorators/#login-required-decorator +def fas_login_required(function): + """ Flask decorator to ensure that the user is logged in against FAS. + To use this decorator you need to have a function named 'auth_login'. + Without that function the redirect if the user is not logged in will not + work. + """ + @wraps(function) + def decorated_function(*args, **kwargs): + if flask.g.fas_user is None: + return flask.redirect(flask.url_for('auth_login', + next=flask.request.url)) + return function(*args, **kwargs) + return decorated_function + + +def cla_plus_one_required(function): + """ Flask decorator to retrict access to CLA+1. + To use this decorator you need to have a function named 'auth_login'. + Without that function the redirect if the user is not logged in will not + work. + """ + @wraps(function) + def decorated_function(*args, **kwargs): + if flask.g.fas_user is None or not flask.g.fas_user.cla_done or len(flask.g.fas_user.groups) < 1: # FAS-OpenID does not return cla_ groups + return flask.redirect(flask.url_for('auth_login', + next=flask.request.url)) + else: + return function(*args, **kwargs) + return decorated_function diff --git a/roles/fedocal/templates/nuancier_admin.cfg b/roles/fedocal/templates/nuancier_admin.cfg new file mode 100644 index 0000000000..95288741ec --- /dev/null +++ b/roles/fedocal/templates/nuancier_admin.cfg @@ -0,0 +1,44 @@ +# Beware that the quotes around the values are mandatory + +import os + +### Secret key for the Flask application +SECRET_KEY='{{ nuancier_secret_key }}' + +### url to the database server: +DB_URL='postgresql://{{ nuancier_db_admin_user }}:{{ nuancier_db_admin_pass }}@{{ nuancier_db_host }}/{{ nuancier_db_name }}' + +### The FAS groups in which the admin of nuancier-lite are +### This can either be a single group or multiple, defined between +### parenthesis. +ADMIN_GROUP=('sysadmin-nuancier', 'sysadmin-main') + + +### Static folder +### The folder containing the css, javascript as well as the pictures +### candidates and the cache of those pictures. +### This directory should be somewhere where apache can access, it's +### proposed in '/var/www/nuancier' +STATIC_FOLDER = '/var/www/nuancier' + +### Pictures folder +### The folder in which are located the pictures of the different elections. +### This folder does not have to be writable by the application but should be +### readable. +### /!\ It should be the full path to this folder +PICTURE_FOLDER = os.path.join(STATIC_FOLDER, 'pictures') + +### Cache folder +### The folder in which the application will generate thumbnails of the pictures +### selected for an election. +### This folder *must* be *writable* by the application. +### /!\ It should be the full path to this folder +CACHE_FOLDER = os.path.join(STATIC_FOLDER, 'cache') + +### Size of the thumbnails (keeping the ratio) +### In order to reduce the loading page of the election page that might contains +### more than hundreds pictures, the application generates thumbnails of each +### pictures. +### The application will keep the ratio intact and just make sure that either +### length or width of the picture fit the length and width specified below. +THUMB_SIZE = (256, 256)