From c9e3508451ed283d556cdc3363793553b1e31e13 Mon Sep 17 00:00:00 2001 From: Kevin Fenzi Date: Thu, 9 Jan 2014 16:50:45 +0000 Subject: [PATCH] Add ask01.stg and first cut at ask playbook and roles to ansible. --- inventory/group_vars/ask-stg | 9 + .../ask01.stg.phx2.fedoraproject.org | 10 + playbooks/groups/ask.yml | 50 ++ roles/ask/files/askbot.conf | 43 + roles/ask/files/askbot.wsgi | 16 + roles/ask/files/backends.py | 42 + roles/ask/files/cron-ask-send-reminders | 1 + roles/ask/files/cron-post-office-send-mail | 2 + roles/ask/files/fedora-openid.png | Bin 0 -> 2235 bytes roles/ask/files/login_providers.py | 108 +++ roles/ask/files/sanction-client.py | 195 ++++ roles/ask/files/util.py | 839 ++++++++++++++++++ roles/ask/tasks/main.yml | 133 +++ roles/ask/templates/settings.py | 344 +++++++ 14 files changed, 1792 insertions(+) create mode 100644 inventory/group_vars/ask-stg create mode 100644 inventory/host_vars/ask01.stg.phx2.fedoraproject.org create mode 100644 playbooks/groups/ask.yml create mode 100644 roles/ask/files/askbot.conf create mode 100644 roles/ask/files/askbot.wsgi create mode 100644 roles/ask/files/backends.py create mode 100644 roles/ask/files/cron-ask-send-reminders create mode 100644 roles/ask/files/cron-post-office-send-mail create mode 100644 roles/ask/files/fedora-openid.png create mode 100644 roles/ask/files/login_providers.py create mode 100644 roles/ask/files/sanction-client.py create mode 100644 roles/ask/files/util.py create mode 100644 roles/ask/tasks/main.yml create mode 100644 roles/ask/templates/settings.py diff --git a/inventory/group_vars/ask-stg b/inventory/group_vars/ask-stg new file mode 100644 index 0000000000..1ea6ee9fc5 --- /dev/null +++ b/inventory/group_vars/ask-stg @@ -0,0 +1,9 @@ +--- +# Define resources for this group of hosts here. +lvm_size: 20000 +mem_size: 2048 +num_cpus: 2 + +tcp_ports: [ 443 ] + +fas_client_groups: sysadmin-noc,sysadmin-ask,fi-apprentice diff --git a/inventory/host_vars/ask01.stg.phx2.fedoraproject.org b/inventory/host_vars/ask01.stg.phx2.fedoraproject.org new file mode 100644 index 0000000000..d0ff6bdbea --- /dev/null +++ b/inventory/host_vars/ask01.stg.phx2.fedoraproject.org @@ -0,0 +1,10 @@ +--- +nm: 255.255.255.0 +gw: 10.5.126.254 +dns: 10.5.126.21 +ks_url: http://10.5.126.23/repo/rhel/ks/kvm-rhel-6 +ks_repo: http://10.5.126.23/repo/rhel/RHEL6-x86_64/ +volgroup: /dev/vg_guests +eth0_ip: 10.5.126.80 +vmhost: virthost12.phx2.fedoraproject.org +datacenter: phx2 diff --git a/playbooks/groups/ask.yml b/playbooks/groups/ask.yml new file mode 100644 index 0000000000..6cfb7a7dd4 --- /dev/null +++ b/playbooks/groups/ask.yml @@ -0,0 +1,50 @@ +- name: make ask + hosts: ask-stg + user: root + gather_facts: False + accelerate: True + + vars_files: + - /srv/web/infra/ansible/vars/global.yml + - "{{ private }}/vars.yml" + - /srv/web/infra/ansible/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: ask-stg + user: root + gather_facts: True + accelerate: True + + vars_files: + - /srv/web/infra/ansible/vars/global.yml + - "{{ private }}/vars.yml" + - /srv/web/infra/ansible/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 + - /srv/web/infra/ansible/roles/ask + + 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" diff --git a/roles/ask/files/askbot.conf b/roles/ask/files/askbot.conf new file mode 100644 index 0000000000..631ffb793f --- /dev/null +++ b/roles/ask/files/askbot.conf @@ -0,0 +1,43 @@ +LoadModule deflate_module modules/mod_deflate.so + +# If we don't do this ourselves, the askbot wsgi app will do it incorrectly +# it seems that it doesn't evaluate the X-Forwarded-For header appropriately. +#RedirectMatch ^/$ https://ask.stg.fedoraproject.org/questions/ + +#The below needs to be apache writable +Alias /m/ /var/www/html/askbot/static/ +Alias /admin/media/ /usr/lib/python2.6/site-packages/django/contrib/admin/media/ + + + Order deny,allow + Allow from all + + +WSGIDaemonProcess askbot user=apache group=apache maximum-requests=1000 display-name=askbot processes=6 threads=1 shutdown-timeout=10 python-path=/etc/askbot/sites/ask +WSGISocketPrefix run/wsgi +WSGIRestrictStdout On +WSGIRestrictSignal Off +WSGIPythonOptimize 1 + +WSGIScriptAlias / /usr/sbin/askbot.wsgi + + +ExpiresActive On +ExpiresByType text/css "access plus 1 week" +ExpiresByType text/javascript "access plus 1 week" +ExpiresByType image/png "access plus 1 week" +ExpiresByType image/gif "access plus 1 week" + + + SetOutputFilter DEFLATE + WSGIProcessGroup askbot + Order deny,allow + Allow from all + + +Alias /upfiles/ /var/lib/askbot/upfiles/ask/ + + + Order deny,allow + Allow from all + diff --git a/roles/ask/files/askbot.wsgi b/roles/ask/files/askbot.wsgi new file mode 100644 index 0000000000..7676891dfa --- /dev/null +++ b/roles/ask/files/askbot.wsgi @@ -0,0 +1,16 @@ +#!/usr/bin/python +import os +import sys +sys.stdout = sys.stderr + +os.environ['DJANGO_SETTINGS_MODULE'] = 'config.settings' + +# Here we have to trick askbot into thinking its using ssl so it +# produces the correct URLs for openid/auth stuff (mostly for stg). +os.environ['HTTPS'] = "on" +def is_secure(self): + return True +import django.core.handlers.wsgi +django.core.handlers.wsgi.WSGIRequest.is_secure = is_secure + +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/roles/ask/files/backends.py b/roles/ask/files/backends.py new file mode 100644 index 0000000000..73d636849e --- /dev/null +++ b/roles/ask/files/backends.py @@ -0,0 +1,42 @@ +from django.core.mail.backends.base import BaseEmailBackend + +from .models import Email, PRIORITY, STATUS + + +class EmailBackend(BaseEmailBackend): + + def open(self): + pass + + def close(self): + pass + + def send_messages(self, email_messages): + """ + Queue one or more EmailMessage objects and returns the number of + email messages sent. + """ + if not email_messages: + return + num_sent = 0 + + for email in email_messages: + num_sent += 1 + subject = email.subject + from_email = email.from_email + message = email.body + + # Check whether email has 'text/html' alternative + alternatives = getattr(email, 'alternatives', ()) + for alternative in alternatives: + if alternative[1] == 'text/html': + html_message = alternative[0] + break + else: + html_message = '' + + for recipient in email.to: + Email.objects.create(from_email=from_email, to=recipient, + subject=subject, html_message=html_message, + message=message, status=STATUS.queued, + priority=PRIORITY.medium) diff --git a/roles/ask/files/cron-ask-send-reminders b/roles/ask/files/cron-ask-send-reminders new file mode 100644 index 0000000000..c05068d20c --- /dev/null +++ b/roles/ask/files/cron-ask-send-reminders @@ -0,0 +1 @@ +0 0 * * * root /usr/bin/python /etc/askbot/sites/ask/config/manage.py send_accept_answer_reminders > /dev/null 2> /dev/null diff --git a/roles/ask/files/cron-post-office-send-mail b/roles/ask/files/cron-post-office-send-mail new file mode 100644 index 0000000000..697ab9caa0 --- /dev/null +++ b/roles/ask/files/cron-post-office-send-mail @@ -0,0 +1,2 @@ +0 * * * * root /usr/bin/python /etc/askbot/sites/ask/config/manage.py send_queued_mail > /dev/null 2> /dev/null + diff --git a/roles/ask/files/fedora-openid.png b/roles/ask/files/fedora-openid.png new file mode 100644 index 0000000000000000000000000000000000000000..ffdf8aba772af21ee95dd4bad7319d6851d65edb GIT binary patch literal 2235 zcmV;s2t@aZP)2m75|3xk;;FMTDZn353m-o6uaO zbtqJ=&Iomeg2N`^GT1?h*xJh!X|iBx5K48ZP$-4Q-Q|)_gJcb%O(e7;A~yvDTP~&C zLb7|#+dp;}HdtvA*wN|i@1LD~-<#j}o@c)IJKuRlRY@p*GCuDF{@;}V9A_l}$5{!$ zaaICwoRt6^XC(l~SqZ>#mgDnI;ESdKqvm9SJxP@Z5gHV&#gNzb{^{ip+TtgPFS)Q} z(j>G0$=(YQ+(at7XV2ToA3KzaZFP-@4jrBKUggWLwAMFqL`2m8)E%=B3W+e8@W^QV zD`zomNMG(8oSBl^rPq>sN6pG@t#4un1NXX$y5leo0jbe3->Uh%zM+QSt=~#=r>>;m zeivu=)g0g3<;WencCFP?@5BxU4AUG=4BU<#+w+wkov7QrFFc?&A_78>{wMqNZ;f~2 zB?nky-!BMeN;=Nr8RWXc42!0IlP%Q;2z+pnhJStts;Jn5fOlK!eM5j<1-?gBc?zMA zD6_YF@_aYq2jFu1rYTHnXwemv9&@Q=SH`@2^kCBvw!x1j*P{_u%L!3MG z4xxtg(clkT_%m+dUj|Xn8U9ZK(c3ZqYB@qEd zZIPSe*FvewF_ej-0qAh!<yBba?Ul6sY^;r{bZ1;kz_kA?5Gotk#At}3OrkAs&=)^gy$M*u}QdOG2QUr0%@XtW>$3VNtWRsxJS9Pvi z7<-Jkio6e~Ex!fsi3|l%c@bd@=+{+^VF6E04S;LLrr~PXzXtBUx}``~9BAW(?fE!hk2QB)h;@9~5g`lA6A%(!7k%V%Dl1oOEmI!O)pYWnjBNh5~zm z9S$`s!i#&0L5hK62unbNs`3QL&zxs6l7N>0vSxaFnzn2MIsnH}RwIl?^oKxG?3&qy z{!9$r7Rj{^zQQ?UQ;rz;BbL=5 ze+B(6@K8wXw?{s+Zoq!O=N!FfgU)k)tMTNVbGd!TROkzk zq@iw+L;LWxA06so4;I)$_vULE$D03Xb=Po5U|*yHugbLcU>N{+w)S~js+$JbG~^vy z6_lfa#d!l&Y4-M%1q+NTFFDcof!;<)CIThVnBhhK!A1lwfN@K^{q~uQmx24TW_a(Z zUY!5_mAQ~iL_lEI_3}z-Osl_~+NeNQ>@5R8ESVU!a~Ar>3-lA!dQ;7Pm7bhq*>3OG zsyz9-+fL;sHhV5MblU;cBMfl5y~QJoHr@i^v)NVP-{L%zaiye0ics?VMZQA-m8w9b|emE>MzJt~Qos7^f}Kd08{d`>M$78|Bg_z=jZQ*IjAb)s_tj zs%bxg%2ANwkezfQyTEraWLt+79i`~2(b%fRxvwd@9_WRzA}v+UsME629x&e5#e`aHyMbgXSR-w##MnG|DT$Ss+|@ zF;eV~re%B&xS`z(Z3G-L@Poc#sH}<|4=j3kPikZGKtz88Q~@7@l!~F5fzte+MB`M^ zKxzKBMP+gX^e%)l+mZ~ijY%LYlW*;?3!r+@#D>6awWAfC25b`H?>K3#FKuoPyu}>OmE(HDp)U}0A ztP&0PI4jX`kFyehWriting->Remote Publishing and check the box for XML-RPC') + ) +) + +settings.register( + livesettings.ImageValue( + LOGIN_PROVIDERS, + 'WORDPRESS_SITE_ICON', + default='/images/logo.gif', + description=_('Upload your icon'), + url_resolver=skin_utils.get_media_url + ) +) + +providers = ( + 'local', + 'AOL', + 'FAS-OpenID', + 'Blogger', + 'ClaimID', + 'Facebook', + 'Flickr', + 'Google', + 'Twitter', + 'LinkedIn', + 'LiveJournal', + #'myOpenID', + 'OpenID', + 'Technorati', + 'Wordpress', + 'Vidoop', + 'Verisign', + 'Yahoo', + 'identi.ca', +) + +need_extra_setup = ('Twitter', 'Facebook', 'LinkedIn', 'identi.ca',) + +for provider in providers: + kwargs = { + 'description': _('Activate %(provider)s login') % {'provider': provider}, + 'default': True, + } + if provider in need_extra_setup: + kwargs['help_text'] = _( + 'Note: to really enable %(provider)s login ' + 'some additional parameters will need to be set ' + 'in the "External keys" section' + ) % {'provider': provider} + + setting_name = 'SIGNIN_%s_ENABLED' % provider.upper() + settings.register( + livesettings.BooleanValue( + LOGIN_PROVIDERS, + setting_name, + **kwargs + ) + ) diff --git a/roles/ask/files/sanction-client.py b/roles/ask/files/sanction-client.py new file mode 100644 index 0000000000..737f2114ef --- /dev/null +++ b/roles/ask/files/sanction-client.py @@ -0,0 +1,195 @@ +# vim: set ts=4 sw=) +""" OAuth 2.0 client librar +""" + +from json import loads +from datetime import datetime, timedelta +from time import mktime +try: + from urllib import urlencode + from urllib2 import Request, urlopen + from urlparse import urlsplit, urlunsplit, parse_qsl + + # monkeypatch httpmessage + from httplib import HTTPMessage + def get_charset(self): + try: + data = filter(lambda s: 'Content-Type' in s, self.headers)[0] + if 'charset' in data: + cs = data[data.index(';') + 1:-2].split('=')[1].lower() + return cs + except IndexError: + pass + + return 'utf-8' + HTTPMessage.get_content_charset = get_charset +except ImportError: + from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl + from urllib.request import Request, urlopen + + +class Client(object): + """ OAuth 2.0 client object + """ + + def __init__(self, auth_endpoint=None, token_endpoint=None, + resource_endpoint=None, client_id=None, client_secret=None, + redirect_uri=None, token_transport=None): + assert(hasattr(token_transport, '__call__') or + token_transport in ('headers', 'query', None)) + + self.auth_endpoint = auth_endpoint + self.token_endpoint = token_endpoint + self.resource_endpoint = resource_endpoint + self.redirect_uri = redirect_uri + self.client_id = client_id + self.client_secret = client_secret + self.access_token = None + self.token_transport = token_transport or 'query' + self.token_expires = -1 + self.refresh_token = None + + def auth_uri(self, scope=None, scope_delim=None, state=None, **kwargs): + """ Builds the auth URI for the authorization endpoint + """ + scope_delim = scope_delim and scope_delim or ' ' + kwargs.update({ + 'client_id': self.client_id, + 'response_type': 'code', + }) + + if scope is not None: + kwargs['scope'] = scope_delim.join(scope) + + if state is not None: + kwargs['state'] = state + + if self.redirect_uri is not None: + kwargs['redirect_uri'] = self.redirect_uri + + return '%s?%s' % (self.auth_endpoint, urlencode(kwargs)) + + def request_token(self, parser=None, exclude=None, **kwargs): + """ Request an access token from the token endpoint. + This is largely a helper method and expects the client code to + understand what the server expects. Anything that's passed into + ``**kwargs`` will be sent (``urlencode``d) to the endpoint. Client + secret and client ID are automatically included, so are not required + as kwargs. For example:: + + # if requesting access token from auth flow: + { + 'code': rval_from_auth, + } + + # if refreshing access token: + { + 'refresh_token': stored_refresh_token, + 'grant_type': 'refresh_token', + } + + :param exclude: An iterable of fields to exclude from the ``POST`` + data. This is useful for fields such as ``redirect_uri`` + that are required during initial code/token exchange, + but will cause errors with some providers when + exchanging refresh tokens for new access tokens. + :param parser: Callback to deal with returned data. Not all providers + use JSON. + """ + kwargs = kwargs and kwargs or {} + exclude = exclude or {} + + parser = parser and parser or loads + kwargs.update({ + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'grant_type' in kwargs and kwargs['grant_type'] or \ + 'authorization_code' + }) + if self.redirect_uri is not None and 'redirect_uri' not in exclude: + kwargs.update({'redirect_uri': self.redirect_uri}) + + msg = urlopen(self.token_endpoint, urlencode(kwargs).encode( + 'utf-8')) + data = parser(msg.read().decode(msg.info().get_content_charset() or + 'utf-8')) + + for key in data: + setattr(self, key, data[key]) + + # expires_in is RFC-compliant. if anything else is used by the + # provider, token_expires must be set manually + if hasattr(self, 'expires_in'): + self.token_expires = mktime((datetime.utcnow() + timedelta( + seconds=self.expires_in)).timetuple()) + + assert(self.access_token is not None) + + def refresh(self): + assert self.refresh_token is not None + self.request_token(refresh_token=self.refresh_token, + grant_type='refresh_token', exclude=('redirect_uri',)) + + def request(self, url, method=None, data=None, parser=None): + """ Request user data from the resource endpoint + :param url: The path to the resource and querystring if required + :param method: HTTP method. Defaults to ``GET`` unless data is not None + in which case it defaults to ``POST`` + :param data: Data to be POSTed to the resource endpoint + :param parser: Parser callback to deal with the returned data. Defaults + to ``json.loads`.` + """ + assert(self.access_token is not None) + parser = parser or loads + + if not method: + method = 'GET' if not data else 'POST' + + if not hasattr(self.token_transport, '__call__'): + transport = globals()['_transport_{0}'.format(self.token_transport)] + else: + transport = self.token_transport + + req = transport('{0}{1}'.format(self.resource_endpoint, + url), self.access_token, data=data, method=method) + + resp = urlopen(req) + data = resp.read() + try: + # try to decode it first using either the content charset, falling + # back to utf8 + return parser(data.decode(resp.info().get_content_charset() or + 'utf-8')) + except UnicodeDecodeError: + # if we've gotten a decoder error, the calling code better know how + # to deal with it. some providers (i.e. stackexchange) like to gzip + # their responses, so this allows the client code to handle it + # directly. + return parser(data) + +def _transport_headers(url, access_token, data=None, method=None): + try: + req = Request(url, data=data, method=method) + except TypeError: + req = Request(url, data=data) + req.get_method = lambda: method + + req.headers.update({ + 'Authorization': 'Bearer {0}'.format(access_token) + }) + return req + +def _transport_query(url, access_token, data=None, method=None): + parts = urlsplit(url) + query = dict(parse_qsl(parts.query)) + query.update({ + 'access_token': access_token + }) + url = urlunsplit((parts.scheme, parts.netloc, parts.path, + urlencode(query), parts.fragment)) + try: + req = Request(url, data=data, method=method) + except TypeError: + req = Request(url, data=data) + req.get_method = lambda: method + return req diff --git a/roles/ask/files/util.py b/roles/ask/files/util.py new file mode 100644 index 0000000000..4c4e47b4e7 --- /dev/null +++ b/roles/ask/files/util.py @@ -0,0 +1,839 @@ +# -*- coding: utf-8 -*- +import cgi +import urllib +import urlparse +import functools +import re +import random +from openid.store.interface import OpenIDStore +from openid.association import Association as OIDAssociation +from openid.extensions import sreg +from openid import store as openid_store +import oauth2 as oauth # OAuth1 protocol + +from django.db.models.query import Q +from django.conf import settings +from django.core.urlresolvers import reverse +from django.utils import simplejson +from django.utils.datastructures import SortedDict +from django.utils.translation import ugettext as _ +from django.core.exceptions import ImproperlyConfigured + +try: + from hashlib import md5 +except: + from md5 import md5 + +from askbot.conf import settings as askbot_settings + +# needed for some linux distributions like debian +try: + from openid.yadis import xri +except: + from yadis import xri + +import time, base64, hmac, hashlib, operator, logging +from models import Association, Nonce + +__all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next'] + +ALLOWED_LOGIN_TYPES = ('password', 'oauth', 'openid-direct', 'openid-username', 'wordpress') + +class OpenID: + def __init__(self, openid_, issued, attrs=None, sreg_=None): + logging.debug('init janrain openid object') + self.openid = openid_ + self.issued = issued + self.attrs = attrs or {} + self.sreg = sreg_ or {} + self.is_iname = (xri.identifierScheme(openid_) == 'XRI') + + def __repr__(self): + return '' % self.openid + + def __str__(self): + return self.openid + +class DjangoOpenIDStore(OpenIDStore): + def __init__(self): + self.max_nonce_age = 6 * 60 * 60 # Six hours + + def storeAssociation(self, server_url, association): + assoc = Association( + server_url = server_url, + handle = association.handle, + secret = base64.encodestring(association.secret), + issued = association.issued, + lifetime = association.issued, + assoc_type = association.assoc_type + ) + assoc.save() + + def getAssociation(self, server_url, handle=None): + assocs = [] + if handle is not None: + assocs = Association.objects.filter( + server_url = server_url, handle = handle + ) + else: + assocs = Association.objects.filter( + server_url = server_url + ) + if not assocs: + return None + associations = [] + for assoc in assocs: + association = OIDAssociation( + assoc.handle, base64.decodestring(assoc.secret), assoc.issued, + assoc.lifetime, assoc.assoc_type + ) + if association.getExpiresIn() == 0: + self.removeAssociation(server_url, assoc.handle) + else: + associations.append((association.issued, association)) + if not associations: + return None + return associations[-1][1] + + def removeAssociation(self, server_url, handle): + assocs = list(Association.objects.filter( + server_url = server_url, handle = handle + )) + assocs_exist = len(assocs) > 0 + for assoc in assocs: + assoc.delete() + return assocs_exist + + def useNonce(self, server_url, timestamp, salt): + if abs(timestamp - time.time()) > openid_store.nonce.SKEW: + return False + + query = [ + Q(server_url__exact=server_url), + Q(timestamp__exact=timestamp), + Q(salt__exact=salt), + ] + try: + ononce = Nonce.objects.get(reduce(operator.and_, query)) + except Nonce.DoesNotExist: + ononce = Nonce( + server_url=server_url, + timestamp=timestamp, + salt=salt + ) + ononce.save() + return True + + ononce.delete() + + return False + + def cleanupAssociations(self): + Association.objects.extra(where=['issued + lifetimeint<(%s)' % time.time()]).delete() + + def getAuthKey(self): + # Use first AUTH_KEY_LEN characters of md5 hash of SECRET_KEY + return hashlib.md5(settings.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN] + + def isDumb(self): + return False + +def from_openid_response(openid_response): + """ return openid object from response """ + issued = int(time.time()) + sreg_resp = sreg.SRegResponse.fromSuccessResponse(openid_response) \ + or [] + + return OpenID( + openid_response.identity_url, issued, openid_response.signed_fields, + dict(sreg_resp) + ) + +def get_provider_name(openid_url): + """returns provider name from the openid_url + """ + openid_str = openid_url + bits = openid_str.split('/') + base_url = bits[2] #assume this is base url + url_bits = base_url.split('.') + return url_bits[-2].lower() + +def use_password_login(): + """password login is activated + either if USE_RECAPTCHA is false + of if recaptcha keys are set correctly + + Currently hotfixed to disable this + """ + return False + if askbot_settings.SIGNIN_WORDPRESS_SITE_ENABLED: + return True + if askbot_settings.USE_RECAPTCHA: + if askbot_settings.RECAPTCHA_KEY and askbot_settings.RECAPTCHA_SECRET: + return True + else: + logging.critical('if USE_RECAPTCHA == True, set recaptcha keys!!!') + return False + else: + return True + +def filter_enabled_providers(data): + """deletes data about disabled providers from + the input dictionary + """ + delete_list = list() + for provider_key, provider_settings in data.items(): + name = provider_settings['name'] + if name == 'fasopenid': + is_enabled = True + else: + is_enabled = getattr(askbot_settings, 'SIGNIN_' + name.upper() + '_ENABLED') + if is_enabled == False: + delete_list.append(provider_key) + + for provider_key in delete_list: + del data[provider_key] + + return data + +class LoginMethod(object): + """Helper class to add custom authentication modules + as plugins for the askbot's version of django_authopenid + """ + def __init__(self, login_module_path): + from askbot.utils.loading import load_module + self.mod = load_module(login_module_path) + self.mod_path = login_module_path + self.read_params() + + def get_required_attr(self, attr_name, required_for_what): + attr_value = getattr(self.mod, attr_name, None) + if attr_value is None: + raise ImproperlyConfigured( + '%s.%s is required for %s' % ( + self.mod_path, + attr_name, + required_for_what + ) + ) + return attr_value + + def read_params(self): + self.is_major = getattr(self.mod, 'BIG_BUTTON', True) + if not isinstance(self.is_major, bool): + raise ImproperlyConfigured( + 'Boolean value expected for %s.BIG_BUTTON' % self.mod_path + ) + + self.order_number = getattr(self.mod, 'ORDER_NUMBER', 1) + if not isinstance(self.order_number, int): + raise ImproperlyConfigured( + 'Integer value expected for %s.ORDER_NUMBER' % self.mod_path + ) + + self.name = getattr(self.mod, 'NAME', None) + if self.name is None or not isinstance(self.name, basestring): + raise ImproperlyConfigured( + '%s.NAME is required as a string parameter' % self.mod_path + ) + if not re.search(r'^[a-zA-Z0-9]+$', self.name): + raise ImproperlyConfigured( + '%s.NAME must be a string of ASCII letters and digits only' + ) + + self.display_name = getattr(self.mod, 'DISPLAY_NAME', None) + if self.display_name is None or not isinstance(self.display_name, basestring): + raise ImproperlyConfigured( + '%s.DISPLAY_NAME is required as a string parameter' % self.mod_path + ) + self.extra_token_name = getattr(self.mod, 'EXTRA_TOKEN_NAME', None) + self.login_type = getattr(self.mod, 'TYPE', None) + if self.login_type is None or self.login_type not in ALLOWED_LOGIN_TYPES: + raise ImproperlyConfigured( + "%s.TYPE must be a string " + "and the possible values are : 'password', 'oauth', " + "'openid-direct', 'openid-username'." % self.mod_path + ) + self.icon_media_path = getattr(self.mod, 'ICON_MEDIA_PATH', None) + if self.icon_media_path is None: + raise ImproperlyConfigured( + '%s.ICON_MEDIA_PATH is required and must be a url ' + 'to the image used as login button' % self.mod_path + ) + + self.create_password_prompt = getattr(self.mod, 'CREATE_PASSWORD_PROMPT', None) + self.change_password_prompt = getattr(self.mod, 'CHANGE_PASSWORD_PROMPT', None) + + if self.login_type == 'password': + self.check_password_function = self.get_required_attr( + 'check_password', + 'custom password login' + ) + if self.login_type == 'oauth': + for_what = 'custom OAuth login' + self.oauth_consumer_key = self.get_required_attr('OAUTH_CONSUMER_KEY', for_what) + self.oauth_consumer_secret = self.get_required_attr('OAUTH_CONSUMER_SECRET', for_what) + self.oauth_request_token_url = self.get_required_attr('OAUTH_REQUEST_TOKEN_URL', for_what) + self.oauth_access_token_url = self.get_required_attr('OAUTH_ACCESS_TOKEN_URL', for_what) + self.oauth_authorize_url = self.get_required_attr('OAUTH_AUTHORIZE_URL', for_what) + self.oauth_get_user_id_function = self.get_required_attr('oauth_get_user_id_function', for_what) + + if self.login_type.startswith('openid'): + self.openid_endpoint = self.get_required_attr('OPENID_ENDPOINT', 'custom OpenID login') + if self.login_type == 'openid-username': + if '%(username)s' not in self.openid_endpoint: + msg = 'If OpenID provider requires a username, ' + \ + 'then value of %s.OPENID_ENDPOINT must contain ' + \ + '%(username)s so that the username can be transmitted to the provider' + raise ImproperlyConfigured(msg % self.mod_path) + + self.tooltip_text = getattr(self.mod, 'TOOLTIP_TEXT', None) + + def as_dict(self): + """returns parameters as dictionary that + can be inserted into one of the provider data dictionaries + for the use in the UI""" + params = ( + 'name', 'display_name', 'type', 'icon_media_path', + 'extra_token_name', 'create_password_prompt', + 'change_password_prompt', 'consumer_key', 'consumer_secret', + 'request_token_url', 'access_token_url', 'authorize_url', + 'get_user_id_function', 'openid_endpoint', 'tooltip_text', + 'check_password', + ) + #some parameters in the class have different names from those + #in the dictionary + parameter_map = { + 'type': 'login_type', + 'consumer_key': 'oauth_consumer_key', + 'consumer_secret': 'oauth_consumer_secret', + 'request_token_url': 'oauth_request_token_url', + 'access_token_url': 'oauth_access_token_url', + 'authorize_url': 'oauth_authorize_url', + 'get_user_id_function': 'oauth_get_user_id_function', + 'check_password': 'check_password_function' + } + data = dict() + for param in params: + attr_name = parameter_map.get(param, param) + data[param] = getattr(self, attr_name, None) + if self.login_type == 'password': + #passwords in external login systems are not changeable + data['password_changeable'] = False + return data + +def add_custom_provider(func): + @functools.wraps(func) + def wrapper(): + providers = func() + login_module_path = getattr(settings, 'ASKBOT_CUSTOM_AUTH_MODULE', None) + if login_module_path: + mod = LoginMethod(login_module_path) + if mod.is_major != func.is_major: + return providers#only patch the matching provider set + providers.insert(mod.order_number - 1, mod.name, mod.as_dict()) + return providers + return wrapper + +def get_enabled_major_login_providers(): + """returns a dictionary with data about login providers + whose icons are to be shown in large format + + disabled providers are excluded + + items of the dictionary are dictionaries with keys: + + * name + * display_name + * icon_media_path (relative to /media directory) + * type (oauth|openid-direct|openid-generic|openid-username|password) + + Fields dependent on type of the login provider type + --------------------------------------------------- + + Password (type = password) - login provider using login name and password: + + * extra_token_name - a phrase describing what the login name and the + password are from + * create_password_prompt - a phrase prompting to create an account + * change_password_prompt - a phrase prompting to change password + + OpenID (type = openid) - Provider of login using the OpenID protocol + + * openid_endpoint (required for type=openid|openid-username) + for type openid-username - the string must have %(username)s + format variable, plain string url otherwise + * extra_token_name - required for type=openid-username + describes name of required extra token - e.g. "XYZ user name" + + OAuth2 (type = oauth) + + * request_token_url - url to initiate OAuth2 protocol with the resource + * access_token_url - url to access users data on the resource via OAuth2 + * authorize_url - url at which user can authorize the app to access a resource + * authenticate_url - url to authenticate user (lower privilege than authorize) + * get_user_id_function - a function that returns user id from data dictionary + containing: response to the access token url & consumer_key + and consumer secret. The purpose of this function is to hide the differences + between the ways user id is accessed from the different OAuth providers + """ + data = SortedDict() + + if use_password_login(): + site_name = askbot_settings.APP_SHORT_NAME + prompt = _('%(site)s user name and password') % {'site': site_name} + data['local'] = { + 'name': 'local', + 'display_name': site_name, + 'extra_token_name': prompt, + 'type': 'password', + 'create_password_prompt': _('Create a password-protected account'), + 'change_password_prompt': _('Change your password'), + 'icon_media_path': askbot_settings.LOCAL_LOGIN_ICON, + 'password_changeable': True + } + + data['fasopenid'] = { + 'name': 'fasopenid', + 'display_name': 'FAS-OpenID', + 'type': 'openid-direct', + 'icon_media_path': '/jquery-openid/images/fedora-openid.png', + 'openid_endpoint': 'http://id.fedoraproject.org/', + } + + + def get_facebook_user_id(client): + """returns facebook user id given the access token""" + profile = client.request('me') + return profile['id'] + + if askbot_settings.FACEBOOK_KEY and askbot_settings.FACEBOOK_SECRET: + data['facebook'] = { + 'name': 'facebook', + 'display_name': 'Facebook', + 'type': 'oauth2', + 'auth_endpoint': 'https://www.facebook.com/dialog/oauth/', + 'token_endpoint': 'https://graph.facebook.com/oauth/access_token', + 'resource_endpoint': 'https://graph.facebook.com/', + 'icon_media_path': '/jquery-openid/images/facebook.gif', + 'get_user_id_function': get_facebook_user_id, + 'response_parser': lambda data: dict(urlparse.parse_qsl(data)) + + } + if askbot_settings.TWITTER_KEY and askbot_settings.TWITTER_SECRET: + data['twitter'] = { + 'name': 'twitter', + 'display_name': 'Twitter', + 'type': 'oauth', + 'request_token_url': 'https://api.twitter.com/oauth/request_token', + 'access_token_url': 'https://api.twitter.com/oauth/access_token', + 'authorize_url': 'https://api.twitter.com/oauth/authorize', + 'authenticate_url': 'https://api.twitter.com/oauth/authenticate', + 'get_user_id_url': 'https://twitter.com/account/verify_credentials.json', + 'icon_media_path': '/jquery-openid/images/twitter.gif', + 'get_user_id_function': lambda data: data['user_id'], + } + def get_identica_user_id(data): + consumer = oauth.Consumer(data['consumer_key'], data['consumer_secret']) + token = oauth.Token(data['oauth_token'], data['oauth_token_secret']) + client = oauth.Client(consumer, token=token) + url = 'https://identi.ca/api/account/verify_credentials.json' + response, content = client.request(url, 'GET') + json = simplejson.loads(content) + return json['id'] + if askbot_settings.IDENTICA_KEY and askbot_settings.IDENTICA_SECRET: + data['identi.ca'] = { + 'name': 'identi.ca', + 'display_name': 'identi.ca', + 'type': 'oauth', + 'request_token_url': 'https://identi.ca/api/oauth/request_token', + 'access_token_url': 'https://identi.ca/api/oauth/access_token', + 'authorize_url': 'https://identi.ca/api/oauth/authorize', + 'authenticate_url': 'https://identi.ca/api/oauth/authorize', + 'icon_media_path': '/jquery-openid/images/identica.png', + 'get_user_id_function': get_identica_user_id, + } + def get_linked_in_user_id(data): + consumer = oauth.Consumer(data['consumer_key'], data['consumer_secret']) + token = oauth.Token(data['oauth_token'], data['oauth_token_secret']) + client = oauth.Client(consumer, token=token) + url = 'https://api.linkedin.com/v1/people/~:(first-name,last-name,id)' + response, content = client.request(url, 'GET') + if response['status'] == '200': + id_re = re.compile(r'([^<]+)') + matches = id_re.search(content) + if matches: + return matches.group(1) + raise OAuthError() + + if askbot_settings.SIGNIN_WORDPRESS_SITE_ENABLED and askbot_settings.WORDPRESS_SITE_URL: + data['wordpress_site'] = { + 'name': 'wordpress_site', + 'display_name': 'Self hosted wordpress blog', #need to be added as setting. + 'icon_media_path': askbot_settings.WORDPRESS_SITE_ICON, + 'type': 'wordpress_site', + } + if askbot_settings.LINKEDIN_KEY and askbot_settings.LINKEDIN_SECRET: + data['linkedin'] = { + 'name': 'linkedin', + 'display_name': 'LinkedIn', + 'type': 'oauth', + 'request_token_url': 'https://api.linkedin.com/uas/oauth/requestToken', + 'access_token_url': 'https://api.linkedin.com/uas/oauth/accessToken', + 'authorize_url': 'https://www.linkedin.com/uas/oauth/authorize', + 'authenticate_url': 'https://www.linkedin.com/uas/oauth/authenticate', + 'icon_media_path': '/jquery-openid/images/linkedin.gif', + 'get_user_id_function': get_linked_in_user_id + } + data['google'] = { + 'name': 'google', + 'display_name': 'Google', + 'type': 'openid-direct', + 'icon_media_path': '/jquery-openid/images/google.gif', + 'openid_endpoint': 'https://www.google.com/accounts/o8/id', + } + data['yahoo'] = { + 'name': 'yahoo', + 'display_name': 'Yahoo', + 'type': 'openid-direct', + 'icon_media_path': '/jquery-openid/images/yahoo.gif', + 'tooltip_text': _('Sign in with Yahoo'), + 'openid_endpoint': 'http://yahoo.com', + } + data['aol'] = { + 'name': 'aol', + 'display_name': 'AOL', + 'type': 'openid-username', + 'extra_token_name': _('AOL screen name'), + 'icon_media_path': '/jquery-openid/images/aol.gif', + 'openid_endpoint': 'http://openid.aol.com/%(username)s' + } + data['openid'] = { + 'name': 'openid', + 'display_name': 'OpenID', + 'type': 'openid-generic', + 'extra_token_name': _('OpenID url'), + 'icon_media_path': '/jquery-openid/images/openid.gif', + 'openid_endpoint': None, + } + return filter_enabled_providers(data) +get_enabled_major_login_providers.is_major = True +get_enabled_major_login_providers = add_custom_provider(get_enabled_major_login_providers) + +def get_enabled_minor_login_providers(): + """same as get_enabled_major_login_providers + but those that are to be displayed with small buttons + + disabled providers are excluded + + structure of dictionary values is the same as in get_enabled_major_login_providers + """ + data = SortedDict() + #data['myopenid'] = { + # 'name': 'myopenid', + # 'display_name': 'MyOpenid', + # 'type': 'openid-username', + # 'extra_token_name': _('MyOpenid user name'), + # 'icon_media_path': '/jquery-openid/images/myopenid-2.png', + # 'openid_endpoint': 'http://%(username)s.myopenid.com' + #} + data['flickr'] = { + 'name': 'flickr', + 'display_name': 'Flickr', + 'type': 'openid-username', + 'extra_token_name': _('Flickr user name'), + 'icon_media_path': '/jquery-openid/images/flickr.png', + 'openid_endpoint': 'http://flickr.com/%(username)s/' + } + data['technorati'] = { + 'name': 'technorati', + 'display_name': 'Technorati', + 'type': 'openid-username', + 'extra_token_name': _('Technorati user name'), + 'icon_media_path': '/jquery-openid/images/technorati-1.png', + 'openid_endpoint': 'http://technorati.com/people/technorati/%(username)s/' + } + data['wordpress'] = { + 'name': 'wordpress', + 'display_name': 'WordPress', + 'type': 'openid-username', + 'extra_token_name': _('WordPress blog name'), + 'icon_media_path': '/jquery-openid/images/wordpress.png', + 'openid_endpoint': 'http://%(username)s.wordpress.com' + } + data['blogger'] = { + 'name': 'blogger', + 'display_name': 'Blogger', + 'type': 'openid-username', + 'extra_token_name': _('Blogger blog name'), + 'icon_media_path': '/jquery-openid/images/blogger-1.png', + 'openid_endpoint': 'http://%(username)s.blogspot.com' + } + data['livejournal'] = { + 'name': 'livejournal', + 'display_name': 'LiveJournal', + 'type': 'openid-username', + 'extra_token_name': _('LiveJournal blog name'), + 'icon_media_path': '/jquery-openid/images/livejournal-1.png', + 'openid_endpoint': 'http://%(username)s.livejournal.com' + } + data['claimid'] = { + 'name': 'claimid', + 'display_name': 'ClaimID', + 'type': 'openid-username', + 'extra_token_name': _('ClaimID user name'), + 'icon_media_path': '/jquery-openid/images/claimid-0.png', + 'openid_endpoint': 'http://claimid.com/%(username)s/' + } + data['vidoop'] = { + 'name': 'vidoop', + 'display_name': 'Vidoop', + 'type': 'openid-username', + 'extra_token_name': _('Vidoop user name'), + 'icon_media_path': '/jquery-openid/images/vidoop.png', + 'openid_endpoint': 'http://%(username)s.myvidoop.com/' + } + data['verisign'] = { + 'name': 'verisign', + 'display_name': 'Verisign', + 'type': 'openid-username', + 'extra_token_name': _('Verisign user name'), + 'icon_media_path': '/jquery-openid/images/verisign-2.png', + 'openid_endpoint': 'http://%(username)s.pip.verisignlabs.com/' + } + return filter_enabled_providers(data) +get_enabled_minor_login_providers.is_major = False +get_enabled_minor_login_providers = add_custom_provider(get_enabled_minor_login_providers) + +def have_enabled_federated_login_methods(): + providers = get_enabled_major_login_providers() + providers.update(get_enabled_minor_login_providers()) + provider_types = [provider['type'] for provider in providers.values()] + for provider_type in provider_types: + if provider_type.startswith('openid') or provider_type == 'oauth': + return True + return False + +def get_enabled_login_providers(): + """return all login providers in one sorted dict + """ + data = get_enabled_major_login_providers() + data.update(get_enabled_minor_login_providers()) + return data + +def set_login_provider_tooltips(provider_dict, active_provider_names = None): + """adds appropriate tooltip_text field to each provider + record, if second argument is None, then tooltip is of type + signin with ..., otherwise it's more elaborate - + depending on the type of provider and whether or not it's one of + currently used + """ + for provider in provider_dict.values(): + if active_provider_names: + if provider['name'] in active_provider_names: + if provider['type'] == 'password': + tooltip = _('Change your %(provider)s password') % \ + {'provider': provider['display_name']} + else: + tooltip = _( + 'Click to see if your %(provider)s ' + 'signin still works for %(site_name)s' + ) % { + 'provider': provider['display_name'], + 'site_name': askbot_settings.APP_SHORT_NAME + } + else: + if provider['type'] == 'password': + tooltip = _( + 'Create password for %(provider)s' + ) % {'provider': provider['display_name']} + else: + tooltip = _( + 'Connect your %(provider)s account ' + 'to %(site_name)s' + ) % { + 'provider': provider['display_name'], + 'site_name': askbot_settings.APP_SHORT_NAME + } + else: + if provider['type'] == 'password': + tooltip = _( + 'Signin with %(provider)s user name and password' + ) % { + 'provider': provider['display_name'], + 'site_name': askbot_settings.APP_SHORT_NAME + } + else: + tooltip = _( + 'Sign in with your %(provider)s account' + ) % {'provider': provider['display_name']} + provider['tooltip_text'] = tooltip + + +def get_oauth_parameters(provider_name): + """retrieves OAuth protocol parameters + from hardcoded settings and adds some + from the livesettings + + because this function uses livesettings + it should not be called at compile time + otherwise there may be strange errors + """ + providers = get_enabled_login_providers() + data = providers[provider_name] + if data['type'] != 'oauth': + raise ValueError('oauth provider expected, %s found' % data['type']) + + if provider_name == 'twitter': + consumer_key = askbot_settings.TWITTER_KEY + consumer_secret = askbot_settings.TWITTER_SECRET + elif provider_name == 'linkedin': + consumer_key = askbot_settings.LINKEDIN_KEY + consumer_secret = askbot_settings.LINKEDIN_SECRET + elif provider_name == 'identi.ca': + consumer_key = askbot_settings.IDENTICA_KEY + consumer_secret = askbot_settings.IDENTICA_SECRET + elif provider_name == 'facebook': + consumer_key = askbot_settings.FACEBOOK_KEY + consumer_secret = askbot_settings.FACEBOOK_SECRET + else: + raise ValueError('unexpected oauth provider %s' % provider_name) + + data['consumer_key'] = consumer_key + data['consumer_secret'] = consumer_secret + + return data + + +class OAuthError(Exception): + """Error raised by the OAuthConnection class + """ + pass + + +class OAuthConnection(object): + """a simple class wrapping oauth2 library + """ + + def __init__(self, provider_name, callback_url = None): + """initializes oauth connection + """ + self.provider_name = provider_name + self.parameters = get_oauth_parameters(provider_name) + self.callback_url = callback_url + self.consumer = oauth.Consumer( + self.parameters['consumer_key'], + self.parameters['consumer_secret'], + ) + + def start(self, callback_url = None): + """starts the OAuth protocol communication and + saves request token as :attr:`request_token`""" + + if callback_url is None: + callback_url = self.callback_url + + client = oauth.Client(self.consumer) + request_url = self.parameters['request_token_url'] + + if callback_url: + callback_url = '%s%s' % (askbot_settings.APP_URL, callback_url) + request_body = urllib.urlencode(dict(oauth_callback=callback_url)) + + self.request_token = self.send_request( + client = client, + url = request_url, + method = 'POST', + body = request_body + ) + else: + self.request_token = self.send_request( + client, + request_url, + 'GET' + ) + + def send_request(self, client=None, url=None, method='GET', **kwargs): + + response, content = client.request(url, method, **kwargs) + if response['status'] == '200': + return dict(cgi.parse_qsl(content)) + else: + raise OAuthError('response is %s' % response) + + def get_token(self): + return self.request_token + + def get_user_id(self, oauth_token = None, oauth_verifier = None): + """Returns user ID within the OAuth provider system, + based on ``oauth_token`` and ``oauth_verifier`` + """ + + token = oauth.Token( + oauth_token['oauth_token'], + oauth_token['oauth_token_secret'] + ) + token.set_verifier(oauth_verifier) + client = oauth.Client(self.consumer, token = token) + url = self.parameters['access_token_url'] + #there must be some provider-specific post-processing + data = self.send_request(client = client, url=url, method='GET') + data['consumer_key'] = self.parameters['consumer_key'] + data['consumer_secret'] = self.parameters['consumer_secret'] + return self.parameters['get_user_id_function'](data) + + def get_auth_url(self, login_only = False): + """returns OAuth redirect url. + if ``login_only`` is True, authentication + endpoint will be used, if available, otherwise authorization + url (potentially granting full access to the server) will + be used. + + Typically, authentication-only endpoint simplifies the + signin process, but does not allow advanced access to the + content on the OAuth-enabled server + """ + + endpoint_url = self.parameters.get('authorize_url', None) + if login_only == True: + endpoint_url = self.parameters.get( + 'authenticate_url', + endpoint_url + ) + if endpoint_url is None: + raise ImproperlyConfigured('oauth parameters are incorrect') + + auth_url = '%s?oauth_token=%s' % \ + ( + endpoint_url, + self.request_token['oauth_token'], + ) + + return auth_url + +def get_oauth2_starter_url(provider_name, csrf_token): + """returns redirect url for the oauth2 protocol for a given provider""" + from sanction.client import Client + + providers = get_enabled_login_providers() + params = providers[provider_name] + client_id = getattr(askbot_settings, provider_name.upper() + '_KEY') + redirect_uri = askbot_settings.APP_URL + reverse('user_complete_oauth2_signin') + client = Client( + auth_endpoint=params['auth_endpoint'], + client_id=client_id, + redirect_uri=redirect_uri + ) + return client.auth_uri(state=csrf_token) + + +def ldap_check_password(username, password): + import ldap + try: + ldap_session = ldap.initialize(askbot_settings.LDAP_URL) + ldap_session.simple_bind_s(username, password) + ldap_session.unbind_s() + return True + except ldap.LDAPError, e: + logging.critical(unicode(e)) + return False diff --git a/roles/ask/tasks/main.yml b/roles/ask/tasks/main.yml new file mode 100644 index 0000000000..81769e1f3a --- /dev/null +++ b/roles/ask/tasks/main.yml @@ -0,0 +1,133 @@ +--- +# +# Setup askbot for ask.fedoraproject.org site. +# +- name: install needed packages + yum: pkg={{ item }} state=installed + with_items: + - askbot + - python-memcached + - python-askbot-fedmsg + - python-psycopg2 + - python-django-post_office + tags: + - packages + +- name: install askbot settings.py template + template: > + src={{ item }} dest="/etc/askbot/sites/ask/config/settings.py" + owner=fedmsg group=fedmsg mode=0600 + with_items: + - settings.py + tags: + - config + +- name: Install askbot.conf httpd config + copy: > + src=askbot.conf dest=/etc/httpd/conf.d/askbot.conf + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd + +# +# we add this wsgi to handle ssl issues in stg +# +- name: Install askbot.wsgi httpd config + copy: > + src=askbot.wsgi dest=/usr/sbin/askbot.wsgi + owner=root group=root mode=0755 + tags: + - files + notify: + - restart httpd + +- name: Install askbot cron jobs + copy: > + src={{ item }} dest=/etc/cron.d/{{ item }} + owner=root group=root mode=0644 + with_items: + - cron-ask-send-reminders + - cron-post-office-send-mail + tags: + - files + +- name: setup default skin link needed for askbot + file: state=link src=/usr/lib/python2.6/site-packages/askbot/skins/default dest=/usr/lib/python2.6/site-packages/askbot/static/default + +- name: setup admin skin link needed for askbot + file: state=link src=/usr/lib/python2.6/site-packages/askbot/skins/admin dest=/usr/lib/python2.6/site-packages/askbot/static/admin + +# +# ? +# +- name: HOTFIX: askbot backends.py + copy: > + src=backends.py dest=/usr/lib/python2.6/site-packages/post_office/backends.py + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd + +# +# Adds fedora openid login button +# +- name: HOTFIX: askbot login_providers.py + copy: > + src=login_providers.py dest=/usr/lib/python2.6/site-packages/askbot/conf/login_providers.py + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd + +# +# Adds fedora openid login button +# +- name: HOTFIX: askbot util.py + copy: > + src=util.py dest=/usr/lib/python2.6/site-packages/askbot/deps/django_authopenid/util.py + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd + +# +# fedora openid icon for login screen +# +- name: HOTFIX: askbot fedora-openid.png + copy: > + src=fedora-openid.png dest=/usr/lib/python2.6/site-packages/askbot/media/jquery-openid/images/fedora-openid.png + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd + +# +# fedora openid icon for login screen +# +- name: HOTFIX: askbot fedora-openid.png + copy: > + src=fedora-openid.png dest=/usr/lib/python2.6/site-packages/askbot/static/default/media/jquery-openid/images/fedora-openid.png + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd + +# +# fixes login with facebook. +# + +- name: HOTFIX: askbot sanction-client.py + copy: > + src=sanction-client.py dest=/usr/lib/python2.6/site-packages/sanction/client.py + owner=root group=root mode=0644 + tags: + - files + notify: + - restart httpd diff --git a/roles/ask/templates/settings.py b/roles/ask/templates/settings.py new file mode 100644 index 0000000000..7a763f81cb --- /dev/null +++ b/roles/ask/templates/settings.py @@ -0,0 +1,344 @@ +## Django settings for ASKBOT enabled project. +import os.path +import logging +import sys +import askbot +import site + +#this line is added so that we can import pre-packaged askbot dependencies +ASKBOT_ROOT = os.path.abspath(os.path.dirname(askbot.__file__)) +site.addsitedir(os.path.join(ASKBOT_ROOT, 'deps')) + +DEBUG = False#set to True to enable debugging +TEMPLATE_DEBUG = False#keep false when debugging jinja2 templates +INTERNAL_IPS = ('127.0.0.1',) +ALLOWED_HOSTS = ['*',]#change this for better security on your site + +ADMINS = ( + ('AskFedora Sysadmins', 'sysadmin-ask-members@fedoraproject.org'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'askfedora', + 'USER': 'askfedora', # Not used with sqlite3. + 'PASSWORD': '<%= askbotDBPassword %>', # Not used with sqlite3. + {% if env == "staging" %} + 'HOST' : 'db01', + {% else %} + 'HOST' : 'db-ask', + {% end %} + # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '5432', # Set to empty string for default. Not used with sqlite3. + 'TEST_CHARSET': 'utf8', # Setting the character set and collation to utf-8 + 'TEST_COLLATION': 'utf8_general_ci', # is necessary for MySQL tests to work properly. + } +} + +#outgoing mail server settings +SERVER_EMAIL = 'nobody@fedoraproject.org' +DEFAULT_FROM_EMAIL = 'nobody@fedoraproject.org' +EMAIL_HOST_USER = '' +EMAIL_HOST_PASSWORD = '' +EMAIL_SUBJECT_PREFIX = '' +EMAIL_HOST='bastion' +EMAIL_PORT='25' +EMAIL_USE_TLS=False +EMAIL_BACKEND = 'post_office.EmailBackend' + + +#incoming mail settings +#after filling out these settings - please +#go to the site's live settings and enable the feature +#"Email settings" -> "allow asking by email" +# +# WARNING: command post_emailed_questions DELETES all +# emails from the mailbox each time +# do not use your personal mail box here!!! +# +IMAP_HOST = '' +IMAP_HOST_USER = '' +IMAP_HOST_PASSWORD = '' +IMAP_PORT = '' +IMAP_USE_TLS = False + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'UTC' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True +LANGUAGE_CODE = 'en' + +# Absolute path to the directory that holds uploaded media +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') +MEDIA_URL = '/upfiles/' +STATIC_URL = '/m/'#this must be different from MEDIA_URL + +PROJECT_ROOT = os.path.dirname(__file__) +#STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') +STATIC_ROOT = '/var/www/html/askbot/static' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/' + +# Make up some unique string, and don't share it with anybody. +SECRET_KEY = '<%= askbotSecretKeyPassword %>' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'askbot.skins.loaders.Loader', + 'django.template.loaders.app_directories.Loader', + 'django.template.loaders.filesystem.Loader', + #'django.template.loaders.eggs.load_template_source', +) + + +MIDDLEWARE_CLASSES = ( + #'django.middleware.gzip.GZipMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ## Enable the following middleware if you want to enable + ## language selection in the site settings. + #'askbot.middleware.locale.LocaleMiddleware', + #'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + #'django.middleware.cache.FetchFromCacheMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + #'django.middleware.sqlprint.SqlPrintingMiddleware', + + #below is askbot stuff for this tuple + 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', + 'askbot.middleware.forum_mode.ForumModeMiddleware', + 'askbot.middleware.cancel.CancelActionMiddleware', + 'django.middleware.transaction.TransactionMiddleware', + #'debug_toolbar.middleware.DebugToolbarMiddleware', + 'askbot.middleware.view_log.ViewLogMiddleware', + 'askbot.middleware.spaceless.SpacelessMiddleware', +) + + +ROOT_URLCONF = os.path.basename(os.path.dirname(__file__)) + '.urls' + + +#UPLOAD SETTINGS +FILE_UPLOAD_TEMP_DIR = os.path.join( + os.path.dirname(__file__), + 'tmp' + ).replace('\\','/') + +FILE_UPLOAD_HANDLERS = ( + 'django.core.files.uploadhandler.MemoryFileUploadHandler', + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', +) +ASKBOT_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') +ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes +DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' + + +#TEMPLATE_DIRS = (,) #template have no effect in askbot, use the variable below +#ASKBOT_EXTRA_SKINS_DIR = #path to your private skin collection +#take a look here http://askbot.org/en/question/207/ + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.request', + 'askbot.context.application_settings', + #'django.core.context_processors.i18n', + 'askbot.user_messages.context_processors.user_messages',#must be before auth + 'django.contrib.auth.context_processors.auth', #this is required for the admin app + 'django.core.context_processors.csrf', #necessary for csrf protection +) + + +INSTALLED_APPS = ( + 'longerusername', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.staticfiles', + + #all of these are needed for the askbot + 'django.contrib.admin', + 'django.contrib.humanize', + 'django.contrib.sitemaps', + 'django.contrib.messages', + #'debug_toolbar', + #Optional, to enable haystack search + #'haystack', + 'compressor', + 'askbot', + 'askbot.deps.django_authopenid', + #'askbot.importers.stackexchange', #se loader + 'south', + 'askbot.deps.livesettings', + 'keyedcache', + 'robots', + 'django_countries', + #'djcelery', + 'djkombu', + 'followit', + 'tinymce', + 'group_messaging', + #'avatar',#experimental use git clone git://github.com/ericflo/django-avatar.git$ +) + + +#setup memcached for production use! +#see http://docs.djangoproject.com/en/1.1/topics/cache/ for details +{% if env == "staging" %} +CACHE_BACKEND = 'locmem://' +{% else %} +CACHE_BACKEND='memcached://memcached04:11211/' +{% end %} +#needed for django-keyedcache +CACHE_TIMEOUT = 6000 +#sets a special timeout for livesettings if you want to make them different +LIVESETTINGS_CACHE_TIMEOUT = CACHE_TIMEOUT +CACHE_PREFIX = 'askbot' #make this unique +CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True +#If you use memcache you may want to uncomment the following line to enable memcached based sessions +#SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'askbot.deps.django_authopenid.backends.AuthBackend', +) + +#logging settings +LOG_FILENAME = 'askfedora.log' +logging.basicConfig( + filename=os.path.join('/var', 'log', 'askbot', LOG_FILENAME), + level=logging.CRITICAL, + format='%(pathname)s TIME: %(asctime)s MSG: %(filename)s:%(funcName)s:%(lineno)d %(message)s', +) + +########################### +# +# this will allow running your forum with url like http://site.com/forum +# +# ASKBOT_URL = 'forum/' +# +ASKBOT_URL = '' #no leading slash, default = '' empty string +ASKBOT_TRANSLATE_URL = True #translate specific URLs +_ = lambda v:v #fake translation function for the login url +LOGIN_URL = '/%s%s%s' % (ASKBOT_URL,_('account/'),_('signin/')) +LOGIN_REDIRECT_URL = ASKBOT_URL #adjust, if needed +#note - it is important that upload dir url is NOT translated!!! +#also, this url must not have the leading slash +ALLOW_UNICODE_SLUGS = False +ASKBOT_USE_STACKEXCHANGE_URLS = False #mimic url scheme of stackexchange + +#Celery Settings +BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" +CELERY_ALWAYS_EAGER = True + + + +{% if environment == "staging" %} +DOMAIN_NAME = 'ask.stg.fedoraproject.org' +{% else %} +DOMAIN_NAME = 'ask.fedoraproject.org' +{% end %} +#https://docs.djangoproject.com/en/1.3/ref/contrib/csrf/ +CSRF_COOKIE_DOMAIN = DOMAIN_NAME + +#STATIC_ROOT = os.path.join(PROJECT_ROOT, "static") +STATICFILES_DIRS = ( + ('default/media', os.path.join(ASKBOT_ROOT, 'media')), +) +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'compressor.finders.CompressorFinder', +) + +# Since Django 1.2.7 we need to add this or it will not forward openid requests correctly. +USE_X_FORWARDED_HOST = True + +# Since askbot 0.7.32 we need to have a redirect url after login +LOGIN_REDIRECT_URL = ASKBOT_URL + +RECAPTCHA_USE_SSL = True + +#HAYSTACK_SETTINGS +ENABLE_HAYSTACK_SEARCH = False +#Uncomment for multilingual setup: +#HAYSTACK_ROUTERS = ['askbot.search.haystack.routers.LanguageRouter',] + +#Uncomment if you use haystack +#More info in http://django-haystack.readthedocs.org/en/latest/settings.html +#HAYSTACK_CONNECTIONS = { +# 'default': { +# 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', +# } +#} + + +TINYMCE_COMPRESSOR = True +TINYMCE_SPELLCHECKER = False +TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'default/media/js/tinymce/') +#TINYMCE_JS_URL = STATIC_URL + 'default/media/js/tinymce/tiny_mce.js' +TINYMCE_DEFAULT_CONFIG = { + 'plugins': 'askbot_imageuploader,askbot_attachment', + 'convert_urls': False, + 'theme': 'advanced', + 'content_css': STATIC_URL + 'default/media/style/tinymce/content.css', + 'force_br_newlines': True, + 'force_p_newlines': False, + 'forced_root_block': '', + 'mode' : 'textareas', + 'oninit': "TinyMCE.onInitHook", + 'plugins': 'askbot_imageuploader,askbot_attachment', + 'theme_advanced_toolbar_location' : 'top', + 'theme_advanced_toolbar_align': 'left', + 'theme_advanced_buttons1': 'bold,italic,underline,|,bullist,numlist,|,undo,redo,|,link,unlink,askbot_imageuploader,askbot_attachment', + 'theme_advanced_buttons2': '', + 'theme_advanced_buttons3' : '', + 'theme_advanced_path': False, + 'theme_advanced_resizing': True, + 'theme_advanced_resize_horizontal': False, + 'theme_advanced_statusbar_location': 'bottom', + 'width': '730', + 'height': '250' +} + +#delayed notifications, time in seconds, 15 mins by default +NOTIFICATION_DELAY_TIME = 60 * 15 + +GROUP_MESSAGING = { + 'BASE_URL_GETTER_FUNCTION': 'askbot.models.user_get_profile_url', + 'BASE_URL_PARAMS': {'section': 'messages', 'sort': 'inbox'} +} + +ASKBOT_MULTILINGUAL = False + +ASKBOT_CSS_DEVEL = False +if 'ASKBOT_CSS_DEVEL' in locals() and ASKBOT_CSS_DEVEL == True: + COMPRESS_PRECOMPILERS = ( + ('text/less', 'lessc {infile} {outfile}'), + ) + +COMPRESS_JS_FILTERS = [] +COMPRESS_PARSER = 'compressor.parser.HtmlParser' +JINJA2_EXTENSIONS = ('compressor.contrib.jinja2ext.CompressorExtension',) + +# Use syncdb for tests instead of South migrations. Without this, some tests +# fail spuriously in MySQL. +SOUTH_TESTS_MIGRATE = False + +VERIFIER_EXPIRE_DAYS = 3