diff --git a/.gitignore b/.gitignore index 8c060f8e441a21ac91ce0010886a15095dd369e9..5c112ccaae9b09a9f9313a172110e87f808dfc48 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ deploy-latest.sh *~ media/* venv -celerybeat-* \ No newline at end of file +celerybeat-* +env.sh \ No newline at end of file diff --git a/helios/security.py b/helios/security.py index 1a5a78df6e1ae875f2e0dd5b7379658dac8e3c52..971499e7c18fe50e268591a39aea9cf0cacc61f5 100644 --- a/helios/security.py +++ b/helios/security.py @@ -187,7 +187,7 @@ def can_create_election(request): if helios.ADMIN_ONLY: return user.admin_p else: - return user != None + return user.can_create_election() def user_can_feature_election(user, election): if not user: diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py index 202fe95c079bc270639b57941e33f72f53078990..5a0e9233ba4df89067688283e90649aef4f1ae70 100644 --- a/helios_auth/auth_systems/__init__.py +++ b/helios_auth/auth_systems/__init__.py @@ -1,7 +1,7 @@ AUTH_SYSTEMS = {} -import twitter, password, cas, facebook, google, yahoo, linkedin +import twitter, password, cas, facebook, google, yahoo, linkedin, clever AUTH_SYSTEMS['twitter'] = twitter AUTH_SYSTEMS['linkedin'] = linkedin AUTH_SYSTEMS['password'] = password @@ -9,6 +9,7 @@ AUTH_SYSTEMS['cas'] = cas AUTH_SYSTEMS['facebook'] = facebook AUTH_SYSTEMS['google'] = google AUTH_SYSTEMS['yahoo'] = yahoo +AUTH_SYSTEMS['clever'] = clever # not ready #import live diff --git a/helios_auth/auth_systems/cas.py b/helios_auth/auth_systems/cas.py index 139a6d7c5ab0905e9d276cccdfd0f874a3719b4f..8202ad262712f527b8f8896b02b7a2b47b445917 100644 --- a/helios_auth/auth_systems/cas.py +++ b/helios_auth/auth_systems/cas.py @@ -242,3 +242,11 @@ def eligibility_category_id(constraint): def pretty_eligibility(constraint): return "Members of the Class of %s" % constraint['year'] + + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/clever.py b/helios_auth/auth_systems/clever.py new file mode 100644 index 0000000000000000000000000000000000000000..dcd52d0e522b4bf2039a6d5752dca26090eefa9c --- /dev/null +++ b/helios_auth/auth_systems/clever.py @@ -0,0 +1,136 @@ +""" +Clever Authentication + +""" + +from django.http import * +from django.core.mail import send_mail +from django.conf import settings + +import httplib2,json,base64 + +import sys, os, cgi, urllib, urllib2, re + +from oauth2client.client import OAuth2WebServerFlow, OAuth2Credentials + +# some parameters to indicate that status updating is not possible +STATUS_UPDATES = False + +# display tweaks +LOGIN_MESSAGE = "Log in with Clever" + +def get_flow(redirect_url=None): + return OAuth2WebServerFlow( + client_id=settings.CLEVER_CLIENT_ID, + client_secret=settings.CLEVER_CLIENT_SECRET, + scope='read:students read:teachers read:user_id read:sis', + auth_uri="https://clever.com/oauth/authorize", + #token_uri="https://clever.com/oauth/tokens", + token_uri="http://requestb.in/1b18gwf1", + redirect_uri=redirect_url) + +def get_auth_url(request, redirect_url): + flow = get_flow(redirect_url) + + request.session['clever-redirect-url'] = redirect_url + return flow.step1_get_authorize_url() + +def get_user_info_after_auth(request): + redirect_uri = request.session['clever-redirect-url'] + del request.session['clever-redirect-url'] + flow = get_flow(redirect_uri) + + code = request.GET['code'] + + # do the POST manually, because OAuth2WebFlow can't do auth header for token exchange + http = httplib2.Http(".cache") + auth_header = "Basic %s" % base64.b64encode(settings.CLEVER_CLIENT_ID + ":" + settings.CLEVER_CLIENT_SECRET) + resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.urlencode({ + "code" : code, + "grant_type": "authorization_code", + "redirect_uri": redirect_uri + }), headers = { + 'Authorization': auth_header, + 'Content-Type': "application/x-www-form-urlencoded" + }) + + token_response = json.loads(content) + access_token = token_response['access_token'] + + # package the credentials + credentials = OAuth2Credentials(access_token, settings.CLEVER_CLIENT_ID, settings.CLEVER_CLIENT_SECRET, None, None, None, None) + + # get the nice name + http = credentials.authorize(http) + (resp_headers, content) = http.request("https://api.clever.com/me", "GET") + + # {"type":"student","data":{"id":"563395179f7408755c0006b7","district":"5633941748c07c0100000aac","type":"student","created":"2015-10-30T16:04:39.262Z","credentials":{"district_password":"eel7Thohd","district_username":"dianes10"},"dob":"1998-11-01T00:00:00.000Z","ell_status":"Y","email":"diane.s@example.org","gender":"F","grade":"9","hispanic_ethnicity":"Y","last_modified":"2015-10-30T16:04:39.274Z","location":{"zip":"11433"},"name":{"first":"Diane","last":"Schmeler","middle":"J"},"race":"Asian","school":"5633950c62fc41c041000005","sis_id":"738733110","state_id":"114327752","student_number":"738733110"},"links":[{"rel":"self","uri":"/me"},{"rel":"canonical","uri":"/v1.1/students/563395179f7408755c0006b7"},{"rel":"district","uri":"/v1.1/districts/5633941748c07c0100000aac"}]} + response = json.loads(content) + + user_id = response['data']['id'] + user_name = "%s %s" % (response['data']['name']['first'], response['data']['name']['last']) + user_type = response['type'] + user_district = response['data']['district'] + user_grade = response['data'].get('grade', None) + + print content + + # watch out, response also contains email addresses, but not sure whether thsoe are verified or not + # so for email address we will only look at the id_token + + return {'type' : 'clever', 'user_id': user_id, 'name': user_name , 'info': {"district": user_district, "type": user_type, "grade": user_grade}, 'token': {'access_token': access_token}} + +def do_logout(user): + """ + logout of Google + """ + return None + +def update_status(token, message): + """ + simple update + """ + pass + +def send_message(user_id, name, user_info, subject, body): + """ + send email to google users. user_id is the email for google. + """ + pass + +# +# eligibility +# + +def check_constraint(constraint, user): + if not user.info.has_key('grade'): + return False + return constraint['grade'] == user.info['grade'] + +def generate_constraint(category, user): + """ + generate the proper basic data structure to express a constraint + based on the category string + """ + return {'grade': category} + +def list_categories(user): + return [{"id": str(g), "name": "Grade %d" % g} for g in range(3,13)] + +def eligibility_category_id(constraint): + return constraint['grade'] + +def pretty_eligibility(constraint): + return "Grade %s" % constraint['grade'] + + + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + """ + Teachers only for now + """ + return user_info['type'] == 'teacher' diff --git a/helios_auth/auth_systems/facebook.py b/helios_auth/auth_systems/facebook.py index d510ced0caa2417b04f83941aeb0a2fc7517f5a5..179013584b0fad8d7087bd22af6ac56bb99de896 100644 --- a/helios_auth/auth_systems/facebook.py +++ b/helios_auth/auth_systems/facebook.py @@ -116,3 +116,10 @@ def eligibility_category_id(constraint): def pretty_eligibility(constraint): return "Facebook users who are members of the \"%s\" group" % constraint['group']['name'] + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/google.py b/helios_auth/auth_systems/google.py index 00a6bb7180ffab81d5b5bf56f65f3b5b08b1d664..2bdd38c7c0d093bd349a582946dfb31fdd7a57e7 100644 --- a/helios_auth/auth_systems/google.py +++ b/helios_auth/auth_systems/google.py @@ -82,3 +82,11 @@ def check_constraint(constraint, user_info): for eligibility """ pass + + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/linkedin.py b/helios_auth/auth_systems/linkedin.py index 69e3e9a083b441b65c6d5fff9b810db6323df226..32b0033c14fc0c3277671470055d91dbd1271556 100644 --- a/helios_auth/auth_systems/linkedin.py +++ b/helios_auth/auth_systems/linkedin.py @@ -89,3 +89,10 @@ def send_notification(user_id, user_info, message): pass + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/live.py b/helios_auth/auth_systems/live.py index 6a1b20eb42780964ebac1378cff80ba6ea97dd2c..9f34a2783198003f09a74abb1671d9630cf3895a 100644 --- a/helios_auth/auth_systems/live.py +++ b/helios_auth/auth_systems/live.py @@ -65,3 +65,11 @@ def update_status(user_id, user_info, token, message): def send_message(user_id, user_name, user_info, subject, body): pass + + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/password.py b/helios_auth/auth_systems/password.py index 7f7bb572ef6852ea0359f3facecdb614d2d5cb30..f78d9f6a1fffe679570b5f4e3a83837b14e082a5 100644 --- a/helios_auth/auth_systems/password.py +++ b/helios_auth/auth_systems/password.py @@ -117,3 +117,11 @@ def send_message(user_id, user_name, user_info, subject, body): email = user_id name = user_name or user_info.get('name', email) send_mail(subject, body, settings.SERVER_EMAIL, ["\"%s\" <%s>" % (name, email)], fail_silently=False) + + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/twitter.py b/helios_auth/auth_systems/twitter.py index 343b555191bb0ee1877ec6f1fb02138ac874799c..9963f9121d42b3c53edaac0c128f67499d98c761 100644 --- a/helios_auth/auth_systems/twitter.py +++ b/helios_auth/auth_systems/twitter.py @@ -118,3 +118,10 @@ def follow_view(request): return HttpResponseRedirect(reverse(after_intervention)) + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/auth_systems/yahoo.py b/helios_auth/auth_systems/yahoo.py index dc29ab75ccb5a08b29a2aab1c806d9d647d496f6..16bc0343cd40b78386a95fb0279c05c02b23f317 100644 --- a/helios_auth/auth_systems/yahoo.py +++ b/helios_auth/auth_systems/yahoo.py @@ -52,3 +52,11 @@ def check_constraint(constraint, user_info): for eligibility """ pass + + +# +# Election Creation +# + +def can_create_election(user_id, user_info): + return True diff --git a/helios_auth/media/login-icons/clever.png b/helios_auth/media/login-icons/clever.png new file mode 100644 index 0000000000000000000000000000000000000000..80a7f3840816666eaeb2892727165949b87c7f37 Binary files /dev/null and b/helios_auth/media/login-icons/clever.png differ diff --git a/helios_auth/models.py b/helios_auth/models.py index 5ec2adfbe8f150d87120560c6164865e362985a3..b9179958e5bbca1ad7d9eacc4c2264c020fecf4b 100644 --- a/helios_auth/models.py +++ b/helios_auth/models.py @@ -70,6 +70,16 @@ class User(models.Model): return AUTH_SYSTEMS[self.user_type].STATUS_UPDATES + def can_create_election(self): + """ + Certain auth systems can choose to limit election creation + to certain users. + """ + if not AUTH_SYSTEMS.has_key(self.user_type): + return False + + return AUTH_SYSTEMS[self.user_type].can_create_election(self.user_id, self.info) + def update_status_template(self): if not self.can_update_status(): return None diff --git a/helios_auth/templates/login_box.html b/helios_auth/templates/login_box.html index d3fb7f4f882e43579654f4d1021820c98942adb5..0b0ee0415e63179675a26c0b6fb5badf3ac4b5cf 100644 --- a/helios_auth/templates/login_box.html +++ b/helios_auth/templates/login_box.html @@ -6,7 +6,7 @@ {% else %} <p> <a href="{{SECURE_URL_HOST}}{% url "helios_auth.views.start" system_name=auth_system %}?return_url={{return_url}}" style="font-size: 1.4em;"> -<img border="0" height="35" src="/static/auth/login-icons/{{auth_system}}.png" alt="{{auth_system}}" /> {{auth_system}} +<img style="height: 35px; border: 0px;" src="/static/auth/login-icons/{{auth_system}}.png" alt="{{auth_system}}" /> {{auth_system}} {% endifequal %} </a> </p> diff --git a/helios_auth/tests.py b/helios_auth/tests.py index 7e5b1005c6894a8099776628a5efaa4254155210..f07f309fb5176007ca67a25ce14c712efc33af35 100644 --- a/helios_auth/tests.py +++ b/helios_auth/tests.py @@ -47,6 +47,16 @@ class UserModelTests(unittest.TestCase): self.assertEquals(u2.info['name'], new_name) + def test_can_create_election(self): + """ + check that auth systems have the can_create_election call and that it's true for the common ones + """ + for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + assert(hasattr(auth_system_module, 'can_create_election')) + if auth_system != 'clever': + assert(auth_system_module.can_create_election('foobar', {})) + + def test_status_update(self): """ check that a user set up with status update ability reports it as such, diff --git a/requirements.txt b/requirements.txt index 01eb5d922e40a915275b017807d406803aa650ac..2cc4eb872cc1522a3d333dceee03caf92aa9d8d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,6 @@ dj_database_url==0.3.0 django-sslify==0.2.7 django_webtest==1.7.8 webtest==2.0.18 -django-db-pool==0.0.10 django-secure==1.0.1 bleach==1.4.1 boto==2.27.0 diff --git a/settings.py b/settings.py index 25fbcb2563cf24a69a0904ba9d952710a9f3920a..f1e2ab28b09a12f6e826af7d676aa31f06f6575c 100644 --- a/settings.py +++ b/settings.py @@ -1,9 +1,13 @@ import os, json +# a massive hack to see if we're testing, in which case we use different settings +import sys +TESTING = 'test' in sys.argv + # go through environment variables and override them def get_from_env(var, default): - if os.environ.has_key(var): + if not TESTING and os.environ.has_key(var): return os.environ[var] else: return default @@ -41,8 +45,8 @@ SOUTH_DATABASE_ADAPTERS = {'default':'south.db.postgresql_psycopg2'} if get_from_env('DATABASE_URL', None): import dj_database_url DATABASES['default'] = dj_database_url.config() - DATABASES['default']['ENGINE'] = 'dbpool.db.backends.postgresql_psycopg2' - DATABASES['default']['OPTIONS'] = {'MAX_CONNS': 1} + DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2' + DATABASES['default']['CONN_MAX_AGE'] = 600 # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -234,6 +238,10 @@ CAS_PASSWORD = get_from_env('CAS_PASSWORD', "") CAS_ELIGIBILITY_URL = get_from_env('CAS_ELIGIBILITY_URL', "") CAS_ELIGIBILITY_REALM = get_from_env('CAS_ELIGIBILITY_REALM', "") +# Clever +CLEVER_CLIENT_ID = get_from_env('CLEVER_CLIENT_ID', "") +CLEVER_CLIENT_SECRET = get_from_env('CLEVER_CLIENT_SECRET', "") + # email server EMAIL_HOST = get_from_env('EMAIL_HOST', 'localhost') EMAIL_PORT = int(get_from_env('EMAIL_PORT', "2525"))