diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..1c6935bb9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,61 @@
+name: CI
+
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+ branches: [main, master]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.13']
+ postgres-version: ['16']
+
+ services:
+ postgres:
+ image: postgres:${{ matrix.postgres-version }}
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: helios
+ POSTGRES_HOST_AUTH_METHOD: trust
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ env:
+ PGHOST: localhost
+ PGUSER: postgres
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libldap2-dev libsasl2-dev
+
+ - name: Install Python dependencies
+ run: |
+ uv sync
+ uv pip freeze
+
+ - name: Run tests
+ run: uv run python -Wall manage.py test -v 2 --settings=settings_ci
diff --git a/.gitignore b/.gitignore
index 681947f7f..430d84e58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,9 @@ deploy-latest.sh
.DS_Store
*~
media/*
-venv
+.venv
+venv*
celerybeat-*
env.sh
-.cache
\ No newline at end of file
+.cache
+.idea/
\ No newline at end of file
diff --git a/.python-version b/.python-version
new file mode 100644
index 000000000..24ee5b1be
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index be482e4e4..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-sudo: false
-language: python
-python:
- - "2.7"
-# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
-install:
- - pip install setuptools==24.3.1
- - pip install -r requirements.txt
-# command to run tests, e.g. python setup.py test
-script: "python manage.py test"
-addons:
- postgresql: "9.3"
-before_script:
- - psql -c 'create database helios;' -U postgres
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..5e6c02db0
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,158 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code when working with the Helios Election System codebase.
+
+## Project Overview
+
+Helios is an end-to-end verifiable voting system that provides secure, transparent online elections with cryptographic verification. It supports multiple authentication systems (Google, Facebook, GitHub, LDAP, CAS, password, etc.) and uses homomorphic encryption for privacy-preserving vote tallying.
+
+## Critical Instructions for Claude when preparing a PR
+
+- always run the tests. Install everything and run the tests. Every time.
+
+## Technology Stack
+
+- **Python**: 3.13
+- **Framework**: Django 5.2
+- **Database**: PostgreSQL 9.5+
+- **Task Queue**: Celery with RabbitMQ
+- **Crypto**: pycryptodome
+- **Package Manager**: uv
+
+## Common Commands
+
+```bash
+# Install dependencies
+uv sync
+
+# Run development server
+uv run python manage.py runserver
+
+# Run all tests
+uv run python manage.py test -v 2
+
+# Run tests for a specific app
+uv run python manage.py test helios -v 2
+uv run python manage.py test helios_auth -v 2
+
+# Run a specific test class
+uv run python manage.py test helios.tests.ElectionModelTests -v 2
+
+# Database migrations
+uv run python manage.py makemigrations
+uv run python manage.py migrate
+
+# Reset database (drops and recreates)
+./reset.sh
+
+# Start Celery worker (for background tasks)
+uv run celery --app helios worker --events --beat --concurrency 1
+```
+
+## Project Structure
+
+- `helios/` - Core election system (models, views, crypto, forms)
+- `helios_auth/` - Authentication system with multiple backends
+- `server_ui/` - Admin web interface
+- `heliosbooth/` - JavaScript voting booth interface
+- `heliosverifier/` - JavaScript ballot verification interface
+
+## Code Style Conventions
+
+### Naming
+
+- **Boolean fields**: Use `_p` suffix (e.g., `private_p`, `frozen_p`, `admin_p`, `featured_p`)
+- **Datetime fields**: Use `_at` suffix (e.g., `created_at`, `frozen_at`, `voting_ends_at`)
+- **Functions/methods**: snake_case
+- **Classes**: PascalCase
+
+### Indentation
+
+- Use 2-space indentation throughout Python files
+
+### Imports
+
+```python
+# Standard library
+import copy, csv, datetime, uuid
+
+# Third-party
+from django.db import models, transaction
+import bleach
+
+# Local
+from helios import datatypes, utils
+from helios_auth.jsonfield import JSONField
+```
+
+## Key Patterns
+
+### View Decorators
+
+Use existing security decorators for views:
+
+```python
+from helios.security import election_view, election_admin, trustee_check
+
+@election_view(frozen=True)
+def my_view(request, election):
+ pass
+
+@election_admin()
+def admin_view(request, election):
+ pass
+```
+
+### Model Base Class
+
+All domain models inherit from `HeliosModel`:
+
+```python
+class MyModel(HeliosModel):
+ class Meta:
+ app_label = 'helios'
+```
+
+### JSON Responses
+
+```python
+from helios.views import render_json
+return render_json({'key': 'value'})
+```
+
+### Template Rendering
+
+```python
+from helios.views import render_template
+return render_template(request, 'template_name', {'context': 'vars'})
+```
+
+### Database Queries
+
+- Use `@transaction.atomic` for operations that need atomicity
+- Prefer `select_related()` for foreign key joins
+- Use `get_or_create()` pattern for safe creation
+
+## Security Considerations
+
+- Always use `check_csrf(request)` for POST handlers
+- Use `bleach.clean()` for user-provided HTML (see `description_bleached` pattern)
+- Never store plaintext passwords; use the auth system's hashing
+- Check permissions with `user_can_admin_election()` and similar helpers
+
+## Configuration
+
+Settings use environment variables with defaults:
+
+```python
+from settings import get_from_env
+MY_SETTING = get_from_env('MY_SETTING', 'default_value')
+```
+
+Key environment variables: `DEBUG`, `SECRET_KEY`, `DATABASE_URL`, `CELERY_BROKER_URL`, `AUTH_ENABLED_AUTH_SYSTEMS`
+
+## Testing
+
+- Tests use Django's TestCase with django-webtest
+- Fixtures are in `helios/fixtures/`
+- Test classes: `ElectionModelTests`, `VoterModelTests`, `ElectionBlackboxTests`, etc.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 61ac19608..a0c88cf51 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -7,3 +7,6 @@ Significant contributors:
- Olivier de Marneffe
- Emily Stark, Mike Hamburg, Tom Wu, and Dan Boneh for SJCL and integration of javascript crypto.
- Nicholas Chang-Fong and Aleksander Essex for security reports and fixes.
+- Shirley Chaves
+- Marco Ciotola
+- Lucas Araujo
diff --git a/INSTALL.md b/INSTALL.md
index fdb0ae10f..282956c91 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,50 +1,69 @@
-* install PostgreSQL 8.3+
+# Helios Server Installation
-* make sure you have virtualenv installed:
-http://www.virtualenv.org/en/latest/
+## Prerequisites
-* download helios-server
+* Install PostgreSQL 12+
-* cd into the helios-server directory
+* Install RabbitMQ
+ This is needed for Celery to work, which does background processing such as
+ the processing of uploaded list-of-voter CSV files.
-* create a virtualenv:
+* Download helios-server
+
+* `cd` into the helios-server directory
+
+## Python Setup
+
+* Install Python 3.13 including dev packages
```
-virtualenv venv
+sudo apt install python3 python3-dev
```
-* activate virtual environment
+* Install uv (modern Python package manager)
```
-source venv/bin/activate
-````
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
-* install requirements
+* You'll also need Postgres dev libraries. For example on Ubuntu:
```
-pip install -r requirements.txt
+sudo apt install libpq-dev
```
-* reset database
+* Install dependencies (uv creates a virtual environment automatically)
+
+```
+uv sync
+```
+
+## Database Setup
+
+* Reset database
```
./reset.sh
```
-* start server
+## Running the Server
+
+* Start server
```
-python manage.py runserver
+uv run python manage.py runserver
```
-* to get Google Auth working:
+## Google Auth Configuration
+
+To get Google Auth working:
-** go to https://console.developers.google.com
+* Go to https://console.developers.google.com
-** create an application
+* Create an application
-** set up oauth2 credentials as a web application, with your origin, e.g. https://myhelios.example.com, and your auth callback, which, based on our example, is https://myhelios.example.com/auth/after/
+* Set up OAuth2 credentials as a web application, with your origin, e.g. `https://myhelios.example.com`, and your auth callback, which based on our example is `https://myhelios.example.com/auth/after/`
-** still in the developer console, enable the Google+ API.
+* In the developer console, enable the Google People API
-** set the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET configuration variables accordingly.
\ No newline at end of file
+* Set the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` configuration variables accordingly
diff --git a/Procfile b/Procfile
index 63d8d0cf6..04ed6cbca 100644
--- a/Procfile
+++ b/Procfile
@@ -1,2 +1,2 @@
web: gunicorn wsgi:application -b 0.0.0.0:$PORT -w 8
-worker: python manage.py celeryd -E -B --beat --concurrency=1
\ No newline at end of file
+worker: celery --app helios worker --events --beat --concurrency 1
\ No newline at end of file
diff --git a/README.md b/README.md
index 5c90a9c20..9a5519bfc 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,6 @@
Helios is an end-to-end verifiable voting system.
-
+[](https://travis-ci.org/benadida/helios-server)
[](https://waffle.io/benadida/helios-server)
diff --git a/docs/design-plans/2026-01-18-helios-booth-lit-redesign.md b/docs/design-plans/2026-01-18-helios-booth-lit-redesign.md
new file mode 100644
index 000000000..ba7db1759
--- /dev/null
+++ b/docs/design-plans/2026-01-18-helios-booth-lit-redesign.md
@@ -0,0 +1,153 @@
+# Helios Booth Lit Redesign
+
+## Summary
+
+The Helios Booth Lit Redesign builds a modern replacement for the existing jQuery-based voting booth using Lit web components, TypeScript, and Vite. The new booth runs independently at `/booth2026/` without requiring server changes, preserving the election system's offline security model and byte-for-byte identical cryptographic operations.
+
+The implementation follows a props-drilling state management pattern where the main `booth-app` component holds all voter state and distributes data to screen components (election, question, review, submit, and audit screens) through reactive properties, with screens communicating state changes via events. By copying and adapting the existing crypto libraries (`jscrypto/`), reusing the Web Worker encryption pattern, and maintaining identical ballot formatting, the new booth achieves code clarity and maintainability while ensuring full compatibility with existing election servers and verification tools.
+
+## Definition of Done
+
+**Deliverables:**
+- A design plan for a new Lit-based voting booth at `/booth2026/`
+- Runs alongside existing booth (separate URL, no server changes needed)
+- Preserves all cryptographic operations unchanged
+- Preserves offline security model (no network during voting)
+- Basic accessibility from the start (semantic HTML, ARIA labels, keyboard navigation)
+- i18n architecture noted but implementation deferred to future iteration
+- 4 high-level implementation phases
+
+**Success criteria:**
+A simpler, clearer version of the existing detailed plan that's actionable across multiple PRs.
+
+## Glossary
+
+- **Lit 3.x**: A lightweight web components library for building fast, efficient UI with template literals and reactive properties.
+- **Web Components**: Browser-native reusable UI elements defined as custom HTML tags with encapsulated DOM and styling.
+- **Vite**: A modern frontend build tool that uses native ES modules during development and creates optimized production bundles.
+- **Props drilling**: A state management pattern where parent components pass data down to children via properties, and children communicate back via events.
+- **Web Worker**: A browser API enabling background JavaScript execution in a separate thread, used here for non-blocking ballot encryption.
+- **jscrypto**: Helios's JavaScript cryptographic library implementing ElGamal encryption, BigInt arithmetic, and ballot serialization.
+- **ElGamal encryption**: The public-key cryptographic scheme used by Helios for encrypting individual votes.
+- **@lit/localize**: Lit's internationalization library using the `msg()` function for translation (planned for future iteration).
+- **ARIA labels**: Accessibility attributes describing UI elements for screen readers and assistive technologies.
+
+## Architecture
+
+A new Lit-based voting booth at `heliosbooth2026/` running independently alongside the existing jQuery booth at `heliosbooth/`.
+
+**Technology stack:**
+- Lit 3.x for web components
+- TypeScript for type safety
+- Vite for bundling (single bundle, no code splitting for offline operation)
+
+**State management:** Props drilling pattern. `booth-app` holds all state as reactive properties and passes data to screen components via props. Screens emit events for state changes. No global state or context API.
+
+**Component structure:**
+
+```
+booth-app (main container, holds all state)
+├── election-screen (election info, start button)
+├── question-screen (question display, answer selection, navigation)
+├── review-screen (encryption progress, ballot review)
+├── submit-screen (ballot hash, cast form)
+└── audit-screen (audit trail display)
+```
+
+**Data flow:**
+1. `booth-app` loads election JSON on mount
+2. User navigates through questions, `booth-app` tracks answers
+3. On review, Web Worker encrypts ballot, posts progress back
+4. On submit, form POSTs encrypted ballot to server's cast URL
+
+**Crypto integration:** Copy `heliosbooth/js/jscrypto/` into `heliosbooth2026/lib/jscrypto/`. Create TypeScript type declarations for the global objects (`HELIOS`, `ElGamal`, `BigInt`). Adapt `boothworker-single.js` for the new structure. Encrypted ballot JSON must be byte-for-byte identical to old booth output.
+
+**Critical constraint:** Zero network calls between "Start" and "Submit". All assets bundled, all encryption client-side.
+
+## Existing Patterns
+
+Investigation found the existing booth at `heliosbooth/` uses:
+- Single HTML entry point (`vote.html`) loading a compressed JS bundle
+- `BOOTH` object as monolithic state container
+- jQuery jTemplates for rendering
+- Panel-based UI with show/hide
+- Web Worker for async encryption
+
+**Patterns this design follows:**
+- Same crypto layer (unchanged `jscrypto/` directory)
+- Same Web Worker encryption approach
+- Same API endpoints and ballot format
+- Same panel-based screen switching concept
+
+**Patterns this design changes:**
+- jQuery → Lit web components
+- Monolithic BOOTH object → typed state in booth-app
+- jQuery templates → Lit template literals
+- Implicit globals → explicit TypeScript types
+
+The new booth produces identical encrypted ballots, ensuring server compatibility.
+
+## Implementation Phases
+
+### Phase 1: Setup & Core Shell
+**Goal:** Project scaffolding and basic navigation working
+
+**Components:**
+- `heliosbooth2026/` directory with Vite + TypeScript + Lit configuration
+- `lib/jscrypto/` — copied crypto libraries from old booth
+- `src/crypto/types.ts` — TypeScript declarations for crypto globals
+- `src/booth-app.ts` — main component with screen switching
+- `src/screens/election-screen.ts` — loads election, shows info, start button
+- Basic CSS structure
+
+**Dependencies:** None (first phase)
+
+**Done when:** Can load an election from URL parameter, display election info, click "Start" to switch screens
+
+### Phase 2: Voting Flow
+**Goal:** Complete question navigation and answer selection
+
+**Components:**
+- `src/screens/question-screen.ts` — question display, answer checkboxes, validation
+- Navigation logic in `booth-app` — previous/next/review
+- Answer state tracking — selections per question
+- Progress indicator — "Question N of M"
+- Answer randomization — shuffle if election specifies
+
+**Dependencies:** Phase 1 (booth-app, election loading)
+
+**Done when:** Can navigate through all questions, select answers respecting min/max constraints, see progress, proceed to review
+
+### Phase 3: Crypto & Submission
+**Goal:** Full voting flow including encryption and submission
+
+**Components:**
+- `workers/encryption-worker.js` — adapted from `boothworker-single.js`
+- `src/screens/review-screen.ts` — encryption progress, ballot summary, seal/audit/submit buttons
+- `src/screens/submit-screen.ts` — ballot hash display, cast form
+- `src/screens/audit-screen.ts` — audit trail JSON, back-to-voting flow
+- Encryption orchestration in `booth-app` — worker communication, progress tracking
+
+**Dependencies:** Phase 2 (answers collected)
+
+**Done when:** Can encrypt ballot, see progress, view ballot hash, submit via form POST, or audit and re-vote
+
+### Phase 4: Polish & Deployment
+**Goal:** Production-ready booth at `/booth2026/`
+
+**Components:**
+- Accessibility — semantic HTML, ARIA labels, keyboard navigation
+- Error handling — loading states, error messages
+- CSS polish — responsive layout, visual consistency
+- Offline verification — test zero network during voting
+- Deployment configuration — serve from `/booth2026/` URL
+
+**Dependencies:** Phase 3 (complete flow working)
+
+**Done when:** Booth passes accessibility basics, handles errors gracefully, works offline during voting, deployed and accessible at `/booth2026/`
+
+## Additional Considerations
+
+**i18n architecture:** Design supports future i18n via `@lit/localize`. All user-facing strings would use `msg()` function. Implementation deferred — first iteration is English only.
+
+**Offline verification:** Each phase should verify no unexpected network calls. Phase 4 includes explicit offline testing (disconnect network after load, complete vote).
diff --git a/extract-passwords-for-email.py b/extract-passwords-for-email.py
index d72705792..dc9ac9564 100644
--- a/extract-passwords-for-email.py
+++ b/extract-passwords-for-email.py
@@ -5,12 +5,16 @@
# python extract-passwords-for-email.py
#
-from django.core.management import setup_environ
-import settings, sys, csv
+import sys
-setup_environ(settings)
+import csv
+import django
+import os
-from helios.models import *
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+django.setup()
+
+from helios.models import Election
election_uuid = sys.argv[1]
email = sys.argv[2]
diff --git a/helios/__init__.py b/helios/__init__.py
index 06f05140b..21f21bc59 100644
--- a/helios/__init__.py
+++ b/helios/__init__.py
@@ -1,7 +1,9 @@
-
from django.conf import settings
-from django.core.urlresolvers import reverse
-from helios.views import election_shortcut
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery_app import app as celery_app
+
+__all__ = ('celery_app', 'TEMPLATE_BASE', 'ADMIN_ONLY', 'VOTERS_UPLOAD', 'VOTERS_EMAIL',)
TEMPLATE_BASE = settings.HELIOS_TEMPLATE_BASE or "helios/templates/base.html"
diff --git a/helios/apps.py b/helios/apps.py
new file mode 100644
index 000000000..900974328
--- /dev/null
+++ b/helios/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+class HeliosConfig(AppConfig):
+ name = 'helios'
+ verbose_name = "Helios"
diff --git a/helios/celery_app.py b/helios/celery_app.py
new file mode 100644
index 000000000..89f0ecb54
--- /dev/null
+++ b/helios/celery_app.py
@@ -0,0 +1,21 @@
+import os
+
+# set the default Django settings module for the 'celery' program.
+from celery import Celery
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings')
+
+app = Celery()
+
+# Using a string here means the worker doesn't have to serialize
+# the configuration object to child processes.
+# - namespace='CELERY' means all celery-related configuration keys
+# should have a `CELERY_` prefix.
+app.config_from_object('django.conf:settings', namespace='CELERY')
+
+# Load task modules from all registered Django app configs.
+app.autodiscover_tasks()
+
+@app.task(bind=True)
+def debug_task(self):
+ print('Request: {0!r}'.format(self.request))
diff --git a/helios/crypto/algs.py b/helios/crypto/algs.py
index acac4f44b..b7439d9bd 100644
--- a/helios/crypto/algs.py
+++ b/helios/crypto/algs.py
@@ -7,157 +7,61 @@
ben@adida.net
"""
-import math, hashlib, logging
-import randpool, number
+import logging
-import numtheory
+from Crypto.Hash import SHA1
+from Crypto.Util import number
-# some utilities
-class Utils:
- RAND = randpool.RandomPool()
-
- @classmethod
- def random_seed(cls, data):
- cls.RAND.add_event(data)
-
- @classmethod
- def random_mpz(cls, n_bits):
- low = 2**(n_bits-1)
- high = low * 2
-
- # increment and find a prime
- # return randrange(low, high)
-
- return number.getRandomNumber(n_bits, cls.RAND.get_bytes)
-
- @classmethod
- def random_mpz_lt(cls, max):
- # return randrange(0, max)
- n_bits = int(math.floor(math.log(max, 2)))
- return (number.getRandomNumber(n_bits, cls.RAND.get_bytes) % max)
-
- @classmethod
- def random_prime(cls, n_bits):
- return number.getPrime(n_bits, cls.RAND.get_bytes)
-
- @classmethod
- def is_prime(cls, mpz):
- #return numtheory.miller_rabin(mpz)
- return number.isPrime(mpz)
-
- @classmethod
- def xgcd(cls, a, b):
- """
- Euclid's Extended GCD algorithm
- """
- mod = a%b
-
- if mod == 0:
- return 0,1
- else:
- x,y = cls.xgcd(b, mod)
- return y, x-(y*(a/b))
-
- @classmethod
- def inverse(cls, mpz, mod):
- # return cls.xgcd(mpz,mod)[0]
- return number.inverse(mpz, mod)
-
- @classmethod
- def random_safe_prime(cls, n_bits):
- p = None
- q = None
-
- while True:
- p = cls.random_prime(n_bits)
- q = (p-1)/2
- if cls.is_prime(q):
- return p
-
- @classmethod
- def random_special_prime(cls, q_n_bits, p_n_bits):
- p = None
- q = None
-
- z_n_bits = p_n_bits - q_n_bits
-
- q = cls.random_prime(q_n_bits)
-
- while True:
- z = cls.random_mpz(z_n_bits)
- p = q*z + 1
- if cls.is_prime(p):
- return p, q, z
+from helios.crypto.utils import random
+from helios.utils import to_json
class ElGamal:
def __init__(self):
- self.p = None
- self.q = None
- self.g = None
-
- @classmethod
- def generate(cls, n_bits):
- """
- generate an El-Gamal environment. Returns an instance
- of ElGamal(), with prime p, group size q, and generator g
- """
-
- EG = ElGamal()
-
- # find a prime p such that (p-1)/2 is prime q
- EG.p = Utils.random_safe_prime(n_bits)
-
- # q is the order of the group
- # FIXME: not always p-1/2
- EG.q = (EG.p-1)/2
-
- # find g that generates the q-order subgroup
- while True:
- EG.g = Utils.random_mpz_lt(EG.p)
- if pow(EG.g, EG.q, EG.p) == 1:
- break
-
- return EG
+ self.p = None
+ self.q = None
+ self.g = None
def generate_keypair(self):
- """
- generates a keypair in the setting
- """
+ """
+ generates a keypair in the setting
+ """
- keypair = EGKeyPair()
- keypair.generate(self.p, self.q, self.g)
+ keypair = EGKeyPair()
+ keypair.generate(self.p, self.q, self.g)
- return keypair
+ return keypair
def toJSONDict(self):
- return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)}
+ return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)}
@classmethod
def fromJSONDict(cls, d):
- eg = cls()
- eg.p = int(d['p'])
- eg.q = int(d['q'])
- eg.g = int(d['g'])
- return eg
+ eg = cls()
+ eg.p = int(d['p'])
+ eg.q = int(d['q'])
+ eg.g = int(d['g'])
+ return eg
+
class EGKeyPair:
def __init__(self):
- self.pk = EGPublicKey()
- self.sk = EGSecretKey()
+ self.pk = EGPublicKey()
+ self.sk = EGSecretKey()
def generate(self, p, q, g):
- """
- Generate an ElGamal keypair
- """
- self.pk.g = g
- self.pk.p = p
- self.pk.q = q
+ """
+ Generate an ElGamal keypair
+ """
+ self.pk.g = g
+ self.pk.p = p
+ self.pk.q = q
+
+ self.sk.x = random.mpz_lt(q)
+ self.pk.y = pow(g, self.sk.x, p)
- self.sk.x = Utils.random_mpz_lt(q)
- self.pk.y = pow(g, self.sk.x, p)
+ self.sk.pk = self.pk
- self.sk.pk = self.pk
class EGPublicKey:
def __init__(self):
@@ -166,7 +70,7 @@ def __init__(self):
self.g = None
self.q = None
- def encrypt_with_r(self, plaintext, r, encode_message= False):
+ def encrypt_with_r(self, plaintext, r, encode_message=False):
"""
expecting plaintext.m to be a big integer
"""
@@ -175,13 +79,13 @@ def encrypt_with_r(self, plaintext, r, encode_message= False):
# make sure m is in the right subgroup
if encode_message:
- y = plaintext.m + 1
- if pow(y, self.q, self.p) == 1:
- m = y
- else:
- m = -y % self.p
+ y = plaintext.m + 1
+ if pow(y, self.q, self.p) == 1:
+ m = y
+ else:
+ m = -y % self.p
else:
- m = plaintext.m
+ m = plaintext.m
ciphertext.alpha = pow(self.g, r, self.p)
ciphertext.beta = (m * pow(self.y, r, self.p)) % self.p
@@ -192,7 +96,7 @@ def encrypt_return_r(self, plaintext):
"""
Encrypt a plaintext and return the randomness just generated and used.
"""
- r = Utils.random_mpz_lt(self.q)
+ r = random.mpz_lt(self.q)
ciphertext = self.encrypt_with_r(plaintext, r)
return [ciphertext, r]
@@ -207,70 +111,69 @@ def to_dict(self):
"""
Serialize to dictionary.
"""
- return {'y' : str(self.y), 'p' : str(self.p), 'g' : str(self.g) , 'q' : str(self.q)}
+ return {'y': str(self.y), 'p': str(self.p), 'g': str(self.g), 'q': str(self.q)}
toJSONDict = to_dict
# quick hack FIXME
def toJSON(self):
- import utils
- return utils.to_json(self.toJSONDict())
+ return to_json(self.toJSONDict())
- def __mul__(self,other):
- if other == 0 or other == 1:
- return self
+ def __mul__(self, other):
+ if other == 0 or other == 1:
+ return self
- # check p and q
- if self.p != other.p or self.q != other.q or self.g != other.g:
- raise Exception("incompatible public keys")
+ # check p and q
+ if self.p != other.p or self.q != other.q or self.g != other.g:
+ raise Exception("incompatible public keys")
- result = EGPublicKey()
- result.p = self.p
- result.q = self.q
- result.g = self.g
- result.y = (self.y * other.y) % result.p
- return result
+ result = EGPublicKey()
+ result.p = self.p
+ result.q = self.q
+ result.g = self.g
+ result.y = (self.y * other.y) % result.p
+ return result
- def verify_sk_proof(self, dlog_proof, challenge_generator = None):
- """
- verify the proof of knowledge of the secret key
- g^response = commitment * y^challenge
- """
- left_side = pow(self.g, dlog_proof.response, self.p)
- right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p
+ def verify_sk_proof(self, dlog_proof, challenge_generator=None):
+ """
+ verify the proof of knowledge of the secret key
+ g^response = commitment * y^challenge
+ """
+ left_side = pow(self.g, dlog_proof.response, self.p)
+ right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p
- expected_challenge = challenge_generator(dlog_proof.commitment) % self.q
+ expected_challenge = challenge_generator(dlog_proof.commitment) % self.q
- return ((left_side == right_side) and (dlog_proof.challenge == expected_challenge))
+ return (left_side == right_side) and (dlog_proof.challenge == expected_challenge)
def validate_pk_params(self):
- # check primality of p
- if not number.isPrime(self.p):
- raise Exception("p is not prime.")
+ # check primality of p
+ if not number.isPrime(self.p):
+ raise Exception("p is not prime.")
- # check length of p
- if not (number.size(self.p) >= 2048):
- raise Exception("p of insufficient length. Should be 2048 bits or greater.")
+ # check length of p
+ if not (number.size(self.p) >= 2048):
+ raise Exception("p of insufficient length. Should be 2048 bits or greater.")
- # check primality of q
- if not number.isPrime(self.q):
- raise Exception("q is not prime.")
+ # check primality of q
+ if not number.isPrime(self.q):
+ raise Exception("q is not prime.")
- # check length of q
- if not (number.size(self.q) >= 256):
- raise Exception("q of insufficient length. Should be 256 bits or greater.")
+ # check length of q
+ if not (number.size(self.q) >= 256):
+ raise Exception("q of insufficient length. Should be 256 bits or greater.")
- if (pow(self.g,self.q,self.p)!=1):
- raise Exception("g does not generate subgroup of order q.")
+ if pow(self.g, self.q, self.p) != 1:
+ raise Exception("g does not generate subgroup of order q.")
- if not (1 < self.g < self.p-1):
- raise Exception("g out of range.")
+ if not (1 < self.g < self.p - 1):
+ raise Exception("g out of range.")
- if not (1 < self.y < self.p-1):
- raise Exception("y out of range.")
+ if not (1 < self.y < self.p - 1):
+ raise Exception("y out of range.")
- if (pow(self.y,self.q,self.p)!=1):
- raise Exception("g does not generate proper group.")
+ if pow(self.y, self.q, self.p) != 1:
+ raise Exception("g does not generate proper group.")
@classmethod
def from_dict(cls, d):
@@ -284,14 +187,15 @@ def from_dict(cls, d):
pk.q = int(d['q'])
try:
- pk.validate_pk_params()
+ pk.validate_pk_params()
except Exception as e:
- raise
+ raise e
return pk
fromJSONDict = from_dict
+
class EGSecretKey:
def __init__(self):
self.x = None
@@ -317,25 +221,25 @@ def decryption_factor_and_proof(self, ciphertext, challenge_generator=None):
return dec_factor, proof
- def decrypt(self, ciphertext, dec_factor = None, decode_m=False):
+ def decrypt(self, ciphertext, dec_factor=None, decode_m=False):
"""
Decrypt a ciphertext. Optional parameter decides whether to encode the message into the proper subgroup.
"""
if not dec_factor:
dec_factor = self.decryption_factor(ciphertext)
- m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
+ m = (number.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
if decode_m:
- # get m back from the q-order subgroup
- if m < self.pk.q:
- y = m
- else:
- y = -m % self.pk.p
+ # get m back from the q-order subgroup
+ if m < self.pk.q:
+ y = m
+ else:
+ y = -m % self.pk.p
- return EGPlaintext(y-1, self.pk)
+ return EGPlaintext(y - 1, self.pk)
else:
- return EGPlaintext(m, self.pk)
+ return EGPlaintext(m, self.pk)
def prove_decryption(self, ciphertext):
"""
@@ -350,66 +254,66 @@ def prove_decryption(self, ciphertext):
and alpha^t = b * beta/m ^ c
"""
- m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
- beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p
+ m = (number.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
+ beta_over_m = (ciphertext.beta * number.inverse(m, self.pk.p)) % self.pk.p
# pick a random w
- w = Utils.random_mpz_lt(self.pk.q)
+ w = random.mpz_lt(self.pk.q)
a = pow(self.pk.g, w, self.pk.p)
b = pow(ciphertext.alpha, w, self.pk.p)
- c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16)
+ c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(), 16)
t = (w + self.x * c) % self.pk.q
return m, {
- 'commitment' : {'A' : str(a), 'B': str(b)},
- 'challenge' : str(c),
- 'response' : str(t)
- }
+ 'commitment': {'A': str(a), 'B': str(b)},
+ 'challenge': str(c),
+ 'response': str(t)
+ }
def to_dict(self):
- return {'x' : str(self.x), 'public_key' : self.pk.to_dict()}
+ return {'x': str(self.x), 'public_key': self.pk.to_dict()}
toJSONDict = to_dict
def prove_sk(self, challenge_generator):
- """
- Generate a PoK of the secret key
- Prover generates w, a random integer modulo q, and computes commitment = g^w mod p.
- Verifier provides challenge modulo q.
- Prover computes response = w + x*challenge mod q, where x is the secret key.
- """
- w = Utils.random_mpz_lt(self.pk.q)
- commitment = pow(self.pk.g, w, self.pk.p)
- challenge = challenge_generator(commitment) % self.pk.q
- response = (w + (self.x * challenge)) % self.pk.q
-
- return DLogProof(commitment, challenge, response)
+ """
+ Generate a PoK of the secret key
+ Prover generates w, a random integer modulo q, and computes commitment = g^w mod p.
+ Verifier provides challenge modulo q.
+ Prover computes response = w + x*challenge mod q, where x is the secret key.
+ """
+ w = random.mpz_lt(self.pk.q)
+ commitment = pow(self.pk.g, w, self.pk.p)
+ challenge = challenge_generator(commitment) % self.pk.q
+ response = (w + (self.x * challenge)) % self.pk.q
+ return DLogProof(commitment, challenge, response)
@classmethod
def from_dict(cls, d):
if not d:
- return None
+ return None
sk = cls()
sk.x = int(d['x'])
- if d.has_key('public_key'):
- sk.pk = EGPublicKey.from_dict(d['public_key'])
+ if 'public_key' in d:
+ sk.pk = EGPublicKey.from_dict(d['public_key'])
else:
- sk.pk = None
+ sk.pk = None
return sk
fromJSONDict = from_dict
+
class EGPlaintext:
- def __init__(self, m = None, pk = None):
+ def __init__(self, m=None, pk=None):
self.m = m
self.pk = pk
def to_dict(self):
- return {'m' : self.m}
+ return {'m': self.m}
@classmethod
def from_dict(cls, d):
@@ -424,17 +328,17 @@ def __init__(self, alpha=None, beta=None, pk=None):
self.alpha = alpha
self.beta = beta
- def __mul__(self,other):
+ def __mul__(self, other):
"""
Homomorphic Multiplication of ciphertexts.
"""
- if type(other) == int and (other == 0 or other == 1):
- return self
+ if isinstance(other, int) and (other == 0 or other == 1):
+ return self
if self.pk != other.pk:
- logging.info(self.pk)
- logging.info(other.pk)
- raise Exception('different PKs!')
+ logging.info(self.pk)
+ logging.info(other.pk)
+ raise Exception('different PKs!')
new = EGCiphertext()
@@ -460,7 +364,7 @@ def reenc_return_r(self):
"""
Reencryption with fresh randomness, which is returned.
"""
- r = Utils.random_mpz_lt(self.pk.q)
+ r = random.mpz_lt(self.pk.q)
new_c = self.reenc_with_r(r)
return [new_c, r]
@@ -471,189 +375,195 @@ def reenc(self):
return self.reenc_return_r()[0]
def __eq__(self, other):
- """
- Check for ciphertext equality.
- """
- if other == None:
- return False
+ """
+ Check for ciphertext equality.
+ """
+ if other is None:
+ return False
- return (self.alpha == other.alpha and self.beta == other.beta)
+ return self.alpha == other.alpha and self.beta == other.beta
def generate_encryption_proof(self, plaintext, randomness, challenge_generator):
- """
- Generate the disjunctive encryption proof of encryption
- """
- # random W
- w = Utils.random_mpz_lt(self.pk.q)
+ """
+ Generate the disjunctive encryption proof of encryption
+ """
+ # random W
+ w = random.mpz_lt(self.pk.q)
- # build the proof
- proof = EGZKProof()
+ # build the proof
+ proof = EGZKProof()
- # compute A=g^w, B=y^w
- proof.commitment['A'] = pow(self.pk.g, w, self.pk.p)
- proof.commitment['B'] = pow(self.pk.y, w, self.pk.p)
+ # compute A=g^w, B=y^w
+ proof.commitment['A'] = pow(self.pk.g, w, self.pk.p)
+ proof.commitment['B'] = pow(self.pk.y, w, self.pk.p)
- # generate challenge
- proof.challenge = challenge_generator(proof.commitment);
+ # generate challenge
+ proof.challenge = challenge_generator(proof.commitment)
- # Compute response = w + randomness * challenge
- proof.response = (w + (randomness * proof.challenge)) % self.pk.q;
+ # Compute response = w + randomness * challenge
+ proof.response = (w + (randomness * proof.challenge)) % self.pk.q
- return proof;
+ return proof
def simulate_encryption_proof(self, plaintext, challenge=None):
- # generate a random challenge if not provided
- if not challenge:
- challenge = Utils.random_mpz_lt(self.pk.q)
+ # generate a random challenge if not provided
+ if not challenge:
+ challenge = random.mpz_lt(self.pk.q)
- proof = EGZKProof()
- proof.challenge = challenge
+ proof = EGZKProof()
+ proof.challenge = challenge
- # compute beta/plaintext, the completion of the DH tuple
- beta_over_plaintext = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
+ # compute beta/plaintext, the completion of the DH tuple
+ beta_over_plaintext = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p
- # random response, does not even need to depend on the challenge
- proof.response = Utils.random_mpz_lt(self.pk.q);
+ # random response, does not even need to depend on the challenge
+ proof.response = random.mpz_lt(self.pk.q)
- # now we compute A and B
- proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p
- proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p
+ # now we compute A and B
+ proof.commitment['A'] = (number.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p)
+ * pow(self.pk.g, proof.response, self.pk.p)
+ ) % self.pk.p
+ proof.commitment['B'] = (number.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(
+ self.pk.y, proof.response, self.pk.p)) % self.pk.p
- return proof
+ return proof
def generate_disjunctive_encryption_proof(self, plaintexts, real_index, randomness, challenge_generator):
- # note how the interface is as such so that the result does not reveal which is the real proof.
+ # note how the interface is as such so that the result does not reveal which is the real proof.
- proofs = [None for p in plaintexts]
+ proofs = [None for _ in plaintexts]
- # go through all plaintexts and simulate the ones that must be simulated.
- for p_num in range(len(plaintexts)):
- if p_num != real_index:
- proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num])
+ # go through all plaintexts and simulate the ones that must be simulated.
+ for p_num in range(len(plaintexts)):
+ if p_num != real_index:
+ proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num])
- # the function that generates the challenge
- def real_challenge_generator(commitment):
- # set up the partial real proof so we're ready to get the hash
- proofs[real_index] = EGZKProof()
- proofs[real_index].commitment = commitment
+ # the function that generates the challenge
+ def real_challenge_generator(commitment):
+ # set up the partial real proof so we're ready to get the hash
+ proofs[real_index] = EGZKProof()
+ proofs[real_index].commitment = commitment
- # get the commitments in a list and generate the whole disjunctive challenge
- commitments = [p.commitment for p in proofs]
- disjunctive_challenge = challenge_generator(commitments);
+ # get the commitments in a list and generate the whole disjunctive challenge
+ commitments = [p.commitment for p in proofs]
+ disjunctive_challenge = challenge_generator(commitments)
- # now we must subtract all of the other challenges from this challenge.
- real_challenge = disjunctive_challenge
- for p_num in range(len(proofs)):
- if p_num != real_index:
- real_challenge = real_challenge - proofs[p_num].challenge
+ # now we must subtract all of the other challenges from this challenge.
+ real_challenge = disjunctive_challenge
+ for p_num in range(len(proofs)):
+ if p_num != real_index:
+ real_challenge = real_challenge - proofs[p_num].challenge
- # make sure we mod q, the exponent modulus
- return real_challenge % self.pk.q
+ # make sure we mod q, the exponent modulus
+ return real_challenge % self.pk.q
- # do the real proof
- real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator)
+ # do the real proof
+ real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator)
- # set the real proof
- proofs[real_index] = real_proof
+ # set the real proof
+ proofs[real_index] = real_proof
- return EGZKDisjunctiveProof(proofs)
+ return EGZKDisjunctiveProof(proofs)
def verify_encryption_proof(self, plaintext, proof):
- """
- Checks for the DDH tuple g, y, alpha, beta/plaintext.
- (PoK of randomness r.)
-
- Proof contains commitment = {A, B}, challenge, response
- """
- # check that A, B are in the correct group
- if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1):
- return False
+ """
+ Checks for the DDH tuple g, y, alpha, beta/plaintext.
+ (PoK of randomness r.)
+
+ Proof contains commitment = {A, B}, challenge, response
+ """
+ # check that A, B are in the correct group
+ if not (pow(proof.commitment['A'], self.pk.q, self.pk.p) == 1 and pow(proof.commitment['B'], self.pk.q,
+ self.pk.p) == 1):
+ return False
- # check that g^response = A * alpha^challenge
- first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p))
+ # check that g^response = A * alpha^challenge
+ first_check = (pow(self.pk.g, proof.response, self.pk.p) == (
+ (pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p))
- # check that y^response = B * (beta/m)^challenge
- beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
- second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p))
+ # check that y^response = B * (beta/m)^challenge
+ beta_over_m = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p
+ second_check = (pow(self.pk.y, proof.response, self.pk.p) == (
+ (pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p))
- # print "1,2: %s %s " % (first_check, second_check)
- return (first_check and second_check)
+ # print "1,2: %s %s " % (first_check, second_check)
+ return first_check and second_check
def verify_disjunctive_encryption_proof(self, plaintexts, proof, challenge_generator):
- """
- plaintexts and proofs are all lists of equal length, with matching.
+ """
+ plaintexts and proofs are all lists of equal length, with matching.
- overall_challenge is what all of the challenges combined should yield.
- """
- if len(plaintexts) != len(proof.proofs):
- print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs)))
- return False
+ overall_challenge is what all of the challenges combined should yield.
+ """
+ if len(plaintexts) != len(proof.proofs):
+ print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs)))
+ return False
- for i in range(len(plaintexts)):
- # if a proof fails, stop right there
- if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
- print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])
- return False
+ for i in range(len(plaintexts)):
+ # if a proof fails, stop right there
+ if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
+ print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]))
+ return False
- # logging.info("made it past the two encryption proofs")
+ # logging.info("made it past the two encryption proofs")
- # check the overall challenge
- return (challenge_generator([p.commitment for p in proof.proofs]) == (sum([p.challenge for p in proof.proofs]) % self.pk.q))
+ # check the overall challenge
+ return (challenge_generator([p.commitment for p in proof.proofs]) == (
+ sum([p.challenge for p in proof.proofs]) % self.pk.q))
def verify_decryption_proof(self, plaintext, proof):
- """
- Checks for the DDH tuple g, alpha, y, beta/plaintext
- (PoK of secret key x.)
- """
- return False
+ """
+ Checks for the DDH tuple g, alpha, y, beta/plaintext
+ (PoK of secret key x.)
+ """
+ return False
def verify_decryption_factor(self, dec_factor, dec_proof, public_key):
- """
- when a ciphertext is decrypted by a dec factor, the proof needs to be checked
- """
- pass
+ """
+ when a ciphertext is decrypted by a dec factor, the proof needs to be checked
+ """
+ pass
def decrypt(self, decryption_factors, public_key):
- """
- decrypt a ciphertext given a list of decryption factors (from multiple trustees)
- For now, no support for threshold
- """
- running_decryption = self.beta
- for dec_factor in decryption_factors:
- running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p
+ """
+ decrypt a ciphertext given a list of decryption factors (from multiple trustees)
+ For now, no support for threshold
+ """
+ running_decryption = self.beta
+ for dec_factor in decryption_factors:
+ running_decryption = (running_decryption * number.inverse(dec_factor, public_key.p)) % public_key.p
- return running_decryption
+ return running_decryption
def check_group_membership(self, pk):
- """
- checks to see if an ElGamal element belongs to the group in the pk
- """
- if not (1 < self.alpha < pk.p-1):
- return False
-
- elif not (1 < self.beta < pk.p-1):
- return False
+ """
+ checks to see if an ElGamal element belongs to the group in the pk
+ """
+ if not (1 < self.alpha < pk.p - 1):
+ return False
- elif (pow(self.alpha, pk.q, pk.p)!=1):
- return False
+ elif not (1 < self.beta < pk.p - 1):
+ return False
- elif (pow(self.beta, pk.q, pk.p)!=1):
- return False
+ elif pow(self.alpha, pk.q, pk.p) != 1:
+ return False
- else:
- return True
+ elif pow(self.beta, pk.q, pk.p) != 1:
+ return False
+ else:
+ return True
def to_dict(self):
return {'alpha': str(self.alpha), 'beta': str(self.beta)}
- toJSONDict= to_dict
+ toJSONDict = to_dict
def to_string(self):
return "%s,%s" % (self.alpha, self.beta)
@classmethod
- def from_dict(cls, d, pk = None):
+ def from_dict(cls, d, pk=None):
result = cls()
result.alpha = int(d['alpha'])
result.beta = int(d['beta'])
@@ -668,127 +578,134 @@ def from_string(cls, str):
expects alpha,beta
"""
split = str.split(",")
- return cls.from_dict({'alpha' : split[0], 'beta' : split[1]})
+ return cls.from_dict({'alpha': split[0], 'beta': split[1]})
+
class EGZKProof(object):
- def __init__(self):
- self.commitment = {'A':None, 'B':None}
- self.challenge = None
- self.response = None
+ def __init__(self):
+ self.commitment = {'A': None, 'B': None}
+ self.challenge = None
+ self.response = None
- @classmethod
- def generate(cls, little_g, little_h, x, p, q, challenge_generator):
- """
- generate a DDH tuple proof, where challenge generator is
- almost certainly EG_fiatshamir_challenge_generator
- """
+ @classmethod
+ def generate(cls, little_g, little_h, x, p, q, challenge_generator):
+ """
+ generate a DDH tuple proof, where challenge generator is
+ almost certainly EG_fiatshamir_challenge_generator
+ """
- # generate random w
- w = Utils.random_mpz_lt(q)
+ # generate random w
+ w = random.mpz_lt(q)
- # create proof instance
- proof = cls()
+ # create proof instance
+ proof = cls()
- # compute A = little_g^w, B=little_h^w
- proof.commitment['A'] = pow(little_g, w, p)
- proof.commitment['B'] = pow(little_h, w, p)
+ # compute A = little_g^w, B=little_h^w
+ proof.commitment['A'] = pow(little_g, w, p)
+ proof.commitment['B'] = pow(little_h, w, p)
- # get challenge
- proof.challenge = challenge_generator(proof.commitment)
+ # get challenge
+ proof.challenge = challenge_generator(proof.commitment)
- # compute response
- proof.response = (w + (x * proof.challenge)) % q
+ # compute response
+ proof.response = (w + (x * proof.challenge)) % q
- # return proof
- return proof
+ # return proof
+ return proof
- @classmethod
- def from_dict(cls, d):
- p = cls()
- p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])}
- p.challenge = int(d['challenge'])
- p.response = int(d['response'])
- return p
+ @classmethod
+ def from_dict(cls, d):
+ p = cls()
+ p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])}
+ p.challenge = int(d['challenge'])
+ p.response = int(d['response'])
+ return p
- fromJSONDict = from_dict
+ fromJSONDict = from_dict
+
+ def to_dict(self):
+ return {
+ 'commitment': {'A': str(self.commitment['A']), 'B': str(self.commitment['B'])},
+ 'challenge': str(self.challenge),
+ 'response': str(self.response)
+ }
- def to_dict(self):
- return {
- 'commitment' : {'A' : str(self.commitment['A']), 'B' : str(self.commitment['B'])},
- 'challenge': str(self.challenge),
- 'response': str(self.response)
- }
+ toJSONDict = to_dict
- def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None):
- """
- Verify a DH tuple proof
- """
- # check that A, B are in the correct group
- if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1):
- return False
+ def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None):
+ """
+ Verify a DH tuple proof
+ """
+ # check that A, B are in the correct group
+ if not (pow(self.commitment['A'], self.pk.q, self.pk.p) == 1
+ and pow(self.commitment['B'], self.pk.q, self.pk.p) == 1):
+ return False
- # check that little_g^response = A * big_g^challenge
- first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p))
+ # check that little_g^response = A * big_g^challenge
+ first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p))
- # check that little_h^response = B * big_h^challenge
- second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p))
+ # check that little_h^response = B * big_h^challenge
+ second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p))
- # check the challenge?
- third_check = True
+ # check the challenge?
+ third_check = True
- if challenge_generator:
- third_check = (self.challenge == challenge_generator(self.commitment))
+ if challenge_generator:
+ third_check = (self.challenge == challenge_generator(self.commitment))
- return (first_check and second_check and third_check)
+ return first_check and second_check and third_check
- toJSONDict = to_dict
class EGZKDisjunctiveProof:
- def __init__(self, proofs = None):
- self.proofs = proofs
+ def __init__(self, proofs=None):
+ self.proofs = proofs
- @classmethod
- def from_dict(cls, d):
- dp = cls()
- dp.proofs = [EGZKProof.from_dict(p) for p in d]
- return dp
+ @classmethod
+ def from_dict(cls, d):
+ dp = cls()
+ dp.proofs = [EGZKProof.from_dict(p) for p in d]
+ return dp
- def to_dict(self):
- return [p.to_dict() for p in self.proofs]
+ def to_dict(self):
+ return [p.to_dict() for p in self.proofs]
+
+ toJSONDict = to_dict
- toJSONDict = to_dict
class DLogProof(object):
- def __init__(self, commitment, challenge, response):
- self.commitment = commitment
- self.challenge = challenge
- self.response = response
+ def __init__(self, commitment, challenge, response):
+ self.commitment = commitment
+ self.challenge = challenge
+ self.response = response
- def to_dict(self):
- return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response' : str(self.response)}
+ def to_dict(self):
+ return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response': str(self.response)}
- toJSONDict = to_dict
+ toJSONDict = to_dict
- @classmethod
- def from_dict(cls, d):
- dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response']))
- return dlp
+ @classmethod
+ def from_dict(cls, d):
+ dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response']))
+ return dlp
+
+ fromJSONDict = from_dict
- fromJSONDict = from_dict
def EG_disjunctive_challenge_generator(commitments):
- array_to_hash = []
- for commitment in commitments:
- array_to_hash.append(str(commitment['A']))
- array_to_hash.append(str(commitment['B']))
+ array_to_hash = []
+ for commitment in commitments:
+ array_to_hash.append(str(commitment['A']))
+ array_to_hash.append(str(commitment['B']))
+
+ string_to_hash = ",".join(array_to_hash)
+ return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16)
- string_to_hash = ",".join(array_to_hash)
- return int(hashlib.sha1(string_to_hash).hexdigest(),16)
# a challenge generator for Fiat-Shamir with A,B commitment
def EG_fiatshamir_challenge_generator(commitment):
- return EG_disjunctive_challenge_generator([commitment])
+ return EG_disjunctive_challenge_generator([commitment])
+
def DLog_challenge_generator(commitment):
- string_to_hash = str(commitment)
- return int(hashlib.sha1(string_to_hash).hexdigest(),16)
+ string_to_hash = str(commitment)
+ return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16)
diff --git a/helios/crypto/electionalgs.py b/helios/crypto/electionalgs.py
index c67714043..605d9b4ab 100644
--- a/helios/crypto/electionalgs.py
+++ b/helios/crypto/electionalgs.py
@@ -4,778 +4,807 @@
Ben Adida
2008-08-30
"""
-
-import algs
-import logging
-import utils
-import uuid
import datetime
+import uuid
+import logging
-class HeliosObject(object):
- """
- A base class to ease serialization and de-serialization
- crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out
- and deserialized from jsonobjects on the way in
- """
- FIELDS = []
- JSON_FIELDS = None
-
- def __init__(self, **kwargs):
- self.set_from_args(**kwargs)
-
- # generate uuid if need be
- if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid == None):
- self.uuid = str(uuid.uuid4())
-
- def set_from_args(self, **kwargs):
- for f in self.FIELDS:
- if kwargs.has_key(f):
- new_val = self.process_value_in(f, kwargs[f])
- setattr(self, f, new_val)
- else:
- setattr(self, f, None)
-
- def set_from_other_object(self, o):
- for f in self.FIELDS:
- if hasattr(o, f):
- setattr(self, f, self.process_value_in(f, getattr(o,f)))
- else:
- setattr(self, f, None)
-
- def toJSON(self):
- return utils.to_json(self.toJSONDict())
-
- def toJSONDict(self, alternate_fields=None):
- val = {}
- for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS):
- val[f] = self.process_value_out(f, getattr(self, f))
- return val
-
- @classmethod
- def fromJSONDict(cls, d):
- # go through the keys and fix them
- new_d = {}
- for k in d.keys():
- new_d[str(k)] = d[k]
-
- return cls(**new_d)
-
- @classmethod
- def fromOtherObject(cls, o):
- obj = cls()
- obj.set_from_other_object(o)
- return obj
-
- def toOtherObject(self, o):
- for f in self.FIELDS:
- # FIXME: why isn't this working?
- if hasattr(o, f):
- # BIG HAMMER
- try:
- setattr(o, f, self.process_value_out(f, getattr(self,f)))
- except:
- pass
-
- @property
- def hash(self):
- s = utils.to_json(self.toJSONDict())
- return utils.hash_b64(s)
-
- def process_value_in(self, field_name, field_value):
- """
- process some fields on the way into the object
- """
- if field_value == None:
- return None
-
- val = self._process_value_in(field_name, field_value)
- if val != None:
- return val
- else:
- return field_value
+from helios.utils import to_json
+from . import algs
+from . import utils
- def _process_value_in(self, field_name, field_value):
- return None
- def process_value_out(self, field_name, field_value):
+class HeliosObject(object):
"""
- process some fields on the way out of the object
+ A base class to ease serialization and de-serialization
+ crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out
+ and deserialized from jsonobjects on the way in
"""
- if field_value == None:
- return None
-
- val = self._process_value_out(field_name, field_value)
- if val != None:
- return val
- else:
- return field_value
-
- def _process_value_out(self, field_name, field_value):
- return None
+ FIELDS = []
+ JSON_FIELDS = None
+
+ def __init__(self, **kwargs):
+ self.set_from_args(**kwargs)
+
+ # generate uuid if need be
+ if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid is None):
+ self.uuid = str(uuid.uuid4())
+
+ def set_from_args(self, **kwargs):
+ for f in self.FIELDS:
+ if f in kwargs:
+ new_val = self.process_value_in(f, kwargs[f])
+ setattr(self, f, new_val)
+ else:
+ setattr(self, f, None)
+
+ def set_from_other_object(self, o):
+ for f in self.FIELDS:
+ if hasattr(o, f):
+ setattr(self, f, self.process_value_in(f, getattr(o, f)))
+ else:
+ setattr(self, f, None)
+
+ def toJSON(self):
+ return to_json(self.toJSONDict())
+
+ def toJSONDict(self, alternate_fields=None):
+ val = {}
+ for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS):
+ val[f] = self.process_value_out(f, getattr(self, f))
+ return val
+
+ @classmethod
+ def fromJSONDict(cls, d):
+ # go through the keys and fix them
+ new_d = {}
+ for k in list(d.keys()):
+ new_d[str(k)] = d[k]
+
+ return cls(**new_d)
+
+ @classmethod
+ def fromOtherObject(cls, o):
+ obj = cls()
+ obj.set_from_other_object(o)
+ return obj
+
+ def toOtherObject(self, o):
+ for f in self.FIELDS:
+ # FIXME: why isn't this working?
+ if hasattr(o, f):
+ # BIG HAMMER
+ try:
+ setattr(o, f, self.process_value_out(f, getattr(self, f)))
+ except:
+ pass
+
+ @property
+ def hash(self):
+ s = to_json(self.toJSONDict())
+ return utils.hash_b64(s)
+
+ def process_value_in(self, field_name, field_value):
+ """
+ process some fields on the way into the object
+ """
+ if field_value is None:
+ return None
+
+ val = self._process_value_in(field_name, field_value)
+ if val is not None:
+ return val
+ else:
+ return field_value
+
+ def _process_value_in(self, field_name, field_value):
+ return None
+
+ def process_value_out(self, field_name, field_value):
+ """
+ process some fields on the way out of the object
+ """
+ if field_value is None:
+ return None
+
+ val = self._process_value_out(field_name, field_value)
+ if val is not None:
+ return val
+ else:
+ return field_value
+
+ def _process_value_out(self, field_name, field_value):
+ return None
+
+ def __eq__(self, other):
+ if not hasattr(self, 'uuid'):
+ return super(HeliosObject, self) == other
+
+ return other is not None and self.uuid == other.uuid
- def __eq__(self, other):
- if not hasattr(self, 'uuid'):
- return super(HeliosObject,self) == other
-
- return other != None and self.uuid == other.uuid
class EncryptedAnswer(HeliosObject):
- """
- An encrypted answer to a single election question
- """
-
- FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer']
-
- # FIXME: remove this constructor and use only named-var constructor from HeliosObject
- def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None):
- self.choices = choices
- self.individual_proofs = individual_proofs
- self.overall_proof = overall_proof
- self.randomness = randomness
- self.answer = answer
-
- @classmethod
- def generate_plaintexts(cls, pk, min=0, max=1):
- plaintexts = []
- running_product = 1
-
- # run the product up to the min
- for i in range(max+1):
- # if we're in the range, add it to the array
- if i >= min:
- plaintexts.append(algs.EGPlaintext(running_product, pk))
-
- # next value in running product
- running_product = (running_product * pk.g) % pk.p
-
- return plaintexts
-
- def verify_plaintexts_and_randomness(self, pk):
"""
- this applies only if the explicit answers and randomness factors are given
- we do not verify the proofs here, that is the verify() method
+ An encrypted answer to a single election question
"""
- if not hasattr(self, 'answer'):
- return False
-
- for choice_num in range(len(self.choices)):
- choice = self.choices[choice_num]
- choice.pk = pk
- # redo the encryption
- # WORK HERE (paste from below encryption)
+ FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer']
- return False
+ # FIXME: remove this constructor and use only named-var constructor from HeliosObject
+ def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None):
+ self.choices = choices
+ self.individual_proofs = individual_proofs
+ self.overall_proof = overall_proof
+ self.randomness = randomness
+ self.answer = answer
- def verify(self, pk, min=0, max=1):
- possible_plaintexts = self.generate_plaintexts(pk)
- homomorphic_sum = 0
+ @classmethod
+ def generate_plaintexts(cls, pk, min=0, max=1):
+ plaintexts = []
+ running_product = 1
- for choice_num in range(len(self.choices)):
- choice = self.choices[choice_num]
- choice.pk = pk
- individual_proof = self.individual_proofs[choice_num]
+ # run the product up to the min
+ for i in range(max + 1):
+ # if we're in the range, add it to the array
+ if i >= min:
+ plaintexts.append(algs.EGPlaintext(running_product, pk))
- # verify that elements belong to the proper group
- if not choice.check_group_membership(pk):
- return False
-
- # verify the proof on the encryption of that choice
- if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof, algs.EG_disjunctive_challenge_generator):
- return False
+ # next value in running product
+ running_product = (running_product * pk.g) % pk.p
- # compute homomorphic sum if needed
- if max != None:
- homomorphic_sum = choice * homomorphic_sum
+ return plaintexts
- if max != None:
- # determine possible plaintexts for the sum
- sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
+ def verify_plaintexts_and_randomness(self, pk):
+ """
+ this applies only if the explicit answers and randomness factors are given
+ we do not verify the proofs here, that is the verify() method
+ """
+ if not hasattr(self, 'answer'):
+ return False
- # verify the sum
- return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof, algs.EG_disjunctive_challenge_generator)
- else:
- # approval voting, no need for overall proof verification
- return True
+ for choice_num in range(len(self.choices)):
+ choice = self.choices[choice_num]
+ choice.pk = pk
- def toJSONDict(self, with_randomness=False):
- value = {
- 'choices': [c.to_dict() for c in self.choices],
- 'individual_proofs' : [p.to_dict() for p in self.individual_proofs]
- }
+ # redo the encryption
+ # WORK HERE (paste from below encryption)
- if self.overall_proof:
- value['overall_proof'] = self.overall_proof.to_dict()
- else:
- value['overall_proof'] = None
-
- if with_randomness:
- value['randomness'] = [str(r) for r in self.randomness]
- value['answer'] = self.answer
-
- return value
-
- @classmethod
- def fromJSONDict(cls, d, pk=None):
- ea = cls()
-
- ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']]
- ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']]
-
- if d['overall_proof']:
- ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof'])
- else:
- ea.overall_proof = None
+ return False
- if d.has_key('randomness'):
- ea.randomness = [int(r) for r in d['randomness']]
- ea.answer = d['answer']
+ def verify(self, pk, min=0, max=1):
+ possible_plaintexts = self.generate_plaintexts(pk)
+ homomorphic_sum = 0
+
+ for choice_num in range(len(self.choices)):
+ choice = self.choices[choice_num]
+ choice.pk = pk
+ individual_proof = self.individual_proofs[choice_num]
+
+ # verify that elements belong to the proper group
+ if not choice.check_group_membership(pk):
+ return False
+
+ # verify the proof on the encryption of that choice
+ if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof,
+ algs.EG_disjunctive_challenge_generator):
+ return False
+
+ # compute homomorphic sum if needed
+ if max is not None:
+ homomorphic_sum = choice * homomorphic_sum
+
+ if max is not None:
+ # determine possible plaintexts for the sum
+ sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
+
+ # verify the sum
+ return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof,
+ algs.EG_disjunctive_challenge_generator)
+ else:
+ # approval voting, no need for overall proof verification
+ return True
+
+ def toJSONDict(self, with_randomness=False):
+ value = {
+ 'choices': [c.to_dict() for c in self.choices],
+ 'individual_proofs': [p.to_dict() for p in self.individual_proofs]
+ }
+
+ if self.overall_proof:
+ value['overall_proof'] = self.overall_proof.to_dict()
+ else:
+ value['overall_proof'] = None
+
+ if with_randomness:
+ value['randomness'] = [str(r) for r in self.randomness]
+ value['answer'] = self.answer
+
+ return value
+
+ @classmethod
+ def fromJSONDict(cls, d, pk=None):
+ ea = cls()
+
+ ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']]
+ ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']]
+
+ if d['overall_proof']:
+ ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof'])
+ else:
+ ea.overall_proof = None
+
+ if 'randomness' in d:
+ ea.randomness = [int(r) for r in d['randomness']]
+ ea.answer = d['answer']
+
+ return ea
+
+ @classmethod
+ def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
+ """
+ Given an election, a question number, and a list of answers to that question
+ in the form of an array of 0-based indexes into the answer array,
+ produce an EncryptedAnswer that works.
+ """
+ question = election.questions[question_num]
+ answers = question['answers']
+ pk = election.public_key
+
+ # initialize choices, individual proofs, randomness and overall proof
+ choices = [None for _ in range(len(answers))]
+ individual_proofs = [None for _ in range(len(answers))]
+ randomness = [None for _ in range(len(answers))]
+
+ # possible plaintexts [0, 1]
+ plaintexts = cls.generate_plaintexts(pk)
+
+ # keep track of number of options selected.
+ num_selected_answers = 0
+
+ # homomorphic sum of all
+ homomorphic_sum = 0
+ randomness_sum = 0
+
+ # min and max for number of answers, useful later
+ min_answers = 0
+ if 'min' in question:
+ min_answers = question['min']
+ max_answers = question['max']
+
+ # go through each possible answer and encrypt either a g^0 or a g^1.
+ for answer_num in range(len(answers)):
+ plaintext_index = 0
+
+ # assuming a list of answers
+ if answer_num in answer_indexes:
+ plaintext_index = 1
+ num_selected_answers += 1
+
+ # randomness and encryption
+ randomness[answer_num] = utils.random.mpz_lt(pk.q)
+ choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
+
+ # generate proof
+ individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts,
+ plaintext_index,
+ randomness[
+ answer_num],
+ algs.EG_disjunctive_challenge_generator)
+
+ # sum things up homomorphically if needed
+ if max_answers is not None:
+ homomorphic_sum = choices[answer_num] * homomorphic_sum
+ randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
+
+ # prove that the sum is 0 or 1 (can be "blank vote" for this answer)
+ # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded
+
+ if num_selected_answers < min_answers:
+ raise Exception("Need to select at least %s answer(s)" % min_answers)
+
+ if max_answers is not None:
+ sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
+
+ # need to subtract the min from the offset
+ overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts,
+ num_selected_answers - min_answers,
+ randomness_sum,
+ algs.EG_disjunctive_challenge_generator);
+ else:
+ # approval voting
+ overall_proof = None
+
+ return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes)
- return ea
- @classmethod
- def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
+class EncryptedVote(HeliosObject):
"""
- Given an election, a question number, and a list of answers to that question
- in the form of an array of 0-based indexes into the answer array,
- produce an EncryptedAnswer that works.
+ An encrypted ballot
"""
- question = election.questions[question_num]
- answers = question['answers']
- pk = election.public_key
-
- # initialize choices, individual proofs, randomness and overall proof
- choices = [None for a in range(len(answers))]
- individual_proofs = [None for a in range(len(answers))]
- overall_proof = None
- randomness = [None for a in range(len(answers))]
-
- # possible plaintexts [0, 1]
- plaintexts = cls.generate_plaintexts(pk)
-
- # keep track of number of options selected.
- num_selected_answers = 0;
-
- # homomorphic sum of all
- homomorphic_sum = 0
- randomness_sum = 0
-
- # min and max for number of answers, useful later
- min_answers = 0
- if question.has_key('min'):
- min_answers = question['min']
- max_answers = question['max']
+ FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid']
+
+ def verify(self, election):
+ # correct number of answers
+ # noinspection PyUnresolvedReferences
+ n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0
+ n_questions = len(election.questions) if election.questions is not None else 0
+ if n_answers != n_questions:
+ logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})")
+ return False
+
+ # check hash
+ # noinspection PyUnresolvedReferences
+ our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode()
+ actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode()
+ if our_election_hash != actual_election_hash:
+ logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ")
+ return False
+
+ # check ID
+ # noinspection PyUnresolvedReferences
+ our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode()
+ actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode()
+ if our_election_uuid != actual_election_uuid:
+ logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_election_uuid} ")
+ return False
+
+ # check proofs on all of answers
+ for question_num in range(len(election.questions)):
+ ea = self.encrypted_answers[question_num]
+
+ question = election.questions[question_num]
+ min_answers = 0
+ if 'min' in question:
+ min_answers = question['min']
+
+ if not ea.verify(election.public_key, min=min_answers, max=question['max']):
+ return False
+
+ return True
+
+ def get_hash(self):
+ return utils.hash_b64(to_json(self.toJSONDict()))
+
+ def toJSONDict(self, with_randomness=False):
+ return {
+ 'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers],
+ 'election_hash': self.election_hash,
+ 'election_uuid': self.election_uuid
+ }
+
+ @classmethod
+ def fromJSONDict(cls, d, pk=None):
+ ev = cls()
+
+ ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']]
+ ev.election_hash = d['election_hash']
+ ev.election_uuid = d['election_uuid']
+
+ return ev
+
+ @classmethod
+ def fromElectionAndAnswers(cls, election, answers):
+ pk = election.public_key
+
+ # each answer is an index into the answer array
+ encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for
+ answer_num in range(len(answers))]
+ return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid=election.uuid)
- # go through each possible answer and encrypt either a g^0 or a g^1.
- for answer_num in range(len(answers)):
- plaintext_index = 0
- # assuming a list of answers
- if answer_num in answer_indexes:
- plaintext_index = 1
- num_selected_answers += 1
-
- # randomness and encryption
- randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q)
- choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
-
- # generate proof
- individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts, plaintext_index,
- randomness[answer_num], algs.EG_disjunctive_challenge_generator)
-
- # sum things up homomorphically if needed
- if max_answers != None:
- homomorphic_sum = choices[answer_num] * homomorphic_sum
- randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
-
- # prove that the sum is 0 or 1 (can be "blank vote" for this answer)
- # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded
-
- if num_selected_answers < min_answers:
- raise Exception("Need to select at least %s answer(s)" % min_answers)
-
- if max_answers != None:
- sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
+def one_question_winner(question, result, num_cast_votes):
+ """
+ determining the winner for one question
+ """
+ # sort the answers , keep track of the index
+ counts = sorted(enumerate(result), key=lambda x: x[1])
+ counts.reverse()
- # need to subtract the min from the offset
- overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts, num_selected_answers - min_answers, randomness_sum, algs.EG_disjunctive_challenge_generator);
- else:
- # approval voting
- overall_proof = None
+ # if there's a max > 1, we assume that the top MAX win
+ if question['max'] > 1:
+ return [c[0] for c in counts[:question['max']]]
- return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes)
+ # if max = 1, then depends on absolute or relative
+ if question['result_type'] == 'absolute':
+ if counts[0][1] >= (num_cast_votes // 2 + 1):
+ return [counts[0][0]]
+ else:
+ return []
-class EncryptedVote(HeliosObject):
- """
- An encrypted ballot
- """
- FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid']
-
- def verify(self, election):
- # right number of answers
- if len(self.encrypted_answers) != len(election.questions):
- return False
-
- # check hash
- if self.election_hash != election.hash:
- # print "%s / %s " % (self.election_hash, election.hash)
- return False
-
- # check ID
- if self.election_uuid != election.uuid:
- return False
-
- # check proofs on all of answers
- for question_num in range(len(election.questions)):
- ea = self.encrypted_answers[question_num]
-
- question = election.questions[question_num]
- min_answers = 0
- if question.has_key('min'):
- min_answers = question['min']
-
- if not ea.verify(election.public_key, min=min_answers, max=question['max']):
- return False
+ if question['result_type'] == 'relative':
+ return [counts[0][0]]
- return True
-
- def get_hash(self):
- return utils.hash_b64(utils.to_json(self.toJSONDict()))
-
- def toJSONDict(self, with_randomness=False):
- return {
- 'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers],
- 'election_hash': self.election_hash,
- 'election_uuid': self.election_uuid
- }
+
+class Election(HeliosObject):
+ FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
+ 'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases',
+ 'voting_starts_at', 'voting_ends_at', 'election_type']
- @classmethod
- def fromJSONDict(cls, d, pk=None):
- ev = cls()
+ JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
+ 'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at']
- ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']]
- ev.election_hash = d['election_hash']
- ev.election_uuid = d['election_uuid']
+ # need to add in v3.1: use_advanced_audit_features, election_type, and probably more
- return ev
+ def init_tally(self):
+ return Tally(election=self)
- @classmethod
- def fromElectionAndAnswers(cls, election, answers):
- pk = election.public_key
+ def _process_value_in(self, field_name, field_value):
+ if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
+ if isinstance(field_value, str):
+ return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
- # each answer is an index into the answer array
- encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for answer_num in range(len(answers))]
- return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid = election.uuid)
+ if field_name == 'public_key':
+ return algs.EGPublicKey.fromJSONDict(field_value)
+ if field_name == 'private_key':
+ return algs.EGSecretKey.fromJSONDict(field_value)
-def one_question_winner(question, result, num_cast_votes):
- """
- determining the winner for one question
- """
- # sort the answers , keep track of the index
- counts = sorted(enumerate(result), key=lambda(x): x[1])
- counts.reverse()
-
- # if there's a max > 1, we assume that the top MAX win
- if question['max'] > 1:
- return [c[0] for c in counts[:question['max']]]
-
- # if max = 1, then depends on absolute or relative
- if question['result_type'] == 'absolute':
- if counts[0][1] >= (num_cast_votes/2 + 1):
- return [counts[0][0]]
- else:
- return []
-
- if question['result_type'] == 'relative':
- return [counts[0][0]]
+ def _process_value_out(self, field_name, field_value):
+ # the date
+ if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
+ return str(field_value)
-class Election(HeliosObject):
+ if field_name == 'public_key' or field_name == 'private_key':
+ return field_value.toJSONDict()
- FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
- 'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at', 'election_type']
+ @property
+ def registration_status_pretty(self):
+ if self.openreg:
+ return "Open"
+ else:
+ return "Closed"
- JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
- 'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at']
+ @property
+ def winners(self):
+ """
+ Depending on the type of each question, determine the winners
+ returns an array of winners for each question, aka an array of arrays.
+ assumes that if there is a max to the question, that's how many winners there are.
+ """
+ return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in
+ range(len(self.questions))]
- # need to add in v3.1: use_advanced_audit_features, election_type, and probably more
+ @property
+ def pretty_result(self):
+ if not self.result:
+ return None
- def init_tally(self):
- return Tally(election=self)
+ # get the winners
+ winners = self.winners
- def _process_value_in(self, field_name, field_value):
- if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
- if type(field_value) == str or type(field_value) == unicode:
- return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
+ raw_result = self.result
+ prettified_result = []
- if field_name == 'public_key':
- return algs.EGPublicKey.fromJSONDict(field_value)
+ # loop through questions
+ for i in range(len(self.questions)):
+ q = self.questions[i]
+ pretty_question = []
- if field_name == 'private_key':
- return algs.EGSecretKey.fromJSONDict(field_value)
+ # go through answers
+ for j in range(len(q['answers'])):
+ a = q['answers'][j]
+ count = raw_result[i][j]
+ pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])})
- def _process_value_out(self, field_name, field_value):
- # the date
- if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
- return str(field_value)
+ prettified_result.append({'question': q['short_name'], 'answers': pretty_question})
- if field_name == 'public_key' or field_name == 'private_key':
- return field_value.toJSONDict()
+ return prettified_result
- @property
- def registration_status_pretty(self):
- if self.openreg:
- return "Open"
- else:
- return "Closed"
- @property
- def winners(self):
+class Voter(HeliosObject):
"""
- Depending on the type of each question, determine the winners
- returns an array of winners for each question, aka an array of arrays.
- assumes that if there is a max to the question, that's how many winners there are.
+ A voter in an election
"""
- return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in range(len(self.questions))]
+ FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias']
+ JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name']
- @property
- def pretty_result(self):
- if not self.result:
- return None
+ # alternative, for when the voter is aliased
+ ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias']
- # get the winners
- winners = self.winners
+ def toJSONDict(self):
+ if self.alias is not None:
+ return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS)
+ else:
+ return super(Voter, self).toJSONDict()
- raw_result = self.result
- prettified_result = []
+ @property
+ def voter_id_hash(self):
+ if self.voter_login_id:
+ # for backwards compatibility with v3.0, and since it doesn't matter
+ # too much if we hash the email or the unique login ID here.
+ return utils.hash_b64(self.voter_login_id)
+ else:
+ return utils.hash_b64(self.voter_id)
- # loop through questions
- for i in range(len(self.questions)):
- q = self.questions[i]
- pretty_question = []
-
- # go through answers
- for j in range(len(q['answers'])):
- a = q['answers'][j]
- count = raw_result[i][j]
- pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])})
-
- prettified_result.append({'question': q['short_name'], 'answers': pretty_question})
-
- return prettified_result
-
-
-class Voter(HeliosObject):
- """
- A voter in an election
- """
- FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias']
- JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name']
-
- # alternative, for when the voter is aliased
- ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias']
-
- def toJSONDict(self):
- fields = None
- if self.alias != None:
- return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS)
- else:
- return super(Voter,self).toJSONDict()
-
- @property
- def voter_id_hash(self):
- if self.voter_login_id:
- # for backwards compatibility with v3.0, and since it doesn't matter
- # too much if we hash the email or the unique login ID here.
- return utils.hash_b64(self.voter_login_id)
- else:
- return utils.hash_b64(self.voter_id)
class Trustee(HeliosObject):
- """
- a trustee
- """
- FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email']
-
- def _process_value_in(self, field_name, field_value):
- if field_name == 'public_key':
- return algs.EGPublicKey.fromJSONDict(field_value)
-
- if field_name == 'pok':
- return algs.DLogProof.fromJSONDict(field_value)
-
- def _process_value_out(self, field_name, field_value):
- if field_name == 'public_key' or field_name == 'pok':
- return field_value.toJSONDict()
-
-class CastVote(HeliosObject):
- """
- A cast vote, which includes an encrypted vote and some cast metadata
- """
- FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash']
-
- def __init__(self, *args, **kwargs):
- super(CastVote, self).__init__(*args, **kwargs)
- self.election = None
-
- @classmethod
- def fromJSONDict(cls, d, election=None):
- o = cls()
- o.election = election
- o.set_from_args(**d)
- return o
-
- def toJSONDict(self, include_vote=True):
- result = super(CastVote,self).toJSONDict()
- if not include_vote:
- del result['vote']
- return result
-
- @classmethod
- def fromOtherObject(cls, o, election):
- obj = cls()
- obj.election = election
- obj.set_from_other_object(o)
- return obj
-
- def _process_value_in(self, field_name, field_value):
- if field_name == 'cast_at':
- if type(field_value) == str:
- return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
-
- if field_name == 'vote':
- return EncryptedVote.fromJSONDict(field_value, self.election.public_key)
-
- def _process_value_out(self, field_name, field_value):
- # the date
- if field_name == 'cast_at':
- return str(field_value)
-
- if field_name == 'vote':
- return field_value.toJSONDict()
-
- def issues(self, election):
"""
- Look for consistency problems
+ a trustee
"""
- issues = []
-
- # check the election
- if self.vote.election_uuid != election.uuid:
- issues.append("the vote's election UUID does not match the election for which this vote is being cast")
-
- return issues
-
-class DLogTable(object):
- """
- Keeping track of discrete logs
- """
-
- def __init__(self, base, modulus):
- self.dlogs = {}
- self.dlogs[1] = 0
- self.last_dlog_result = 1
- self.counter = 0
-
- self.base = base
- self.modulus = modulus
+ FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email']
- def increment(self):
- self.counter += 1
+ def _process_value_in(self, field_name, field_value):
+ if field_name == 'public_key':
+ return algs.EGPublicKey.fromJSONDict(field_value)
- # new value
- new_value = (self.last_dlog_result * self.base) % self.modulus
+ if field_name == 'pok':
+ return algs.DLogProof.fromJSONDict(field_value)
- # record the discrete log
- self.dlogs[new_value] = self.counter
+ def _process_value_out(self, field_name, field_value):
+ if field_name == 'public_key' or field_name == 'pok':
+ return field_value.toJSONDict()
- # record the last value
- self.last_dlog_result = new_value
-
- def precompute(self, up_to):
- while self.counter < up_to:
- self.increment()
-
- def lookup(self, value):
- return self.dlogs.get(value, None)
+class CastVote(HeliosObject):
+ """
+ A cast vote, which includes an encrypted vote and some cast metadata
+ """
+ FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash']
-class Tally(HeliosObject):
- """
- A running homomorphic tally
- """
+ def __init__(self, *args, **kwargs):
+ super(CastVote, self).__init__(*args, **kwargs)
+ self.election = None
- FIELDS = ['num_tallied', 'tally']
- JSON_FIELDS = ['num_tallied', 'tally']
+ @classmethod
+ def fromJSONDict(cls, d, election=None):
+ o = cls()
+ o.election = election
+ o.set_from_args(**d)
+ return o
- def __init__(self, *args, **kwargs):
- super(Tally, self).__init__(*args, **kwargs)
+ def toJSONDict(self, include_vote=True):
+ result = super(CastVote, self).toJSONDict()
+ if not include_vote:
+ del result['vote']
+ return result
- self.election = kwargs.get('election',None)
+ @classmethod
+ def fromOtherObject(cls, o, election):
+ obj = cls()
+ obj.election = election
+ obj.set_from_other_object(o)
+ return obj
- if self.election:
- self.init_election(self.election)
- else:
- self.questions = None
- self.public_key = None
+ def _process_value_in(self, field_name, field_value):
+ if field_name == 'cast_at':
+ if isinstance(field_value, str):
+ return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
- if not self.tally:
- self.tally = None
+ if field_name == 'vote':
+ return EncryptedVote.fromJSONDict(field_value, self.election.public_key)
- # initialize
- if self.num_tallied == None:
- self.num_tallied = 0
+ def _process_value_out(self, field_name, field_value):
+ # the date
+ if field_name == 'cast_at':
+ return str(field_value)
- def init_election(self, election):
- """
- given the election, initialize some params
- """
- self.questions = election.questions
- self.public_key = election.public_key
+ if field_name == 'vote':
+ return field_value.toJSONDict()
- if not self.tally:
- self.tally = [[0 for a in q['answers']] for q in self.questions]
+ def issues(self, election):
+ """
+ Look for consistency problems
+ """
+ issues = []
- def add_vote_batch(self, encrypted_votes, verify_p=True):
- """
- Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification
- rather than a whole proof verif for each vote.
- """
- for vote in encrypted_votes:
- self.add_vote(vote, verify_p)
-
- def add_vote(self, encrypted_vote, verify_p=True):
- # do we verify?
- if verify_p:
- if not encrypted_vote.verify(self.election):
- raise Exception('Bad Vote')
-
- # for each question
- for question_num in range(len(self.questions)):
- question = self.questions[question_num]
- answers = question['answers']
-
- # for each possible answer to each question
- for answer_num in range(len(answers)):
- # do the homomorphic addition into the tally
- enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num]
- enc_vote_choice.pk = self.public_key
- self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[answer_num] * self.tally[question_num][answer_num]
-
- self.num_tallied += 1
-
- def decryption_factors_and_proofs(self, sk):
- """
- returns an array of decryption factors and a corresponding array of decryption proofs.
- makes the decryption factors into strings, for general Helios / JS compatibility.
- """
- # for all choices of all questions (double list comprehension)
- decryption_factors = []
- decryption_proof = []
+ # check the election
+ if self.vote.election_uuid != election.uuid:
+ issues.append("the vote's election UUID does not match the election for which this vote is being cast")
- for question_num, question in enumerate(self.questions):
- answers = question['answers']
- question_factors = []
- question_proof = []
+ return issues
- for answer_num, answer in enumerate(answers):
- # do decryption and proof of it
- dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num])
- # look up appropriate discrete log
- # this is the string conversion
- question_factors.append(str(dec_factor))
- question_proof.append(proof.toJSONDict())
-
- decryption_factors.append(question_factors)
- decryption_proof.append(question_proof)
-
- return decryption_factors, decryption_proof
-
- def decrypt_and_prove(self, sk, discrete_logs=None):
+class DLogTable(object):
"""
- returns an array of tallies and a corresponding array of decryption proofs.
+ Keeping track of discrete logs
"""
- # who's keeping track of discrete logs?
- if not discrete_logs:
- discrete_logs = self.discrete_logs
+ def __init__(self, base, modulus):
+ self.dlogs = {1: 0}
+ self.last_dlog_result = 1
+ self.counter = 0
- # for all choices of all questions (double list comprehension)
- decrypted_tally = []
- decryption_proof = []
+ self.base = base
+ self.modulus = modulus
- for question_num in range(len(self.questions)):
- question = self.questions[question_num]
- answers = question['answers']
- question_tally = []
- question_proof = []
+ def increment(self):
+ self.counter += 1
- for answer_num in range(len(answers)):
- # do decryption and proof of it
- plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num])
+ # new value
+ new_value = (self.last_dlog_result * self.base) % self.modulus
- # look up appropriate discrete log
- question_tally.append(discrete_logs[plaintext])
- question_proof.append(proof)
+ # record the discrete log
+ self.dlogs[new_value] = self.counter
- decrypted_tally.append(question_tally)
- decryption_proof.append(question_proof)
+ # record the last value
+ self.last_dlog_result = new_value
- return decrypted_tally, decryption_proof
+ def precompute(self, up_to):
+ while self.counter < up_to:
+ self.increment()
- def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator):
- """
- decryption_factors is a list of lists of dec factors
- decryption_proofs are the corresponding proofs
- public_key is, of course, the public key of the trustee
- """
+ def lookup(self, value):
+ return self.dlogs.get(value, None)
- # go through each one
- for q_num, q in enumerate(self.tally):
- for a_num, answer_tally in enumerate(q):
- # parse the proof
- proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num])
- # check that g, alpha, y, dec_factor is a DH tuple
- if not proof.verify(public_key.g, answer_tally.alpha, public_key.y, int(decryption_factors[q_num][a_num]), public_key.p, public_key.q, challenge_generator):
- return False
-
- return True
-
- def decrypt_from_factors(self, decryption_factors, public_key):
+class Tally(HeliosObject):
"""
- decrypt a tally given decryption factors
-
- The decryption factors are a list of decryption factor sets, for each trustee.
- Each decryption factor set is a list of lists of decryption factors (questions/answers).
+ A running homomorphic tally
"""
- # pre-compute a dlog table
- dlog_table = DLogTable(base = public_key.g, modulus = public_key.p)
- dlog_table.precompute(self.num_tallied)
-
- result = []
+ FIELDS = ['num_tallied', 'tally']
+ JSON_FIELDS = ['num_tallied', 'tally']
+
+ def __init__(self, *args, **kwargs):
+ super(Tally, self).__init__(*args, **kwargs)
+
+ self.election = kwargs.get('election', None)
+
+ if self.election:
+ self.init_election(self.election)
+ else:
+ self.questions = None
+ self.public_key = None
+
+ if not self.tally:
+ self.tally = None
+
+ # initialize
+ if self.num_tallied is None:
+ self.num_tallied = 0
+
+ def init_election(self, election):
+ """
+ given the election, initialize some params
+ """
+ self.questions = election.questions
+ self.public_key = election.public_key
+
+ if not self.tally:
+ self.tally = [[0 for _ in q['answers']] for q in self.questions]
+
+ def add_vote_batch(self, encrypted_votes, verify_p=True):
+ """
+ Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification
+ rather than a whole proof verif for each vote.
+ """
+ for vote in encrypted_votes:
+ self.add_vote(vote, verify_p)
+
+ def add_vote(self, encrypted_vote, verify_p=True):
+ # do we verify?
+ if verify_p:
+ if not encrypted_vote.verify(self.election):
+ raise Exception('Bad Vote')
+
+ # for each question
+ for question_num in range(len(self.questions)):
+ question = self.questions[question_num]
+ answers = question['answers']
+
+ # for each possible answer to each question
+ for answer_num in range(len(answers)):
+ # do the homomorphic addition into the tally
+ enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num]
+ enc_vote_choice.pk = self.public_key
+ self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[
+ answer_num] * self.tally[question_num][answer_num]
+
+ self.num_tallied += 1
+
+ def decryption_factors_and_proofs(self, sk):
+ """
+ returns an array of decryption factors and a corresponding array of decryption proofs.
+ makes the decryption factors into strings, for general Helios / JS compatibility.
+ """
+ # for all choices of all questions (double list comprehension)
+ decryption_factors = []
+ decryption_proof = []
+
+ for question_num, question in enumerate(self.questions):
+ answers = question['answers']
+ question_factors = []
+ question_proof = []
+
+ for answer_num, answer in enumerate(answers):
+ # do decryption and proof of it
+ dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num])
+
+ # look up appropriate discrete log
+ # this is the string conversion
+ question_factors.append(str(dec_factor))
+ question_proof.append(proof.toJSONDict())
+
+ decryption_factors.append(question_factors)
+ decryption_proof.append(question_proof)
+
+ return decryption_factors, decryption_proof
+
+ def decrypt_and_prove(self, sk, discrete_logs=None):
+ """
+ returns an array of tallies and a corresponding array of decryption proofs.
+ """
+
+ # who's keeping track of discrete logs?
+ if not discrete_logs:
+ discrete_logs = self.discrete_logs
+
+ # for all choices of all questions (double list comprehension)
+ decrypted_tally = []
+ decryption_proof = []
+
+ for question_num in range(len(self.questions)):
+ question = self.questions[question_num]
+ answers = question['answers']
+ question_tally = []
+ question_proof = []
+
+ for answer_num in range(len(answers)):
+ # do decryption and proof of it
+ plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num])
+
+ # look up appropriate discrete log
+ question_tally.append(discrete_logs[plaintext])
+ question_proof.append(proof)
+
+ decrypted_tally.append(question_tally)
+ decryption_proof.append(question_proof)
+
+ return decrypted_tally, decryption_proof
+
+ def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator):
+ """
+ decryption_factors is a list of lists of dec factors
+ decryption_proofs are the corresponding proofs
+ public_key is, of course, the public key of the trustee
+ """
+
+ # go through each one
+ for q_num, q in enumerate(self.tally):
+ for a_num, answer_tally in enumerate(q):
+ # parse the proof
+ proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num])
+
+ # check that g, alpha, y, dec_factor is a DH tuple
+ if not proof.verify(public_key.g, answer_tally.alpha, public_key.y,
+ int(decryption_factors[q_num][a_num]), public_key.p, public_key.q,
+ challenge_generator):
+ return False
+
+ return True
+
+ def decrypt_from_factors(self, decryption_factors, public_key):
+ """
+ decrypt a tally given decryption factors
+
+ The decryption factors are a list of decryption factor sets, for each trustee.
+ Each decryption factor set is a list of lists of decryption factors (questions/answers).
+ """
+
+ # pre-compute a dlog table
+ dlog_table = DLogTable(base=public_key.g, modulus=public_key.p)
+ dlog_table.precompute(self.num_tallied)
+
+ result = []
- # go through each one
- for q_num, q in enumerate(self.tally):
- q_result = []
+ # go through each one
+ for q_num, q in enumerate(self.tally):
+ q_result = []
- for a_num, a in enumerate(q):
- # coalesce the decryption factors into one list
- dec_factor_list = [df[q_num][a_num] for df in decryption_factors]
- raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key)
+ for a_num, a in enumerate(q):
+ # coalesce the decryption factors into one list
+ dec_factor_list = [df[q_num][a_num] for df in decryption_factors]
+ raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key)
- q_result.append(dlog_table.lookup(raw_value))
+ q_result.append(dlog_table.lookup(raw_value))
- result.append(q_result)
+ result.append(q_result)
- return result
+ return result
- def _process_value_in(self, field_name, field_value):
- if field_name == 'tally':
- return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value]
+ def _process_value_in(self, field_name, field_value):
+ if field_name == 'tally':
+ return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value]
- def _process_value_out(self, field_name, field_value):
- if field_name == 'tally':
- return [[a.toJSONDict() for a in q] for q in field_value]
+ def _process_value_out(self, field_name, field_value):
+ if field_name == 'tally':
+ return [[a.toJSONDict() for a in q] for q in field_value]
diff --git a/helios/crypto/elgamal.py b/helios/crypto/elgamal.py
index 88a08c01b..33eb03083 100644
--- a/helios/crypto/elgamal.py
+++ b/helios/crypto/elgamal.py
@@ -8,12 +8,13 @@
ben@adida.net
"""
-import math, hashlib, logging
-import randpool, number
+import logging
-import numtheory
+from Crypto.Hash import SHA1
+from Crypto.Util.number import inverse
+
+from helios.crypto.utils import random
-from algs import Utils
class Cryptosystem(object):
def __init__(self):
@@ -21,30 +22,6 @@ def __init__(self):
self.q = None
self.g = None
- @classmethod
- def generate(cls, n_bits):
- """
- generate an El-Gamal environment. Returns an instance
- of ElGamal(), with prime p, group size q, and generator g
- """
-
- EG = cls()
-
- # find a prime p such that (p-1)/2 is prime q
- EG.p = Utils.random_safe_prime(n_bits)
-
- # q is the order of the group
- # FIXME: not always p-1/2
- EG.q = (EG.p-1)/2
-
- # find g that generates the q-order subgroup
- while True:
- EG.g = Utils.random_mpz_lt(EG.p)
- if pow(EG.g, EG.q, EG.p) == 1:
- break
-
- return EG
-
def generate_keypair(self):
"""
generates a keypair in the setting
@@ -68,7 +45,7 @@ def generate(self, p, q, g):
self.pk.p = p
self.pk.q = q
- self.sk.x = Utils.random_mpz_lt(q)
+ self.sk.x = random.mpz_lt(q)
self.pk.y = pow(g, self.sk.x, p)
self.sk.public_key = self.pk
@@ -106,7 +83,7 @@ def encrypt_return_r(self, plaintext):
"""
Encrypt a plaintext and return the randomness just generated and used.
"""
- r = Utils.random_mpz_lt(self.q)
+ r = random.mpz_lt(self.q)
ciphertext = self.encrypt_with_r(plaintext, r)
return [ciphertext, r]
@@ -181,7 +158,7 @@ def decrypt(self, ciphertext, dec_factor = None, decode_m=False):
if not dec_factor:
dec_factor = self.decryption_factor(ciphertext)
- m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
+ m = (inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
if decode_m:
# get m back from the q-order subgroup
@@ -207,15 +184,15 @@ def prove_decryption(self, ciphertext):
and alpha^t = b * beta/m ^ c
"""
- m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
- beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p
+ m = (inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
+ beta_over_m = (ciphertext.beta * inverse(m, self.pk.p)) % self.pk.p
# pick a random w
- w = Utils.random_mpz_lt(self.pk.q)
+ w = random.mpz_lt(self.pk.q)
a = pow(self.pk.g, w, self.pk.p)
b = pow(ciphertext.alpha, w, self.pk.p)
- c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16)
+ c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(),16)
t = (w + self.x * c) % self.pk.q
@@ -232,7 +209,7 @@ def prove_sk(self, challenge_generator):
Verifier provides challenge modulo q.
Prover computes response = w + x*challenge mod q, where x is the secret key.
"""
- w = Utils.random_mpz_lt(self.pk.q)
+ w = random.mpz_lt(self.pk.q)
commitment = pow(self.pk.g, w, self.pk.p)
challenge = challenge_generator(commitment) % self.pk.q
response = (w + (self.x * challenge)) % self.pk.q
@@ -255,7 +232,7 @@ def __mul__(self,other):
"""
Homomorphic Multiplication of ciphertexts.
"""
- if type(other) == int and (other == 0 or other == 1):
+ if isinstance(other, int) and (other == 0 or other == 1):
return self
if self.pk != other.pk:
@@ -287,7 +264,7 @@ def reenc_return_r(self):
"""
Reencryption with fresh randomness, which is returned.
"""
- r = Utils.random_mpz_lt(self.pk.q)
+ r = random.mpz_lt(self.pk.q)
new_c = self.reenc_with_r(r)
return [new_c, r]
@@ -301,17 +278,17 @@ def __eq__(self, other):
"""
Check for ciphertext equality.
"""
- if other == None:
+ if other is None:
return False
- return (self.alpha == other.alpha and self.beta == other.beta)
+ return self.alpha == other.alpha and self.beta == other.beta
def generate_encryption_proof(self, plaintext, randomness, challenge_generator):
"""
Generate the disjunctive encryption proof of encryption
"""
# random W
- w = Utils.random_mpz_lt(self.pk.q)
+ w = random.mpz_lt(self.pk.q)
# build the proof
proof = ZKProof()
@@ -331,20 +308,20 @@ def generate_encryption_proof(self, plaintext, randomness, challenge_generator):
def simulate_encryption_proof(self, plaintext, challenge=None):
# generate a random challenge if not provided
if not challenge:
- challenge = Utils.random_mpz_lt(self.pk.q)
+ challenge = random.mpz_lt(self.pk.q)
proof = ZKProof()
proof.challenge = challenge
# compute beta/plaintext, the completion of the DH tuple
- beta_over_plaintext = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
+ beta_over_plaintext = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p
# random response, does not even need to depend on the challenge
- proof.response = Utils.random_mpz_lt(self.pk.q);
+ proof.response = random.mpz_lt(self.pk.q);
# now we compute A and B
- proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p
- proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p
+ proof.commitment['A'] = (inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p
+ proof.commitment['B'] = (inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p
return proof
@@ -397,7 +374,7 @@ def verify_encryption_proof(self, plaintext, proof):
first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p))
# check that y^response = B * (beta/m)^challenge
- beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
+ beta_over_m = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p
second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p))
# print "1,2: %s %s " % (first_check, second_check)
@@ -416,7 +393,7 @@ def verify_disjunctive_encryption_proof(self, plaintexts, proof, challenge_gener
for i in range(len(plaintexts)):
# if a proof fails, stop right there
if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
- print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])
+ print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]))
return False
# logging.info("made it past the two encryption proofs")
@@ -444,7 +421,7 @@ def decrypt(self, decryption_factors, public_key):
"""
running_decryption = self.beta
for dec_factor in decryption_factors:
- running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p
+ running_decryption = (running_decryption * inverse(dec_factor, public_key.p)) % public_key.p
return running_decryption
@@ -473,7 +450,7 @@ def generate(cls, little_g, little_h, x, p, q, challenge_generator):
"""
# generate random w
- w = Utils.random_mpz_lt(q)
+ w = random.mpz_lt(q)
# create proof instance
proof = cls()
@@ -526,7 +503,7 @@ def disjunctive_challenge_generator(commitments):
array_to_hash.append(str(commitment['B']))
string_to_hash = ",".join(array_to_hash)
- return int(hashlib.sha1(string_to_hash).hexdigest(),16)
+ return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16)
# a challenge generator for Fiat-Shamir with A,B commitment
def fiatshamir_challenge_generator(commitment):
@@ -534,5 +511,5 @@ def fiatshamir_challenge_generator(commitment):
def DLog_challenge_generator(commitment):
string_to_hash = str(commitment)
- return int(hashlib.sha1(string_to_hash).hexdigest(),16)
+ return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16)
diff --git a/helios/crypto/number.py b/helios/crypto/number.py
deleted file mode 100644
index 9d50563e9..000000000
--- a/helios/crypto/number.py
+++ /dev/null
@@ -1,201 +0,0 @@
-#
-# number.py : Number-theoretic functions
-#
-# Part of the Python Cryptography Toolkit
-#
-# Distribute and use freely; there are no restrictions on further
-# dissemination and usage except those imposed by the laws of your
-# country of residence. This software is provided "as is" without
-# warranty of fitness for use or suitability for any purpose, express
-# or implied. Use at your own risk or not at all.
-#
-
-__revision__ = "$Id: number.py,v 1.13 2003/04/04 18:21:07 akuchling Exp $"
-
-bignum = long
-try:
- from Crypto.PublicKey import _fastmath
-except ImportError:
- _fastmath = None
-
-# Commented out and replaced with faster versions below
-## def long2str(n):
-## s=''
-## while n>0:
-## s=chr(n & 255)+s
-## n=n>>8
-## return s
-
-## import types
-## def str2long(s):
-## if type(s)!=types.StringType: return s # Integers will be left alone
-## return reduce(lambda x,y : x*256+ord(y), s, 0L)
-
-def size (N):
- """size(N:long) : int
- Returns the size of the number N in bits.
- """
- bits, power = 0,1L
- while N >= power:
- bits += 1
- power = power << 1
- return bits
-
-def getRandomNumber(N, randfunc):
- """getRandomNumber(N:int, randfunc:callable):long
- Return an N-bit random number."""
-
- S = randfunc(N/8)
- odd_bits = N % 8
- if odd_bits != 0:
- char = ord(randfunc(1)) >> (8-odd_bits)
- S = chr(char) + S
- value = bytes_to_long(S)
- value |= 2L ** (N-1) # Ensure high bit is set
- assert size(value) >= N
- return value
-
-def GCD(x,y):
- """GCD(x:long, y:long): long
- Return the GCD of x and y.
- """
- x = abs(x) ; y = abs(y)
- while x > 0:
- x, y = y % x, x
- return y
-
-def inverse(u, v):
- """inverse(u:long, u:long):long
- Return the inverse of u mod v.
- """
- u3, v3 = long(u), long(v)
- u1, v1 = 1L, 0L
- while v3 > 0:
- q=u3 / v3
- u1, v1 = v1, u1 - v1*q
- u3, v3 = v3, u3 - v3*q
- while u1<0:
- u1 = u1 + v
- return u1
-
-# Given a number of bits to generate and a random generation function,
-# find a prime number of the appropriate size.
-
-def getPrime(N, randfunc):
- """getPrime(N:int, randfunc:callable):long
- Return a random N-bit prime number.
- """
-
- number=getRandomNumber(N, randfunc) | 1
- while (not isPrime(number)):
- number=number+2
- return number
-
-def isPrime(N):
- """isPrime(N:long):bool
- Return true if N is prime.
- """
- if N == 1:
- return 0
- if N in sieve:
- return 1
- for i in sieve:
- if (N % i)==0:
- return 0
-
- # Use the accelerator if available
- if _fastmath is not None:
- return _fastmath.isPrime(N)
-
- # Compute the highest bit that's set in N
- N1 = N - 1L
- n = 1L
- while (n> 1L
-
- # Rabin-Miller test
- for c in sieve[:7]:
- a=long(c) ; d=1L ; t=n
- while (t): # Iterate over the bits in N1
- x=(d*d) % N
- if x==1L and d!=1L and d!=N1:
- return 0 # Square root of 1 found
- if N1 & t:
- d=(x*a) % N
- else:
- d=x
- t = t >> 1L
- if d!=1L:
- return 0
- return 1
-
-# Small primes used for checking primality; these are all the primes
-# less than 256. This should be enough to eliminate most of the odd
-# numbers before needing to do a Rabin-Miller test at all.
-
-sieve=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,
- 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
- 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
- 197, 199, 211, 223, 227, 229, 233, 239, 241, 251]
-
-# Improved conversion functions contributed by Barry Warsaw, after
-# careful benchmarking
-
-import struct
-
-def long_to_bytes(n, blocksize=0):
- """long_to_bytes(n:long, blocksize:int) : string
- Convert a long integer to a byte string.
-
- If optional blocksize is given and greater than zero, pad the front of the
- byte string with binary zeros so that the length is a multiple of
- blocksize.
- """
- # after much testing, this algorithm was deemed to be the fastest
- s = ''
- n = long(n)
- pack = struct.pack
- while n > 0:
- s = pack('>I', n & 0xffffffffL) + s
- n = n >> 32
- # strip off leading zeros
- for i in range(len(s)):
- if s[i] != '\000':
- break
- else:
- # only happens when n == 0
- s = '\000'
- i = 0
- s = s[i:]
- # add back some pad bytes. this could be done more efficiently w.r.t. the
- # de-padding being done above, but sigh...
- if blocksize > 0 and len(s) % blocksize:
- s = (blocksize - len(s) % blocksize) * '\000' + s
- return s
-
-def bytes_to_long(s):
- """bytes_to_long(string) : long
- Convert a byte string to a long integer.
-
- This is (essentially) the inverse of long_to_bytes().
- """
- acc = 0L
- unpack = struct.unpack
- length = len(s)
- if length % 4:
- extra = (4 - length % 4)
- s = '\000' * extra + s
- length = length + extra
- for i in range(0, length, 4):
- acc = (acc << 32) + unpack('>I', s[i:i+4])[0]
- return acc
-
-# For backwards compatibility...
-import warnings
-def long2str(n, blocksize=0):
- warnings.warn("long2str() has been replaced by long_to_bytes()")
- return long_to_bytes(n, blocksize)
-def str2long(s):
- warnings.warn("str2long() has been replaced by bytes_to_long()")
- return bytes_to_long(s)
diff --git a/helios/crypto/numtheory.py b/helios/crypto/numtheory.py
index 16fcf9aa0..e16691745 100644
--- a/helios/crypto/numtheory.py
+++ b/helios/crypto/numtheory.py
@@ -103,7 +103,7 @@ def trial_division(n, bound=None):
if n == 1: return 1
for p in [2, 3, 5]:
if n%p == 0: return p
- if bound == None: bound = n
+ if bound is None: bound = n
dif = [6, 4, 2, 4, 2, 4, 6, 2]
m = 7; i = 1
while m <= bound and m*m <= n:
@@ -207,7 +207,7 @@ def inversemod(a, n):
"""
g, x, y = xgcd(a, n)
if g != 1:
- raise ZeroDivisionError, (a,n)
+ raise ZeroDivisionError(a,n)
assert g == 1, "a must be coprime to n."
return x%n
@@ -225,7 +225,7 @@ def solve_linear(a,b,n):
Examples:
>>> solve_linear(4, 2, 10)
8
- >>> solve_linear(2, 1, 4) == None
+ >>> solve_linear(2, 1, 4) is None
True
"""
g, c, _ = xgcd(a,n) # (1)
@@ -1014,7 +1014,7 @@ def elliptic_curve_method(N, m, tries=5):
E, P = randcurve(N) # (2)
try: # (3)
Q = ellcurve_mul(E, m, P) # (4)
- except ZeroDivisionError, x: # (5)
+ except ZeroDivisionError as x: # (5)
g = gcd(x[0],N) # (6)
if g != 1 or g != N: return g # (7)
return N
@@ -1153,7 +1153,7 @@ def __mul__(self, other):
return r
def __neg__(self):
v = {}
- for m in self.v.keys():
+ for m in list(self.v.keys()):
v[m] = -self.v[m]
return Poly(v)
def __div__(self, other):
@@ -1161,7 +1161,7 @@ def __div__(self, other):
def __getitem__(self, m): # (6)
m = tuple(m)
- if not self.v.has_key(m): self.v[m] = 0
+ if m not in self.v: self.v[m] = 0
return self.v[m]
def __setitem__(self, m, c):
self.v[tuple(m)] = c
@@ -1169,7 +1169,7 @@ def __delitem__(self, m):
del self.v[tuple(m)]
def monomials(self): # (7)
- return self.v.keys()
+ return list(self.v.keys())
def normalize(self): # (8)
while True:
finished = True
@@ -1244,8 +1244,8 @@ def prove_associative(): # (15)
- (x3 + x4)*(x3 - x4)*(x3 - x4))
s2 = (x3 - x4)*(x3 - x4)*((y1 - y5)*(y1 - y5) \
- (x1 + x5)*(x1 - x5)*(x1 - x5))
- print "Associative?"
- print s1 == s2 # (17)
+ print("Associative?")
+ print(s1 == s2) # (17)
diff --git a/helios/crypto/randpool.py b/helios/crypto/randpool.py
deleted file mode 100644
index 53a8acc03..000000000
--- a/helios/crypto/randpool.py
+++ /dev/null
@@ -1,422 +0,0 @@
-#
-# randpool.py : Cryptographically strong random number generation
-#
-# Part of the Python Cryptography Toolkit
-#
-# Distribute and use freely; there are no restrictions on further
-# dissemination and usage except those imposed by the laws of your
-# country of residence. This software is provided "as is" without
-# warranty of fitness for use or suitability for any purpose, express
-# or implied. Use at your own risk or not at all.
-#
-
-__revision__ = "$Id: randpool.py,v 1.14 2004/05/06 12:56:54 akuchling Exp $"
-
-import time, array, types, warnings, os.path
-from number import long_to_bytes
-try:
- import Crypto.Util.winrandom as winrandom
-except:
- winrandom = None
-
-STIRNUM = 3
-
-class RandomPool:
- """randpool.py : Cryptographically strong random number generation.
-
- The implementation here is similar to the one in PGP. To be
- cryptographically strong, it must be difficult to determine the RNG's
- output, whether in the future or the past. This is done by using
- a cryptographic hash function to "stir" the random data.
-
- Entropy is gathered in the same fashion as PGP; the highest-resolution
- clock around is read and the data is added to the random number pool.
- A conservative estimate of the entropy is then kept.
-
- If a cryptographically secure random source is available (/dev/urandom
- on many Unixes, Windows CryptGenRandom on most Windows), then use
- it.
-
- Instance Attributes:
- bits : int
- Maximum size of pool in bits
- bytes : int
- Maximum size of pool in bytes
- entropy : int
- Number of bits of entropy in this pool.
-
- Methods:
- add_event([s]) : add some entropy to the pool
- get_bytes(int) : get N bytes of random data
- randomize([N]) : get N bytes of randomness from external source
- """
-
-
- def __init__(self, numbytes = 160, cipher=None, hash=None):
- if hash is None:
- from hashlib import sha1 as hash
-
- # The cipher argument is vestigial; it was removed from
- # version 1.1 so RandomPool would work even in the limited
- # exportable subset of the code
- if cipher is not None:
- warnings.warn("'cipher' parameter is no longer used")
-
- if isinstance(hash, types.StringType):
- # ugly hack to force __import__ to give us the end-path module
- hash = __import__('Crypto.Hash.'+hash,
- None, None, ['new'])
- warnings.warn("'hash' parameter should now be a hashing module")
-
- self.bytes = numbytes
- self.bits = self.bytes*8
- self.entropy = 0
- self._hash = hash
-
- # Construct an array to hold the random pool,
- # initializing it to 0.
- self._randpool = array.array('B', [0]*self.bytes)
-
- self._event1 = self._event2 = 0
- self._addPos = 0
- self._getPos = hash().digest_size
- self._lastcounter=time.time()
- self.__counter = 0
-
- self._measureTickSize() # Estimate timer resolution
- self._randomize()
-
- def _updateEntropyEstimate(self, nbits):
- self.entropy += nbits
- if self.entropy < 0:
- self.entropy = 0
- elif self.entropy > self.bits:
- self.entropy = self.bits
-
- def _randomize(self, N = 0, devname = '/dev/urandom'):
- """_randomize(N, DEVNAME:device-filepath)
- collects N bits of randomness from some entropy source (e.g.,
- /dev/urandom on Unixes that have it, Windows CryptoAPI
- CryptGenRandom, etc)
- DEVNAME is optional, defaults to /dev/urandom. You can change it
- to /dev/random if you want to block till you get enough
- entropy.
- """
- data = ''
- if N <= 0:
- nbytes = int((self.bits - self.entropy)/8+0.5)
- else:
- nbytes = int(N/8+0.5)
- if winrandom:
- # Windows CryptGenRandom provides random data.
- data = winrandom.new().get_bytes(nbytes)
- # GAE fix, benadida
- #elif os.path.exists(devname):
- # # Many OSes support a /dev/urandom device
- # try:
- # f=open(devname)
- # data=f.read(nbytes)
- # f.close()
- # except IOError, (num, msg):
- # if num!=2: raise IOError, (num, msg)
- # # If the file wasn't found, ignore the error
- if data:
- self._addBytes(data)
- # Entropy estimate: The number of bits of
- # data obtained from the random source.
- self._updateEntropyEstimate(8*len(data))
- self.stir_n() # Wash the random pool
-
- def randomize(self, N=0):
- """randomize(N:int)
- use the class entropy source to get some entropy data.
- This is overridden by KeyboardRandomize().
- """
- return self._randomize(N)
-
- def stir_n(self, N = STIRNUM):
- """stir_n(N)
- stirs the random pool N times
- """
- for i in xrange(N):
- self.stir()
-
- def stir (self, s = ''):
- """stir(s:string)
- Mix up the randomness pool. This will call add_event() twice,
- but out of paranoia the entropy attribute will not be
- increased. The optional 's' parameter is a string that will
- be hashed with the randomness pool.
- """
-
- entropy=self.entropy # Save inital entropy value
- self.add_event()
-
- # Loop over the randomness pool: hash its contents
- # along with a counter, and add the resulting digest
- # back into the pool.
- for i in range(self.bytes / self._hash().digest_size):
- h = self._hash(self._randpool)
- h.update(str(self.__counter) + str(i) + str(self._addPos) + s)
- self._addBytes( h.digest() )
- self.__counter = (self.__counter + 1) & 0xFFFFffffL
-
- self._addPos, self._getPos = 0, self._hash().digest_size
- self.add_event()
-
- # Restore the old value of the entropy.
- self.entropy=entropy
-
-
- def get_bytes (self, N):
- """get_bytes(N:int) : string
- Return N bytes of random data.
- """
-
- s=''
- i, pool = self._getPos, self._randpool
- h=self._hash()
- dsize = self._hash().digest_size
- num = N
- while num > 0:
- h.update( self._randpool[i:i+dsize] )
- s = s + h.digest()
- num = num - dsize
- i = (i + dsize) % self.bytes
- if i>1, bits+1
- if bits>8: bits=8
-
- self._event1, self._event2 = event, self._event1
-
- self._updateEntropyEstimate(bits)
- return bits
-
- # Private functions
- def _noise(self):
- # Adds a bit of noise to the random pool, by adding in the
- # current time and CPU usage of this process.
- # The difference from the previous call to _noise() is taken
- # in an effort to estimate the entropy.
- t=time.time()
- delta = (t - self._lastcounter)/self._ticksize*1e6
- self._lastcounter = t
- self._addBytes(long_to_bytes(long(1000*time.time())))
- self._addBytes(long_to_bytes(long(1000*time.clock())))
- self._addBytes(long_to_bytes(long(1000*time.time())))
- self._addBytes(long_to_bytes(long(delta)))
-
- # Reduce delta to a maximum of 8 bits so we don't add too much
- # entropy as a result of this call.
- delta=delta % 0xff
- return int(delta)
-
-
- def _measureTickSize(self):
- # _measureTickSize() tries to estimate a rough average of the
- # resolution of time that you can see from Python. It does
- # this by measuring the time 100 times, computing the delay
- # between measurements, and taking the median of the resulting
- # list. (We also hash all the times and add them to the pool)
- interval = [None] * 100
- h = self._hash(`(id(self),id(interval))`)
-
- # Compute 100 differences
- t=time.time()
- h.update(`t`)
- i = 0
- j = 0
- while i < 100:
- t2=time.time()
- h.update(`(i,j,t2)`)
- j += 1
- delta=int((t2-t)*1e6)
- if delta:
- interval[i] = delta
- i += 1
- t=t2
-
- # Take the median of the array of intervals
- interval.sort()
- self._ticksize=interval[len(interval)/2]
- h.update(`(interval,self._ticksize)`)
- # mix in the measurement times and wash the random pool
- self.stir(h.digest())
-
- def _addBytes(self, s):
- "XOR the contents of the string S into the random pool"
- i, pool = self._addPos, self._randpool
- for j in range(0, len(s)):
- pool[i]=pool[i] ^ ord(s[j])
- i=(i+1) % self.bytes
- self._addPos = i
-
- # Deprecated method names: remove in PCT 2.1 or later.
- def getBytes(self, N):
- warnings.warn("getBytes() method replaced by get_bytes()",
- DeprecationWarning)
- return self.get_bytes(N)
-
- def addEvent (self, event, s=""):
- warnings.warn("addEvent() method replaced by add_event()",
- DeprecationWarning)
- return self.add_event(s + str(event))
-
-class PersistentRandomPool (RandomPool):
- def __init__ (self, filename=None, *args, **kwargs):
- RandomPool.__init__(self, *args, **kwargs)
- self.filename = filename
- if filename:
- try:
- # the time taken to open and read the file might have
- # a little disk variability, modulo disk/kernel caching...
- f=open(filename, 'rb')
- self.add_event()
- data = f.read()
- self.add_event()
- # mix in the data from the file and wash the random pool
- self.stir(data)
- f.close()
- except IOError:
- # Oh, well; the file doesn't exist or is unreadable, so
- # we'll just ignore it.
- pass
-
- def save(self):
- if self.filename == "":
- raise ValueError, "No filename set for this object"
- # wash the random pool before save, provides some forward secrecy for
- # old values of the pool.
- self.stir_n()
- f=open(self.filename, 'wb')
- self.add_event()
- f.write(self._randpool.tostring())
- f.close()
- self.add_event()
- # wash the pool again, provide some protection for future values
- self.stir()
-
-# non-echoing Windows keyboard entry
-_kb = 0
-if not _kb:
- try:
- import msvcrt
- class KeyboardEntry:
- def getch(self):
- c = msvcrt.getch()
- if c in ('\000', '\xe0'):
- # function key
- c += msvcrt.getch()
- return c
- def close(self, delay = 0):
- if delay:
- time.sleep(delay)
- while msvcrt.kbhit():
- msvcrt.getch()
- _kb = 1
- except:
- pass
-
-# non-echoing Posix keyboard entry
-if not _kb:
- try:
- import termios
- class KeyboardEntry:
- def __init__(self, fd = 0):
- self._fd = fd
- self._old = termios.tcgetattr(fd)
- new = termios.tcgetattr(fd)
- new[3]=new[3] & ~termios.ICANON & ~termios.ECHO
- termios.tcsetattr(fd, termios.TCSANOW, new)
- def getch(self):
- termios.tcflush(0, termios.TCIFLUSH) # XXX Leave this in?
- return os.read(self._fd, 1)
- def close(self, delay = 0):
- if delay:
- time.sleep(delay)
- termios.tcflush(self._fd, termios.TCIFLUSH)
- termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._old)
- _kb = 1
- except:
- pass
-
-class KeyboardRandomPool (PersistentRandomPool):
- def __init__(self, *args, **kwargs):
- PersistentRandomPool.__init__(self, *args, **kwargs)
-
- def randomize(self, N = 0):
- "Adds N bits of entropy to random pool. If N is 0, fill up pool."
- import os, string, time
- if N <= 0:
- bits = self.bits - self.entropy
- else:
- bits = N*8
- if bits == 0:
- return
- print bits,'bits of entropy are now required. Please type on the keyboard'
- print 'until enough randomness has been accumulated.'
- kb = KeyboardEntry()
- s='' # We'll save the characters typed and add them to the pool.
- hash = self._hash
- e = 0
- try:
- while e < bits:
- temp=str(bits-e).rjust(6)
- os.write(1, temp)
- s=s+kb.getch()
- e += self.add_event(s)
- os.write(1, 6*chr(8))
- self.add_event(s+hash.new(s).digest() )
- finally:
- kb.close()
- print '\n\007 Enough. Please wait a moment.\n'
- self.stir_n() # wash the random pool.
- kb.close(4)
-
-if __name__ == '__main__':
- pool = RandomPool()
- print 'random pool entropy', pool.entropy, 'bits'
- pool.add_event('something')
- print `pool.get_bytes(100)`
- import tempfile, os
- fname = tempfile.mktemp()
- pool = KeyboardRandomPool(filename=fname)
- print 'keyboard random pool entropy', pool.entropy, 'bits'
- pool.randomize()
- print 'keyboard random pool entropy', pool.entropy, 'bits'
- pool.randomize(128)
- pool.save()
- saved = open(fname, 'rb').read()
- print 'saved', `saved`
- print 'pool ', `pool._randpool.tostring()`
- newpool = PersistentRandomPool(fname)
- print 'persistent random pool entropy', pool.entropy, 'bits'
- os.remove(fname)
diff --git a/helios/crypto/utils.py b/helios/crypto/utils.py
index dd395a598..2fcce307f 100644
--- a/helios/crypto/utils.py
+++ b/helios/crypto/utils.py
@@ -1,23 +1,31 @@
"""
Crypto Utils
"""
+import base64
+import math
+
+from Crypto.Hash import SHA256
+from Crypto.Random.random import StrongRandom
+
+random = StrongRandom()
+
+
+def random_mpz_lt(maximum, strong_random=random):
+ n_bits = int(math.floor(math.log(maximum, 2)))
+ res = strong_random.getrandbits(n_bits)
+ while res >= maximum:
+ res = strong_random.getrandbits(n_bits)
+ return res
+
+
+random.mpz_lt = random_mpz_lt
-import hmac, base64, json
-from hashlib import sha256
-
def hash_b64(s):
- """
- hash the string using sha1 and produce a base64 output
- removes the trailing "="
- """
- hasher = sha256(s)
- result= base64.b64encode(hasher.digest())[:-1]
- return result
-
-def to_json(d):
- return json.dumps(d, sort_keys=True)
-
-def from_json(json_str):
- if not json_str: return None
- return json.loads(json_str)
+ """
+ hash the string using sha256 and produce a base64 output
+ removes the trailing "="
+ """
+ hasher = SHA256.new(s.encode('utf-8'))
+ result = base64.b64encode(hasher.digest())[:-1].decode('ascii')
+ return result
diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py
index b0ede25f2..574f8eb93 100644
--- a/helios/datatypes/__init__.py
+++ b/helios/datatypes/__init__.py
@@ -25,6 +25,7 @@
# but is not necessary for full JSON-LD objects.
LDObject.deserialize(json_string, type=...)
"""
+import importlib
from helios import utils
from helios.crypto import utils as cryptoutils
@@ -33,32 +34,32 @@
## utility function
##
def recursiveToDict(obj):
- if obj == None:
+ if obj is None:
return None
- if type(obj) == list:
+ if isinstance(obj, list):
return [recursiveToDict(el) for el in obj]
else:
return obj.toDict()
def get_class(datatype):
# already done?
- if not isinstance(datatype, basestring):
+ if not isinstance(datatype, str):
return datatype
# parse datatype string "v31/Election" --> from v31 import Election
parsed_datatype = datatype.split("/")
# get the module
- dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1)
+ dynamic_module = importlib.import_module("helios.datatypes." + (".".join(parsed_datatype[:-1])))
if not dynamic_module:
- raise Exception("no module for %s" % datatpye)
+ raise Exception("no module for %s" % datatype)
# go down the attributes to get to the class
try:
dynamic_ptr = dynamic_module
- for attr in parsed_datatype[1:]:
+ for attr in parsed_datatype[-1:]:
dynamic_ptr = getattr(dynamic_ptr, attr)
dynamic_cls = dynamic_ptr
except AttributeError:
@@ -119,7 +120,7 @@ def __init__(self, wrapped_obj):
@classmethod
def instantiate(cls, obj, datatype=None):
- "FIXME: should datatype override the object's internal datatype? probably not"
+ """FIXME: should datatype override the object's internal datatype? probably not"""
if isinstance(obj, LDObject):
return obj
@@ -130,7 +131,7 @@ def instantiate(cls, obj, datatype=None):
raise Exception("no datatype found")
# nulls
- if obj == None:
+ if obj is None:
return None
# the class
@@ -149,9 +150,11 @@ def _setattr_wrapped(self, attr, val):
setattr(self.wrapped_obj, attr, val)
def loadData(self):
- "load data using from the wrapped object"
+ """
+ load data using from the wrapped object
+ """
# go through the subfields and instantiate them too
- for subfield_name, subfield_type in self.STRUCTURED_FIELDS.iteritems():
+ for subfield_name, subfield_type in self.STRUCTURED_FIELDS.items():
self.structured_fields[subfield_name] = self.instantiate(self._getattr_wrapped(subfield_name), datatype = subfield_type)
def loadDataFromDict(self, d):
@@ -160,7 +163,7 @@ def loadDataFromDict(self, d):
"""
# the structured fields
- structured_fields = self.STRUCTURED_FIELDS.keys()
+ structured_fields = list(self.STRUCTURED_FIELDS.keys())
# go through the fields and set them properly
# on the newly instantiated object
@@ -171,7 +174,7 @@ def loadDataFromDict(self, d):
self.structured_fields[f] = sub_ld_object
# set the field on the wrapped object too
- if sub_ld_object != None:
+ if sub_ld_object is not None:
self._setattr_wrapped(f, sub_ld_object.wrapped_obj)
else:
self._setattr_wrapped(f, None)
@@ -190,12 +193,12 @@ def toDict(self, alternate_fields=None, complete=False):
fields = self.FIELDS
if not self.structured_fields:
- if self.wrapped_obj.alias != None:
+ if self.wrapped_obj.alias is not None:
fields = self.ALIASED_VOTER_FIELDS
for f in (alternate_fields or fields):
# is it a structured subfield?
- if self.structured_fields.has_key(f):
+ if f in self.structured_fields:
val[f] = recursiveToDict(self.structured_fields[f])
else:
val[f] = self.process_value_out(f, self._getattr_wrapped(f))
@@ -214,7 +217,7 @@ def toDict(self, alternate_fields=None, complete=False):
@classmethod
def fromDict(cls, d, type_hint=None):
# null objects
- if d == None:
+ if d is None:
return None
# the LD type is either in d or in type_hint
@@ -248,11 +251,11 @@ def process_value_in(self, field_name, field_value):
"""
process some fields on the way into the object
"""
- if field_value == None:
+ if field_value is None:
return None
val = self._process_value_in(field_name, field_value)
- if val != None:
+ if val is not None:
return val
else:
return field_value
@@ -264,23 +267,25 @@ def process_value_out(self, field_name, field_value):
"""
process some fields on the way out of the object
"""
- if field_value == None:
+ if field_value is None:
return None
val = self._process_value_out(field_name, field_value)
- if val != None:
+ if val is not None:
return val
else:
return field_value
def _process_value_out(self, field_name, field_value):
+ if isinstance(field_value, bytes):
+ return field_value.decode('utf-8')
return None
-
+
def __eq__(self, other):
if not hasattr(self, 'uuid'):
- return super(LDObject,self) == other
+ return super(LDObject, self) == other
- return other != None and self.uuid == other.uuid
+ return other is not None and self.uuid == other.uuid
class BaseArrayOfObjects(LDObject):
diff --git a/helios/datatypes/djangofield.py b/helios/datatypes/djangofield.py
index e0eb1b4fb..a299ace6c 100644
--- a/helios/datatypes/djangofield.py
+++ b/helios/datatypes/djangofield.py
@@ -6,15 +6,12 @@
and adapted to LDObject
"""
-import datetime
-import json
from django.db import models
-from django.db.models import signals
-from django.conf import settings
-from django.core.serializers.json import DjangoJSONEncoder
+from helios import utils
from . import LDObject
+
class LDObjectField(models.TextField):
"""
LDObject is a generic textfield that neatly serializes/unserializes
@@ -23,9 +20,6 @@ class LDObjectField(models.TextField):
deserialization_params added on 2011-01-09 to provide additional hints at deserialization time
"""
- # Used so to_python() is called
- __metaclass__ = models.SubfieldBase
-
def __init__(self, type_hint=None, **kwargs):
self.type_hint = type_hint
super(LDObjectField, self).__init__(**kwargs)
@@ -34,35 +28,29 @@ def to_python(self, value):
"""Convert our string value to LDObject after we load it from the DB"""
# did we already convert this?
- if not isinstance(value, basestring):
+ if not isinstance(value, str):
return value
- if value == None:
- return None
+ return self.from_db_value(value)
+ # noinspection PyUnusedLocal
+ def from_db_value(self, value, *args, **kwargs):
# in some cases, we're loading an existing array or dict,
- # we skip this part but instantiate the LD object
- if isinstance(value, basestring):
- try:
- parsed_value = json.loads(value)
- except:
- raise Exception("value is not JSON parseable, that's bad news")
- else:
- parsed_value = value
-
- if parsed_value != None:
- "we give the wrapped object back because we're not dealing with serialization types"
- return_val = LDObject.fromDict(parsed_value, type_hint = self.type_hint).wrapped_obj
- return return_val
- else:
+ # from_json takes care of this duality
+ parsed_value = utils.from_json(value)
+ if parsed_value is None:
return None
+ # we give the wrapped object back because we're not dealing with serialization types
+ return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj
+ return return_val
+
def get_prep_value(self, value):
"""Convert our JSON object to a string before we save"""
- if isinstance(value, basestring):
+ if isinstance(value, str):
return value
- if value == None:
+ if value is None:
return None
# instantiate the proper LDObject to dump it appropriately
@@ -71,4 +59,4 @@ def get_prep_value(self, value):
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
- return self.get_db_prep_value(value)
+ return self.get_db_prep_value(value, None)
diff --git a/helios/datatypes/legacy.py b/helios/datatypes/legacy.py
index c0a24ffc2..d469b4418 100644
--- a/helios/datatypes/legacy.py
+++ b/helios/datatypes/legacy.py
@@ -77,7 +77,7 @@ def toDict(self, complete=False):
"""
depending on whether the voter is aliased, use different fields
"""
- if self.wrapped_obj.alias != None:
+ if self.wrapped_obj.alias is not None:
return super(Voter, self).toDict(self.ALIASED_VOTER_FIELDS, complete = complete)
else:
return super(Voter,self).toDict(complete = complete)
diff --git a/helios/datetimewidget.py b/helios/datetimewidget.py
index 5a9e0d40a..dfd7ec04a 100644
--- a/helios/datetimewidget.py
+++ b/helios/datetimewidget.py
@@ -14,7 +14,7 @@
from django.utils.safestring import mark_safe
# DATETIMEWIDGET
-calbtn = u'''
+calbtn = '''
'''
class DateTimeWidget(forms.widgets.TextInput):
+ template_name = ''
+
class Media:
css = {
'all': (
@@ -49,13 +51,13 @@ def render(self, name, value, attrs=None):
except:
final_attrs['value'] = \
force_unicode(value)
- if not final_attrs.has_key('id'):
- final_attrs['id'] = u'%s_id' % (name)
+ if 'id' not in final_attrs:
+ final_attrs['id'] = '%s_id' % (name)
id = final_attrs['id']
jsdformat = self.dformat #.replace('%', '%%')
cal = calbtn % (settings.MEDIA_URL, id, id, jsdformat, id)
- a = u' %s%s' % (forms.util.flatatt(final_attrs), self.media, cal)
+ a = ' %s%s' % (forms.util.flatatt(final_attrs), self.media, cal)
return mark_safe(a)
def value_from_datadict(self, data, files, name):
@@ -82,12 +84,12 @@ def _has_changed(self, initial, data):
Copy of parent's method, but modify value with strftime function before final comparsion
"""
if data is None:
- data_value = u''
+ data_value = ''
else:
data_value = data
if initial is None:
- initial_value = u''
+ initial_value = ''
else:
initial_value = initial
diff --git a/helios/election_url_names.py b/helios/election_url_names.py
new file mode 100644
index 000000000..0700de268
--- /dev/null
+++ b/helios/election_url_names.py
@@ -0,0 +1,70 @@
+ELECTION_HOME="election@home"
+ELECTION_VIEW="election@view"
+ELECTION_META="election@meta"
+ELECTION_EDIT="election@edit"
+ELECTION_SCHEDULE="election@schedule"
+ELECTION_EXTEND="election@extend"
+ELECTION_ARCHIVE="election@archive"
+ELECTION_DELETE="election@delete"
+ELECTION_COPY="election@copy"
+ELECTION_BADGE="election@badge"
+
+ELECTION_TRUSTEES_HOME="election@trustees"
+ELECTION_TRUSTEES_VIEW="election@trustees@view"
+ELECTION_TRUSTEES_NEW="election@trustees@new"
+ELECTION_TRUSTEES_ADD_HELIOS="election@trustees@add-helios"
+ELECTION_TRUSTEES_DELETE="election@trustees@delete"
+
+ELECTION_ADMINS_LIST="election@admins"
+ELECTION_ADMINS_ADD="election@admins@add"
+ELECTION_ADMINS_REMOVE="election@admins@remove"
+
+ELECTION_TRUSTEE_HOME="election@trustee"
+ELECTION_TRUSTEE_SEND_URL="election@trustee@send-url"
+ELECTION_TRUSTEE_KEY_GENERATOR="election@trustee@key-generator"
+ELECTION_TRUSTEE_CHECK_SK="election@trustee@check-sk"
+ELECTION_TRUSTEE_UPLOAD_PK="election@trustee@upload-pk"
+ELECTION_TRUSTEE_DECRYPT_AND_PROVE="election@trustee@decrypt-and-prove"
+ELECTION_TRUSTEE_UPLOAD_DECRYPTION="election@trustee@upload-decryption"
+
+ELECTION_RESULT="election@result"
+ELECTION_RESULT_PROOF="election@result@proof"
+ELECTION_BBOARD="election@bboard"
+ELECTION_AUDITED_BALLOTS="election@audited-ballots"
+
+ELECTION_GET_RANDOMNESS="election@get-randomness"
+ELECTION_QUESTIONS="election@questions"
+ELECTION_SET_REG="election@set-reg"
+ELECTION_SET_FEATURED="election@set-featured"
+ELECTION_SAVE_QUESTIONS="election@save-questions"
+ELECTION_REGISTER="election@register"
+ELECTION_FREEZE="election@freeze"
+
+ELECTION_COMPUTE_TALLY="election@compute-tally"
+ELECTION_COMBINE_DECRYPTIONS="election@combine-decryptions"
+ELECTION_RELEASE_RESULT="election@release-result"
+
+ELECTION_CAST="election@cast"
+ELECTION_CAST_CONFIRM="election@cast-confirm"
+ELECTION_PASSWORD_VOTER_LOGIN="election@password-voter-login"
+ELECTION_PASSWORD_VOTER_RESEND="election@password-voter-resend"
+ELECTION_CAST_DONE="election@cast-done"
+
+ELECTION_POST_AUDITED_BALLOT="election@post-audited-ballot"
+
+ELECTION_VOTERS_HOME="election@voters"
+ELECTION_VOTERS_UPLOAD="election@voters@upload"
+ELECTION_VOTERS_UPLOAD_CANCEL="election@voters@upload-cancel"
+ELECTION_VOTERS_LIST="election@voters@list"
+ELECTION_VOTERS_LIST_PRETTY="election@voters@list-pretty"
+ELECTION_VOTERS_ELIGIBILITY="election@voters@eligibility"
+ELECTION_VOTERS_EMAIL="election@voters@email"
+ELECTION_VOTER="election@voter"
+ELECTION_VOTER_DELETE="election@voter@delete"
+ELECTION_VOTERS_CLEAR="election@voters@clear"
+
+ELECTION_BALLOTS_LIST="election@ballots@list"
+ELECTION_BALLOTS_VOTER="election@ballots@voter"
+ELECTION_BALLOTS_VOTER_LAST="election@ballots@voter@last"
+
+ELECTION_LOG_DOWNLOAD_CSV="election@log@download-csv"
diff --git a/helios/election_urls.py b/helios/election_urls.py
index 6622c5568..db01c871e 100644
--- a/helios/election_urls.py
+++ b/helios/election_urls.py
@@ -4,91 +4,108 @@
Ben Adida (ben@adida.net)
"""
-from django.conf.urls import *
+from django.urls import path, re_path
-from helios.views import *
+from helios import views
+from helios import election_url_names as names
-urlpatterns = patterns('',
+urlpatterns = [
# election data that is cryptographically verified
- (r'^$', one_election),
+ path('', views.one_election, name=names.ELECTION_HOME),
# metadata that need not be verified
- (r'^/meta$', one_election_meta),
+ path('/meta', views.one_election_meta, name=names.ELECTION_META),
# edit election params
- (r'^/edit$', one_election_edit),
- (r'^/schedule$', one_election_schedule),
- (r'^/extend$', one_election_extend),
- (r'^/archive$', one_election_archive),
- (r'^/copy$', one_election_copy),
+ path('/edit', views.one_election_edit, name=names.ELECTION_EDIT),
+ path('/schedule', views.one_election_schedule, name=names.ELECTION_SCHEDULE),
+ path('/extend', views.one_election_extend, name=names.ELECTION_EXTEND),
+ path('/archive', views.one_election_archive, name=names.ELECTION_ARCHIVE),
+ path('/delete', views.one_election_delete, name=names.ELECTION_DELETE),
+ path('/copy', views.one_election_copy, name=names.ELECTION_COPY),
# badge
- (r'^/badge$', election_badge),
+ path('/badge', views.election_badge, name=names.ELECTION_BADGE),
# adding trustees
- (r'^/trustees/$', list_trustees),
- (r'^/trustees/view$', list_trustees_view),
- (r'^/trustees/new$', new_trustee),
- (r'^/trustees/add-helios$', new_trustee_helios),
- (r'^/trustees/delete$', delete_trustee),
+ path('/trustees/', views.list_trustees, name=names.ELECTION_TRUSTEES_HOME),
+ path('/trustees/view', views.list_trustees_view, name=names.ELECTION_TRUSTEES_VIEW),
+ path('/trustees/new', views.new_trustee, name=names.ELECTION_TRUSTEES_NEW),
+ path('/trustees/add-helios', views.new_trustee_helios, name=names.ELECTION_TRUSTEES_ADD_HELIOS),
+ path('/trustees/delete', views.delete_trustee, name=names.ELECTION_TRUSTEES_DELETE),
+
+ # managing administrators
+ path('/admins/', views.election_admin_list, name=names.ELECTION_ADMINS_LIST),
+ path('/admins/add', views.election_admin_add, name=names.ELECTION_ADMINS_ADD),
+ path('/admins/remove', views.election_admin_remove, name=names.ELECTION_ADMINS_REMOVE),
# trustee pages
- (r'^/trustees/(?P[^/]+)/home$', trustee_home),
- (r'^/trustees/(?P[^/]+)/sendurl$', trustee_send_url),
- (r'^/trustees/(?P[^/]+)/keygenerator$', trustee_keygenerator),
- (r'^/trustees/(?P[^/]+)/check-sk$', trustee_check_sk),
- (r'^/trustees/(?P[^/]+)/upoad-pk$', trustee_upload_pk),
- (r'^/trustees/(?P[^/]+)/decrypt-and-prove$', trustee_decrypt_and_prove),
- (r'^/trustees/(?P[^/]+)/upload-decryption$', trustee_upload_decryption),
+ path('/trustees//home',
+ views.trustee_home, name=names.ELECTION_TRUSTEE_HOME),
+ path('/trustees//sendurl',
+ views.trustee_send_url, name=names.ELECTION_TRUSTEE_SEND_URL),
+ path('/trustees//keygenerator',
+ views.trustee_keygenerator, name=names.ELECTION_TRUSTEE_KEY_GENERATOR),
+ path('/trustees//check-sk',
+ views.trustee_check_sk, name=names.ELECTION_TRUSTEE_CHECK_SK),
+ path('/trustees//upoad-pk',
+ views.trustee_upload_pk, name=names.ELECTION_TRUSTEE_UPLOAD_PK),
+ path('/trustees//decrypt-and-prove',
+ views.trustee_decrypt_and_prove, name=names.ELECTION_TRUSTEE_DECRYPT_AND_PROVE),
+ path('/trustees//upload-decryption',
+ views.trustee_upload_decryption, name=names.ELECTION_TRUSTEE_UPLOAD_DECRYPTION),
# election voting-process actions
- (r'^/view$', one_election_view),
- (r'^/result$', one_election_result),
- (r'^/result_proof$', one_election_result_proof),
- # (r'^/bboard$', one_election_bboard),
- (r'^/audited-ballots/$', one_election_audited_ballots),
+ path('/view', views.one_election_view, name=names.ELECTION_VIEW),
+ path('/result', views.one_election_result, name=names.ELECTION_RESULT),
+ path('/result_proof', views.one_election_result_proof, name=names.ELECTION_RESULT_PROOF),
+ # url(r'^/bboard$', views.one_election_bboard, name=names.ELECTION_BBOARD),
+ path('/audited-ballots/', views.one_election_audited_ballots, name=names.ELECTION_AUDITED_BALLOTS),
# get randomness
- (r'^/get-randomness$', get_randomness),
-
- # server-side encryption
- (r'^/encrypt-ballot$', encrypt_ballot),
+ path('/get-randomness', views.get_randomness, name=names.ELECTION_GET_RANDOMNESS),
# construct election
- (r'^/questions$', one_election_questions),
- (r'^/set_reg$', one_election_set_reg),
- (r'^/set_featured$', one_election_set_featured),
- (r'^/save_questions$', one_election_save_questions),
- (r'^/register$', one_election_register),
- (r'^/freeze$', one_election_freeze), # includes freeze_2 as POST target
+ path('/questions', views.one_election_questions, name=names.ELECTION_QUESTIONS),
+ path('/set_reg', views.one_election_set_reg, name=names.ELECTION_SET_REG),
+ path('/set_featured', views.one_election_set_featured, name=names.ELECTION_SET_FEATURED),
+ path('/save_questions', views.one_election_save_questions, name=names.ELECTION_SAVE_QUESTIONS),
+ path('/register', views.one_election_register, name=names.ELECTION_REGISTER),
+ path('/freeze', views.one_election_freeze, name=names.ELECTION_FREEZE), # includes freeze_2 as POST target
# computing tally
- (r'^/compute_tally$', one_election_compute_tally),
- (r'^/combine_decryptions$', combine_decryptions),
- (r'^/release_result$', release_result),
+ path('/compute_tally', views.one_election_compute_tally, name=names.ELECTION_COMPUTE_TALLY),
+ path('/combine_decryptions', views.combine_decryptions, name=names.ELECTION_COMBINE_DECRYPTIONS),
+ path('/release_result', views.release_result, name=names.ELECTION_RELEASE_RESULT),
# casting a ballot before we know who the voter is
- (r'^/cast$', one_election_cast),
- (r'^/cast_confirm$', one_election_cast_confirm),
- (r'^/password_voter_login$', password_voter_login),
- (r'^/cast_done$', one_election_cast_done),
+ path('/cast', views.one_election_cast, name=names.ELECTION_CAST),
+ path('/cast_confirm', views.one_election_cast_confirm, name=names.ELECTION_CAST_CONFIRM),
+ path('/password_voter_login', views.password_voter_login, name=names.ELECTION_PASSWORD_VOTER_LOGIN),
+ path('/password_voter_resend', views.password_voter_resend, name=names.ELECTION_PASSWORD_VOTER_RESEND),
+ path('/cast_done', views.one_election_cast_done, name=names.ELECTION_CAST_DONE),
# post audited ballot
- (r'^/post-audited-ballot', post_audited_ballot),
+ re_path(r'^/post-audited-ballot', views.post_audited_ballot, name=names.ELECTION_POST_AUDITED_BALLOT),
# managing voters
- (r'^/voters/$', voter_list),
- (r'^/voters/upload$', voters_upload),
- (r'^/voters/upload-cancel$', voters_upload_cancel),
- (r'^/voters/list$', voters_list_pretty),
- (r'^/voters/eligibility$', voters_eligibility),
- (r'^/voters/email$', voters_email),
- (r'^/voters/(?P[^/]+)$', one_voter),
- (r'^/voters/(?P[^/]+)/delete$', voter_delete),
+ path('/voters/', views.voter_list, name=names.ELECTION_VOTERS_LIST),
+ path('/voters/upload', views.voters_upload, name=names.ELECTION_VOTERS_UPLOAD),
+ path('/voters/upload-cancel', views.voters_upload_cancel, name=names.ELECTION_VOTERS_UPLOAD_CANCEL),
+ path('/voters/list', views.voters_list_pretty, name=names.ELECTION_VOTERS_LIST_PRETTY),
+ path('/voters/download-csv', views.voters_download_csv, name='election@voters@download-csv'),
+ path('/voters/eligibility', views.voters_eligibility, name=names.ELECTION_VOTERS_ELIGIBILITY),
+ path('/voters/email', views.voters_email, name=names.ELECTION_VOTERS_EMAIL),
+ path('/voters/clear', views.voters_clear, name=names.ELECTION_VOTERS_CLEAR),
+ path('/voters/', views.one_voter, name=names.ELECTION_VOTER),
+ path('/voters//delete', views.voter_delete, name=names.ELECTION_VOTER_DELETE),
# ballots
- (r'^/ballots/$', ballot_list),
- (r'^/ballots/(?P[^/]+)/all$', voter_votes),
- (r'^/ballots/(?P[^/]+)/last$', voter_last_vote),
+ path('/ballots/', views.ballot_list, name=names.ELECTION_BALLOTS_LIST),
+ path('/ballots//all', views.voter_votes, name=names.ELECTION_BALLOTS_VOTER),
+ path('/ballots//last', views.voter_last_vote, name=names.ELECTION_BALLOTS_VOTER_LAST),
+
+ # election log
+ path('/log/download-csv', views.election_log_download_csv, name=names.ELECTION_LOG_DOWNLOAD_CSV),
-)
+]
diff --git a/helios/fields.py b/helios/fields.py
index cf2ad6c3e..da57fb58f 100644
--- a/helios/fields.py
+++ b/helios/fields.py
@@ -1,9 +1,9 @@
-from time import strptime, strftime
import datetime
-from django import forms
-from django.db import models
+
from django.forms import fields
-from widgets import SplitSelectDateTimeWidget
+
+from .widgets import SplitSelectDateTimeWidget, DateTimeLocalWidget
+
class SplitDateTimeField(fields.MultiValueField):
widget = SplitSelectDateTimeWidget
@@ -28,3 +28,17 @@ def compress(self, data_list):
return datetime.datetime.combine(*data_list)
return None
+
+class DateTimeLocalField(fields.DateTimeField):
+ """
+ A field for HTML5 datetime-local input widget.
+ Handles datetime input in the format: YYYY-MM-DDTHH:MM
+ """
+ widget = DateTimeLocalWidget
+ input_formats = ['%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S']
+
+ def __init__(self, *args, **kwargs):
+ if 'input_formats' not in kwargs or kwargs.get('input_formats') is None:
+ kwargs['input_formats'] = self.input_formats
+ super(DateTimeLocalField, self).__init__(*args, **kwargs)
+
diff --git a/helios/fixtures/election.json b/helios/fixtures/election.json
index cd7a61655..1d54b4e44 100644
--- a/helios/fixtures/election.json
+++ b/helios/fixtures/election.json
@@ -1,25 +1,30 @@
-[{"pk": 1000,
- "model": "helios.election",
- "fields":
- {
- "admin": 1,
- "uuid" : "206ef039-05c9-4e9c-bb8f-963da50c08d4",
- "short_name" : "test",
- "name" : "Test Election",
- "election_type" : "election",
- "use_advanced_audit_features" : true,
- "created_at" : "2013-02-22 12:00:00",
- "modified_at" : "2013-02-22 12:00:00",
- "private_p" : false,
- "description" : "test description",
- "public_key" : null,
- "private_key" : null,
- "questions" : [],
- "eligibility": null,
- "openreg": true,
- "featured_p": false,
- "use_voter_aliases" : false,
- "cast_url" : "/helios/elections/206ef039-05c9-4e9c-bb8f-963da50c08d4/cast"
- }
+[
+ {
+ "pk": 1,
+ "model": "helios.election",
+ "fields": {
+ "admin": 1,
+ "uuid": "43b498b7-2b84-4e4e-ba82-3c2ba1a63519",
+ "datatype": "legacy/Election",
+ "short_name": "test",
+ "name": "Test Election",
+ "election_type": "election",
+ "private_p": false,
+ "description": "Test Election for Unit Tests",
+ "public_key": null,
+ "private_key": null,
+ "questions": [{"answer_urls": [null, null, null], "answers": ["a", "b", "c"], "choice_type": "approval", "max": 1, "min": 0, "question": "Test Question?", "result_type": "absolute", "short_name": "q1", "tally_type": "homomorphic"}],
+ "eligibility": null,
+ "openreg": false,
+ "featured_p": false,
+ "use_voter_aliases": false,
+ "use_advanced_audit_features": true,
+ "randomize_answer_order": false,
+ "cast_url": "",
+ "created_at": "2023-01-01T00:00:00Z",
+ "modified_at": "2023-01-01T00:00:00Z",
+ "frozen_at": null,
+ "archived_at": null
+ }
}
- ]
+]
\ No newline at end of file
diff --git a/helios/fixtures/users.json b/helios/fixtures/users.json
index c588ce475..c6e18b53d 100644
--- a/helios/fixtures/users.json
+++ b/helios/fixtures/users.json
@@ -1 +1,40 @@
-[{"pk": 1, "model": "helios_auth.user", "fields": {"info": "{}", "user_id": "ben@adida.net", "name": "Ben Adida", "user_type": "google", "token": null, "admin_p": false}},{"pk": 2, "model": "helios_auth.user", "fields": {"info": "{}", "user_id": "12345", "name": "Ben Adida", "user_type": "facebook", "token": {"access_token":"1234"}, "admin_p": false}}]
\ No newline at end of file
+[
+ {
+ "pk": 1,
+ "model": "helios_auth.user",
+ "fields": {
+ "info": "{}",
+ "user_id": "ben@adida.net",
+ "name": "Ben Adida",
+ "user_type": "google",
+ "token": null,
+ "admin_p": false
+ }
+ },
+ {
+ "pk": 2,
+ "model": "helios_auth.user",
+ "fields": {
+ "info": "{}",
+ "user_id": "12345",
+ "name": "Ben Adida",
+ "user_type": "facebook",
+ "token": {
+ "access_token": "1234"
+ },
+ "admin_p": false
+ }
+ },
+ {
+ "pk": 3,
+ "model": "helios_auth.user",
+ "fields": {
+ "info": "{}",
+ "user_id": "mccio@github.com",
+ "name": "Marco Ciotola",
+ "user_type": "google",
+ "token": null,
+ "admin_p": true
+ }
+ }
+]
\ No newline at end of file
diff --git a/helios/fixtures/voter-badfile.csv b/helios/fixtures/voter-badfile.csv
index fd674a999..cc858ace2 100644
--- a/helios/fixtures/voter-badfile.csv
+++ b/helios/fixtures/voter-badfile.csv
@@ -1,5 +1,5 @@
-Ben78@adida.net,Ben78 Adida
- benadida5,ben5@adida.net , Ben5 Adida
-benadida6,ben6@adida.net,Ben6 Adida
-benadida7,ben7@adida.net,Ben7 Adida
-ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios
\ No newline at end of file
+password,Ben78@adida.net,Ben78 Adida
+password, benadida5,ben5@adida.net , Ben5 Adida
+password,benadida6,ben6@adida.net,Ben6 Adida
+password,benadida7,ben7@adida.net,Ben7 Adida
+password,ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios
\ No newline at end of file
diff --git a/helios/fixtures/voter-file-2.csv b/helios/fixtures/voter-file-2.csv
new file mode 100644
index 000000000..a8cf727f7
--- /dev/null
+++ b/helios/fixtures/voter-file-2.csv
@@ -0,0 +1,3 @@
+password,voter8,voter8@example.com,Voter Eight
+password,voter9,voter9@example.com,Voter Nine
+password,voter10,voter10@example.com,Voter Ten
diff --git a/helios/fixtures/voter-file-latin1.csv b/helios/fixtures/voter-file-latin1.csv
new file mode 100644
index 000000000..4c2e42a48
--- /dev/null
+++ b/helios/fixtures/voter-file-latin1.csv
@@ -0,0 +1,4 @@
+password, benadida5,ben5@adida.net , Ben5 Adida
+password,benadida6,ben6@adida.net,Ben6 Adida
+password,benadida7,ben7@adida.net,Ben7 Adida
+password,testlatin1,Test Latin1,J�NIO LUIZ CORREIA J�NIOR
diff --git a/helios/fixtures/voter-file.csv b/helios/fixtures/voter-file.csv
index b94bd1a4f..70795def3 100644
--- a/helios/fixtures/voter-file.csv
+++ b/helios/fixtures/voter-file.csv
@@ -1,4 +1,4 @@
- benadida5,ben5@adida.net , Ben5 Adida
-benadida6,ben6@adida.net,Ben6 Adida
-benadida7,ben7@adida.net,Ben7 Adida
-ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios
\ No newline at end of file
+password, benadida5,ben5@adida.net , Ben5 Adida
+password,benadida6,ben6@adida.net,Ben6 Adida
+password,benadida7,ben7@adida.net,Ben7 Adida
+password,ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios
\ No newline at end of file
diff --git a/helios/forms.py b/helios/forms.py
index cb10cfab8..7a6f6a42c 100644
--- a/helios/forms.py
+++ b/helios/forms.py
@@ -3,11 +3,11 @@
"""
from django import forms
-from models import Election
-from widgets import *
-from fields import *
from django.conf import settings
+from .fields import DateTimeLocalField
+from .models import Election
+
class ElectionForm(forms.Form):
short_name = forms.SlugField(max_length=40, help_text='no spaces, will be part of the URL for your election, e.g. my-club-2010')
@@ -24,14 +24,14 @@ class ElectionForm(forms.Form):
election_info_url = forms.CharField(required=False, initial="", label="Election Info Download URL", help_text="the URL of a PDF document that contains extra election information, e.g. candidate bios and statements")
# times
- voting_starts_at = SplitDateTimeField(help_text = 'UTC date and time when voting begins',
- widget=SplitSelectDateTimeWidget, required=False)
- voting_ends_at = SplitDateTimeField(help_text = 'UTC date and time when voting ends',
- widget=SplitSelectDateTimeWidget, required=False)
+ voting_starts_at = DateTimeLocalField(help_text = 'UTC date and time when voting begins',
+ required=False)
+ voting_ends_at = DateTimeLocalField(help_text = 'UTC date and time when voting ends',
+ required=False)
class ElectionTimeExtensionForm(forms.Form):
- voting_extended_until = SplitDateTimeField(help_text = 'UTC date and time voting extended to',
- widget=SplitSelectDateTimeWidget, required=False)
+ voting_extended_until = DateTimeLocalField(help_text = 'UTC date and time voting extended to',
+ required=False)
class EmailVotersForm(forms.Form):
subject = forms.CharField(max_length=80)
@@ -47,3 +47,6 @@ class VoterPasswordForm(forms.Form):
voter_id = forms.CharField(max_length=50, label="Voter ID")
password = forms.CharField(widget=forms.PasswordInput(), max_length=100)
+class VoterPasswordResendForm(forms.Form):
+ voter_id = forms.CharField(max_length=50, label="Voter ID", help_text="Enter the voter ID you were assigned for this election")
+
diff --git a/helios/management/commands/helios_trustee_decrypt.py b/helios/management/commands/helios_trustee_decrypt.py
index 3dc75a4af..478833020 100644
--- a/helios/management/commands/helios_trustee_decrypt.py
+++ b/helios/management/commands/helios_trustee_decrypt.py
@@ -8,12 +8,10 @@
2010-05-22
"""
-from django.core.management.base import BaseCommand, CommandError
-import csv, datetime
+from django.core.management.base import BaseCommand
-from helios import utils as helios_utils
+from helios.models import Trustee
-from helios.models import *
class Command(BaseCommand):
args = ''
diff --git a/helios/management/commands/load_voter_files.py b/helios/management/commands/load_voter_files.py
index 5b82285a6..549434702 100644
--- a/helios/management/commands/load_voter_files.py
+++ b/helios/management/commands/load_voter_files.py
@@ -8,12 +8,15 @@
2010-05-22
"""
-from django.core.management.base import BaseCommand, CommandError
-import csv, datetime
+import datetime
+
+import csv
+import uuid
+from django.core.management.base import BaseCommand
from helios import utils as helios_utils
+from helios.models import User, Voter, VoterFile
-from helios.models import *
##
## UTF8 craziness for CSV
@@ -25,44 +28,47 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
dialect=dialect, **kwargs)
for row in csv_reader:
# decode UTF-8 back to Unicode, cell by cell:
- yield [unicode(cell, 'utf-8') for cell in row]
+ yield [str(cell, 'utf-8') for cell in row]
+
def utf_8_encoder(unicode_csv_data):
for line in unicode_csv_data:
yield line.encode('utf-8')
-
+
+
def process_csv_file(election, f):
reader = unicode_csv_reader(f)
-
+
num_voters = 0
for voter in reader:
- # bad line
- if len(voter) < 1:
- continue
-
- num_voters += 1
- voter_id = voter[0]
- name = voter_id
- email = voter_id
-
- if len(voter) > 1:
- email = voter[1]
-
- if len(voter) > 2:
- name = voter[2]
-
- # create the user
- user = User.update_or_create(user_type='password', user_id=voter_id, info = {'password': helios_utils.random_string(10), 'email': email, 'name': name})
- user.save()
-
- # does voter for this user already exist
- voter = Voter.get_by_election_and_user(election, user)
-
- # create the voter
- if not voter:
- voter_uuid = str(uuid.uuid1())
- voter = Voter(uuid= voter_uuid, voter_type = 'password', voter_id = voter_id, name = name, election = election)
- voter.save()
+ # bad line
+ if len(voter) < 1:
+ continue
+
+ num_voters += 1
+ voter_id = voter[0]
+ name = voter_id
+ email = voter_id
+
+ if len(voter) > 1:
+ email = voter[1]
+
+ if len(voter) > 2:
+ name = voter[2]
+
+ # create the user
+ user = User.update_or_create(user_type='password', user_id=voter_id,
+ info={'password': helios_utils.random_string(10), 'email': email, 'name': name})
+ user.save()
+
+ # does voter for this user already exist
+ voter = Voter.get_by_election_and_user(election, user)
+
+ # create the voter
+ if not voter:
+ voter_uuid = str(uuid.uuid4())
+ voter = Voter(uuid=voter_uuid, voter_type='password', voter_id=voter_id, name=name, election=election)
+ voter.save()
return num_voters
@@ -70,7 +76,7 @@ def process_csv_file(election, f):
class Command(BaseCommand):
args = ''
help = 'load up voters from unprocessed voter files'
-
+
def handle(self, *args, **options):
# load up the voter files in order of last uploaded
files_to_process = VoterFile.objects.filter(processing_started_at=None).order_by('uploaded_at')
@@ -86,5 +92,3 @@ def handle(self, *args, **options):
file_to_process.processing_finished_at = datetime.datetime.utcnow()
file_to_process.num_voters = num_voters
file_to_process.save()
-
-
diff --git a/helios/management/commands/verify_cast_votes.py b/helios/management/commands/verify_cast_votes.py
index 5b7f39253..e2fab7186 100644
--- a/helios/management/commands/verify_cast_votes.py
+++ b/helios/management/commands/verify_cast_votes.py
@@ -6,12 +6,10 @@
2010-05-22
"""
-from django.core.management.base import BaseCommand, CommandError
-import csv, datetime
+from django.core.management.base import BaseCommand
-from helios import utils as helios_utils
+from helios.models import CastVote
-from helios.models import *
def get_cast_vote_to_verify():
# fixme: add "select for update" functionality here
diff --git a/helios/media/datetime-local.css b/helios/media/datetime-local.css
new file mode 100644
index 000000000..893001df7
--- /dev/null
+++ b/helios/media/datetime-local.css
@@ -0,0 +1,21 @@
+/* Helios DateTime Picker Styles */
+
+.helios-datetime-input {
+ padding: 8px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ width: 250px;
+ box-sizing: border-box;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.helios-datetime-input:focus {
+ border-color: #4CAF50;
+ outline: none;
+ box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);
+}
+
+.helios-datetime-input:hover {
+ border-color: #999;
+}
diff --git a/helios/media/static_templates/question.html b/helios/media/static_templates/question.html
index 5e576916d..c3c1fd200 100644
--- a/helios/media/static_templates/question.html
+++ b/helios/media/static_templates/question.html
@@ -1,132 +1,480 @@
-
{#foreach $T.questions as question}
-
-
-{#if $T.admin_p}[
-{#if $T.question$index > 0}^ ] [{#/if}
-x ] [edit ] {#/if}{$T.question$index + 1}. {$T.question.question} ({$T.question.choice_type}, select between {$T.question.min} and {#if $T.question.max != null}{$T.question.max}{#else}unlimited{#/if} answers, result type {$T.question.result_type}.)
-
-{#foreach $T.question.answers as answer}
- {$T.answer}
-{#if $T.question.answer_urls[$T.answer$index]}
- [more ]
-{#/if}
-
-{#/for}
-
+
+
+
+
+
+ Selection: {$T.question.min} to {#if $T.question.max != null}{$T.question.max}{#else}unlimited{#/if}
+
+
+ Result type: {$T.question.result_type}
+
+ {#if $T.question.randomize_answer_order}
+
+ Order: randomized
+
+ {#/if}
+
+
+
+ {#foreach $T.question.answers as answer}
+
+ {$T.answer}
+ {#if $T.question.answer_urls[$T.answer$index]}
+
+ 🔗
+
+ {#/if}
+
+ {#/for}
+
-
-
+
{#else}
-no questions yet
+
+
No questions have been added yet.
+
{#/for}
{#if $T.admin_p}
-
Add a Question:
-
- cancel
+ cancel
If you cancel now, your ballot will NOT be recorded.
You can start the voting process over again, of course.
diff --git a/helios/templates/_castconfirm_password.html b/helios/templates/_castconfirm_password.html
index 25e31de21..d0d999107 100644
--- a/helios/templates/_castconfirm_password.html
+++ b/helios/templates/_castconfirm_password.html
@@ -1,23 +1,29 @@
-Please provide the voter ID and password you received by email.
-
+
-
- {{password_login_form.as_table}}
-
+
+
+{{password_login_form.as_p}}
{% if bad_voter_login %}
bad voter ID or password, please try again.
{% endif %}
{% if cast_ballot == "1" %}
+
+
+ Your voter ID and password can be found in the email you received.
+ Forgot your password? Click here to resend it.
+ {% if election.help_email %}If you still cannot find your login information, contact your election administrator at {{election.help_email}} .{% endif %}
+
-You may cast as many ballots as you wish: only the last one counts.
+Cast as many ballots as you wish: only the last one counts.
-
{% else %}
{% endif %}
+
+
diff --git a/helios/templates/cast_done.html b/helios/templates/cast_done.html
index 75265abb0..a007ee37c 100644
--- a/helios/templates/cast_done.html
+++ b/helios/templates/cast_done.html
@@ -4,23 +4,24 @@
{% block content %}
{{election.name}} — Vote Successfully Cast!
+{% if vote_hash %}
- Congratulations, your vote has been successfully cast !
+ Your ballot tracker is:
+ {{vote_hash}}
-
- Your smart ballot tracker is:
- {{vote_hash}}
+ This ballot tracker mathematically ensures that your ballot is counted. You can check all ballot trackers by viewing the voter list , where your ballot tracker should appear within a few minutes.
+{% endif %}
{% if logout %}
For your safety, we have logged you out.
-
diff --git a/helios/templates/election_extend.html b/helios/templates/election_extend.html
index 0db2c448b..d3a623800 100644
--- a/helios/templates/election_extend.html
+++ b/helios/templates/election_extend.html
@@ -2,7 +2,7 @@
{% block content %}
-
{{election.name}} — Extend Voting [cancel ]
+
{{election.name}} — Extend Voting [cancel ]
@@ -10,7 +10,7 @@ {{election.name}} — Extend Voting
+
diff --git a/helios/templates/election_freeze.html b/helios/templates/election_freeze.html
index a0aad4dd0..7e3c34baf 100644
--- a/helios/templates/election_freeze.html
+++ b/helios/templates/election_freeze.html
@@ -3,8 +3,7 @@
{% block content %}
{{election.name}} — Freeze Ballot
-Once the ballot is frozen, the questions and options can no longer be modified.
-The list of trustees and their public keys will also be frozen.
+Once an election is frozen, the questions, options, and trustees can no longer be modified.
@@ -29,14 +28,14 @@
{{election.name}} — Freeze Ballot
{{issue.action}}
{% endfor %}
-
go back to the election
+
go back to the election
{% else %}
-never mind
+do NOT freeze yet
{% endif %}
diff --git a/helios/templates/election_keygenerator.html b/helios/templates/election_keygenerator.html
index 60a2c55c7..47afbfb05 100644
--- a/helios/templates/election_keygenerator.html
+++ b/helios/templates/election_keygenerator.html
@@ -35,7 +35,7 @@
$('#generator').hide();
// get some more server-side randomness for keygen
- $.getJSON('../../get-randomness', function(result) {
+ $.getJSON('{% url "election@get-randomness" election.uuid %}', function(result) {
sjcl.random.addEntropy(result.randomness);
BigInt.setup(function() {
ELGAMAL_PARAMS = ElGamal.Params.fromJSONObject({{eg_params_json|safe}});
@@ -76,11 +76,23 @@
}
function download_sk() {
- UTILS.open_window_with_content(jQuery.toJSON(SECRET_KEY), "application/json");
+ $('#pk_content').show();
+ $('#sk_content').html(jQuery.toJSON(SECRET_KEY));
+}
+
+function download_sk_to_file(filename) {
+ var element = document.createElement('a');
+ element.setAttribute('href','data:text/plain;charset=utf-8,'+ encodeURIComponent(jQuery.toJSON(SECRET_KEY)));
+ element.setAttribute('download', filename);
+ element.style.display = 'none';
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
}
function show_pk() {
$('#sk_download').hide();
+ $('#pk_content').hide();
$('#pk_hash').show();
$('#pk_form').show();
}
@@ -99,9 +111,9 @@
{{election.name}} — Trustee {{trustee.name}} — Key
-Generate Election Keys
+Generate Election Keys
-
+
If you've already generated a keypair, you can reuse it .
@@ -115,7 +127,7 @@
Reusing a Key
-
+
@@ -126,15 +138,28 @@ Your Secret Key
- Save your secret key
+ Show my secret key
+
-
- ok, I've saved the key, let's move on .
+
+
Bellow is your trustee secret key content. Please copy its content and save it securely.
+ You can also click to dowload it to a file.
+ And please don't lose it! Otherwise it will not be possible to decrypt the election tally.
+
+
+
+
+
-
+
Your Public Key
It's time to upload the public key to the server.
@@ -146,7 +171,7 @@
Your Public Key
-
+
diff --git a/helios/templates/election_new.html b/helios/templates/election_new.html
index 1e5e2ec0f..b1e382dd4 100644
--- a/helios/templates/election_new.html
+++ b/helios/templates/election_new.html
@@ -16,7 +16,7 @@ Create a New Election
{{election_form.as_table}}
-
+
diff --git a/helios/templates/election_new_2.html b/helios/templates/election_new_2.html
index 16061c2d0..4fa0d23d5 100644
--- a/helios/templates/election_new_2.html
+++ b/helios/templates/election_new_2.html
@@ -6,11 +6,11 @@
var SECRET_KEY;
function before_create() {
-{% ifequal election_type "one" %}
+{% if election_type == "one" %}
return confirm('Have you made sure to copy the private key to a safe place?\n\nOnce you click OK, Helios will not be able to recover the secret key without your help!');
{% else %}
return true;
-{% endifequal %}
+{% endif %}
}
function generate_keypair() {
@@ -26,44 +26,44 @@
$('#pk').val(jQuery.toJSON(SECRET_KEY.pk));
-{% ifequal election_type "one" %}
+{% if election_type == "one" %}
$('#sk_textarea').val(jQuery.toJSON(SECRET_KEY));
$('#sk_form').show();
{% else %}
-{% ifequal election_type "helios" %}
+{% if election_type == "helios" %}
$('#sk').val(jQuery.toJSON(SECRET_KEY));
-{% endifequal %}
-{% endifequal %}
+{% endif %}
+{% endif %}
$('#submit').show();
}
$(document).ready(function() {
-{% ifnotequal election_type "multiple" %}
+{% if election_type != "multiple" %}
$('#submit').hide();
-{% endifnotequal %}
+{% endif %}
$('#sk_form').hide();
});
Create a New Election: {{name}}
-{% ifequal election_type "helios" %}
+{% if election_type == "helios" %}
An election managed by Helios .
{% else %}
-{% ifequal election_type "one" %}
+{% if election_type == "one" %}
An election managed by you, the single administrator .
{% else %}
An election managed by multiple trustees .
-{% endifequal %}
-{% endifequal %}
+{% endif %}
+{% endif %}
-{% ifnotequal election_type "multiple" %}
- Generate Election Keys
+{% if election_type != "multiple" %}
+ Generate Election Keys
{% else %}
Trustees (up to 5)
@@ -73,12 +73,12 @@ Trustees (up to 5)
-{% endifnotequal %}
+{% endif %}
-
+
-{% ifequal election_type "one" %}
+{% if election_type == "one" %}
Your Election's Secret Key:
@@ -88,5 +88,5 @@ Trustees (up to 5)
(You need to copy and paste this key and keep it safe,
otherwise you won't be able to tally your election.)
-{% endifequal %}
+{% endif %}
{% endblock %}
\ No newline at end of file
diff --git a/helios/templates/election_not_started.html b/helios/templates/election_not_started.html
index 4ba944e1a..7e123be88 100644
--- a/helios/templates/election_not_started.html
+++ b/helios/templates/election_not_started.html
@@ -1,14 +1,20 @@
{% extends TEMPLATE_BASE %}
+{% load timezone_tags %}
{% block content %}
Election {{election.name}} Not Yet Open
- This election is not yet open. You probably got here from the Ballot Preview.
+ This election is not yet open.
- back to the election
+ {% if election.voting_start_at %}Voting start at {{election.voting_start_at|utc_time}} {% endif %}
+ {% if election.voting_end_at %}Voting end at {{election.voting_end_at|utc_time}} {% endif %}
+
+
+
+ back to the election
{% endblock %}
diff --git a/helios/templates/election_questions.html b/helios/templates/election_questions.html
index d7b8f7353..b9fbfdbcb 100644
--- a/helios/templates/election_questions.html
+++ b/helios/templates/election_questions.html
@@ -2,7 +2,7 @@
{% block title %}Questions for {{election.name}}{% endblock %}
{% block content %}
-
+
-
-
-
diff --git a/helios/templates/stats.html b/helios/templates/stats.html
index 37468b715..b1dbba1ce 100644
--- a/helios/templates/stats.html
+++ b/helios/templates/stats.html
@@ -5,11 +5,13 @@
Admin
-{{num_votes_in_queue}} votes in queue. {% if num_votes_in_queue %}[force it ]{% endif %}
+{{num_votes_in_queue}} votes in queue. {% if num_votes_in_queue %}[force it ]{% endif %}
{% endblock %}
diff --git a/helios/templates/stats_deleted_elections.html b/helios/templates/stats_deleted_elections.html
new file mode 100644
index 000000000..728183408
--- /dev/null
+++ b/helios/templates/stats_deleted_elections.html
@@ -0,0 +1,44 @@
+{% extends TEMPLATE_BASE %}
+{% load timezone_tags %}
+{% block title %}Deleted Elections{% endblock %}
+
+{% block content %}
+Deleted Elections
+
+
+
+search :
+ clear search
+
+
+
+
+
+{% if elections_page.has_previous %}
+previous {{limit}}
+{% endif %}
+
+Elections {{elections_page.start_index}} - {{elections_page.end_index}} (of {{total_elections}})
+
+{% if elections_page.has_next %}
+next {{limit}}
+{% endif %}
+
+
+{% if elections %}
+{% for election in elections %}
+
+{{election.name}} by {{election.admin.pretty_name}}
+Deleted at: {{election.deleted_at|utc_time}}
+{{election.num_voters}} voters / {{election.num_cast_votes}} cast votes
+
+
+undelete
+
+
+{% endfor %}
+{% else %}
+No deleted elections found.
+{% endif %}
+
+{% endblock %}
diff --git a/helios/templates/stats_elections.html b/helios/templates/stats_elections.html
index 717969833..e9a498ef8 100644
--- a/helios/templates/stats_elections.html
+++ b/helios/templates/stats_elections.html
@@ -5,7 +5,7 @@
Elections
-
+
search :
clear search
@@ -26,7 +26,7 @@ Elections
{% for election in elections %}
-{{election.name}} by {{election.admin.pretty_name}} -- {{election.num_voters}} voters / {{election.num_cast_votes}} cast votes
+{{election.name}} by {{election.admin.pretty_name}} -- {{election.num_voters}} voters / {{election.num_cast_votes}} cast votes
{% endfor %}
diff --git a/helios/templates/stats_problem_elections.html b/helios/templates/stats_problem_elections.html
index 9f8c1dadd..2f82f74b9 100644
--- a/helios/templates/stats_problem_elections.html
+++ b/helios/templates/stats_problem_elections.html
@@ -8,7 +8,7 @@ Problematic Elections
{% for election in elections %}
-{{election.name}} -- {{election.num_voters}} voters
+{{election.name}} -- {{election.num_voters}} voters
{% endfor %}
diff --git a/helios/templates/stats_recent_votes.html b/helios/templates/stats_recent_votes.html
index 37c074161..bf360a284 100644
--- a/helios/templates/stats_recent_votes.html
+++ b/helios/templates/stats_recent_votes.html
@@ -8,7 +8,7 @@ Recent Votes
{% for election in elections %}
-{{election.name}} -- {{election.last_cast_vote}} {{election.num_recent_cast_votes}} recently cast votes
+{{election.name}} -- {{election.last_cast_vote}} {{election.num_recent_cast_votes}} recently cast votes
{% endfor %}
diff --git a/helios/templates/stats_user_search.html b/helios/templates/stats_user_search.html
new file mode 100644
index 000000000..400bfc5e0
--- /dev/null
+++ b/helios/templates/stats_user_search.html
@@ -0,0 +1,83 @@
+{% extends TEMPLATE_BASE %}
+{% block title %}User Search{% endblock %}
+
+{% block content %}
+User Search
+
+
+
+Search for user :
+ clear search
+
+
+
+{% if q %}
+ {% if users_with_elections %}
+ Found {{users_with_elections|length}} user(s)
+
+ {% for item in users_with_elections %}
+
+
{{item.user.pretty_name}}
+
+ User ID: {{item.user.user_id}}
+ User Type: {{item.user.user_type}}
+ Site Admin: {% if item.user.admin_p %}Yes{% else %}No{% endif %}
+
+
+
Elections as Administrator ({{item.elections_as_admin|length}})
+ {% if item.elections_as_admin %}
+
+ {% for election in item.elections_as_admin %}
+
+ {{election.name}}
+ ({{election.num_voters}} voters / {{election.num_cast_votes}} cast votes)
+ {% if election.frozen_at %}[frozen]{% endif %}
+ {% if election.archived_at %}[archived]{% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
None
+ {% endif %}
+
+
Elections as Voter ({{item.elections_as_voter|length}})
+ {% if item.elections_as_voter %}
+
+ {% for election in item.elections_as_voter %}
+
+ {{election.name}}
+ ({{election.num_voters}} voters / {{election.num_cast_votes}} cast votes)
+ {% if election.frozen_at %}[frozen]{% endif %}
+ {% if election.archived_at %}[archived]{% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
None
+ {% endif %}
+
+
Elections as Trustee ({{item.elections_as_trustee|length}})
+ {% if item.elections_as_trustee %}
+
+ {% for election in item.elections_as_trustee %}
+
+ {{election.name}}
+ ({{election.num_voters}} voters / {{election.num_cast_votes}} cast votes)
+ {% if election.frozen_at %}[frozen]{% endif %}
+ {% if election.archived_at %}[archived]{% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
None
+ {% endif %}
+
+ {% endfor %}
+ {% else %}
+ No users found matching "{{q}}"
+ {% endif %}
+{% else %}
+ Enter a search term to find users
+{% endif %}
+
+{% endblock %}
diff --git a/helios/templates/trustee_check_sk.html b/helios/templates/trustee_check_sk.html
index f6b615e43..b5791eff8 100644
--- a/helios/templates/trustee_check_sk.html
+++ b/helios/templates/trustee_check_sk.html
@@ -26,13 +26,21 @@
try {
var secret_key = ElGamal.SecretKey.fromJSONObject(jQuery.secureEvalJSON(sk_value));
+ // Check 1: Verify the public key hash matches
var pk_hash = b64_sha256(jQuery.toJSON(secret_key.pk));
- var key_ok_p = (pk_hash == PK_HASH);
+ var hash_ok = (pk_hash == PK_HASH);
+
+ // Check 2: Verify that g^x = y mod p (cryptographic verification)
+ // This ensures the secret exponent actually corresponds to the public key
+ var computed_y = secret_key.pk.g.modPow(secret_key.x, secret_key.pk.p);
+ var crypto_ok = computed_y.equals(secret_key.pk.y);
+
+ var key_ok_p = hash_ok && crypto_ok;
} catch (e) {
debugger;
var key_ok_p = false;
}
-
+
$('#processing').hide();
var reset_link = "try again ";
@@ -43,7 +51,7 @@
}
}
- {{election.name}} — Trustee {{trustee.name}} — Check Secret Key [back to trustee home ]
+ {{election.name}} — Trustee {{trustee.name}} — Check Secret Key [back to trustee home ]
Your public key fingerprint is: {{trustee.public_key_hash}}
@@ -59,10 +67,10 @@
{{election.name}} — Trustee {{trustee.name}} — Che
-
+
-
+
diff --git a/helios/templates/trustee_decrypt_and_prove.html b/helios/templates/trustee_decrypt_and_prove.html
index d4cbe4384..6e1e63079 100644
--- a/helios/templates/trustee_decrypt_and_prove.html
+++ b/helios/templates/trustee_decrypt_and_prove.html
@@ -157,10 +157,10 @@ Trustee {{trustee.name}} — Decrypt Result for {{election
FIRST STEP: enter your secret key
-
+
- Generate partial decryption
+ Generate partial decryption
@@ -187,17 +187,15 @@
SECOND STEP: upload your partial decryption
When you're ready, you can submit this result to the server.
Your partial decryption:
-
-
-
-
-
- reset and restart decryption process
-
+
+
+ Upload decryption factors to server
+
+ reset and restart decryption process
diff --git a/helios/templates/trustee_home.html b/helios/templates/trustee_home.html
index 5f6ee9c54..8d6dd1151 100644
--- a/helios/templates/trustee_home.html
+++ b/helios/templates/trustee_home.html
@@ -7,9 +7,9 @@
{{election.name}} — Trustee {{trustee.name}} Home
{% if trustee.public_key_hash %}
You have successfully uploaded your public key.
Your public key fingerprint is: {{trustee.public_key_hash}} .
-You can verify that you have the right secret key .
+You can verify that you have the right secret key .
{% else %}
-setup your key
+setup your key
{% endif %}
@@ -19,7 +19,7 @@ {{election.name}} — Trustee {{trustee.name}} Home
You have successfully uploaded your decryption.
{% else %}
The encrypted tally for this election is ready.
- decrypt with your key
+ decrypt with your key
{% endif %}
{% else %}
Once the tally is computed, come back here to provide your secret key for decryption purposes.
diff --git a/helios/templates/voters_eligibility.html b/helios/templates/voters_eligibility.html
index 51343befd..8d4c7a159 100644
--- a/helios/templates/voters_eligibility.html
+++ b/helios/templates/voters_eligibility.html
@@ -2,7 +2,7 @@
{% block title %}Voter Eligibility for {{election.name}}{% endblock %}
{% block content %}
- {{election.name}} — Voter Eligibility [back to voters ]
+ {{election.name}} — Voter Eligibility [back to voters ]
{{election.pretty_eligibility|safe}}
@@ -20,7 +20,7 @@
{{election.name}} — Voter Eligibility {{category.name}}
{% endfor %}
-
+
{% endblock %}
diff --git a/helios/templates/voters_email.html b/helios/templates/voters_email.html
index 535f7533f..62a6c7d63 100644
--- a/helios/templates/voters_email.html
+++ b/helios/templates/voters_email.html
@@ -9,7 +9,7 @@
{% endif %}
- {{election.name}} — Contact Voters [back to election ]
+ {{election.name}} — Contact Voters [back to election ]
{% if voter %}
@@ -55,7 +55,7 @@
diff --git a/helios/templates/voters_list.html b/helios/templates/voters_list.html
index 140977086..ed4a2ba21 100644
--- a/helios/templates/voters_list.html
+++ b/helios/templates/voters_list.html
@@ -1,8 +1,9 @@
{% extends TEMPLATE_BASE %}
+{% load timezone_tags %}
{% block title %}Voters & Ballot Tracking Center for {{election.name}}{% endblock %}
{% block content %}
-
{{election.name}} — Voters and Ballot Tracking Center [back to election ]
+
{{election.name}} — Voters and Ballot Tracking Center [back to election ]
Who can vote?
@@ -17,8 +18,7 @@
{{election.name}} — Voters and Ballot Tracking Center Your election is marked private, which means you cannot open registration up more widely.
{% else %}
-You can change this setting:
-
+
anyone can vote
only voters listed explicitly below can vote
@@ -31,13 +31,19 @@ {{election.name}} — Voters and Ballot Tracking Center
{% endif %}
-
+
{% endif %}
{% endif %}
{% if email_voters and election.frozen_at and admin_p %}
- email voters
+
+{% if can_send_emails %}
+email voters
+{% else %}
+email voters (disabled: {{email_disabled_reason}})
+{% endif %}
+
{% endif %}
{% if election.num_voters > 20 %}
@@ -45,16 +51,24 @@
{{election.name}} — Voters and Ballot Tracking Center searching for {{q}} . [clear search ]
{% else %}
-search :
+search :
{% endif %}
{% endif %}
+
+Download Voters as CSV
+
+
{% if admin_p %}
{% if upload_p and not election.openreg %}
-bulk upload voters
+{% if can_modify_voters %}
+bulk upload voters
+{% else %}
+bulk upload voters (disabled: {{modify_voters_disabled_reason}})
+{% endif %}
{% if voter_files %}
@@ -67,7 +81,7 @@
{{election.name}} — Voters and Ballot Tracking Center done processing: {{vf.num_voters}} voters loaded
{% else %}
@@ -84,6 +98,16 @@ {{election.name}} — Voters and Ballot Tracking Center
{% endif %}
{% endif %}
+
+{% if admin_p and not election.frozen_at and election.num_voters > 0 %}
+
+
+
+
+
+
+{% endif %}
+
{% endif %}
{% if voters %}
@@ -91,7 +115,7 @@
{{election.name}} — Voters and Ballot Tracking Center
{% if election.num_cast_votes %}
-{{election.num_cast_votes}} cast vote{% ifequal election.num_cast_votes 1 %}{% else %}s{% endifequal %}
+{{election.num_cast_votes}} cast vote{% if election.num_cast_votes == 1 %}{% else %}s{% endif %}
{% else %}
no votes yet
{% endif %}
@@ -99,19 +123,24 @@ {{election.name}} — Voters and Ballot Tracking Center
{% if voters_page.has_previous %}
-previous {{limit}}
+previous {{limit}}
{% endif %}
Voters {{voters_page.start_index}} - {{voters_page.end_index}} (of {{total_voters}})
{% if voters_page.has_next %}
-next {{limit}}
+next {{limit}}
{% endif %}
{% if admin_p or not election.use_voter_aliases %}
+{% if admin_p %}
+Actions
+Login
+Email Address
+{% endif %}
Name
{% endif %}
@@ -123,19 +152,26 @@ {{election.name}} — Voters and Ballot Tracking Center
{% if admin_p or not election.use_voter_aliases %}
-
{% if admin_p %}
+
{% if election.frozen_at %}
-[email ]
+{% if can_send_emails %}
+[email ]
+{% else %}
+[email ]
+{% endif %}
{% endif %}
-[x ]
+{% if can_modify_voters %}[x ]{% endif %}
+
+{{voter.voter_login_id}}
+{{voter.voter_email}}
{% endif %}
- {{voter.name}}
+ {{voter.name}}
{% endif %}
{% if election.use_voter_aliases %}
{{voter.alias}}
{% endif %}
-{% if voter.vote_hash %}{{voter.vote_hash}} [view ] {% else %}—{% endif %}
+{% if voter.vote_hash %}{{voter.vote_hash}} {% else %}—{% endif %}
{% endfor %}
diff --git a/helios/templates/voters_manage.html b/helios/templates/voters_manage.html
index ccd54906a..cbcca1ebf 100644
--- a/helios/templates/voters_manage.html
+++ b/helios/templates/voters_manage.html
@@ -1,12 +1,12 @@
{% extends TEMPLATE_BASE %}
{% block content %}
- {{election.name}} — Manage Voters [back to election ]
+ {{election.name}} — Manage Voters [back to election ]
search :
{% if upload_p %}
- bulk upload voters
+
bulk upload voters
{% endif %}
Voters {{offset_plus_one}} - {{offset_plus_limit}}
@@ -15,10 +15,10 @@
{{election.name}} — Manage Voters next {{limit}}
{% endif %}
-{% ifequal offset 0 %}
+{% if offset == 0 %}
{% else %}
back to start
-{% endifequal %}
+{% endif %}
{% if more_p %}
next {{limit}}
{% endif %}
@@ -29,7 +29,7 @@ {{election.name}} — Manage Voters x]
+[x ]
{% endif %}
{% endfor %}
diff --git a/helios/templates/voters_search.html b/helios/templates/voters_search.html
index 1adfb33fe..162051f70 100644
--- a/helios/templates/voters_search.html
+++ b/helios/templates/voters_search.html
@@ -1,12 +1,12 @@
{% extends TEMPLATE_BASE %}
{% block content %}
- {{election.name}} — Search Voters for '{{search_term}}' [back to election ]
+ {{election.name}} — Search Voters for '{{search_term}}' [back to election ]
{% if voter %}
Voter Found: {{voter.name}} ({{voter.voter_id}})
{% if election.frozen_at %}
-email this voter
+email this voter
{% else %}
once this election is frozen, you'll be able to email this voter.
{% endif %}
diff --git a/helios/templates/voters_upload.html b/helios/templates/voters_upload.html
index a38956d01..8a316414c 100644
--- a/helios/templates/voters_upload.html
+++ b/helios/templates/voters_upload.html
@@ -1,25 +1,31 @@
{% extends TEMPLATE_BASE %}
{% block content %}
- {{election.name}} — Bulk Upload Voters [back to election ]
+ {{election.name}} — Bulk Upload Voters [back to election ]
-
+
If you would like to specify your list of voters by name and email address,
you can bulk upload a list of such voters here.
Please prepare a text file of comma-separated values with the fields:
- <unique_id>,<email>,<full name>
+ password,<unique_id>,<email>,<full name>
+
+
+or
+
+
+ github,<username>
For example:
- benadida,ben@adida.net,Ben Adida
- bobsmith,bob@acme.org,Bob Smith
+ password,bobsmith,bob@acme.org,Bob Smith
+ github,benadida
...
@@ -32,7 +38,7 @@ {{election.name}} — Bulk Upload Voters
-
+
diff --git a/helios/templates/voters_upload_confirm.html b/helios/templates/voters_upload_confirm.html
index c3d7c084b..ad0f7fb59 100644
--- a/helios/templates/voters_upload_confirm.html
+++ b/helios/templates/voters_upload_confirm.html
@@ -1,16 +1,16 @@
{% extends TEMPLATE_BASE %}
{% block content %}
-
{{election.name}} — Bulk Upload Voters — Confirm[back to election ]
+
{{election.name}} — Bulk Upload Voters — Confirm[back to election ]
You have uploaded a file of voters. The first few rows of this file are:
-Voter Login Email Address Name
+Voter Type Voter Login Email Address Name
{% for v in voters %}
-{{v.voter_id}} {{v.email}} {{v.name}}
+{{v.voter_type}} {{v.voter_id}} {{v.email}} {{v.name}}
{% endfor %}
@@ -23,7 +23,7 @@
{{election.name}} — Bulk Upload Voters — Confirm
-never mind, upload a different file
+never mind, upload a different file
{% else %}
@@ -31,10 +31,10 @@ {{election.name}} — Bulk Upload Voters — Confirm
Does this look right to you?
-
+
-no, let me upload a different file
+no, let me upload a different file
{% endif %}
diff --git a/helios/templatetags/__init__.py b/helios/templatetags/__init__.py
new file mode 100644
index 000000000..e67311381
--- /dev/null
+++ b/helios/templatetags/__init__.py
@@ -0,0 +1 @@
+# Django template tags package
diff --git a/helios/templatetags/timezone_tags.py b/helios/templatetags/timezone_tags.py
new file mode 100644
index 000000000..b00604583
--- /dev/null
+++ b/helios/templatetags/timezone_tags.py
@@ -0,0 +1,41 @@
+"""
+Template tags for timezone display in Helios
+"""
+
+from django import template
+from django.utils.safestring import mark_safe
+from django.utils.html import escape
+import datetime
+
+register = template.Library()
+
+
+@register.filter(name='utc_time')
+def utc_time(value):
+ """
+ Marks a datetime value for automatic timezone conversion.
+ The JavaScript will convert this to show both UTC and local timezone.
+
+ Usage in templates:
+ {{ election.voting_starts_at|utc_time }}
+ """
+ if value is None:
+ return ''
+
+ # Only accept datetime objects to prevent XSS
+ # Note: datetime.datetime is a subclass of datetime.date, so check datetime first
+ if isinstance(value, datetime.datetime):
+ # Format datetime with time
+ formatted = value.strftime('%Y-%m-%d %H:%M')
+ elif isinstance(value, datetime.date):
+ # Format date only (no time component)
+ formatted = value.strftime('%Y-%m-%d 00:00')
+ else:
+ # Reject any other type and escape it
+ return escape(str(value))
+
+ # Escape the formatted string to prevent XSS
+ escaped_formatted = escape(formatted)
+
+ # Return HTML with data attribute for JavaScript processing
+ return mark_safe(f'{escaped_formatted} UTC ')
diff --git a/helios/test.py b/helios/test.py
index 086f117c1..798d8deda 100644
--- a/helios/test.py
+++ b/helios/test.py
@@ -2,18 +2,21 @@
Testing Helios Features
"""
-from helios.models import *
-from helios_auth.models import *
import uuid
-def generate_voters(election, num_voters = 1000, start_with = 1):
- # generate the user
- for v_num in range(start_with, start_with + num_voters):
- user = User(user_type='password', user_id='testuser%s' % v_num, name='Test User %s' % v_num)
- user.put()
- voter = Voter(uuid=str(uuid.uuid1()), election = election, voter_type=user.user_type, voter_id = user.user_id)
- voter.put()
+from helios.models import Voter
+from helios_auth.models import User
+
+
+def generate_voters(election, num_voters=1000, start_with=1):
+ # generate the user
+ for v_num in range(start_with, start_with + num_voters):
+ user = User(user_type='password', user_id='testuser%s' % v_num, name='Test User %s' % v_num)
+ user.save()
+ voter = Voter(uuid=str(uuid.uuid4()), election=election, voter_type=user.user_type, voter_id=user.user_id)
+ voter.save()
+
def delete_voters(election):
- for v in Voter.get_by_election(election):
- v.delete()
\ No newline at end of file
+ for v in Voter.get_by_election(election):
+ v.delete()
diff --git a/helios/tests.py b/helios/tests.py
index 542df0333..71f9b60ce 100644
--- a/helios/tests.py
+++ b/helios/tests.py
@@ -2,32 +2,31 @@
Unit Tests for Helios
"""
-import unittest, datetime, re, urllib
-import django_webtest
-
-import models
-import datatypes
-
-from helios_auth import models as auth_models
-from views import ELGAMAL_PARAMS
-import views
-import utils
+import datetime
+import logging
+import re
+import uuid
+from urllib.parse import urlencode
-from django.db import IntegrityError, transaction
-from django.test.client import Client
+import django_webtest
+from django.conf import settings
+from django.core import mail
+from django.core.files import File
from django.test import TestCase
from django.utils.html import escape as html_escape
-from django.core import mail
-from django.core.files import File
-from django.core.urlresolvers import reverse
-from django.conf import settings
-from django.core.exceptions import PermissionDenied
+import helios.datatypes as datatypes
+import helios.models as models
+import helios.utils as utils
+import helios.views as views
+from helios import tasks
+from helios.crypto import electionalgs
+from helios_auth import models as auth_models
-import uuid
class ElectionModelTests(TestCase):
fixtures = ['users.json']
+ allow_database_queries = True
def create_election(self):
return models.Election.get_or_create(
@@ -41,7 +40,7 @@ def setup_questions(self):
self.election.questions = QUESTIONS
def setup_trustee(self):
- self.election.generate_trustee(ELGAMAL_PARAMS)
+ self.election.generate_trustee(views.ELGAMAL_PARAMS)
def setup_openreg(self):
self.election.openreg=True
@@ -57,45 +56,46 @@ def test_create_election(self):
self.assertTrue(self.created_p)
# should have a creation time
- self.assertNotEquals(self.election.created_at, None)
+ self.assertNotEqual(self.election.created_at, None)
self.assertTrue(self.election.created_at < datetime.datetime.utcnow())
def test_find_election(self):
election = models.Election.get_by_user_as_admin(self.user)[0]
- self.assertEquals(self.election, election)
+ self.assertEqual(self.election, election)
election = models.Election.get_by_uuid(self.election.uuid)
- self.assertEquals(self.election, election)
+ self.assertEqual(self.election, election)
election = models.Election.get_by_short_name(self.election.short_name)
- self.assertEquals(self.election, election)
+ self.assertEqual(self.election, election)
def test_setup_trustee(self):
self.setup_trustee()
- self.assertEquals(self.election.num_trustees, 1)
+ self.assertEqual(self.election.num_trustees, 1)
def test_add_voters_file(self):
election = self.election
FILE = "helios/fixtures/voter-file.csv"
- vf = models.VoterFile.objects.create(election = election, voter_file = File(open(FILE), "voter_file.css"))
- vf.process()
+ with open(FILE, 'r', encoding='utf-8') as f:
+ vf = models.VoterFile.objects.create(election = election, voter_file = File(f, "voter_file.css"))
+ vf.process()
# make sure that we stripped things correctly
voter = election.voter_set.get(voter_login_id = 'benadida5')
- self.assertEquals(voter.voter_email, 'ben5@adida.net')
- self.assertEquals(voter.voter_name, 'Ben5 Adida')
+ self.assertEqual(voter.voter_email, 'ben5@adida.net')
+ self.assertEqual(voter.voter_name, 'Ben5 Adida')
def test_check_issues_before_freeze(self):
# should be three issues: no trustees, and no questions, and no voters
issues = self.election.issues_before_freeze
- self.assertEquals(len(issues), 3)
+ self.assertEqual(len(issues), 3)
self.setup_questions()
# should be two issues: no trustees, and no voters
issues = self.election.issues_before_freeze
- self.assertEquals(len(issues), 2)
+ self.assertEqual(len(issues), 2)
self.election.questions = None
@@ -103,7 +103,7 @@ def test_check_issues_before_freeze(self):
# should be two issues: no questions, and no voters
issues = self.election.issues_before_freeze
- self.assertEquals(len(issues), 2)
+ self.assertEqual(len(issues), 2)
self.setup_questions()
@@ -111,15 +111,15 @@ def test_check_issues_before_freeze(self):
self.setup_openreg()
issues = self.election.issues_before_freeze
- self.assertEquals(len(issues), 0)
+ self.assertEqual(len(issues), 0)
def test_helios_trustee(self):
- self.election.generate_trustee(ELGAMAL_PARAMS)
+ self.election.generate_trustee(views.ELGAMAL_PARAMS)
self.assertTrue(self.election.has_helios_trustee())
trustee = self.election.get_helios_trustee()
- self.assertNotEquals(trustee, None)
+ self.assertNotEqual(trustee, None)
def test_log(self):
LOGS = ["testing 1", "testing 2", "testing 3"]
@@ -130,7 +130,7 @@ def test_log(self):
pulled_logs = [l.log for l in self.election.get_log().all()]
pulled_logs.reverse()
- self.assertEquals(LOGS,pulled_logs)
+ self.assertEqual(LOGS,pulled_logs)
def test_eligibility(self):
self.election.eligibility = [{'auth_system': self.user.user_type}]
@@ -141,7 +141,7 @@ def test_eligibility(self):
# what about after saving?
self.election.save()
e = models.Election.objects.get(uuid = self.election.uuid)
- self.assertEquals(e.eligibility, [{'auth_system': self.user.user_type}])
+ self.assertEqual(e.eligibility, [{'auth_system': self.user.user_type}])
self.election.openreg = True
@@ -154,6 +154,13 @@ def test_eligibility(self):
def test_facebook_eligibility(self):
self.election.eligibility = [{'auth_system': 'facebook', 'constraint':[{'group': {'id': '123', 'name':'Fake Group'}}]}]
+ import settings
+ fb_enabled = 'facebook' in settings.AUTH_ENABLED_SYSTEMS
+ if not fb_enabled:
+ logging.error("'facebook' not enabled for auth, cannot its constraints.")
+ self.assertFalse(self.election.user_eligible_p(self.fb_user))
+ return
+
# without openreg, this should be false
self.assertFalse(self.election.user_eligible_p(self.fb_user))
@@ -170,7 +177,7 @@ def fake_check_constraint(constraint, user):
self.assertTrue(self.election.user_eligible_p(self.fb_user))
# also check that eligibility_category_id does the right thing
- self.assertEquals(self.election.eligibility_category_id('facebook'), '123')
+ self.assertEqual(self.election.eligibility_category_id('facebook'), '123')
def test_freeze(self):
# freezing without trustees and questions, no good
@@ -195,18 +202,57 @@ def test_archive(self):
self.election.archived_at = None
self.assertFalse(self.election.is_archived)
+ def test_soft_delete(self):
+ # Test that soft delete sets the flags correctly
+ self.assertFalse(self.election.is_deleted)
+ self.assertIsNone(self.election.deleted_at)
+
+ # Soft delete the election
+ self.election.soft_delete()
+ self.assertTrue(self.election.is_deleted)
+ self.assertIsNotNone(self.election.deleted_at)
+
+ # Verify it's logged
+ log_entries = self.election.get_log().all()
+ self.assertTrue(any('deleted' in log.log.lower() for log in log_entries))
+
+ # Test that deleted elections are excluded from default queries
+ elections = models.Election.objects.filter(uuid=self.election.uuid)
+ self.assertEqual(len(elections), 0)
+
+ # But can still be found with objects_with_deleted
+ election = models.Election.objects_with_deleted.get(uuid=self.election.uuid)
+ self.assertEqual(election, self.election)
+
+ # Test that get_by_uuid respects the default manager (excludes deleted)
+ election = models.Election.get_by_uuid(self.election.uuid)
+ self.assertIsNone(election)
+
+ # But get_by_uuid with include_deleted=True should work
+ election = models.Election.get_by_uuid(self.election.uuid, include_deleted=True)
+ self.assertEqual(election, self.election)
+
+ # Test undelete
+ self.election.undelete()
+ self.assertFalse(self.election.is_deleted)
+ self.assertIsNone(self.election.deleted_at)
+
+ # Should be visible in default queries again
+ elections = models.Election.objects.filter(uuid=self.election.uuid)
+ self.assertEqual(len(elections), 1)
+
def test_voter_registration(self):
# before adding a voter
voters = models.Voter.get_by_election(self.election)
- self.assertTrue(len(voters) == 0)
+ self.assertEqual(0, len(voters))
# make sure no voter yet
voter = models.Voter.get_by_election_and_user(self.election, self.user)
- self.assertTrue(voter == None)
+ self.assertIsNone(voter)
# make sure no voter at all across all elections
voters = models.Voter.get_by_user(self.user)
- self.assertTrue(len(voters) == 0)
+ self.assertEqual(0, len(voters))
# register the voter
voter = models.Voter.register_user_in_election(self.user, self.election)
@@ -214,47 +260,240 @@ def test_voter_registration(self):
# make sure voter is there now
voter_2 = models.Voter.get_by_election_and_user(self.election, self.user)
- self.assertFalse(voter == None)
- self.assertFalse(voter_2 == None)
- self.assertEquals(voter, voter_2)
+ self.assertIsNotNone(voter)
+ self.assertIsNotNone(voter_2)
+ self.assertEqual(voter, voter_2)
# make sure voter is there in this call too
voters = models.Voter.get_by_user(self.user)
- self.assertTrue(len(voters) == 1)
- self.assertEquals(voter, voters[0])
+ self.assertEqual(1, len(voters))
+ self.assertEqual(voter, voters[0])
voter_2 = models.Voter.get_by_election_and_uuid(self.election, voter.uuid)
- self.assertEquals(voter, voter_2)
+ self.assertEqual(voter, voter_2)
+
+ self.assertEqual(voter.user, self.user)
+
+
+class AbsoluteWinnerCalculationTests(TestCase):
+ """
+ Tests for the one_question_winner method with absolute result type.
+
+ These tests verify that integer division is used correctly when calculating
+ the majority threshold. With 5 votes, a candidate needs 3 votes to win
+ (5//2 + 1 = 3). With 6 votes, a candidate needs 4 votes (6//2 + 1 = 4).
+
+ Regression tests for GitHub issue #417.
+ """
+
+ def make_absolute_question(self):
+ """Create a question with absolute result type"""
+ return {
+ "answer_urls": [None, None],
+ "answers": ["Yes", "No"],
+ "choice_type": "approval",
+ "max": 1,
+ "min": 0,
+ "question": "Test?",
+ "result_type": "absolute",
+ "short_name": "Test?",
+ "tally_type": "homomorphic"
+ }
+
+ def test_absolute_winner_with_5_votes_needs_3_to_win(self):
+ """
+ With 5 total votes, a candidate needs 3 votes to win (5//2 + 1 = 3).
+
+ This tests the integer division fix: with float division (5/2 + 1 = 3.5),
+ a candidate with 3 votes would incorrectly not win.
+ """
+ question = self.make_absolute_question()
+ # Result: Yes got 3 votes, No got 2 votes
+ result = [3, 2]
+ num_cast_votes = 5
+
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # Candidate 0 (Yes) should win with 3 votes (absolute majority of 5)
+ self.assertEqual(winners, [0])
+
+ def test_absolute_winner_with_5_votes_2_not_enough(self):
+ """
+ With 5 total votes, 2 votes is not enough to win (needs 3).
+ """
+ question = self.make_absolute_question()
+ # Result: Yes got 2 votes, No got 3 votes (but No needs 3 to win too)
+ result = [2, 3]
+ num_cast_votes = 5
+
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # Candidate 1 (No) wins with 3 votes
+ self.assertEqual(winners, [1])
- self.assertEquals(voter.user, self.user)
+ def test_absolute_no_winner_with_5_votes_when_tie(self):
+ """
+ With 5 votes split without anyone reaching majority, no winner.
+ """
+ question = self.make_absolute_question()
+ # Result: Yes got 2 votes, No got 2 votes, 1 abstention
+ result = [2, 2]
+ num_cast_votes = 5
+
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # No one has 3 votes, so no winner
+ self.assertEqual(winners, [])
+
+ def test_absolute_winner_with_6_votes_needs_4_to_win(self):
+ """
+ With 6 total votes, a candidate needs 4 votes to win (6//2 + 1 = 4).
+ """
+ question = self.make_absolute_question()
+ # Result: Yes got 4 votes, No got 2 votes
+ result = [4, 2]
+ num_cast_votes = 6
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # Candidate 0 (Yes) should win with 4 votes
+ self.assertEqual(winners, [0])
+
+ def test_absolute_no_winner_with_6_votes_only_3(self):
+ """
+ With 6 total votes, 3 votes is not enough to win (needs 4).
+ """
+ question = self.make_absolute_question()
+ # Result: Yes got 3 votes, No got 3 votes
+ result = [3, 3]
+ num_cast_votes = 6
+
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # Neither candidate has 4 votes, so no winner
+ self.assertEqual(winners, [])
+
+ def test_absolute_winner_with_7_votes_needs_4_to_win(self):
+ """
+ With 7 total votes, a candidate needs 4 votes to win (7//2 + 1 = 4).
+ """
+ question = self.make_absolute_question()
+ # Result: Yes got 4 votes, No got 3 votes
+ result = [4, 3]
+ num_cast_votes = 7
+
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # Candidate 0 (Yes) should win with 4 votes
+ self.assertEqual(winners, [0])
+
+ def test_relative_winner_does_not_require_majority(self):
+ """
+ With relative result type, highest vote count wins regardless of majority.
+ """
+ question = self.make_absolute_question()
+ question['result_type'] = 'relative'
+ # Result: Yes got 2 votes, No got 1 vote
+ result = [2, 1]
+ num_cast_votes = 5
+
+ winners = models.Election.one_question_winner(question, result, num_cast_votes)
+
+ # Candidate 0 (Yes) wins even without majority in relative mode
+ self.assertEqual(winners, [0])
+
+
+class ElectionAlgsWinnerCalculationTests(TestCase):
+ """
+ Tests for the one_question_winner function in helios.crypto.electionalgs.
+
+ This is a duplicate of the function in models.py and needs the same
+ integer division fix. These tests verify both implementations are correct.
+
+ Regression tests for GitHub issue #417.
+ """
+
+ def make_absolute_question(self):
+ """Create a question with absolute result type"""
+ return {
+ "answer_urls": [None, None],
+ "answers": ["Yes", "No"],
+ "choice_type": "approval",
+ "max": 1,
+ "min": 0,
+ "question": "Test?",
+ "result_type": "absolute",
+ "short_name": "Test?",
+ "tally_type": "homomorphic"
+ }
+
+ def test_electionalgs_absolute_winner_with_5_votes_needs_3_to_win(self):
+ """
+ With 5 total votes, a candidate needs 3 votes to win (5//2 + 1 = 3).
+ Tests the electionalgs.one_question_winner function.
+ """
+ question = self.make_absolute_question()
+ result = [3, 2]
+ num_cast_votes = 5
+
+ winners = electionalgs.one_question_winner(question, result, num_cast_votes)
+
+ self.assertEqual(winners, [0])
+
+ def test_electionalgs_absolute_winner_with_7_votes_needs_4_to_win(self):
+ """
+ With 7 total votes, a candidate needs 4 votes to win (7//2 + 1 = 4).
+ Tests the electionalgs.one_question_winner function.
+ """
+ question = self.make_absolute_question()
+ result = [4, 3]
+ num_cast_votes = 7
+
+ winners = electionalgs.one_question_winner(question, result, num_cast_votes)
+
+ self.assertEqual(winners, [0])
+
+ def test_electionalgs_absolute_no_winner_when_threshold_not_met(self):
+ """
+ With 5 votes, 2 votes is not enough to win.
+ Tests the electionalgs.one_question_winner function.
+ """
+ question = self.make_absolute_question()
+ result = [2, 2]
+ num_cast_votes = 5
+
+ winners = electionalgs.one_question_winner(question, result, num_cast_votes)
+
+ self.assertEqual(winners, [])
class VoterModelTests(TestCase):
fixtures = ['users.json', 'election.json']
+ allow_database_queries = True
def setUp(self):
self.election = models.Election.objects.get(short_name='test')
def test_create_password_voter(self):
- v = models.Voter(uuid = str(uuid.uuid1()), election = self.election, voter_login_id = 'voter_test_1', voter_name = 'Voter Test 1', voter_email='foobar@acme.com')
+ v = models.Voter(uuid = str(uuid.uuid4()), election = self.election, voter_login_id = 'voter_test_1', voter_name = 'Voter Test 1', voter_email='foobar@acme.com')
v.generate_password()
v.save()
# password has been generated!
- self.assertFalse(v.voter_password == None)
+ self.assertFalse(v.voter_password is None)
# can't generate passwords twice
self.assertRaises(Exception, lambda: v.generate_password())
# check that you can get at the voter user structure
- self.assertEquals(v.get_user().user_id, v.voter_email)
+ self.assertEqual(v.get_user().user_id, v.voter_email)
class CastVoteModelTests(TestCase):
fixtures = ['users.json', 'election.json']
+ allow_database_queries = True
def setUp(self):
self.election = models.Election.objects.get(short_name='test')
@@ -268,10 +507,11 @@ def test_cast_vote(self):
class DatatypeTests(TestCase):
fixtures = ['users.json', 'election.json']
+ allow_database_queries = True
def setUp(self):
self.election = models.Election.objects.all()[0]
- self.election.generate_trustee(ELGAMAL_PARAMS)
+ self.election.generate_trustee(views.ELGAMAL_PARAMS)
def test_instantiate(self):
ld_obj = datatypes.LDObject.instantiate(self.election.get_helios_trustee(), '2011/01/Trustee')
@@ -290,7 +530,7 @@ def test_dictobject_from_dict(self):
'B' : '234324243'}
ld_obj = datatypes.LDObject.fromDict(original_dict, type_hint = 'legacy/EGZKProofCommitment')
- self.assertEquals(original_dict, ld_obj.toDict())
+ self.assertEqual(original_dict, ld_obj.toDict())
@@ -304,9 +544,8 @@ def setUp(self):
self.election = models.Election.objects.all()[0]
def assertEqualsToFile(self, response, file_path):
- expected = open(file_path)
- self.assertEquals(response.content, expected.read())
- expected.close()
+ with open(file_path) as expected:
+ self.assertEqual(response.content, expected.read().encode('utf-8'))
def test_election(self):
response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False)
@@ -334,6 +573,7 @@ def test_ballots_list(self):
class LegacyElectionBlackboxTests(DataFormatBlackboxTests, TestCase):
fixtures = ['legacy-data.json']
+ allow_database_queries = True
EXPECTED_ELECTION_FILE = 'helios/fixtures/legacy-election-expected.json'
EXPECTED_ELECTION_METADATA_FILE = 'helios/fixtures/legacy-election-metadata-expected.json'
EXPECTED_VOTERS_FILE = 'helios/fixtures/legacy-election-voters-expected.json'
@@ -348,45 +588,47 @@ class LegacyElectionBlackboxTests(DataFormatBlackboxTests, TestCase):
# EXPECTED_BALLOTS_FILE = 'helios/fixtures/v3.1-ballots-expected.json'
class WebTest(django_webtest.WebTest):
- def assertRedirects(self, response, url):
+ def assertStatusCode(self, response, status_code):
+ actual_code = response.status_code if hasattr(response, 'status_code') else response.status_int
+ if isinstance(status_code, (list, tuple)):
+ assert actual_code in status_code, "%s instad of %s" % (actual_code, status_code)
+ else:
+ assert actual_code == status_code, "%s instad of %s" % (actual_code, status_code)
+
+
+ def assertRedirects(self, response, url=None):
"""
reimplement this in case it's a WebOp response
and it seems to be screwing up in a few places too
thus the localhost exception
"""
+ self.assertStatusCode(response, (301, 302))
+ location = None
if hasattr(response, 'location'):
- assert url in response.location
+ location = response.location
else:
- assert url in response._headers['location'][1]
-
- if hasattr(response, 'status_code'):
- assert response.status_code == 302
- else:
- assert response.status_int == 302
-
- #self.assertEqual(response.status_code, 302)
-
+ location = response['location']
+ if url is not None:
+ assert url in location, location
#return super(django_webtest.WebTest, self).assertRedirects(response, url)
- #if hasattr(response, 'status_code') and hasattr(response, 'location'):
-
-
#assert url in response.location, "redirected to %s instead of %s" % (response.location, url)
+
def assertContains(self, response, text):
- if hasattr(response, 'status_code'):
- assert response.status_code == 200
-# return super(django_webtest.WebTest, self).assertContains(response, text)
- else:
- assert response.status_int == 200
+ self.assertStatusCode(response, 200)
-
if hasattr(response, "testbody"):
- assert text in response.testbody, "missing text %s" % text
+ t = response.testbody
+ elif hasattr(response, "body"):
+ t = response.body
else:
- if hasattr(response, "body"):
- assert text in response.body, "missing text %s" % text
- else:
- assert text in response.content, "missing text %s" % text
+ t = response.content
+
+ if isinstance(text, bytes):
+ text = text.decode()
+ if isinstance(t, bytes):
+ t = t.decode()
+ assert text in t, "missing text %s" % text
##
@@ -395,31 +637,23 @@ def assertContains(self, response, text):
class ElectionBlackboxTests(WebTest):
fixtures = ['users.json', 'election.json']
+ allow_database_queries = True
def setUp(self):
self.election = models.Election.objects.all()[0]
self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
- def assertContains(self, response, text):
- if hasattr(response, 'status_code'):
- assert response.status_code == 200
-# return super(django_webtest.WebTest, self).assertContains(response, text)
- else:
- assert response.status_int == 200
-
-
- if hasattr(response, "testbody"):
- assert text in response.testbody, "missing text %s" % text
- else:
- if hasattr(response, "body"):
- assert text in response.body, "missing text %s" % text
- else:
- assert text in response.content, "missing text %s" % text
-
- def setup_login(self):
+ def setup_login(self, from_scratch=False, **kwargs):
+ if from_scratch:
+ # a bogus call to set up the session
+ self.client.get("/")
# set up the session
session = self.client.session
- session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id}
+ if kwargs:
+ user = auth_models.User.objects.get(**kwargs)
+ else:
+ user = self.user
+ session['user'] = {'type': user.user_type, 'user_id': user.user_id}
session.save()
# set up the app, too
@@ -434,27 +668,27 @@ def clear_login(self):
def test_election_params(self):
response = self.client.get("/helios/elections/params")
- self.assertEquals(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize())
+ self.assertEqual(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize().encode('utf-8'))
def test_election_404(self):
response = self.client.get("/helios/elections/foobar")
- self.assertEquals(response.status_code, 404)
+ self.assertStatusCode(response, 404)
def test_election_bad_trustee(self):
response = self.client.get("/helios/t/%s/foobar@bar.com/badsecret" % self.election.short_name)
- self.assertEquals(response.status_code, 404)
+ self.assertStatusCode(response, 404)
def test_get_election_shortcut(self):
response = self.client.get("/helios/e/%s" % self.election.short_name, follow=True)
- self.assertContains(response, self.election.description)
+ self.assertContains(response, self.election.description_bleached)
def test_get_election_raw(self):
response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False)
- self.assertEquals(response.content, self.election.toJSON())
+ self.assertEqual(response.content, self.election.toJSON().encode('utf-8'))
def test_get_election(self):
response = self.client.get("/helios/elections/%s/view" % self.election.uuid, follow=False)
- self.assertContains(response, self.election.description)
+ self.assertContains(response, self.election.description_bleached)
def test_get_election_questions(self):
response = self.client.get("/helios/elections/%s/questions" % self.election.uuid, follow=False)
@@ -476,7 +710,7 @@ def test_get_election_voters(self):
def test_get_election_voters_raw(self):
response = self.client.get("/helios/elections/%s/voters/" % self.election.uuid, follow=False)
- self.assertEquals(len(utils.from_json(response.content)), self.election.num_voters)
+ self.assertEqual(len(response.json()), self.election.num_voters)
def test_election_creation_not_logged_in(self):
response = self.client.post("/helios/elections/new", {
@@ -491,10 +725,7 @@ def test_election_creation_not_logged_in(self):
self.assertRedirects(response, "/auth/?return_url=/helios/elections/new")
def test_election_edit(self):
- # a bogus call to set up the session
- self.client.get("/")
-
- self.setup_login()
+ self.setup_login(from_scratch=True)
response = self.client.get("/helios/elections/%s/edit" % self.election.uuid)
response = self.client.post("/helios/elections/%s/edit" % self.election.uuid, {
"short_name" : self.election.short_name + "-2",
@@ -508,16 +739,33 @@ def test_election_edit(self):
self.assertRedirects(response, "/helios/elections/%s/view" % self.election.uuid)
new_election = models.Election.objects.get(uuid = self.election.uuid)
- self.assertEquals(new_election.short_name, self.election.short_name + "-2")
+ self.assertEqual(new_election.short_name, self.election.short_name + "-2")
+
+ def test_get_election_stats(self):
+ self.setup_login(from_scratch=True, user_id='mccio@github.com', user_type='google')
+ response = self.client.get("/helios/stats/", follow=False)
+ self.assertStatusCode(response, 200)
+ response = self.client.get("/helios/stats/force-queue", follow=False)
+ self.assertRedirects(response, "/helios/stats/")
+ response = self.client.get("/helios/stats/elections", follow=False)
+ self.assertStatusCode(response, 200)
+ response = self.client.get("/helios/stats/problem-elections", follow=False)
+ self.assertStatusCode(response, 200)
+ response = self.client.get("/helios/stats/recent-votes", follow=False)
+ self.assertStatusCode(response, 200)
+ self.clear_login()
+ response = self.client.get("/helios/stats/", follow=False)
+ self.assertStatusCode(response, 403)
+ self.setup_login()
+ response = self.client.get("/helios/stats/", follow=False)
+ self.assertStatusCode(response, 403)
+ self.clear_login()
- def _setup_complete_election(self, election_params={}):
+ def _setup_complete_election(self, election_params=None):
"do the setup part of a whole election"
- # a bogus call to set up the session
- self.client.get("/")
-
# REPLACE with params?
- self.setup_login()
+ self.setup_login(from_scratch=True)
# create the election
full_election_params = {
@@ -532,12 +780,15 @@ def _setup_complete_election(self, election_params={}):
}
# override with the given
- full_election_params.update(election_params)
+ full_election_params.update(election_params or {})
response = self.client.post("/helios/elections/new", full_election_params)
+ self.assertRedirects(response)
# we are redirected to the election, let's extract the ID out of the URL
- election_id = re.search('/elections/([^/]+)/', str(response['Location'])).group(1)
+ election_id = re.search('/elections/([^/]+)/', str(response['location']))
+ self.assertIsNotNone(election_id, "Election id not found in redirect: %s" % str(response['location']))
+ election_id = election_id.group(1)
# helios is automatically added as a trustee
@@ -566,10 +817,17 @@ def _setup_complete_election(self, election_params={}):
response = self.client.post("/helios/elections/%s/voters/upload" % election_id, {'confirm_p': "1"})
self.assertRedirects(response, "/helios/elections/%s/voters/list" % election_id)
+ # Try a latin-1 encoded file
+ FILE = "helios/fixtures/voter-file-latin1.csv"
+ voters_file = open(FILE, mode='rb')
+ response = self.client.post("/helios/elections/%s/voters/upload" % election_id, {'voters_file': voters_file})
+ voters_file.close()
+ self.assertContains(response, "first few rows of this file")
+
# and we want to check that there are now voters
response = self.client.get("/helios/elections/%s/voters/" % election_id)
NUM_VOTERS = 4
- self.assertEquals(len(utils.from_json(response.content)), NUM_VOTERS)
+ self.assertEqual(len(response.json()), NUM_VOTERS)
# let's get a single voter
single_voter = models.Election.objects.get(uuid = election_id).voter_set.all()[0]
@@ -577,7 +835,7 @@ def _setup_complete_election(self, election_params={}):
self.assertContains(response, '"uuid": "%s"' % single_voter.uuid)
response = self.client.get("/helios/elections/%s/voters/foobar" % election_id)
- self.assertEquals(response.status_code, 404)
+ self.assertStatusCode(response, 404)
# add questions
response = self.client.post("/helios/elections/%s/save_questions" % election_id, {
@@ -602,7 +860,7 @@ def _setup_complete_election(self, election_params={}):
})
self.assertRedirects(response, "/helios/elections/%s/view" % election_id)
num_messages_after = len(mail.outbox)
- self.assertEquals(num_messages_after - num_messages_before, NUM_VOTERS)
+ self.assertEqual(num_messages_after - num_messages_before, NUM_VOTERS)
email_message = mail.outbox[num_messages_before]
assert "your password" in email_message.subject, "bad subject in email"
@@ -613,7 +871,7 @@ def _setup_complete_election(self, election_params={}):
# now log out as administrator
self.clear_login()
- self.assertEquals(self.client.session.has_key('user'), False)
+ self.assertEqual('user' in self.client.session, False)
# return the voter username and password to vote
return election_id, username, password
@@ -622,23 +880,25 @@ def _cast_ballot(self, election_id, username, password, need_login=True, check_u
"""
check_user_logged_in looks for the "you're already logged" message
"""
- # vote by preparing a ballot via the server-side encryption
- response = self.app.post("/helios/elections/%s/encrypt-ballot" % election_id, {
- 'answers_json': utils.to_json([[1]])})
- self.assertContains(response, "answers")
-
- # parse it as an encrypted vote with randomness, and make sure randomness is there
- the_ballot = utils.from_json(response.testbody)
- assert the_ballot['answers'][0].has_key('randomness'), "no randomness"
+ from helios.workflows import homomorphic
+
+ # get the election and generate an encrypted vote
+ election = models.Election.objects.get(uuid=election_id)
+ answers = [[1]]
+ ev = homomorphic.EncryptedVote.fromElectionAndAnswers(election, answers)
+ the_ballot = ev.ld_object.includeRandomness().toJSONDict()
+
+ # verify randomness is present
+ assert 'randomness' in the_ballot['answers'][0], "no randomness"
assert len(the_ballot['answers'][0]['randomness']) == 2, "not enough randomness"
-
+
# parse it as an encrypted vote, and re-serialize it
- ballot = datatypes.LDObject.fromDict(utils.from_json(response.testbody), type_hint='legacy/EncryptedVote')
+ ballot = datatypes.LDObject.fromDict(the_ballot, type_hint='legacy/EncryptedVote')
encrypted_vote = ballot.serialize()
# cast the ballot
- response = self.app.post("/helios/elections/%s/cast" % election_id, {
- 'encrypted_vote': encrypted_vote})
+ response = self.app.post("/helios/elections/%s/cast" % election_id,
+ params={'encrypted_vote': encrypted_vote})
self.assertRedirects(response, "%s/helios/elections/%s/cast_confirm" % (settings.SECURE_URL_HOST, election_id))
cast_confirm_page = response.follow()
@@ -661,7 +921,7 @@ def _cast_ballot(self, election_id, username, password, need_login=True, check_u
# confirm the vote, now with the actual form
cast_form = cast_confirm_page.form
- if 'status_update' in cast_form.fields.keys():
+ if 'status_update' in list(cast_form.fields.keys()):
cast_form['status_update'] = False
response = cast_form.submit()
@@ -670,7 +930,7 @@ def _cast_ballot(self, election_id, username, password, need_login=True, check_u
# at this point an email should have gone out to the user
# at position num_messages after, since that was the len() before we cast this ballot
email_message = mail.outbox[len(mail.outbox) - 1]
- url = re.search('http://[^/]+(/[^ \n]*)', email_message.body).group(1)
+ url = re.search('https?://[^/]+(/[^ \n]*)', email_message.body).group(1)
# check that we can get at that URL
if not need_login:
@@ -690,7 +950,7 @@ def _cast_ballot(self, election_id, username, password, need_login=True, check_u
login_form['password'] = ' ' + password + ' '
login_form.submit()
- response = self.app.get(url)
+ response = self.app.get(url, auto_follow=True)
self.assertContains(response, ballot.hash)
self.assertContains(response, html_escape(encrypted_vote))
@@ -711,7 +971,7 @@ def _do_tally(self, election_id):
self.assertRedirects(response, "/helios/elections/%s/view" % election_id)
# should trigger helios decryption automatically
- self.assertNotEquals(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None)
+ self.assertNotEqual(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None)
# combine decryptions
response = self.client.post("/helios/elections/%s/combine_decryptions" % election_id, {
@@ -723,7 +983,7 @@ def _do_tally(self, election_id):
# check that we can't get the tally yet
response = self.client.get("/helios/elections/%s/result" % election_id)
- self.assertEquals(response.status_code, 403)
+ self.assertStatusCode(response, 403)
# release
response = self.client.post("/helios/elections/%s/release_result" % election_id, {
@@ -732,7 +992,7 @@ def _do_tally(self, election_id):
# check that tally matches
response = self.client.get("/helios/elections/%s/result" % election_id)
- self.assertEquals(utils.from_json(response.content), [[0,1]])
+ self.assertEqual(response.json(), [[0,1]])
def test_do_complete_election(self):
election_id, username, password = self._setup_complete_election()
@@ -761,7 +1021,7 @@ def test_do_complete_election_private(self):
response = self.app.get("/helios/elections/%s/view" % election_id)
# ensure it redirects
- self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urllib.urlencode({"return_url": "/helios/elections/%s/view" % election_id})))
+ self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urlencode({"return_url": "/helios/elections/%s/view" % election_id})))
login_form = response.follow().form
@@ -776,8 +1036,7 @@ def test_do_complete_election_private(self):
def test_election_voters_eligibility(self):
# create the election
- self.client.get("/")
- self.setup_login()
+ self.setup_login(from_scratch=True)
response = self.client.post("/helios/elections/new", {
"short_name" : "test-eligibility",
"name" : "Test Eligibility",
@@ -788,7 +1047,9 @@ def test_election_voters_eligibility(self):
"private_p" : "False",
'csrf_token': self.client.session['csrf_token']})
- election_id = re.match("(.*)/elections/(.*)/view", response['Location']).group(2)
+ election_id = re.match("(.*)/elections/(.*)/view", str(response['Location']))
+ self.assertIsNotNone(election_id, "Election id not found in redirect: %s" % str(response['Location']))
+ election_id = election_id.group(2)
# update eligiblity
response = self.client.post("/helios/elections/%s/voters/eligibility" % election_id, {
@@ -808,8 +1069,2022 @@ def test_election_voters_eligibility(self):
response = self.client.get("/helios/elections/%s/voters/list" % election_id)
self.assertContains(response, "Only the voters listed here")
+ def test_multiple_voter_uploads_with_aliases(self):
+ """Test that uploading multiple voter files with aliases generates unique sequential aliases"""
+ self.setup_login(from_scratch=True)
+
+ # Create election with voter aliases enabled
+ response = self.client.post("/helios/elections/new", {
+ "short_name": "test-aliases",
+ "name": "Test Voter Aliases",
+ "description": "Testing multiple voter uploads with aliases",
+ "election_type": "referendum",
+ "use_voter_aliases": "1",
+ "use_advanced_audit_features": "1",
+ "private_p": "False",
+ "csrf_token": self.client.session["csrf_token"]
+ })
+ self.assertRedirects(response)
+
+ election_id = re.search('/elections/([^/]+)/', str(response['location'])).group(1)
+ election = models.Election.objects.get(uuid=election_id)
+ self.assertTrue(election.use_voter_aliases)
+
+ # Upload first voter file (4 voters)
+ with open("helios/fixtures/voter-file.csv") as f:
+ response = self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"voters_file": f}
+ )
+ self.assertContains(response, "first few rows")
+
+ # Confirm first upload
+ response = self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"confirm_p": "1"}
+ )
+ self.assertRedirects(response, "/helios/elections/%s/voters/list" % election_id)
+
+ # Verify first batch - should have 4 voters with aliases V1-V4
+ election.refresh_from_db()
+ self.assertEqual(election.voter_set.count(), 4)
+ first_aliases = sorted([v.alias for v in election.voter_set.all()])
+ self.assertEqual(first_aliases, ["V1", "V2", "V3", "V4"])
+
+ # Upload second voter file (3 more voters)
+ with open("helios/fixtures/voter-file-2.csv") as f:
+ response = self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"voters_file": f}
+ )
+ self.assertContains(response, "first few rows")
+
+ # Confirm second upload
+ response = self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"confirm_p": "1"}
+ )
+ self.assertRedirects(response, "/helios/elections/%s/voters/list" % election_id)
+
+ # Verify all 7 voters have unique sequential aliases V1-V7
+ election.refresh_from_db()
+ self.assertEqual(election.voter_set.count(), 7)
+ all_aliases = sorted([v.alias for v in election.voter_set.all()])
+ self.assertEqual(all_aliases, ["V1", "V2", "V3", "V4", "V5", "V6", "V7"])
+
+ # Verify the second batch got V5-V7 (continuing from first batch)
+ for voter_id in ["voter8", "voter9", "voter10"]:
+ voter = election.voter_set.get(voter_login_id=voter_id)
+ alias_num = int(voter.alias[1:])
+ self.assertGreaterEqual(alias_num, 5,
+ f"Voter {voter_id} should have alias >= V5, got {voter.alias}")
+
def test_do_complete_election_with_trustees(self):
"""
FIXME: do the this test
"""
pass
+
+ def test_voters_clear(self):
+ """Test clearing all voters from an unfrozen election"""
+ self.setup_login(from_scratch=True)
+
+ # Create a new election
+ response = self.client.post("/helios/elections/new", {
+ "short_name": "test-clear-voters",
+ "name": "Test Clear Voters",
+ "description": "Testing clearing all voters",
+ "election_type": "referendum",
+ "use_voter_aliases": "0",
+ "use_advanced_audit_features": "1",
+ "private_p": "True",
+ "csrf_token": self.client.session["csrf_token"]
+ })
+ self.assertRedirects(response)
+
+ election_id = re.search('/elections/([^/]+)/', str(response['location'])).group(1)
+ election = models.Election.objects.get(uuid=election_id)
+
+ # Upload voters
+ with open("helios/fixtures/voter-file.csv") as f:
+ response = self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"voters_file": f}
+ )
+ self.assertContains(response, "first few rows")
+
+ # Confirm upload
+ response = self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"confirm_p": "1"}
+ )
+ self.assertRedirects(response, "/helios/elections/%s/voters/list" % election_id)
+
+ # Verify voters were added
+ election.refresh_from_db()
+ initial_voter_count = election.voter_set.count()
+ self.assertGreater(initial_voter_count, 0)
+
+ # Clear all voters
+ response = self.client.post(
+ "/helios/elections/%s/voters/clear" % election_id,
+ {"csrf_token": self.client.session["csrf_token"]}
+ )
+ self.assertRedirects(response, "/helios/elections/%s/voters/list" % election_id)
+
+ # Verify all voters were removed
+ election.refresh_from_db()
+ self.assertEqual(election.voter_set.count(), 0)
+
+ # Verify it was logged
+ logs = list(election.get_log().all())
+ self.assertTrue(any("voters cleared" in log.log.lower() for log in logs))
+
+ def test_voters_clear_requires_admin(self):
+ """Test that only admins can clear voters"""
+ # Use existing election from fixture
+ election = self.election
+
+ # Try to clear without being logged in
+ response = self.client.post(
+ "/helios/elections/%s/voters/clear" % election.uuid,
+ {"csrf_token": "fake"}
+ )
+ # Should get permission denied (403) since not authenticated
+ self.assertStatusCode(response, 403)
+
+ def test_voters_clear_blocked_when_frozen(self):
+ """Test that clearing voters is blocked when election is frozen"""
+ self.setup_login(from_scratch=True)
+
+ # Create a new election
+ response = self.client.post("/helios/elections/new", {
+ "short_name": "test-clear-frozen",
+ "name": "Test Clear Frozen",
+ "description": "Testing clearing voters on frozen election",
+ "election_type": "referendum",
+ "use_voter_aliases": "0",
+ "use_advanced_audit_features": "1",
+ "private_p": "True",
+ "csrf_token": self.client.session["csrf_token"]
+ })
+ self.assertRedirects(response)
+
+ election_id = re.search('/elections/([^/]+)/', str(response['location'])).group(1)
+ election = models.Election.objects.get(uuid=election_id)
+
+ # Add voters
+ with open("helios/fixtures/voter-file.csv") as f:
+ self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"voters_file": f}
+ )
+ self.client.post(
+ "/helios/elections/%s/voters/upload" % election_id,
+ {"confirm_p": "1"}
+ )
+
+ # Add a question
+ self.client.post("/helios/elections/%s/save_questions" % election_id, {
+ "questions_json": '[{"answer_urls":[null,null],"answers":["Yes","No"],"choice_type":"approval","max":1,"min":0,"question":"Test?","result_type":"absolute","short_name":"test","tally_type":"homomorphic"}]',
+ "csrf_token": self.client.session["csrf_token"]
+ })
+
+ # Freeze the election
+ self.client.post("/helios/elections/%s/freeze" % election_id, {
+ "csrf_token": self.client.session["csrf_token"]
+ })
+
+ # Verify election is frozen
+ election.refresh_from_db()
+ self.assertIsNotNone(election.frozen_at)
+
+ # Try to clear voters - should fail with 403
+ response = self.client.post(
+ "/helios/elections/%s/voters/clear" % election_id,
+ {"csrf_token": self.client.session["csrf_token"]}
+ )
+ self.assertStatusCode(response, 403)
+
+
+class ElectionDeleteViewTests(WebTest):
+ fixtures = ['users.json', 'election.json']
+ allow_database_queries = True
+
+ def setUp(self):
+ self.election = models.Election.objects.all()[0]
+ self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
+
+ def setup_login(self, from_scratch=False):
+ if from_scratch:
+ self.client.get("/")
+ session = self.client.session
+ session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id}
+ session.save()
+
+ def test_delete_with_post(self):
+ """Test soft deleting an election via POST"""
+ self.setup_login(from_scratch=True)
+
+ # Verify election is not deleted initially
+ self.assertFalse(self.election.is_deleted)
+
+ # POST to delete endpoint
+ response = self.client.post(
+ "/helios/elections/%s/delete" % self.election.uuid,
+ {"delete_p": "1", "csrf_token": self.client.session.get("csrf_token", "")}
+ )
+ self.assertRedirects(response)
+
+ # Election should be soft deleted
+ election = models.Election.objects_with_deleted.get(uuid=self.election.uuid)
+ self.assertTrue(election.is_deleted)
+ self.assertIsNotNone(election.deleted_at)
+
+ # Should not appear in default queries
+ elections = models.Election.objects.filter(uuid=self.election.uuid)
+ self.assertEqual(len(elections), 0)
+
+ def test_delete_requires_admin(self):
+ """Test that only election admins can delete"""
+ # Don't log in - should get permission denied
+ response = self.client.post(
+ "/helios/elections/%s/delete" % self.election.uuid,
+ {"delete_p": "1", "csrf_token": "fake"}
+ )
+ self.assertStatusCode(response, 403)
+
+ # Election should not be deleted
+ election = models.Election.objects_with_deleted.get(uuid=self.election.uuid)
+ self.assertFalse(election.is_deleted)
+
+ def test_deleted_election_not_accessible_to_non_admins(self):
+ """Test that deleted elections return 404 for non-admin users"""
+ # Soft delete the election
+ self.election.soft_delete()
+
+ # Try to access as non-admin (not logged in)
+ response = self.client.get("/helios/elections/%s/view" % self.election.uuid)
+ self.assertStatusCode(response, 404)
+
+ def test_deleted_election_not_accessible_to_election_admins(self):
+ """Test that deleted elections return 404 even for election admins"""
+ self.setup_login(from_scratch=True)
+
+ # Soft delete the election
+ self.election.soft_delete()
+
+ # Election admin should not be able to access it
+ response = self.client.get("/helios/elections/%s/view" % self.election.uuid)
+ self.assertStatusCode(response, 404)
+
+
+class EmailOptOutTests(TestCase):
+ fixtures = ['users.json']
+ allow_database_queries = True
+
+ def setUp(self):
+ self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
+
+ def test_email_hashing(self):
+ """Test email hashing utility function"""
+ email = "test@example.com"
+ hash1 = utils.hash_email(email)
+ hash2 = utils.hash_email(email.upper()) # Should be same after normalization
+ hash3 = utils.hash_email(" " + email + " ") # Should be same after strip
+
+ self.assertEqual(hash1, hash2)
+ self.assertEqual(hash1, hash3)
+ self.assertEqual(len(hash1), 64) # SHA-256 hex length
+
+ def test_hmac_generation_and_verification(self):
+ """Test HMAC confirmation code generation and verification"""
+ email = "test@example.com"
+ action = "optout"
+
+ code = utils.generate_email_confirmation_code(email, action)
+ self.assertIsNotNone(code)
+ self.assertEqual(len(code), 64) # SHA-256 hex length
+
+ # Valid verification
+ self.assertTrue(utils.verify_email_confirmation_code(email, action, code))
+
+ # Invalid verifications
+ self.assertFalse(utils.verify_email_confirmation_code(email, "optin", code)) # Wrong action
+ self.assertFalse(utils.verify_email_confirmation_code("other@example.com", action, code)) # Wrong email
+ self.assertFalse(utils.verify_email_confirmation_code(email, action, "invalidcode")) # Wrong code
+
+ def test_email_opt_out_model(self):
+ """Test EmailOptOut model functionality"""
+ email = "test@example.com"
+
+ # Initially not opted out
+ self.assertFalse(models.EmailOptOut.is_opted_out(email))
+
+ # Add opt-out
+ opt_out = models.EmailOptOut.add_opt_out(email, "test-agent", "127.0.0.1")
+ self.assertIsNotNone(opt_out)
+
+ # Now should be opted out
+ self.assertTrue(models.EmailOptOut.is_opted_out(email))
+
+ # Adding again should return existing record
+ opt_out2 = models.EmailOptOut.add_opt_out(email)
+ self.assertEqual(opt_out.id, opt_out2.id)
+
+ # Remove opt-out
+ removed = models.EmailOptOut.remove_opt_out(email)
+ self.assertTrue(removed)
+
+ # Should no longer be opted out
+ self.assertFalse(models.EmailOptOut.is_opted_out(email))
+
+ # Removing again should return False
+ removed2 = models.EmailOptOut.remove_opt_out(email)
+ self.assertFalse(removed2)
+
+ def test_voter_registration_with_opted_out_email(self):
+ """Test that voter registration fails for opted-out emails"""
+ email = "opted-out@example.com"
+
+ # Opt out the email
+ models.EmailOptOut.add_opt_out(email)
+
+ # Create a user with opted-out email
+ user = auth_models.User.objects.create(
+ user_type='password',
+ user_id=email,
+ name='Test User'
+ )
+
+ # Create an election
+ election, _ = models.Election.get_or_create(
+ short_name='test-optout',
+ name='Test Opt-Out Election',
+ description='Test Election for Opt-Out',
+ admin=self.user
+ )
+
+ # Trying to register should raise ValueError
+ with self.assertRaises(ValueError):
+ models.Voter.register_user_in_election(user, election)
+
+ def test_opted_out_check_in_tasks(self):
+ """Test that tasks check for opted-out status correctly"""
+ email = "voter@example.com"
+
+ # Create an election and voter
+ election, _ = models.Election.get_or_create(
+ short_name='test-email',
+ name='Test Email Election',
+ description='Test Election for Email',
+ admin=self.user
+ )
+
+ voter = models.Voter.objects.create(
+ uuid=str(uuid.uuid4()),
+ election=election,
+ voter_email=email,
+ voter_name="Test Voter"
+ )
+
+ # Test the opt-out checking logic directly
+ # Before opt-out, voter should not be skipped
+ self.assertFalse(models.EmailOptOut.is_opted_out(email))
+
+ from unittest.mock import patch
+
+ # Test that single_voter_email DOES send emails before opt-out
+ with patch.object(models.Voter, 'send_message') as mock_send_message:
+
+ # Verify voter setup
+ self.assertEqual(voter.voter_email, email)
+ self.assertFalse(models.EmailOptOut.is_opted_out(email))
+
+ # Call single_voter_email before opt-out is applied
+ result = tasks.single_voter_email.apply(args=[voter.uuid, "email/simple_subject.txt", "email/simple_body.txt", {}])
+
+ # Debug: check if task completed successfully
+ self.assertTrue(result.successful())
+
+ # The send_message method should have been called because voter is not opted out
+ mock_send_message.assert_called_once()
+
+ # Now opt out the email
+ models.EmailOptOut.add_opt_out(email)
+
+ # After opt-out, voter should be skipped
+ self.assertTrue(models.EmailOptOut.is_opted_out(email))
+
+ # Test that single_voter_email does NOT send emails after opt-out
+ with patch.object(models.Voter, 'send_message') as mock_send_message:
+
+ # Call single_voter_email after opt-out is applied
+ tasks.single_voter_email.apply(args=[voter.uuid, "email/simple_subject.txt", "email/simple_body.txt", {}])
+
+ # The send_message method should not have been called because voter is opted out
+ mock_send_message.assert_not_called()
+
+ def test_email_contains_unsubscribe_link(self):
+ """Test that emails sent to voters contain unsubscribe links"""
+ from helios.view_utils import render_template_raw
+
+ email = "voter@example.com"
+
+ # Create an election and voter
+ election, _ = models.Election.get_or_create(
+ short_name='test-unsubscribe',
+ name='Test Unsubscribe Election',
+ description='Test Election for Unsubscribe Links',
+ admin=self.user
+ )
+
+ voter = models.Voter.objects.create(
+ uuid=str(uuid.uuid4()),
+ election=election,
+ voter_email=email,
+ voter_name="Test Voter"
+ )
+
+ # Generate the unsubscribe URL as the task would
+ unsubscribe_code = utils.generate_email_confirmation_code(email, 'optout')
+
+ # Prepare template context similar to what single_voter_email does
+ template_vars = {
+ 'election': election,
+ 'voter': voter,
+ 'election_url': 'http://example.com/election/123',
+ 'election_vote_url': 'http://example.com/election/123/vote',
+ 'custom_message': 'Please vote!',
+ 'unsubscribe_url': f'http://example.com/optout/confirm/{email}/{unsubscribe_code}/',
+ 'unsubscribe_code': unsubscribe_code
+ }
+
+ # Render the email body template
+ body = render_template_raw(None, 'email/vote_body.txt', template_vars)
+
+ # Verify the unsubscribe link is in the body
+ self.assertIn('To stop receiving all emails from Helios', body)
+ self.assertIn('/optout/confirm/', body)
+ self.assertIn(email, body)
+ self.assertIn(unsubscribe_code, body)
+
+
+class EmailOptOutViewTests(WebTest):
+ """Test the opt-out/opt-in web views and templates"""
+ allow_database_queries = True
+
+ def test_optout_form_renders(self):
+ """Test that opt-out form page renders correctly"""
+ response = self.client.get('/optout/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Opt Out of Helios Emails')
+ self.assertContains(response, 'Enter your email address to stop receiving all emails')
+ self.assertContains(response, '')
+ self.assertContains(response, 'name="email"')
+
+ def test_optin_form_renders(self):
+ """Test that opt-in form page renders correctly"""
+ response = self.client.get('/optin/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Opt Back Into Helios Emails')
+ self.assertContains(response, 'Enter your email address to resume receiving emails')
+ self.assertContains(response, ' ')
+ self.assertContains(response, 'name="email"')
+
+ def test_optout_request_sends_email(self):
+ """Test that opt-out request sends confirmation email"""
+ with self.settings(DEFAULT_FROM_EMAIL='test@helios.org'):
+ response = self.client.post('/optout/', {'email': 'test@example.com'})
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/optout/success/')
+
+ # Check that email was sent
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Confirm your opt-out from Helios emails')
+ self.assertIn('test@example.com', mail.outbox[0].to)
+ self.assertIn('/optout/confirm/test@example.com/', mail.outbox[0].body)
+
+ def test_optout_success_page_renders(self):
+ """Test that opt-out success page renders correctly"""
+ response = self.client.get('/optout/success/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Opt-Out Confirmation Sent')
+ self.assertContains(response, 'We have sent you a confirmation email')
+
+ def test_optout_confirm_with_valid_code(self):
+ """Test opt-out confirmation with valid HMAC code"""
+ # Set up session for CSRF token
+ self.client.get('/') # Initialize session
+
+ email = 'test@example.com'
+ code = utils.generate_email_confirmation_code(email, 'optout')
+
+ # GET should show the confirmation form
+ response = self.client.get(f'/optout/confirm/{email}/{code}/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Confirm Opt-Out')
+ self.assertContains(response, email)
+ self.assertContains(response, 'Please confirm that you want to opt out')
+ self.assertContains(response, ' {email} is already receiving emails from Helios')
+
+ def test_optin_confirm_with_valid_code(self):
+ """Test opt-in confirmation with valid HMAC code"""
+ # Set up session for CSRF token
+ self.client.get('/') # Initialize session
+
+ email = 'test@example.com'
+ models.EmailOptOut.add_opt_out(email)
+ code = utils.generate_email_confirmation_code(email, 'optin')
+
+ # GET should show the confirmation form
+ response = self.client.get(f'/optin/confirm/{email}/{code}/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Confirm Opt-In')
+ self.assertContains(response, email)
+ self.assertContains(response, 'Please confirm that you want to resume receiving emails')
+ self.assertContains(response, ' '
+ self.assertTrue(any('voter@example.com' in recipient for recipient in email_message.to))
+ self.assertIn('credentials', email_message.subject.lower())
+ self.assertIn(self.voter.voter_login_id, email_message.body)
+ self.assertIn(self.voter.voter_password, email_message.body)
+
+ def test_post_invalid_voter_still_shows_success_but_no_email(self):
+ """Test that POST with invalid voter ID still shows success but sends no email"""
+ self.client.get(self.get_resend_url())
+ csrf_token = self.client.session.get('csrf_token', '')
+
+ num_messages_before = len(mail.outbox)
+
+ response = self.client.post(self.get_resend_url(), {
+ 'csrf_token': csrf_token,
+ 'voter_id': 'nonexistent_voter'
+ })
+ self.assertStatusCode(response, 200)
+ # Should still show success message to prevent enumeration attacks
+ self.assertContains(response, 'email with your voting credentials has been sent')
+
+ # But no email should have been sent
+ self.assertEqual(len(mail.outbox), num_messages_before)
+
+ def test_post_empty_voter_id_shows_error(self):
+ """Test that POST with empty voter ID shows error"""
+ self.client.get(self.get_resend_url())
+ csrf_token = self.client.session.get('csrf_token', '')
+
+ response = self.client.post(self.get_resend_url(), {
+ 'csrf_token': csrf_token,
+ 'voter_id': ''
+ })
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'valid voter ID')
+
+ def test_resend_blocked_when_election_too_old(self):
+ """Test that password resend is blocked when election tally is too old"""
+ self.election.tallying_finished_at = datetime.datetime.utcnow() - datetime.timedelta(weeks=4)
+ self.election.save()
+
+ response = self.client.get(self.get_resend_url())
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'weeks ago')
+
+ def test_link_appears_on_cast_confirm_page(self):
+ """Test that resend link appears on cast confirm password template"""
+ # We need to check the template content includes the resend link
+ # This is more of an integration test - check the template file
+ from django.template.loader import get_template
+ template = get_template('_castconfirm_password.html')
+ template_source = template.template.source
+ self.assertIn('password-voter-resend', template_source)
+ self.assertIn('target="_blank"', template_source)
+
+ def test_voter_without_email_does_not_send_email(self):
+ """Test that voter without email address doesn't cause an error and no email is sent"""
+ # Create voter without email
+ voter_no_email = models.Voter.objects.create(
+ uuid=str(uuid.uuid4()),
+ election=self.election,
+ voter_name='No Email Voter',
+ voter_login_id='noemailvoter'
+ )
+ voter_no_email.generate_password()
+ voter_no_email.save()
+
+ self.client.get(self.get_resend_url())
+ csrf_token = self.client.session.get('csrf_token', '')
+
+ num_messages_before = len(mail.outbox)
+
+ response = self.client.post(self.get_resend_url(), {
+ 'csrf_token': csrf_token,
+ 'voter_id': 'noemailvoter'
+ })
+ # Should still show success (doesn't reveal that email is missing)
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'email with your voting credentials has been sent')
+
+ # But no email should have been sent since voter has no email
+ self.assertEqual(len(mail.outbox), num_messages_before)
+
+
+class PendingVotesTests(TestCase):
+ """Tests for pending votes detection and tabulation blocking"""
+ fixtures = ['users.json']
+ allow_database_queries = True
+
+ def setUp(self):
+ self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
+ self.election, _ = models.Election.get_or_create(
+ short_name='test-pending-votes',
+ name='Test Pending Votes Election',
+ description='Test Election for Pending Votes',
+ admin=self.user
+ )
+ if not self.election.uuid:
+ self.election.uuid = str(uuid.uuid4())
+ self.election.save()
+
+ self.election.questions = [{"answer_urls": [None, None], "answers": ["Yes", "No"], "choice_type": "approval", "max": 1, "min": 0, "question": "Test?", "result_type": "absolute", "short_name": "Test?", "tally_type": "homomorphic"}]
+ self.election.generate_trustee(views.ELGAMAL_PARAMS)
+ self.election.openreg = True
+ self.election.freeze()
+
+ self.voter = models.Voter.objects.create(
+ uuid=str(uuid.uuid4()),
+ election=self.election,
+ voter_email='voter@example.com',
+ voter_name='Test Voter',
+ voter_login_id='testvoter'
+ )
+
+ def _create_cast_vote(self, verified=False, invalidated=False, quarantined=False):
+ """Helper to create a CastVote with specified state"""
+ cast_vote = models.CastVote(
+ voter=self.voter,
+ vote_hash='fakehash' + str(uuid.uuid4())[:8],
+ quarantined_p=quarantined
+ )
+ cast_vote.save()
+ if verified:
+ cast_vote.verified_at = datetime.datetime.utcnow()
+ cast_vote.save()
+ elif invalidated:
+ cast_vote.invalidated_at = datetime.datetime.utcnow()
+ cast_vote.save()
+ return cast_vote
+
+ def test_num_pending_votes_counts_correctly(self):
+ """Test that num_pending_votes only counts unverified, non-quarantined votes"""
+ self.assertEqual(self.election.num_pending_votes, 0)
+
+ # These should NOT be counted as pending
+ self._create_cast_vote(verified=True)
+ self._create_cast_vote(invalidated=True)
+ self._create_cast_vote(quarantined=True)
+ self.assertEqual(self.election.num_pending_votes, 0)
+
+ # These SHOULD be counted as pending
+ self._create_cast_vote()
+ self._create_cast_vote()
+ self.assertEqual(self.election.num_pending_votes, 2)
+
+ def test_compute_tally_blocked_with_pending_votes(self):
+ """Test that tally is blocked when there are pending votes"""
+ # Create a verified vote so election has cast votes
+ verified = self._create_cast_vote(verified=True)
+ self.voter.vote_hash = verified.vote_hash
+ self.voter.cast_at = verified.cast_at
+ self.voter.save()
+
+ # Create a pending vote
+ self._create_cast_vote()
+
+ # Set up admin session
+ session = self.client.session
+ session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id}
+ session.save()
+
+ url = f'/helios/elections/{self.election.uuid}/compute_tally'
+ self.client.get(url)
+ csrf_token = self.client.session.get('csrf_token', '')
+
+ response = self.client.post(url, {'csrf_token': csrf_token})
+
+ # Should show pending votes message, not redirect
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'still being processed')
+
+ # Election should not have started tallying
+ self.election.refresh_from_db()
+ self.assertIsNone(self.election.tallying_started_at)
+
+
+class UserSearchTests(WebTest):
+ """Test user search functionality for site administrators"""
+ fixtures = ['users.json']
+ allow_database_queries = True
+
+ def setUp(self):
+ """Set up test data"""
+ self.site_admin = auth_models.User.objects.get(user_id='mccio@github.com', user_type='google')
+ self.regular_user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
+ self.fb_user = auth_models.User.objects.filter(user_type='facebook').first()
+
+ # Create test elections
+ self.election1, _ = models.Election.get_or_create(
+ short_name='test-user-search-1',
+ name='Test User Search Election 1',
+ description='Test Election 1',
+ admin=self.regular_user
+ )
+ if not self.election1.uuid:
+ self.election1.uuid = str(uuid.uuid4())
+ self.election1.save()
+
+ self.election2, _ = models.Election.get_or_create(
+ short_name='test-user-search-2',
+ name='Test User Search Election 2',
+ description='Test Election 2',
+ admin=self.fb_user
+ )
+ if not self.election2.uuid:
+ self.election2.uuid = str(uuid.uuid4())
+ self.election2.save()
+
+ # Add regular_user as additional admin to election2
+ self.election2.admins.add(self.regular_user)
+
+ # Add regular_user as voter to election2
+ self.voter = models.Voter.objects.create(
+ uuid=str(uuid.uuid4()),
+ election=self.election2,
+ user=self.regular_user,
+ voter_email=self.regular_user.user_id,
+ voter_name=self.regular_user.name
+ )
+
+ # Add regular_user as trustee to election1
+ self.trustee = models.Trustee.objects.create(
+ election=self.election1,
+ uuid=str(uuid.uuid4()),
+ name=self.regular_user.name,
+ email=self.regular_user.user_id,
+ secret='test-secret',
+ public_key_hash='test-hash'
+ )
+
+ def setup_login(self, user):
+ """Set up session for a user"""
+ self.client.get("/") # Initialize session
+ session = self.client.session
+ session['user'] = {'type': user.user_type, 'user_id': user.user_id}
+ session.save()
+
+ def test_user_search_requires_admin(self):
+ """Test that user search page requires site admin privileges"""
+ # Not logged in - should return 403
+ response = self.client.get('/helios/stats/user-search')
+ self.assertStatusCode(response, 403)
+
+ # Regular user - should return 403
+ self.setup_login(self.regular_user)
+ response = self.client.get('/helios/stats/user-search')
+ self.assertStatusCode(response, 403)
+
+ def test_user_search_accessible_to_site_admin(self):
+ """Test that user search page is accessible to site admin"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'User Search')
+
+ def test_user_search_empty_query(self):
+ """Test user search with no query returns prompt"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Enter a search term')
+
+ def test_user_search_by_name(self):
+ """Test searching for users by name"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=Ben')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Ben Adida')
+ self.assertContains(response, 'ben@adida.net')
+
+ def test_user_search_by_user_id(self):
+ """Test searching for users by user ID"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=ben@adida.net')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Ben Adida')
+ self.assertContains(response, 'ben@adida.net')
+
+ def test_user_search_shows_elections_as_admin(self):
+ """Test that search results show elections where user is admin"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=ben@adida.net')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Elections as Administrator')
+ self.assertContains(response, 'Test User Search Election 1')
+ self.assertContains(response, 'Test User Search Election 2')
+
+ def test_user_search_shows_elections_as_voter(self):
+ """Test that search results show elections where user is voter"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=ben@adida.net')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Elections as Voter')
+ self.assertContains(response, 'Test User Search Election 2')
+
+ def test_user_search_shows_elections_as_trustee(self):
+ """Test that search results show elections where user is trustee"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=ben@adida.net')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Elections as Trustee')
+ self.assertContains(response, 'Test User Search Election 1')
+
+ def test_user_search_no_results(self):
+ """Test user search with query that returns no results"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=nonexistent@example.com')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'No users found')
+
+ def test_user_search_case_insensitive(self):
+ """Test that user search is case insensitive"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=BEN')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Ben Adida')
+
+ def test_user_search_shows_user_type(self):
+ """Test that search results show user type"""
+ self.setup_login(self.site_admin)
+ response = self.client.get('/helios/stats/user-search?q=ben@adida.net')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'User Type')
+ self.assertContains(response, 'google')
+
+ def test_user_search_shows_admin_status(self):
+ """Test that search results show site admin status"""
+ self.setup_login(self.site_admin)
+
+ # Search for site admin
+ response = self.client.get('/helios/stats/user-search?q=mccio')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Site Admin')
+ self.assertContains(response, 'Yes')
+
+ # Search for regular user
+ response = self.client.get('/helios/stats/user-search?q=ben@adida.net')
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'Site Admin')
+ self.assertContains(response, 'No')
+
+
+class VoterUploadRestrictionTests(WebTest):
+ """Tests for voter upload restrictions when election is tallied (issue #455)"""
+ fixtures = ['users.json']
+ allow_database_queries = True
+
+ def setUp(self):
+ self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
+ self.election, _ = models.Election.get_or_create(
+ short_name='test-upload-restriction',
+ name='Test Upload Restriction',
+ description='Test',
+ admin=self.user
+ )
+ if not self.election.uuid:
+ self.election.uuid = str(uuid.uuid4())
+ self.election.save()
+
+ def setup_login(self):
+ self.client.get("/")
+ session = self.client.session
+ session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id}
+ session.save()
+
+ def test_can_modify_voters_allowed_by_default(self):
+ can_modify, reason = self.election.can_modify_voters()
+ self.assertTrue(can_modify)
+ self.assertIsNone(reason)
+
+ def test_can_modify_voters_blocked_when_encrypted_tally_exists(self):
+ # Set in memory only - the method just checks truthiness
+ self.election.encrypted_tally = True
+
+ can_modify, reason = self.election.can_modify_voters()
+ self.assertFalse(can_modify)
+ self.assertEqual(reason, "Election has been tallied")
+
+ def test_can_modify_voters_blocked_when_tallying_started(self):
+ self.election.tallying_started_at = datetime.datetime.utcnow()
+ self.election.save()
+
+ can_modify, reason = self.election.can_modify_voters()
+ self.assertFalse(can_modify)
+ self.assertEqual(reason, "Tallying has started")
+
+ def test_voter_upload_view_returns_403_when_blocked(self):
+ self.setup_login()
+ self.election.tallying_started_at = datetime.datetime.utcnow()
+ self.election.save()
+
+ response = self.client.get("/helios/elections/%s/voters/upload" % self.election.uuid)
+ self.assertStatusCode(response, 403)
+
+ def test_voters_list_shows_disabled_button_when_blocked(self):
+ self.setup_login()
+ self.election.tallying_started_at = datetime.datetime.utcnow()
+ self.election.save()
+
+ response = self.client.get("/helios/elections/%s/voters/list" % self.election.uuid)
+ self.assertStatusCode(response, 200)
+ self.assertContains(response, 'disabled')
+ self.assertContains(response, 'Tallying has started')
+
+
+class VoterDeleteRestrictionTests(WebTest):
+ """Tests for voter deletion restrictions when tallying has begun (issue #470)"""
+ fixtures = ['users.json']
+ allow_database_queries = True
+
+ def setUp(self):
+ self.user = auth_models.User.objects.get(user_id='ben@adida.net', user_type='google')
+ self.election, _ = models.Election.get_or_create(
+ short_name='test-delete-restriction',
+ name='Test Delete Restriction',
+ description='Test',
+ admin=self.user
+ )
+ if not self.election.uuid:
+ self.election.uuid = str(uuid.uuid4())
+ self.election.save()
+ # Create a voter to test deletion
+ self.voter = models.Voter.objects.create(
+ uuid=str(uuid.uuid4()),
+ election=self.election,
+ voter_email='voter@test.com',
+ voter_name='Test Voter'
+ )
+
+ def setup_login(self):
+ self.client.get("/")
+ session = self.client.session
+ session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id}
+ session.save()
+
+ def test_voter_delete_allowed_by_default(self):
+ """Voter deletion should be allowed before tallying starts"""
+ self.setup_login()
+ response = self.client.post("/helios/elections/%s/voters/%s/delete" % (
+ self.election.uuid, self.voter.uuid))
+ # Should redirect (302) on successful deletion
+ self.assertStatusCode(response, 302)
+
+ def test_voter_delete_blocked_when_tallying_started(self):
+ """Voter deletion should be blocked once tallying has started"""
+ self.setup_login()
+ self.election.tallying_started_at = datetime.datetime.utcnow()
+ self.election.save()
+
+ response = self.client.post("/helios/elections/%s/voters/%s/delete" % (
+ self.election.uuid, self.voter.uuid))
+ self.assertStatusCode(response, 403)
+
+ def test_voters_list_shows_delete_button_when_allowed(self):
+ """Voter list should show delete [x] button when deletion is allowed"""
+ self.setup_login()
+
+ response = self.client.get("/helios/elections/%s/voters/list" % self.election.uuid)
+ self.assertStatusCode(response, 200)
+ # Check for the delete link with [x] text
+ self.assertContains(response, '>x]')
+
+ def test_voters_list_hides_delete_button_when_blocked(self):
+ """Voter list should hide delete [x] button when tallying has started"""
+ self.setup_login()
+ self.election.tallying_started_at = datetime.datetime.utcnow()
+ self.election.save()
+
+ response = self.client.get("/helios/elections/%s/voters/list" % self.election.uuid)
+ self.assertStatusCode(response, 200)
+ # Check that the delete link is not present
+ self.assertNotContains(response, '>x]')
+
diff --git a/helios/url_names.py b/helios/url_names.py
new file mode 100644
index 000000000..1b0caac65
--- /dev/null
+++ b/helios/url_names.py
@@ -0,0 +1,28 @@
+from helios import election_url_names as election, stats_url_names as stats
+
+__all__ = [
+ "election", "stats",
+ "COOKIE_TEST", "COOKIE_TEST_2", "COOKIE_NO",
+ "ELECTION_SHORTCUT", "ELECTION_SHORTCUT_VOTE", "CAST_VOTE_SHORTCUT", "CAST_VOTE_FULLHASH_SHORTCUT",
+ "TRUSTEE_LOGIN",
+ "ELECTIONS_PARAMS", "ELECTIONS_VERIFIER", "ELECTIONS_VERIFIER_SINGLE_BALLOT",
+ "ELECTIONS_NEW", "ELECTIONS_ADMINISTERED", "ELECTIONS_VOTED",
+]
+
+COOKIE_TEST="cookie@test"
+COOKIE_TEST_2="cookie@test2"
+COOKIE_NO="cookie@no"
+
+ELECTION_SHORTCUT="shortcut@election"
+ELECTION_SHORTCUT_VOTE="shortcut@election@vote"
+CAST_VOTE_SHORTCUT="shortcut@vote"
+CAST_VOTE_FULLHASH_SHORTCUT="shortcut-fullhash@vote"
+
+TRUSTEE_LOGIN="trustee@login"
+
+ELECTIONS_PARAMS="elections@params"
+ELECTIONS_VERIFIER="elections@verifier"
+ELECTIONS_VERIFIER_SINGLE_BALLOT="elections@verifier@single-ballot"
+ELECTIONS_NEW="elections@new"
+ELECTIONS_ADMINISTERED="elections@administered"
+ELECTIONS_VOTED="elections@voted"
diff --git a/helios/urls.py b/helios/urls.py
index 67c007767..6dff38ce1 100644
--- a/helios/urls.py
+++ b/helios/urls.py
@@ -1,39 +1,36 @@
# -*- coding: utf-8 -*-
-from django.conf.urls import *
+from django.urls import include, path
-from django.conf import settings
+from . import views, url_names as names
-from views import *
-
-urlpatterns = None
-
-urlpatterns = patterns('',
- (r'^autologin$', admin_autologin),
- (r'^testcookie$', test_cookie),
- (r'^testcookie_2$', test_cookie_2),
- (r'^nocookies$', nocookies),
- (r'^stats/', include('helios.stats_urls')),
+urlpatterns = [
+ path('autologin', views.admin_autologin),
+ path('testcookie', views.test_cookie, name=names.COOKIE_TEST),
+ path('testcookie_2', views.test_cookie_2, name=names.COOKIE_TEST_2),
+ path('nocookies', views.nocookies, name=names.COOKIE_NO),
+ path('stats/', include('helios.stats_urls')),
# election shortcut by shortname
- (r'^e/(?P[^/]+)$', election_shortcut),
- (r'^e/(?P[^/]+)/vote$', election_vote_shortcut),
+ path('e/', views.election_shortcut, name=names.ELECTION_SHORTCUT),
+ path('e//vote', views.election_vote_shortcut, name=names.ELECTION_SHORTCUT_VOTE),
# vote shortcut
- (r'^v/(?P[^/]+)$', castvote_shortcut),
+ path('v/', views.castvote_shortcut, name=names.CAST_VOTE_SHORTCUT),
+
+ # vote by hash
+ path('vh/', views.castvote_fullhash_shortcut, name=names.CAST_VOTE_FULLHASH_SHORTCUT),
# trustee login
- (r'^t/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)$', trustee_login),
+ path('t///', views.trustee_login,
+ name=names.TRUSTEE_LOGIN),
# election
- (r'^elections/params$', election_params),
- (r'^elections/verifier$', election_verifier),
- (r'^elections/single_ballot_verifier$', election_single_ballot_verifier),
- (r'^elections/new$', election_new),
- (r'^elections/administered$', elections_administered),
- (r'^elections/voted$', elections_voted),
+ path('elections/params', views.election_params, name=names.ELECTIONS_PARAMS),
+ path('elections/verifier', views.election_verifier, name=names.ELECTIONS_VERIFIER),
+ path('elections/single_ballot_verifier', views.election_single_ballot_verifier, name=names.ELECTIONS_VERIFIER_SINGLE_BALLOT),
+ path('elections/new', views.election_new, name=names.ELECTIONS_NEW),
+ path('elections/administered', views.elections_administered, name=names.ELECTIONS_ADMINISTERED),
+ path('elections/voted', views.elections_voted, name=names.ELECTIONS_VOTED),
- (r'^elections/(?P[^/]+)', include('helios.election_urls')),
-
-)
-
-
+ path('elections/', include('helios.election_urls')),
+]
diff --git a/helios/utils.py b/helios/utils.py
index d053dc11f..86ac6a9b9 100644
--- a/helios/utils.py
+++ b/helios/utils.py
@@ -5,25 +5,19 @@
2005-04-11
"""
-import urllib, re, sys, datetime, urlparse, string
+import datetime
+import hashlib
+import hmac
+import re
+import string
+import urllib.parse
-import boto.ses
+from django.conf import settings
+from helios.crypto.utils import random
# utils from helios_auth, too
from helios_auth.utils import *
-from django.conf import settings
-
-import random, logging
-import hashlib, hmac, base64
-
-def do_hmac(k,s):
- """
- HMAC a value with a key, hex output
- """
- mac = hmac.new(k, s, hashlib.sha1)
- return mac.hexdigest()
-
def split_by_length(str, length, rejoin_with=None):
"""
@@ -48,7 +42,7 @@ def urlencode(str):
if not str:
return ""
- return urllib.quote(str)
+ return urllib.parse.quote(str)
def urlencodeall(str):
"""
@@ -63,11 +57,11 @@ def urldecode(str):
if not str:
return ""
- return urllib.unquote(str)
+ return urllib.parse.unquote(str)
def dictToURLParams(d):
if d:
- return '&'.join([i + '=' + urlencode(v) for i,v in d.items()])
+ return '&'.join([i + '=' + urlencode(v) for i,v in list(d.items())])
else:
return None
##
@@ -97,31 +91,28 @@ def fixup(m):
if text[:2] == "":
try:
if text[:3] == "":
- return unichr(int(text[3:-1], 16))
+ return chr(int(text[3:-1], 16))
else:
- return unichr(int(text[2:-1]))
+ return chr(int(text[2:-1]))
except ValueError:
pass
elif text[:1] == "&":
- import htmlentitydefs
- entity = htmlentitydefs.entitydefs.get(text[1:-1])
+ import html.entities
+ entity = html.entities.entitydefs.get(text[1:-1])
if entity:
if entity[:2] == "":
try:
- return unichr(int(entity[2:-1]))
+ return chr(int(entity[2:-1]))
except ValueError:
pass
else:
- return unicode(entity, "iso-8859-1")
+ return str(entity, "iso-8859-1")
return text # leave as is
- return re.sub("(?s)<[^>]*>|?\w+;", fixup, s)
+ return re.sub(r"(?s)<[^>]*>|?\w+;", fixup, s)
-
-random.seed()
def random_string(length=20, alphabet=None):
- random.seed()
ALPHABET = alphabet or 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
r_string = ''
for i in range(length):
@@ -141,7 +132,7 @@ def get_prefix():
##
def string_to_datetime(str, fmt="%Y-%m-%d %H:%M"):
- if str == None:
+ if str is None:
return None
return datetime.datetime.strptime(str, fmt)
@@ -167,7 +158,7 @@ def one_val_raw_sql(raw_sql, values=[]):
"""
for a simple aggregate
"""
- from django.db import connection, transaction
+ from django.db import connection
cursor = connection.cursor()
cursor.execute(raw_sql, values)
@@ -192,3 +183,41 @@ def lock_row(model, pk):
pass
return row
+
+
+##
+## Email opt-out utilities
+##
+
+import hashlib
+import hmac
+
+
+def hash_email(email):
+ if not email:
+ return None
+
+ normalized_email = email.lower().strip()
+ return hashlib.sha256(normalized_email.encode('utf-8')).hexdigest()
+
+
+def generate_email_confirmation_code(email, action):
+ if not email or not action:
+ return None
+
+ normalized_email = email.lower().strip()
+ message = f"{normalized_email}:{action}"
+
+ return hmac.new(
+ settings.EMAIL_OPTOUT_SECRET.encode('utf-8'),
+ message.encode('utf-8'),
+ hashlib.sha256
+ ).hexdigest()
+
+
+def verify_email_confirmation_code(email, action, code):
+ if not email or not action or not code:
+ return False
+
+ expected_code = generate_email_confirmation_code(email, action)
+ return expected_code == code
diff --git a/helios/view_utils.py b/helios/view_utils.py
index 3e3fb5a81..ed98d0945 100644
--- a/helios/view_utils.py
+++ b/helios/view_utils.py
@@ -4,22 +4,16 @@
Ben Adida (12-30-2008)
"""
-from django.template import Context, Template, loader
-from django.http import HttpResponse, Http404
-from django.shortcuts import render_to_response
-
-import utils
-
-from helios import datatypes
-
+from django.conf import settings
+from django.http import HttpResponse
+from django.shortcuts import render
+from django.template import loader
# nicely update the wrapper function
from functools import update_wrapper
-from helios_auth.security import get_user
-
import helios
-
-from django.conf import settings
+from . import utils
+from helios_auth.security import get_user
##
## BASICS
@@ -33,14 +27,14 @@
##
## template abstraction
##
-def prepare_vars(request, vars):
- vars_with_user = vars.copy()
+def prepare_vars(request, values):
+ vars_with_user = values.copy() if values is not None else {}
vars_with_user['user'] = get_user(request)
-
+
# csrf protection
- if request.session.has_key('csrf_token'):
+ if 'csrf_token' in request.session:
vars_with_user['csrf_token'] = request.session['csrf_token']
-
+
vars_with_user['utils'] = utils
vars_with_user['settings'] = settings
vars_with_user['HELIOS_STATIC'] = '/static/helios/helios'
@@ -50,31 +44,31 @@ def prepare_vars(request, vars):
return vars_with_user
-def render_template(request, template_name, vars = {}, include_user=True):
- t = loader.get_template(template_name + '.html')
-
- vars_with_user = prepare_vars(request, vars)
-
+
+def render_template(request, template_name, values = None, include_user=True):
+ vars_with_user = prepare_vars(request, values)
+
if not include_user:
del vars_with_user['user']
-
- return render_to_response('helios/templates/%s.html' % template_name, vars_with_user)
-
-def render_template_raw(request, template_name, vars={}):
+
+ return render(request, 'helios/templates/%s.html' % template_name, vars_with_user)
+
+
+def render_template_raw(request, template_name, values=None):
t = loader.get_template(template_name)
-
+
# if there's a request, prep the vars, otherwise can't do it.
if request:
- full_vars = prepare_vars(request, vars)
+ full_vars = prepare_vars(request, values)
else:
- full_vars = vars
+ full_vars = values or {}
- c = Context(full_vars)
- return t.render(c)
+ return t.render(context=full_vars, request=request)
def render_json(json_txt):
- return HttpResponse(json_txt, "application/json")
+ return HttpResponse(utils.to_json(json_txt), content_type="application/json")
+
# decorator
def return_json(func):
@@ -85,11 +79,10 @@ def return_json(func):
def convert_to_json(self, *args, **kwargs):
return_val = func(self, *args, **kwargs)
try:
- return render_json(utils.to_json(return_val))
- except Exception, e:
+ return render_json(return_val)
+ except Exception as e:
import logging
logging.error("problem with serialization: " + str(return_val) + " / " + str(e))
raise e
return update_wrapper(convert_to_json,func)
-
diff --git a/helios/views.py b/helios/views.py
index 2959463db..1141ac5de 100644
--- a/helios/views.py
+++ b/helios/views.py
@@ -5,50 +5,46 @@
Ben Adida (ben@adida.net)
"""
-from django.core.urlresolvers import reverse
-from django.core.mail import send_mail
-from django.core.paginator import Paginator
+import base64
+import datetime
+import logging
+import os
+import uuid
+from urllib.parse import urlencode
+
from django.core.exceptions import PermissionDenied
-from django.http import *
+from django.core.paginator import Paginator
from django.db import transaction, IntegrityError
+from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden, HttpResponseBadRequest
+from django.urls import reverse
+from django.views.decorators.http import require_http_methods
-from mimetypes import guess_type
-
-from validate_email import validate_email
-
-import csv, urllib, os, base64
-
-from crypto import algs, electionalgs, elgamal
-from crypto import utils as cryptoutils
-from workflows import homomorphic
-from helios import utils as helios_utils
-from view_utils import *
-
-from helios_auth.security import *
+import helios_auth.url_names as helios_auth_urls
+from helios import utils, VOTERS_EMAIL, VOTERS_UPLOAD, url_names
+from helios_auth import views as auth_views
from helios_auth.auth_systems import AUTH_SYSTEMS, can_list_categories
from helios_auth.models import AuthenticationExpired
-
-from helios import security
-from helios_auth import views as auth_views
-
-import tasks
-
-from security import *
-from helios_auth.security import get_user, save_in_session_across_logouts
-
-import uuid, datetime
-
-from models import *
-
-import forms, signals
+from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts
+from . import datatypes
+from . import forms
+from . import tasks
+from .crypto import algs, electionalgs, elgamal
+from .crypto import utils as cryptoutils
+from .models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot
+from .security import (election_view, election_admin,
+ trustee_check, set_logged_in_trustee,
+ can_create_election, user_can_see_election, get_voter,
+ user_can_admin_election, user_can_feature_election)
+from .view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw
+from .workflows import homomorphic
# Parameters for everything
ELGAMAL_PARAMS = elgamal.Cryptosystem()
# trying new ones from OlivierP
-ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071L
-ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917L
-ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533L
+ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071
+ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917
+ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533
# object ready for serialization
ELGAMAL_PARAMS_LD_OBJECT = datatypes.LDObject.instantiate(ELGAMAL_PARAMS, datatype='legacy/EGParams')
@@ -57,16 +53,16 @@
from django.conf import settings
def get_election_url(election):
- return settings.URL_HOST + reverse(election_shortcut, args=[election.short_name])
+ return settings.URL_HOST + reverse(url_names.ELECTION_SHORTCUT, args=[election.short_name])
def get_election_badge_url(election):
- return settings.URL_HOST + reverse(election_badge, args=[election.uuid])
+ return settings.URL_HOST + reverse(url_names.election.ELECTION_BADGE, args=[election.uuid])
def get_election_govote_url(election):
- return settings.URL_HOST + reverse(election_vote_shortcut, args=[election.short_name])
+ return settings.URL_HOST + reverse(url_names.ELECTION_SHORTCUT_VOTE, args=[election.short_name])
def get_castvote_url(cast_vote):
- return settings.URL_HOST + reverse(castvote_shortcut, args=[cast_vote.vote_tinyhash])
+ return settings.URL_HOST + reverse(url_names.CAST_VOTE_SHORTCUT, args=[cast_vote.vote_tinyhash])
##
@@ -76,8 +72,8 @@ def user_reauth(request, user):
# FIXME: should we be wary of infinite redirects here, and
# add a parameter to prevent it? Maybe.
login_url = "%s%s?%s" % (settings.SECURE_URL_HOST,
- reverse(auth_views.start, args=[user.user_type]),
- urllib.urlencode({'return_url':
+ reverse(helios_auth_urls.AUTH_START, args=[user.user_type]),
+ urlencode({'return_url':
request.get_full_path()}))
return HttpResponseRedirect(login_url)
@@ -116,22 +112,26 @@ def election_single_ballot_verifier(request):
def election_shortcut(request, election_short_name):
election = Election.get_by_short_name(election_short_name)
if election:
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
else:
raise Http404
# a hidden view behind the shortcut that performs the actual perm check
@election_view()
def _election_vote_shortcut(request, election):
- vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(one_election, args=[election.uuid])}))
+ vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
- test_cookie_url = "%s?%s" % (reverse(test_cookie), urllib.urlencode({'continue_url' : vote_url}))
+ test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url}))
return HttpResponseRedirect(test_cookie_url)
def election_vote_shortcut(request, election_short_name):
election = Election.get_by_short_name(election_short_name)
if election:
+ if not election.voting_has_started():
+ return render_template(request, 'election_not_started', {'election': election})
+ if election.voting_has_stopped():
+ return render_template(request, 'election_tallied', {'election': election})
return _election_vote_shortcut(request, election_uuid=election.uuid)
else:
raise Http404
@@ -148,6 +148,14 @@ def castvote_shortcut(request, vote_tinyhash):
return _castvote_shortcut_by_election(request, election_uuid = cast_vote.voter.election.uuid, cast_vote=cast_vote)
+def castvote_fullhash_shortcut(request, vote_hash):
+ try:
+ cast_vote = CastVote.objects.get(vote_hash = vote_hash)
+ except CastVote.DoesNotExist:
+ raise Http404
+
+ return _castvote_shortcut_by_election(request, election_uuid = cast_vote.voter.election.uuid, cast_vote=cast_vote)
+
@trustee_check
def trustee_keygenerator(request, election, trustee):
"""
@@ -196,8 +204,8 @@ def election_new(request):
election_params = dict(election_form.cleaned_data)
# is the short name valid
- if helios_utils.urlencode(election_params['short_name']) == election_params['short_name']:
- election_params['uuid'] = str(uuid.uuid1())
+ if utils.urlencode(election_params['short_name']) == election_params['short_name']:
+ election_params['uuid'] = str(uuid.uuid4())
election_params['cast_url'] = settings.SECURE_URL_HOST + reverse(one_election_cast, args=[election_params['uuid']])
# registration starts closed
@@ -208,7 +216,7 @@ def election_new(request):
try:
election = Election.objects.create(**election_params)
election.generate_trustee(ELGAMAL_PARAMS)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
except IntegrityError:
error = "An election with short name %s already exists" % election_params['short_name']
else:
@@ -241,7 +249,7 @@ def one_election_edit(request, election):
setattr(election, attr_name, clean_data[attr_name])
try:
election.save()
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
except IntegrityError:
error = "An election with short name %s already exists" % clean_data['short_name']
@@ -264,7 +272,7 @@ def one_election_extend(request, election):
election.voting_extended_until = clean_data['voting_extended_until']
election.save()
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
return render_template(request, "election_extend", {'election_form' : election_form, 'election' : election})
@@ -293,8 +301,8 @@ def election_badge(request, election):
@election_view()
def one_election_view(request, election):
user = get_user(request)
- admin_p = security.user_can_admin_election(user, election)
- can_feature_p = security.user_can_feature_election(user, election)
+ admin_p = user_can_admin_election(user, election)
+ can_feature_p = user_can_feature_election(user, election)
notregistered = False
eligible_p = True
@@ -303,9 +311,9 @@ def one_election_view(request, election):
election_badge_url = get_election_badge_url(election)
status_update_message = None
- vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(one_election, args=[election.uuid])}))
+ vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
- test_cookie_url = "%s?%s" % (reverse(test_cookie), urllib.urlencode({'continue_url' : vote_url}))
+ test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url}))
if user:
voter = Voter.get_by_election_and_user(election, user)
@@ -328,13 +336,13 @@ def one_election_view(request, election):
# status update message?
if election.openreg:
if election.voting_has_started:
- status_update_message = u"Vote in %s" % election.name
+ status_update_message = "Vote in %s" % election.name
else:
- status_update_message = u"Register to vote in %s" % election.name
+ status_update_message = "Register to vote in %s" % election.name
# result!
if election.result:
- status_update_message = u"Results are in for %s" % election.name
+ status_update_message = "Results are in for %s" % election.name
trustees = Trustee.get_by_election(election)
@@ -352,20 +360,20 @@ def one_election_view(request, election):
def test_cookie(request):
continue_url = request.GET['continue_url']
request.session.set_test_cookie()
- next_url = "%s?%s" % (reverse(test_cookie_2), urllib.urlencode({'continue_url': continue_url}))
+ next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urlencode({'continue_url': continue_url}))
return HttpResponseRedirect(settings.SECURE_URL_HOST + next_url)
def test_cookie_2(request):
continue_url = request.GET['continue_url']
if not request.session.test_cookie_worked():
- return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(nocookies), urllib.urlencode({'continue_url': continue_url}))))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urlencode({'continue_url': continue_url}))))
request.session.delete_test_cookie()
return HttpResponseRedirect(continue_url)
def nocookies(request):
- retest_url = "%s?%s" % (reverse(test_cookie), urllib.urlencode({'continue_url' : request.GET['continue_url']}))
+ retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : request.GET['continue_url']}))
return render_template(request, 'nocookies', {'retest_url': retest_url})
##
@@ -383,7 +391,7 @@ def list_trustees(request, election):
def list_trustees_view(request, election):
trustees = Trustee.get_by_election(election)
user = get_user(request)
- admin_p = security.user_can_admin_election(user, election)
+ admin_p = user_can_admin_election(user, election)
return render_template(request, 'list_trustees', {'election': election, 'trustees': trustees, 'admin_p':admin_p})
@@ -397,9 +405,9 @@ def new_trustee(request, election):
name = request.POST['name']
email = request.POST['email']
- trustee = Trustee(uuid = str(uuid.uuid1()), election = election, name=name, email=email)
+ trustee = Trustee(uuid = str(uuid.uuid4()), election = election, name=name, email=email)
trustee.save()
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(list_trustees_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_TRUSTEES_VIEW, args=[election.uuid]))
@election_admin(frozen=False)
def new_trustee_helios(request, election):
@@ -407,14 +415,144 @@ def new_trustee_helios(request, election):
Make Helios a trustee of the election
"""
election.generate_trustee(ELGAMAL_PARAMS)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(list_trustees_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_TRUSTEES_VIEW, args=[election.uuid]))
@election_admin(frozen=False)
def delete_trustee(request, election):
trustee = Trustee.get_by_election_and_uuid(election, request.GET['uuid'])
trustee.delete()
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(list_trustees_view, args=[election.uuid]))
-
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_TRUSTEES_VIEW, args=[election.uuid]))
+
+##
+## Election Administrators Management
+##
+
+@election_admin()
+def election_admin_list(request, election):
+ """
+ List all administrators for an election.
+ """
+ user = get_user(request)
+ # Get all admins: the creator plus any additional admins
+ additional_admins = list(election.admins.all())
+
+ return render_template(request, 'election_admins', {
+ 'election': election,
+ 'admin': election.admin,
+ 'additional_admins': additional_admins,
+ 'current_user': user,
+ })
+
+@election_admin()
+def election_admin_add(request, election):
+ """
+ Add a new administrator to an election.
+ GET: show form to add admin
+ POST: add the admin (may show selection if multiple users match)
+ """
+ if request.method == "GET":
+ return render_template(request, 'election_admin_add', {'election': election})
+ else:
+ check_csrf(request)
+ email = request.POST.get('email', '').strip()
+
+ if not email:
+ return render_template(request, 'election_admin_add', {
+ 'election': election,
+ 'error': 'Please enter an email address.'
+ })
+
+ # Check if a specific auth type was selected (when multiple users have same email)
+ selected_auth_type = request.POST.get('auth_type', '').strip()
+ if selected_auth_type:
+ try:
+ new_admin = User.objects.get(user_id=email, user_type=selected_auth_type)
+ except User.DoesNotExist:
+ raise Http404("User not found")
+ else:
+ # Find users by email - they must have logged in to Helios at least once
+ # Note: same email can exist across multiple auth systems (google, facebook, etc.)
+ matching_users = list(User.objects.filter(user_id=email))
+
+ if not matching_users:
+ return render_template(request, 'election_admin_add', {
+ 'election': election,
+ 'error': 'No user found with that email. They must log in to Helios at least once before being added as an administrator.'
+ })
+
+ if len(matching_users) > 1:
+ # Multiple users with same email - let admin choose
+ return render_template(request, 'election_admin_add', {
+ 'election': election,
+ 'matching_users': matching_users,
+ 'email': email,
+ })
+
+ new_admin = matching_users[0]
+
+ # Check if already the creator
+ if new_admin == election.admin:
+ return render_template(request, 'election_admin_add', {
+ 'election': election,
+ 'error': 'This user is already the election creator.'
+ })
+
+ # Check if already an admin
+ if election.admins.filter(pk=new_admin.pk).exists():
+ return render_template(request, 'election_admin_add', {
+ 'election': election,
+ 'error': 'This user is already an administrator.'
+ })
+
+ # Add the new admin
+ election.admins.add(new_admin)
+ election.append_log("Administrator %s (%s) added" % (new_admin.user_id, new_admin.user_type))
+
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_ADMINS_LIST, args=[election.uuid]))
+
+@election_admin()
+def election_admin_remove(request, election):
+ """
+ Remove an administrator from an election.
+ GET: show confirmation form
+ POST: remove the admin
+ """
+ user_email = request.GET.get('email') or request.POST.get('email')
+ user_type = request.GET.get('user_type') or request.POST.get('user_type')
+ current_user = get_user(request)
+
+ if not user_email or not user_type:
+ raise Http404("No user specified")
+
+ try:
+ admin_to_remove = User.objects.get(user_id=user_email, user_type=user_type)
+ except User.DoesNotExist:
+ raise Http404("User not found")
+
+ # Cannot remove the original creator
+ if admin_to_remove == election.admin:
+ return HttpResponseForbidden("Cannot remove the election creator.")
+
+ # Cannot remove yourself
+ if admin_to_remove == current_user:
+ return HttpResponseForbidden("You cannot remove yourself as an administrator.")
+
+ # Check if this user is actually an admin
+ if not election.admins.filter(pk=admin_to_remove.pk).exists():
+ raise Http404("User is not an administrator of this election")
+
+ if request.method == "GET":
+ return render_template(request, 'election_admin_remove', {
+ 'election': election,
+ 'admin_to_remove': admin_to_remove,
+ })
+ else:
+ check_csrf(request)
+ election.admins.remove(admin_to_remove)
+ election.append_log("Administrator %s (%s) removed" % (admin_to_remove.user_id, admin_to_remove.user_type))
+
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_ADMINS_LIST, args=[election.uuid]))
+
def trustee_login(request, election_short_name, trustee_email, trustee_secret):
election = Election.get_by_short_name(election_short_name)
if election:
@@ -423,21 +561,16 @@ def trustee_login(request, election_short_name, trustee_email, trustee_secret):
if trustee:
if trustee.secret == trustee_secret:
set_logged_in_trustee(request, trustee)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(trustee_home, args=[election.uuid, trustee.uuid]))
- else:
- # bad secret, we'll let that redirect to the front page
- pass
- else:
- # no such trustee
- raise Http404
-
- return HttpResponseRedirect(settings.SECURE_URL_HOST + "/")
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_TRUSTEE_HOME, args=[election.uuid, trustee.uuid]))
+ # bad secret or no such trustee
+ raise Http404("Trustee not recognized.")
+ raise Http404("No election {} found.".format(election_short_name))
@election_admin()
def trustee_send_url(request, election, trustee_uuid):
trustee = Trustee.get_by_election_and_uuid(election, trustee_uuid)
- url = settings.SECURE_URL_HOST + reverse(trustee_login, args=[election.short_name, trustee.email, trustee.secret])
+ url = settings.SECURE_URL_HOST + reverse(url_names.TRUSTEE_LOGIN, args=[election.short_name, trustee.email, trustee.secret])
body = """
@@ -451,10 +584,10 @@ def trustee_send_url(request, election, trustee_uuid):
Helios
""" % (election.name, url)
- helios_utils.send_email(settings.SERVER_EMAIL, ["%s <%s>" % (trustee.name, trustee.email)], 'your trustee homepage for %s' % election.name, body)
+ utils.send_email(settings.SERVER_EMAIL, ["%s <%s>" % (trustee.name, trustee.email)], 'your trustee homepage for %s' % election.name, body)
logging.info("URL %s " % url)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(list_trustees_view, args = [election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_TRUSTEES_VIEW, args = [election.uuid]))
@trustee_check
def trustee_home(request, election, trustee):
@@ -476,7 +609,7 @@ def trustee_upload_pk(request, election, trustee):
if not trustee.public_key.verify_sk_proof(trustee.pok, algs.DLog_challenge_generator):
raise Exception("bad pok for this public key")
- trustee.public_key_hash = utils.hash_b64(utils.to_json(trustee.public_key.toJSONDict()))
+ trustee.public_key_hash = cryptoutils.hash_b64(utils.to_json(trustee.public_key.toJSONDict()))
trustee.save()
@@ -487,7 +620,7 @@ def trustee_upload_pk(request, election, trustee):
# oh well, no message sent
pass
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(trustee_home, args=[election.uuid, trustee.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_TRUSTEE_HOME, args=[election.uuid, trustee.uuid]))
##
## Ballot Management
@@ -500,23 +633,9 @@ def get_randomness(request, election):
get some randomness to sprinkle into the sjcl entropy pool
"""
return {
- # back to urandom, it's fine
- "randomness" : base64.b64encode(os.urandom(32))
- #"randomness" : base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
- }
+ "randomness" : base64.b64encode(os.urandom(32)).decode('utf-8')
+ }
-@election_view(frozen=True)
-@return_json
-def encrypt_ballot(request, election):
- """
- perform the ballot encryption given answers_json, a JSON'ified list of list of answers
- (list of list because each question could have a list of answers if more than one.)
- """
- # FIXME: maybe make this just request.POST at some point?
- answers = utils.from_json(request.REQUEST['answers_json'])
- ev = homomorphic.EncryptedVote.fromElectionAndAnswers(election, answers)
- return ev.ld_object.includeRandomness().toJSONDict()
-
@election_view(frozen=True)
def post_audited_ballot(request, election):
if request.method == "POST":
@@ -536,14 +655,14 @@ def one_election_cast(request, election):
on a GET, this is a cancellation, on a POST it's a cast
"""
if request.method == "GET":
- return HttpResponseRedirect("%s%s" % (settings.SECURE_URL_HOST, reverse(one_election_view, args = [election.uuid])))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args = [election.uuid]))
user = get_user(request)
encrypted_vote = request.POST['encrypted_vote']
save_in_session_across_logouts(request, 'encrypted_vote', encrypted_vote)
- return HttpResponseRedirect("%s%s" % (settings.SECURE_URL_HOST, reverse(one_election_cast_confirm, args=[election.uuid])))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_cast_confirm, args=[election.uuid]))
@election_view(allow_logins=True)
def password_voter_login(request, election):
@@ -552,14 +671,20 @@ def password_voter_login(request, election):
"""
# the URL to send the user to after they've logged in
- return_url = request.REQUEST.get('return_url', reverse(one_election_cast_confirm, args=[election.uuid]))
+ if request.method == "GET" and 'return_url' in request.GET:
+ return_url = request.GET['return_url']
+ elif request. method == "POST" and 'return_url' in request.POST:
+ return_url = request.POST['return_url']
+ else:
+ return_url = reverse(one_election_cast_confirm, args=[election.uuid])
+
bad_voter_login = (request.GET.get('bad_voter_login', "0") == "1")
if request.method == "GET":
# if user logged in somehow in the interim, e.g. using the login link for administration,
# then go!
if user_can_see_election(request, election):
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args = [election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args = [election.uuid]))
password_login_form = forms.VoterPasswordForm()
return render_template(request, 'password_voter_login',
@@ -568,13 +693,13 @@ def password_voter_login(request, election):
'password_login_form': password_login_form,
'bad_voter_login' : bad_voter_login})
- login_url = request.REQUEST.get('login_url', None)
+ login_url = request.GET.get('login_url', None)
if not login_url:
# login depending on whether this is a private election
# cause if it's private the login is happening on the front page
if election.private_p:
- login_url = reverse(password_voter_login, args=[election.uuid])
+ login_url = reverse(url_names.election.ELECTION_PASSWORD_VOTER_LOGIN, args=[election.uuid])
else:
login_url = reverse(one_election_cast_confirm, args=[election.uuid])
@@ -592,7 +717,7 @@ def password_voter_login(request, election):
return one_election_cast_confirm(request, election.uuid)
except Voter.DoesNotExist:
- redirect_url = login_url + "?" + urllib.urlencode({
+ redirect_url = login_url + "?" + urlencode({
'bad_voter_login' : '1',
'return_url' : return_url
})
@@ -600,21 +725,81 @@ def password_voter_login(request, election):
return HttpResponseRedirect(settings.SECURE_URL_HOST + redirect_url)
else:
# bad form, bad voter login
- redirect_url = login_url + "?" + urllib.urlencode({
+ redirect_url = login_url + "?" + urlencode({
'bad_voter_login' : '1',
'return_url' : return_url
})
return HttpResponseRedirect(settings.SECURE_URL_HOST + redirect_url)
-
+
return HttpResponseRedirect(settings.SECURE_URL_HOST + return_url)
+@election_view()
+def password_voter_resend(request, election):
+ """
+ Resend the password to a voter who has forgotten it.
+ This page opens in a new window so the voter doesn't lose their ballot.
+ """
+ # Only allow this for elections that use password voters and allow emails
+ if not VOTERS_EMAIL:
+ return render_template(request, 'password_voter_resend', {
+ 'election': election,
+ 'error': 'Email sending is not enabled on this server.'
+ })
+
+ can_send, reason = election.can_send_voter_emails()
+ if not can_send:
+ return render_template(request, 'password_voter_resend', {
+ 'election': election,
+ 'error': reason
+ })
+
+ if request.method == "GET":
+ resend_form = forms.VoterPasswordResendForm()
+ return render_template(request, 'password_voter_resend', {
+ 'election': election,
+ 'resend_form': resend_form
+ })
+
+ # POST - process the form
+ check_csrf(request)
+ resend_form = forms.VoterPasswordResendForm(request.POST)
+
+ if not resend_form.is_valid():
+ return render_template(request, 'password_voter_resend', {
+ 'election': election,
+ 'resend_form': resend_form,
+ 'error': 'Please enter a valid voter ID.'
+ })
+
+ voter_id = resend_form.cleaned_data['voter_id'].strip()
+
+ # Look up the voter by voter_login_id
+ voter = Voter.get_by_election_and_voter_id(election, voter_id)
+
+ # Always show success message to prevent enumeration attacks
+ # but only actually send if voter exists and is a password voter
+ if voter and voter.voter_type == 'password' and voter.voter_email:
+ # Queue the email
+ election_vote_url = get_election_govote_url(election)
+ tasks.single_voter_email.delay(
+ voter_uuid=voter.uuid,
+ subject_template='email/password_resend_subject.txt',
+ body_template='email/password_resend_body.txt',
+ extra_vars={'election_vote_url': election_vote_url},
+ )
+
+ return render_template(request, 'password_voter_resend', {
+ 'election': election,
+ 'sent': True
+ })
+
@election_view()
def one_election_cast_confirm(request, election):
user = get_user(request)
# if no encrypted vote, the user is reloading this page or otherwise getting here in a bad way
- if (not request.session.has_key('encrypted_vote')) or request.session['encrypted_vote'] == None:
+ if ('encrypted_vote' not in request.session) or request.session['encrypted_vote'] is None:
return HttpResponseRedirect(settings.URL_HOST)
# election not frozen or started
@@ -678,8 +863,8 @@ def one_election_cast_confirm(request, election):
# status update this vote
if voter and voter.can_update_status():
- status_update_label = voter.user.update_status_template() % "your smart ballot tracker"
- status_update_message = "I voted in %s - my smart tracker is %s.. #heliosvoting" % (get_election_url(election),cast_vote.vote_hash[:10])
+ status_update_label = voter.user.update_status_template() % "your ballot tracker"
+ status_update_message = "I voted in %s - my ballot tracker is %s.. #heliosvoting" % (get_election_url(election),cast_vote.vote_hash[:10])
else:
status_update_label = None
status_update_message = None
@@ -692,7 +877,7 @@ def one_election_cast_confirm(request, election):
password_only = False
- if auth_systems == None or 'password' in auth_systems:
+ if auth_systems is None or 'password' in auth_systems:
show_password = True
password_login_form = forms.VoterPasswordForm()
@@ -742,7 +927,7 @@ def one_election_cast_confirm(request, election):
# remove the vote from the store
del request.session['encrypted_vote']
- return HttpResponseRedirect("%s%s" % (settings.URL_HOST, reverse(one_election_cast_done, args=[election.uuid])))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_cast_done, args=[election.uuid]))
@election_view()
def one_election_cast_done(request, election):
@@ -762,7 +947,7 @@ def one_election_cast_done(request, election):
# only log out if the setting says so *and* we're dealing
# with a site-wide voter. Definitely remove current_voter
# checking that voter.user != None is needed because voter.user may now be None if voter is password only
- if voter.user == user and voter.user != None:
+ if voter.user == user and voter.user is not None:
logout = settings.LOGOUT_ON_CONFIRMATION
else:
logout = False
@@ -771,17 +956,9 @@ def one_election_cast_done(request, election):
save_in_session_across_logouts(request, 'last_vote_hash', vote_hash)
save_in_session_across_logouts(request, 'last_vote_cv_url', cv_url)
else:
- vote_hash = request.session['last_vote_hash']
- cv_url = request.session['last_vote_cv_url']
+ vote_hash = request.session.get('last_vote_hash', None)
logout = False
- # local logout ensures that there's no more
- # user locally
- # WHY DO WE COMMENT THIS OUT? because we want to force a full logout via the iframe, including
- # from remote systems, just in case, i.e. CAS
- # if logout:
- # auth_views.do_local_logout(request)
-
# remote logout is happening asynchronously in an iframe to be modular given the logout mechanism
# include_user is set to False if logout is happening
return render_template(request, 'cast_done', {'election': election,
@@ -808,8 +985,19 @@ def one_election_bboard(request, election):
UI to show election bboard
"""
after = request.GET.get('after', None)
- offset= int(request.GET.get('offset', 0))
- limit = int(request.GET.get('limit', 50))
+ try:
+ offset = int(request.GET.get('offset', 0))
+ except (ValueError, TypeError):
+ offset = 0
+
+ # Allowed pagination limits
+ ALLOWED_LIMITS = [50, 100, 250, 500]
+ try:
+ limit = int(request.GET.get('limit', 50))
+ except (ValueError, TypeError):
+ limit = 50
+ if limit not in ALLOWED_LIMITS:
+ limit = 50
order_by = 'voter_id'
@@ -818,7 +1006,7 @@ def one_election_bboard(request, election):
order_by = 'alias'
# if there's a specific voter
- if request.GET.has_key('q'):
+ if 'q' in request.GET:
# FIXME: figure out the voter by voter_id
voters = []
else:
@@ -834,7 +1022,7 @@ def one_election_bboard(request, election):
return render_template(request, 'election_bboard', {'election': election, 'voters': voters, 'next_after': next_after,
'offset': offset, 'limit': limit, 'offset_plus_one': offset+1, 'offset_plus_limit': offset+limit,
- 'voter_id': request.GET.get('voter_id', '')})
+ 'voter_id': request.GET.get('voter_id', ''), 'allowed_limits': ALLOWED_LIMITS})
@election_view(frozen=True)
def one_election_audited_ballots(request, election):
@@ -842,7 +1030,7 @@ def one_election_audited_ballots(request, election):
UI to show election audited ballots
"""
- if request.GET.has_key('vote_hash'):
+ if 'vote_hash' in request.GET:
b = AuditedBallot.get(election, request.GET['vote_hash'])
return HttpResponse(b.raw_vote, content_type="text/plain")
@@ -865,16 +1053,12 @@ def one_election_audited_ballots(request, election):
@election_admin()
def voter_delete(request, election, voter_uuid):
"""
- Two conditions under which a voter can be deleted:
- - election is not frozen or
- - election is open reg
+ Voter deletion uses the same restrictions as voter file uploads:
+ blocked once tallying has started or election has been tallied,
+ as modifying voters after vote counting begins would compromise election integrity.
"""
- ## FOR NOW we allow this to see if we can redefine the meaning of "closed reg" to be more flexible
- # if election is frozen and has closed registration
- #if election.frozen_at and (not election.openreg):
- # raise PermissionDenied()
-
- if election.encrypted_tally:
+ can_delete, _ = election.can_modify_voters()
+ if not can_delete:
raise PermissionDenied()
voter = Voter.get_by_election_and_uuid(election, voter_uuid)
@@ -882,27 +1066,52 @@ def voter_delete(request, election, voter_uuid):
if voter.vote_hash:
# send email to voter
- subject = "Vote removed"
+ election_url = get_election_url(election)
+ subject = "Your vote has been removed - %s" % election.name
body = """
+Your vote has been removed by an election administrator.
+
+Election: %s
+Election URL: %s
+
+If you believe this was done in error, please contact the election administrator.
-Your vote were removed from the election "%s".
-
--
-Helios
-""" % (election.name)
- voter.user.send_message(subject, body)
+Helios
+""" % (election.name, election_url)
+ voter.send_message(subject, body)
# log it
- election.append_log("Voter %s/%s and their vote were removed after election frozen" % (voter.voter_type,voter.voter_id))
+ election.append_log("Voter %s/%s and their vote were removed after election was frozen" % (voter.voter_type,voter.voter_id))
elif election.frozen_at:
# log it
- election.append_log("Voter %s/%s removed after election frozen" % (voter.voter_type,voter.voter_id))
+ election.append_log("Voter %s/%s removed after election was frozen" % (voter.voter_type,voter.voter_id))
voter.delete()
return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(voters_list_pretty, args=[election.uuid]))
+@election_admin(frozen=False)
+def voters_clear(request, election):
+ """
+ Clear all voters from the election.
+ Only allowed when election is not frozen.
+ Requires POST with confirmation.
+ """
+ check_csrf(request)
+
+ num_voters = election.num_voters
+
+ if num_voters > 0:
+ # Delete all voters for this election
+ Voter.objects.filter(election=election).delete()
+
+ # Log the action
+ election.append_log("All voters cleared (%d voters removed)" % num_voters)
+
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(voters_list_pretty, args=[election.uuid]))
+
@election_admin(frozen=False)
def one_election_set_reg(request, election):
"""
@@ -923,29 +1132,43 @@ def one_election_set_featured(request, election):
"""
user = get_user(request)
- if not security.user_can_feature_election(user, election):
+ if not user_can_feature_election(user, election):
raise PermissionDenied()
featured_p = bool(int(request.GET['featured_p']))
election.featured_p = featured_p
election.save()
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
@election_admin()
def one_election_archive(request, election):
-
+
archive_p = request.GET.get('archive_p', True)
-
+
if bool(int(archive_p)):
election.archived_at = datetime.datetime.utcnow()
else:
election.archived_at = None
-
+
election.save()
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
-
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
+
+@election_admin()
+@require_http_methods(["POST"])
+def one_election_delete(request, election):
+ """
+ Soft delete an election. The election will be hidden from all users except site admins.
+ Requires POST request with CSRF protection.
+ """
+ check_csrf(request)
+
+ election.soft_delete()
+
+ # After deletion, redirect to admin's election list since the election is now invisible
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.ELECTIONS_ADMINISTERED))
+
@election_admin()
def one_election_copy(request, election):
# FIXME: make this a POST and CSRF protect it
@@ -963,7 +1186,8 @@ def one_election_copy(request, election):
name = "Copy of " + election.name,
election_type = election.election_type,
private_p = election.private_p,
- description = election.description,
+ help_email = election.help_email,
+ description = election.description_bleached,
questions = election.questions,
eligibility = election.eligibility,
openreg = election.openreg,
@@ -978,7 +1202,7 @@ def one_election_copy(request, election):
new_election.generate_trustee(ELGAMAL_PARAMS)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[new_election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[new_election.uuid]))
# changed from admin to view because
# anyone can see the questions, the administration aspect is now
@@ -987,7 +1211,7 @@ def one_election_copy(request, election):
def one_election_questions(request, election):
questions_json = utils.to_json(election.questions)
user = get_user(request)
- admin_p = security.user_can_admin_election(user, election)
+ admin_p = user_can_admin_election(user, election)
return render_template(request, 'election_questions', {'election': election, 'questions_json' : questions_json, 'admin_p': admin_p})
@@ -1017,7 +1241,7 @@ def one_election_register(request, election):
if not voter:
voter = _register_voter(election, user)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
@election_admin(frozen=False)
def one_election_save_questions(request, election):
@@ -1046,7 +1270,7 @@ def one_election_freeze(request, election):
election.freeze()
if get_user(request):
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
else:
return SUCCESS
@@ -1062,13 +1286,25 @@ def one_election_compute_tally(request, election):
tallying is done all at a time now
"""
if not _check_election_tally_type(election):
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view,args=[election.election_id]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW,args=[election.election_id]))
+
+ num_pending_votes = election.num_pending_votes
if request.method == "GET":
- return render_template(request, 'election_compute_tally', {'election': election})
-
+ return render_template(request, 'election_compute_tally', {
+ 'election': election,
+ 'num_pending_votes': num_pending_votes
+ })
+
check_csrf(request)
+ # Prevent tabulation if there are votes still waiting to be verified
+ if num_pending_votes > 0:
+ return render_template(request, 'election_compute_tally', {
+ 'election': election,
+ 'num_pending_votes': num_pending_votes
+ })
+
if not election.voting_ended_at:
election.voting_ended_at = datetime.datetime.utcnow()
@@ -1077,18 +1313,18 @@ def one_election_compute_tally(request, election):
tasks.election_compute_tally.delay(election_id = election.id)
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view,args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW,args=[election.uuid]))
@trustee_check
def trustee_decrypt_and_prove(request, election, trustee):
- if not _check_election_tally_type(election) or election.encrypted_tally == None:
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view,args=[election.uuid]))
+ if not _check_election_tally_type(election) or election.encrypted_tally is None:
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW,args=[election.uuid]))
return render_template(request, 'trustee_decrypt_and_prove', {'election': election, 'trustee': trustee})
@election_view(frozen=True)
def trustee_upload_decryption(request, election, trustee_uuid):
- if not _check_election_tally_type(election) or election.encrypted_tally == None:
+ if not _check_election_tally_type(election) or election.encrypted_tally is None:
return FAILURE
trustee = Trustee.get_by_election_and_uuid(election, trustee_uuid)
@@ -1129,9 +1365,9 @@ def release_result(request, election):
election.save()
if request.POST.get('send_email', ''):
- return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urllib.urlencode({'template': 'result'})))
+ return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urlencode({'template': 'result'})))
else:
- return HttpResponseRedirect("%s" % (settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid])))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
# if just viewing the form or the form is not valid
return render_template(request, 'release_result', {'election': election})
@@ -1150,7 +1386,7 @@ def combine_decryptions(request, election):
election.combine_decryptions()
election.save()
- return HttpResponseRedirect("%s" % (settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid])))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
# if just viewing the form or the form is not valid
return render_template(request, 'combine_decryptions', {'election': election})
@@ -1158,7 +1394,7 @@ def combine_decryptions(request, election):
@election_admin(frozen=True)
def one_election_set_result_and_proof(request, election):
if election.tally_type != "homomorphic" or election.encrypted_tally == None:
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view,args=[election.election_id]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.election_id]))
# FIXME: check csrf
@@ -1167,7 +1403,7 @@ def one_election_set_result_and_proof(request, election):
election.save()
if get_user(request):
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
else:
return SUCCESS
@@ -1191,7 +1427,7 @@ def voters_list_pretty(request, election):
order_by = 'alias'
user = get_user(request)
- admin_p = security.user_can_admin_election(user, election)
+ admin_p = user_can_admin_election(user, election)
categories = None
eligibility_category_id = None
@@ -1204,14 +1440,22 @@ def voters_list_pretty(request, election):
return user_reauth(request, user)
# files being processed
- voter_files = election.voterfile_set.all()
+ voter_files = election.voterfile_set.all().order_by('-uploaded_at')
# load a bunch of voters
# voters = Voter.get_by_election(election, order_by=order_by)
voters = Voter.objects.filter(election = election).order_by(order_by).defer('vote')
if q != '':
- if election.use_voter_aliases:
+ from django.db.models import Q
+
+ if admin_p:
+ # Admins can search all fields
+ query = Q(voter_name__icontains=q) | Q(voter_email__icontains=q) | Q(voter_login_id__icontains=q)
+ if election.use_voter_aliases:
+ query |= Q(alias__icontains=q)
+ voters = voters.filter(query)
+ elif election.use_voter_aliases:
voters = voters.filter(alias__icontains = q)
else:
voters = voters.filter(voter_name__icontains = q)
@@ -1220,17 +1464,134 @@ def voters_list_pretty(request, election):
voters_page = voter_paginator.page(page)
total_voters = voter_paginator.count
-
- return render_template(request, 'voters_list',
+
+ # Check if voter emails can be sent
+ can_send_emails, email_disabled_reason = election.can_send_voter_emails()
+
+ # Check if voter modifications (uploads, deletions) are allowed
+ can_modify_voters, modify_voters_disabled_reason = election.can_modify_voters()
+
+ return render_template(request, 'voters_list',
{'election': election, 'voters_page': voters_page,
- 'voters': voters_page.object_list, 'admin_p': admin_p,
- 'email_voters': helios.VOTERS_EMAIL,
+ 'voters': voters_page.object_list, 'admin_p': admin_p,
+ 'email_voters': VOTERS_EMAIL,
+ 'can_send_emails': can_send_emails,
+ 'email_disabled_reason': email_disabled_reason,
'limit': limit, 'total_voters': total_voters,
- 'upload_p': helios.VOTERS_UPLOAD, 'q' : q,
+ 'upload_p': VOTERS_UPLOAD,
+ 'can_modify_voters': can_modify_voters,
+ 'modify_voters_disabled_reason': modify_voters_disabled_reason,
+ 'q' : q,
'voter_files': voter_files,
'categories': categories,
'eligibility_category_id' : eligibility_category_id})
+@election_view()
+def voters_download_csv(request, election):
+ """
+ Download the list of voters as CSV, showing only the fields visible to the current user
+ """
+ import csv
+ from django.http import HttpResponse
+
+ user = get_user(request)
+ admin_p = user_can_admin_election(user, election)
+
+ # Get all voters (no pagination for CSV export)
+ order_by = 'alias' if election.use_voter_aliases else 'user__user_id'
+ voters = Voter.objects.filter(election=election).order_by(order_by).defer('vote')
+
+ # Apply search filter if provided
+ q = request.GET.get('q', '')
+ if q:
+ from django.db.models import Q
+
+ if admin_p:
+ # Admins can search all fields
+ query = Q(voter_name__icontains=q) | Q(voter_email__icontains=q) | Q(voter_login_id__icontains=q)
+ if election.use_voter_aliases:
+ query |= Q(alias__icontains=q)
+ voters = voters.filter(query)
+ elif election.use_voter_aliases:
+ voters = voters.filter(alias__icontains=q)
+ else:
+ voters = voters.filter(voter_name__icontains=q)
+
+ # Create the HttpResponse object with CSV header
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = f'attachment; filename="voters_{election.short_name}_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
+
+ writer = csv.writer(response)
+
+ # Write headers based on what's visible to the user
+ headers = []
+ if admin_p:
+ headers.extend(['Login', 'Email Address'])
+
+ if admin_p or not election.use_voter_aliases:
+ headers.append('Name')
+ headers.append('Voter Type')
+
+ if election.use_voter_aliases:
+ headers.append('Alias')
+
+ headers.append('Smart Ballot Tracker')
+ headers.append('Vote Cast At')
+
+ writer.writerow(headers)
+
+ # Write voter data
+ for voter in voters:
+ row = []
+
+ if admin_p:
+ row.append(voter.voter_login_id)
+ row.append(voter.voter_email)
+
+ if admin_p or not election.use_voter_aliases:
+ row.append(voter.name)
+ row.append(voter.voter_type)
+
+ if election.use_voter_aliases:
+ row.append(voter.alias)
+
+ row.append(voter.vote_hash if voter.vote_hash else '')
+ row.append(voter.cast_at.strftime('%Y-%m-%d %H:%M:%S') if voter.cast_at else '')
+
+ writer.writerow(row)
+
+ return response
+
+@election_admin()
+def election_log_download_csv(request, election):
+ """
+ Download the election log as CSV (admin only)
+ """
+ import csv
+ from django.http import HttpResponse
+ from .models import ElectionLog
+
+ # Get all log entries ordered by timestamp (oldest first for chronological reading)
+ logs = ElectionLog.objects.filter(election=election).order_by('at')
+
+ # Create the HttpResponse object with CSV header
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = f'attachment; filename="election_log_{election.short_name}_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
+
+ writer = csv.writer(response)
+
+ # Write header row
+ writer.writerow(['Timestamp', 'Event'])
+
+ # Write log entries
+ for log in logs:
+ writer.writerow([
+ log.at.strftime('%Y-%m-%d %H:%M:%S') if log.at else '',
+ log.log
+ ])
+
+ return response
+
@election_admin()
def voters_eligibility(request, election):
"""
@@ -1270,15 +1631,14 @@ def voters_eligibility(request, election):
@election_admin()
def voters_upload(request, election):
"""
- Upload a CSV of password-based voters with
- voter_id, email, name
-
- name and email are needed only if voter_type is static
+ Upload a CSV of voters with
+ voter_type, voter_id, optional_additional_params (e.g. email, name)
"""
- ## TRYING this: allowing voters upload by admin when election is frozen
- #if election.frozen_at and not election.openreg:
- # raise PermissionDenied()
+ # don't allow voter upload when election is tallied
+ can_upload, reason = election.can_modify_voters()
+ if not can_upload:
+ raise PermissionDenied()
if request.method == "GET":
return render_template(request, 'voters_upload', {'election': election, 'error': request.GET.get('e',None)})
@@ -1292,7 +1652,7 @@ def voters_upload(request, election):
return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(voters_list_pretty, args=[election.uuid]))
else:
# we need to confirm
- if request.FILES.has_key('voters_file'):
+ if 'voters_file' in request.FILES:
voters_file = request.FILES['voters_file']
voter_file_obj = election.add_voters_file(voters_file)
@@ -1303,17 +1663,15 @@ def voters_upload(request, election):
# import the first few lines to check
try:
voters = [v for v in voter_file_obj.itervoters()][:5]
- except:
+ if len(voters) == 0:
+ raise Exception("no valid lines found in voter file")
+ except Exception as e:
voters = []
- problems.append("your CSV file could not be processed. Please check that it is a proper CSV file.")
-
- # check if voter emails look like emails
- if False in [validate_email(v['email']) for v in voters]:
- problems.append("those don't look like correct email addresses. Are you sure you uploaded a file with email address as second field?")
+ problems.append("your CSV file could not be processed because %s" % str(e))
return render_template(request, 'voters_upload_confirm', {'election': election, 'voters': voters, 'problems': problems})
else:
- return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urllib.urlencode({'e':'no voter file specified, try again'})))
+ return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urlencode({'e':'no voter file specified, try again'})))
@election_admin()
def voters_upload_cancel(request, election):
@@ -1326,12 +1684,18 @@ def voters_upload_cancel(request, election):
vf.delete()
del request.session['voter_file_id']
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
@election_admin(frozen=True)
def voters_email(request, election):
- if not helios.VOTERS_EMAIL:
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ if not VOTERS_EMAIL:
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
+
+ # Check if voter emails can be sent for this election
+ can_send, reason = election.can_send_voter_emails()
+ if not can_send:
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
+
TEMPLATES = [
('vote', 'Time to Vote'),
('simple', 'Simple'),
@@ -1339,14 +1703,16 @@ def voters_email(request, election):
('result', 'Election Result')
]
- template = request.REQUEST.get('template', 'vote')
+ template = request.GET.get('template', 'vote')
if not template in [t[0] for t in TEMPLATES]:
raise Exception("bad template")
- voter_id = request.REQUEST.get('voter_id', None)
+ voter_id = request.GET.get('voter_id', None)
if voter_id:
voter = Voter.get_by_election_and_voter_id(election, voter_id)
+ if not voter:
+ raise Exception("Voter not found")
else:
voter = None
@@ -1387,8 +1753,7 @@ def voters_email(request, election):
'custom_subject' : email_form.cleaned_data['subject'],
'custom_message' : email_form.cleaned_data['body'],
'election_vote_url' : election_vote_url,
- 'election_url' : election_url,
- 'election' : election
+ 'election_url' : election_url
}
voter_constraints_include = None
@@ -1408,7 +1773,7 @@ def voters_email(request, election):
tasks.voters_email.delay(election_id = election.id, subject_template = subject_template, body_template = body_template, extra_vars = extra_vars, voter_constraints_include = voter_constraints_include, voter_constraints_exclude = voter_constraints_exclude)
# this batch process is all async, so we can return a nice note
- return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(one_election_view, args=[election.uuid]))
+ return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
return render_template(request, "voters_email", {
'email_form': email_form, 'election': election,
@@ -1471,9 +1836,9 @@ def ballot_list(request, election):
and optionally take a after parameter.
"""
limit = after = None
- if request.GET.has_key('limit'):
+ if 'limit' in request.GET:
limit = int(request.GET['limit'])
- if request.GET.has_key('after'):
+ if 'after' in request.GET:
after = datetime.datetime.strptime(request.GET['after'], '%Y-%m-%d %H:%M:%S')
voters = Voter.get_by_election(election, cast=True, order_by='cast_at', limit=limit, after=after)
@@ -1482,5 +1847,251 @@ def ballot_list(request, election):
return [v.last_cast_vote().ld_object.short.toDict(complete=True) for v in voters]
+##
+## Email opt-out/opt-in views
+##
+
+from django.views.decorators.http import require_http_methods
+from django.views.decorators.csrf import csrf_exempt
+from django.core.mail import send_mail
+from django.conf import settings
+from validate_email import validate_email
+from .models import EmailOptOut
+
+
+def get_client_ip(request):
+ """Get client IP address from request"""
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0]
+ else:
+ ip = request.META.get('REMOTE_ADDR')
+ return ip
+
+
+@require_http_methods(["GET", "POST"])
+def optout_form(request):
+ """Show the opt-out form and process opt-out requests"""
+ if request.method == "GET":
+ return render_template(request, 'optout_form', {
+ 'action': 'optout',
+ 'title': 'Opt Out of Helios Emails',
+ 'description': 'Enter your email address to stop receiving all emails from Helios voting system.'
+ })
+
+ # POST: Process opt-out request
+ email = request.POST.get('email', '').strip()
+
+ if not email:
+ return render_template(request, 'optout_form', {
+ 'action': 'optout',
+ 'title': 'Opt Out of Helios Emails',
+ 'description': 'Enter your email address to stop receiving all emails from Helios voting system.',
+ 'error': 'Email address is required'
+ })
+
+ if not validate_email(email):
+ return render_template(request, 'optout_form', {
+ 'action': 'optout',
+ 'title': 'Opt Out of Helios Emails',
+ 'description': 'Enter your email address to stop receiving all emails from Helios voting system.',
+ 'error': 'Invalid email address'
+ })
+
+ # Generate confirmation code
+ confirmation_code = utils.generate_email_confirmation_code(email, 'optout')
+
+ # Send confirmation email
+ subject = "Confirm your opt-out from Helios emails"
+ confirmation_path = reverse('optout_confirm', kwargs={'email': email, 'code': confirmation_code})
+ confirmation_url = request.build_absolute_uri(confirmation_path)
+
+ body = f"""
+Please confirm that you want to opt out of all Helios voting system emails.
+
+Click this link to confirm: {confirmation_url}
+
+If you did not request this, please ignore this email.
+
+--
+Helios Voting System
+"""
+
+ try:
+ send_mail(
+ subject,
+ body,
+ settings.DEFAULT_FROM_EMAIL,
+ [email],
+ fail_silently=False
+ )
+ return HttpResponseRedirect(reverse('optout_success'))
+ except Exception as e:
+ return render_template(request, 'optout_form', {
+ 'action': 'optout',
+ 'title': 'Opt Out of Helios Emails',
+ 'description': 'Enter your email address to stop receiving all emails from Helios voting system.',
+ 'error': f'Failed to send confirmation email: {str(e)}'
+ })
+
+
+@require_http_methods(["GET"])
+def optout_success(request):
+ """Show opt-out success page"""
+ return render_template(request, 'optout_success', {
+ 'action': 'optout',
+ 'title': 'Opt-Out Confirmation Sent',
+ 'message': 'We have sent you a confirmation email. Please click the link in the email to complete your opt-out request.'
+ })
+
+
+@require_http_methods(["GET", "POST"])
+def optout_confirm(request, email, code):
+ """Confirm opt-out with HMAC verification"""
+ if not utils.verify_email_confirmation_code(email, 'optout', code):
+ raise Http404("Invalid confirmation link")
+
+ if request.method == "GET":
+ # Show confirmation form
+ return render_template(request, 'optout_confirm_form', {
+ 'action': 'optout',
+ 'title': 'Confirm Opt-Out',
+ 'description': 'Please confirm that you want to opt out of all Helios voting system emails.',
+ 'email': email
+ })
+
+ # POST: Perform the opt-out action
+ check_csrf(request)
+
+ # Add to opt-out list
+ user_agent = request.META.get('HTTP_USER_AGENT', '')
+ ip_address = get_client_ip(request)
+
+ EmailOptOut.add_opt_out(email, user_agent, ip_address)
+
+ return render_template(request, 'optout_confirmed', {
+ 'action': 'optout',
+ 'title': 'Successfully Opted Out',
+ 'message': f'The email address {email} has been successfully opted out of all Helios emails.',
+ 'email': email
+ })
+
+
+@require_http_methods(["GET", "POST"])
+def optin_form(request):
+ """Show the opt-in form and process opt-in requests"""
+ if request.method == "GET":
+ return render_template(request, 'optout_form', {
+ 'action': 'optin',
+ 'title': 'Opt Back Into Helios Emails',
+ 'description': 'Enter your email address to resume receiving emails from Helios voting system.'
+ })
+
+ # POST: Process opt-in request
+ email = request.POST.get('email', '').strip()
+
+ if not email:
+ return render_template(request, 'optout_form', {
+ 'action': 'optin',
+ 'title': 'Opt Back Into Helios Emails',
+ 'description': 'Enter your email address to resume receiving emails from Helios voting system.',
+ 'error': 'Email address is required'
+ })
+
+ if not validate_email(email):
+ return render_template(request, 'optout_form', {
+ 'action': 'optin',
+ 'title': 'Opt Back Into Helios Emails',
+ 'description': 'Enter your email address to resume receiving emails from Helios voting system.',
+ 'error': 'Invalid email address'
+ })
+
+ # Check if email is actually opted out
+ if not EmailOptOut.is_opted_out(email):
+ return render_template(request, 'optout_not_opted_out', {'email': email})
+
+ # Generate confirmation code
+ confirmation_code = utils.generate_email_confirmation_code(email, 'optin')
+
+ # Send confirmation email
+ subject = "Confirm your opt-in to Helios emails"
+ confirmation_path = reverse('optin_confirm', kwargs={'email': email, 'code': confirmation_code})
+ confirmation_url = request.build_absolute_uri(confirmation_path)
+
+ body = f"""
+Please confirm that you want to opt back in to Helios voting system emails.
+
+Click this link to confirm: {confirmation_url}
+
+If you did not request this, please ignore this email.
+
+--
+Helios Voting System
+"""
+
+ try:
+ send_mail(
+ subject,
+ body,
+ settings.DEFAULT_FROM_EMAIL,
+ [email],
+ fail_silently=False
+ )
+ return HttpResponseRedirect(reverse('optin_success'))
+ except Exception as e:
+ return render_template(request, 'optout_form', {
+ 'action': 'optin',
+ 'title': 'Opt Back Into Helios Emails',
+ 'description': 'Enter your email address to resume receiving emails from Helios voting system.',
+ 'error': f'Failed to send confirmation email: {str(e)}'
+ })
+
+
+@require_http_methods(["GET"])
+def optin_success(request):
+ """Show opt-in success page"""
+ return render_template(request, 'optout_success', {
+ 'action': 'optin',
+ 'title': 'Opt-In Confirmation Sent',
+ 'message': 'We have sent you a confirmation email. Please click the link in the email to complete your opt-in request.'
+ })
+
+
+@require_http_methods(["GET", "POST"])
+def optin_confirm(request, email, code):
+ """Confirm opt-in with HMAC verification"""
+ if not utils.verify_email_confirmation_code(email, 'optin', code):
+ raise Http404("Invalid confirmation link")
+
+ if request.method == "GET":
+ # Check if email is actually opted out before showing the form
+ if not EmailOptOut.is_opted_out(email):
+ return render_template(request, 'optout_not_opted_out', {'email': email})
+
+ # Show confirmation form
+ return render_template(request, 'optout_confirm_form', {
+ 'action': 'optin',
+ 'title': 'Confirm Opt-In',
+ 'description': 'Please confirm that you want to resume receiving emails from Helios voting system.',
+ 'email': email
+ })
+
+ # POST: Perform the opt-in action
+ check_csrf(request)
+
+ # Remove from opt-out list
+ removed = EmailOptOut.remove_opt_out(email)
+
+ if not removed:
+ return render_template(request, 'optout_not_opted_out', {'email': email})
+
+ return render_template(request, 'optout_confirmed', {
+ 'action': 'optin',
+ 'title': 'Successfully Opted Back In',
+ 'message': f'The email address {email} has been successfully opted back in to Helios emails.',
+ 'email': email
+ })
+
+
diff --git a/helios/widgets.py b/helios/widgets.py
index 7eff891c1..01b0141d8 100644
--- a/helios/widgets.py
+++ b/helios/widgets.py
@@ -5,14 +5,14 @@
from django import forms
from django.db import models
from django.template.loader import render_to_string
-from django.forms.widgets import Select, MultiWidget, DateInput, TextInput, Widget
-from django.forms.extras.widgets import SelectDateWidget
+from django.forms.widgets import Select, MultiWidget, DateInput, TextInput, Widget, SelectDateWidget
+from django.forms.utils import flatatt
from time import strftime
import re
from django.utils.safestring import mark_safe
-__all__ = ('SelectTimeWidget', 'SplitSelectDateTimeWidget')
+__all__ = ('SelectTimeWidget', 'SplitSelectDateTimeWidget', 'DateTimeLocalWidget')
# Attempt to match many time formats:
# Example: "12:34:56 P.M." matches:
@@ -36,38 +36,39 @@ class SelectTimeWidget(Widget):
Also allows user-defined increments for minutes/seconds
"""
+ template_name = ''
hour_field = '%s_hour'
minute_field = '%s_minute'
meridiem_field = '%s_meridiem'
twelve_hr = False # Default to 24hr.
-
+
def __init__(self, attrs=None, hour_step=None, minute_step=None, twelve_hr=False):
"""
hour_step, minute_step, second_step are optional step values for
for the range of values for the associated select element
twelve_hr: If True, forces the output to be in 12-hr format (rather than 24-hr)
"""
- self.attrs = attrs or {}
-
+ super(SelectTimeWidget, self).__init__(attrs)
+
if twelve_hr:
self.twelve_hr = True # Do 12hr (rather than 24hr)
self.meridiem_val = 'a.m.' # Default to Morning (A.M.)
if hour_step and twelve_hr:
- self.hours = range(1,13,hour_step)
+ self.hours = list(range(1,13,hour_step))
elif hour_step: # 24hr, with stepping.
- self.hours = range(0,24,hour_step)
+ self.hours = list(range(0,24,hour_step))
elif twelve_hr: # 12hr, no stepping
- self.hours = range(1,13)
+ self.hours = list(range(1,13))
else: # 24hr, no stepping
- self.hours = range(0,24)
+ self.hours = list(range(0,24))
if minute_step:
- self.minutes = range(0,60,minute_step)
+ self.minutes = list(range(0,60,minute_step))
else:
- self.minutes = range(0,60)
+ self.minutes = list(range(0,60))
- def render(self, name, value, attrs=None):
+ def render(self, name, value, attrs=None, renderer=None):
try: # try to get time values from a datetime.time object (value)
hour_val, minute_val = value.hour, value.minute
if self.twelve_hr:
@@ -77,10 +78,10 @@ def render(self, name, value, attrs=None):
self.meridiem_val = 'a.m.'
except AttributeError:
hour_val = minute_val = 0
- if isinstance(value, basestring):
+ if isinstance(value, str):
match = RE_TIME.match(value)
if match:
- time_groups = match.groups();
+ time_groups = match.groups()
hour_val = int(time_groups[HOURS]) % 24 # force to range(0-24)
minute_val = int(time_groups[MINUTES])
@@ -113,11 +114,11 @@ def render(self, name, value, attrs=None):
# For times to get displayed correctly, the values MUST be converted to unicode
# When Select builds a list of options, it checks against Unicode values
- hour_val = u"%.2d" % hour_val
- minute_val = u"%.2d" % minute_val
+ hour_val = "%.2d" % hour_val
+ minute_val = "%.2d" % minute_val
hour_choices = [("%.2d"%i, "%.2d"%i) for i in self.hours]
- local_attrs = self.build_attrs(id=self.hour_field % id_)
+ local_attrs = self.build_attrs({'id': self.hour_field % id_})
select_html = Select(choices=hour_choices).render(self.hour_field % name, hour_val, local_attrs)
output.append(select_html)
@@ -137,7 +138,7 @@ def render(self, name, value, attrs=None):
select_html = Select(choices=meridiem_choices).render(self.meridiem_field % name, self.meridiem_val, local_attrs)
output.append(select_html)
- return mark_safe(u'\n'.join(output))
+ return mark_safe('\n'.join(output))
def id_for_label(self, id_):
return '%s_hour' % id_
@@ -169,6 +170,8 @@ class SplitSelectDateTimeWidget(MultiWidget):
This class combines SelectTimeWidget and SelectDateWidget so we have something
like SpliteDateTimeWidget (in django.forms.widgets), but with Select elements.
"""
+ template_name = ''
+
def __init__(self, attrs=None, hour_step=None, minute_step=None, twelve_hr=None, years=None):
""" pass all these parameters to their respective widget constructors..."""
widgets = (SelectDateWidget(attrs=attrs, years=years), SelectTimeWidget(attrs=attrs, hour_step=hour_step, minute_step=minute_step, twelve_hr=twelve_hr))
@@ -177,7 +180,7 @@ def __init__(self, attrs=None, hour_step=None, minute_step=None, twelve_hr=None,
# See https://stackoverflow.com/questions/4324676/django-multiwidget-subclass-not-calling-decompress
def value_from_datadict(self, data, files, name):
if data.get(name, None) is None:
- return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+ return [widget.value_from_datadict(data, files, name ) for widget in self.widgets]
return self.decompress(data.get(name, None))
def decompress(self, value):
@@ -185,13 +188,66 @@ def decompress(self, value):
return [value.date(), value.time().replace(microsecond=0)]
return [None, None]
- def format_output(self, rendered_widgets):
+ def compress(self, data_list):
"""
- Given a list of rendered widgets (as strings), it inserts an HTML
- linebreak between them.
-
- Returns a Unicode string representing the HTML for the whole lot.
+ Takes the values from the MultiWidget and passes them as a
+ list to this function. This function needs to compress the
+ list into a single object in order to be correctly rendered by the widget.
+ For instace, django.forms.widgets.SelectDateWidget.format_value(value)
+ expects a date object or a string, not a list.
+ This method was taken from helios/fields.py
"""
- rendered_widgets.insert(-1, ' ')
- return u''.join(rendered_widgets)
+ if data_list:
+ import datetime
+ if not (data_list[0] and data_list[1]):
+ return None
+ try:
+ return datetime.datetime.combine(*data_list)
+ except:
+ # badly formed date
+ return None
+ return None
+
+ def render(self, name, value, attrs=None, renderer=None):
+ value = self.compress(value)
+ rendered_widgets = list(widget.render(name, value, attrs=attrs, renderer=renderer) for widget in self.widgets)
+ return mark_safe(' '.join(rendered_widgets))
+
+
+class DateTimeLocalWidget(Widget):
+ """
+ A modern datetime picker widget using HTML5 datetime-local input.
+ Provides a native, user-friendly datetime picker interface.
+ """
+ template_name = ''
+
+ class Media:
+ css = {
+ 'all': ('helios/datetime-local.css', 'helios/timezone-display.css',)
+ }
+ js = ('helios/timezone-display.js',)
+
+ def __init__(self, attrs=None):
+ super(DateTimeLocalWidget, self).__init__(attrs)
+ # Set default attributes
+ self.attrs['type'] = 'datetime-local'
+ self.attrs.setdefault('class', 'helios-datetime-input')
+ self.attrs.setdefault('placeholder', 'YYYY-MM-DDTHH:MM')
+
+ def render(self, name, value, attrs=None, renderer=None):
+ if value is None:
+ value = ''
+ elif hasattr(value, 'strftime'):
+ # Convert datetime to the format expected by datetime-local input
+ # Format: YYYY-MM-DDTHH:MM
+ value = value.strftime('%Y-%m-%dT%H:%M')
+
+ # Merge self.attrs with provided attrs and extra attributes
+ final_attrs = {**self.attrs, **(attrs or {}), 'name': name, 'type': 'datetime-local'}
+ if value != '':
+ final_attrs['value'] = value
+
+ return mark_safe(' ' % flatatt(final_attrs))
+ def value_from_datadict(self, data, files, name):
+ return data.get(name, None)
diff --git a/helios/workflows/homomorphic.py b/helios/workflows/homomorphic.py
index 63e2d0c42..9c8039679 100644
--- a/helios/workflows/homomorphic.py
+++ b/helios/workflows/homomorphic.py
@@ -6,11 +6,8 @@
reworked 2011-01-09
"""
-from helios.crypto import algs, utils
import logging
-import uuid
-import datetime
-from helios import models
+from helios.crypto import algs
from . import WorkflowObject
class EncryptedAnswer(WorkflowObject):
@@ -72,10 +69,10 @@ def verify(self, pk, min=0, max=1):
return False
# compute homomorphic sum if needed
- if max != None:
+ if max is not None:
homomorphic_sum = choice * homomorphic_sum
- if max != None:
+ if max is not None:
# determine possible plaintexts for the sum
sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
@@ -114,7 +111,7 @@ def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
# min and max for number of answers, useful later
min_answers = 0
- if question.has_key('min'):
+ if 'min' in question:
min_answers = question['min']
max_answers = question['max']
@@ -128,7 +125,7 @@ def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
num_selected_answers += 1
# randomness and encryption
- randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q)
+ randomness[answer_num] = algs.random.mpz_lt(pk.q)
choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
# generate proof
@@ -136,7 +133,7 @@ def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
randomness[answer_num], algs.EG_disjunctive_challenge_generator)
# sum things up homomorphically if needed
- if max_answers != None:
+ if max_answers is not None:
homomorphic_sum = choices[answer_num] * homomorphic_sum
randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
@@ -146,7 +143,7 @@ def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
if num_selected_answers < min_answers:
raise Exception("Need to select at least %s answer(s)" % min_answers)
- if max_answers != None:
+ if max_answers is not None:
sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
# need to subtract the min from the offset
@@ -164,7 +161,7 @@ class EncryptedVote(WorkflowObject):
An encrypted ballot
"""
def __init__(self):
- self.encrypted_answers = None
+ self.encrypted_answers = []
@property
def datatype(self):
@@ -180,26 +177,37 @@ def _answers_set(self, value):
answers = property(_answers_get, _answers_set)
def verify(self, election):
- # right number of answers
- if len(self.encrypted_answers) != len(election.questions):
+ # correct number of answers
+ # noinspection PyUnresolvedReferences
+ n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0
+ n_questions = len(election.questions) if election.questions is not None else 0
+ if n_answers != n_questions:
+ logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})")
return False
-
+
# check hash
- if self.election_hash != election.hash:
- # print "%s / %s " % (self.election_hash, election.hash)
+ # noinspection PyUnresolvedReferences
+ our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode()
+ actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode()
+ if our_election_hash != actual_election_hash:
+ logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ")
return False
-
+
# check ID
- if self.election_uuid != election.uuid:
+ # noinspection PyUnresolvedReferences
+ our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode()
+ actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode()
+ if our_election_uuid != actual_election_uuid:
+ logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_election_uuid} ")
return False
-
+
# check proofs on all of answers
for question_num in range(len(election.questions)):
ea = self.encrypted_answers[question_num]
question = election.questions[question_num]
min_answers = 0
- if question.has_key('min'):
+ if 'min' in question:
min_answers = question['min']
if not ea.verify(election.public_key, min=min_answers, max=question['max']):
diff --git a/helios_auth/__init__.py b/helios_auth/__init__.py
index ca245ff38..d5e23fa98 100644
--- a/helios_auth/__init__.py
+++ b/helios_auth/__init__.py
@@ -4,7 +4,7 @@
TEMPLATE_BASE = settings.AUTH_TEMPLATE_BASE or "helios_auth/templates/base.html"
# enabled auth systems
-import auth_systems
-ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_AUTH_SYSTEMS or auth_systems.AUTH_SYSTEMS.keys()
-DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_AUTH_SYSTEM or None
+from . import auth_systems
+ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_SYSTEMS or list(auth_systems.AUTH_SYSTEMS.keys())
+DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_SYSTEM or None
diff --git a/helios_auth/apps.py b/helios_auth/apps.py
new file mode 100644
index 000000000..7d1472ae3
--- /dev/null
+++ b/helios_auth/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+class HeliosAuthConfig(AppConfig):
+ name = 'helios_auth'
+ verbose_name = "Helios Authentication"
diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py
index 5a0e9233b..ddb070978 100644
--- a/helios_auth/auth_systems/__init__.py
+++ b/helios_auth/auth_systems/__init__.py
@@ -1,22 +1,39 @@
+from django.conf import settings
+from . import password, linkedin, cas, facebook, google, yahoo, github, ldapauth, gitlab
+
+# Import devlogin only in debug mode
+if settings.DEBUG:
+ from . import devlogin
AUTH_SYSTEMS = {}
-import twitter, password, cas, facebook, google, yahoo, linkedin, clever
-AUTH_SYSTEMS['twitter'] = twitter
-AUTH_SYSTEMS['linkedin'] = linkedin
AUTH_SYSTEMS['password'] = password
+AUTH_SYSTEMS['linkedin'] = linkedin
AUTH_SYSTEMS['cas'] = cas
AUTH_SYSTEMS['facebook'] = facebook
AUTH_SYSTEMS['google'] = google
AUTH_SYSTEMS['yahoo'] = yahoo
-AUTH_SYSTEMS['clever'] = clever
+AUTH_SYSTEMS['github'] = github
+AUTH_SYSTEMS['ldap'] = ldapauth
+AUTH_SYSTEMS['gitlab'] = gitlab
+
+# Add devlogin only in debug mode
+if settings.DEBUG:
+ AUTH_SYSTEMS['devlogin'] = devlogin
# not ready
#import live
#AUTH_SYSTEMS['live'] = live
def can_check_constraint(auth_system):
- return hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint')
+ return auth_system in AUTH_SYSTEMS and hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint')
def can_list_categories(auth_system):
- return hasattr(AUTH_SYSTEMS[auth_system], 'list_categories')
+ return auth_system in AUTH_SYSTEMS and hasattr(AUTH_SYSTEMS[auth_system], 'list_categories')
+
+def uses_case_insensitive_user_id(auth_system):
+ """
+ Check if an auth system uses case-insensitive user IDs.
+ Auth systems can opt in by setting CASE_INSENSITIVE_USER_ID = True.
+ """
+ return auth_system in AUTH_SYSTEMS and getattr(AUTH_SYSTEMS[auth_system], 'CASE_INSENSITIVE_USER_ID', False)
diff --git a/helios_auth/auth_systems/cas.py b/helios_auth/auth_systems/cas.py
index 8202ad262..629104de8 100644
--- a/helios_auth/auth_systems/cas.py
+++ b/helios_auth/auth_systems/cas.py
@@ -5,12 +5,18 @@
https://sp.princeton.edu/oit/sdp/CAS/Wiki%20Pages/Python.aspx
"""
-from django.http import *
-from django.core.mail import send_mail
+import datetime
+import re
+import urllib.parse
+import urllib.request
+import uuid
+from xml.etree import ElementTree
+
from django.conf import settings
+from django.core.mail import send_mail
+from django.http import HttpResponseRedirect
-import sys, os, cgi, urllib, urllib2, re, uuid, datetime
-from xml.etree import ElementTree
+from helios_auth.utils import format_recipient
CAS_EMAIL_DOMAIN = "princeton.edu"
CAS_URL= 'https://fed.princeton.edu/cas/'
@@ -31,25 +37,25 @@
def _get_service_url():
# FIXME current URL
- from helios_auth.views import after
+ from helios_auth import url_names
from django.conf import settings
- from django.core.urlresolvers import reverse
+ from django.urls import reverse
- return settings.SECURE_URL_HOST + reverse(after)
+ return settings.SECURE_URL_HOST + reverse(url_names.AUTH_AFTER)
def get_auth_url(request, redirect_url):
request.session['cas_redirect_url'] = redirect_url
- return CAS_URL + 'login?service=' + urllib.quote(_get_service_url())
+ return CAS_URL + 'login?service=' + urllib.parse.quote(_get_service_url())
def get_user_category(user_id):
theurl = CAS_ELIGIBILITY_URL % user_id
- auth_handler = urllib2.HTTPBasicAuthHandler()
+ auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(realm=CAS_ELIGIBILITY_REALM, uri= theurl, user= CAS_USERNAME, passwd = CAS_PASSWORD)
- opener = urllib2.build_opener(auth_handler)
- urllib2.install_opener(opener)
+ opener = urllib.request.build_opener(auth_handler)
+ urllib.request.install_opener(opener)
- result = urllib2.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip()
+ result = urllib.request.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip()
parsed_result = ElementTree.fromstring(result)
return parsed_result.text
@@ -73,13 +79,13 @@ def get_saml_info(ticket):
-""" % (uuid.uuid1(), datetime.datetime.utcnow().isoformat(), ticket)
+""" % (uuid.uuid4(), datetime.datetime.utcnow().isoformat(), ticket)
- url = CAS_SAML_VALIDATE_URL % urllib.quote(_get_service_url())
+ url = CAS_SAML_VALIDATE_URL % urllib.parse.quote(_get_service_url())
# by virtue of having a body, this is a POST
- req = urllib2.Request(url, saml_request)
- raw_response = urllib2.urlopen(req).read()
+ req = urllib.request.Request(url, saml_request)
+ raw_response = urllib.request.urlopen(req).read()
logging.info("RESP:\n%s\n\n" % raw_response)
@@ -127,8 +133,8 @@ def get_user_info(user_id):
""" % user_id
- req = urllib2.Request(url, request_body, headers)
- response = urllib2.urlopen(req).read()
+ req = urllib.request.Request(url, request_body, headers)
+ response = urllib.request.urlopen(req).read()
# parse the result
from xml.dom.minidom import parseString
@@ -146,12 +152,12 @@ def get_user_info(user_id):
def get_user_info_special(ticket):
# fetch the information from the CAS server
val_url = CAS_URL + "validate" + \
- '?service=' + urllib.quote(_get_service_url()) + \
- '&ticket=' + urllib.quote(ticket)
- r = urllib.urlopen(val_url).readlines() # returns 2 lines
+ '?service=' + urllib.parse.quote(_get_service_url()) + \
+ '&ticket=' + urllib.parse.quote(ticket)
+ r = urllib.request.urlopen(val_url).readlines() # returns 2 lines
# success
- if len(r) == 2 and re.match("yes", r[0]) != None:
+ if len(r) == 2 and re.match("yes", r[0]) is not None:
netid = r[1].strip()
category = get_user_category(netid)
@@ -209,19 +215,19 @@ def send_message(user_id, name, user_info, subject, body):
else:
email = "%s@%s" % (user_id, CAS_EMAIL_DOMAIN)
- if user_info.has_key('name'):
+ if 'name' in user_info:
name = user_info["name"]
else:
name = email
- send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, email)], fail_silently=False)
+ send_mail(subject, body, settings.SERVER_EMAIL, [format_recipient(name, email)], fail_silently=False)
#
# eligibility
#
def check_constraint(constraint, user):
- if not user.info.has_key('category'):
+ if 'category' not in user.info:
return False
return constraint['year'] == user.info['category']
diff --git a/helios_auth/auth_systems/clever.py b/helios_auth/auth_systems/clever.py
deleted file mode 100644
index dcd52d0e5..000000000
--- a/helios_auth/auth_systems/clever.py
+++ /dev/null
@@ -1,136 +0,0 @@
-"""
-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/devlogin.py b/helios_auth/auth_systems/devlogin.py
new file mode 100644
index 000000000..d1ad6b166
--- /dev/null
+++ b/helios_auth/auth_systems/devlogin.py
@@ -0,0 +1,120 @@
+"""
+Development Login Authentication
+A simple development-only authentication system that allows login as user@example.com
+without any external dependencies. Only works on localhost/127.0.0.1.
+"""
+
+from django.conf import settings
+from django.http import HttpResponseRedirect, Http404
+from django.urls import reverse, re_path
+from django.shortcuts import render
+
+from helios_auth import url_names
+
+import logging
+
+# Login message shown in the auth selection box
+LOGIN_MESSAGE = "Dev Login"
+STATUS_UPDATES = False
+
+def get_auth_url(request, redirect_url=None):
+ """
+ Get the URL to start the authentication process.
+ For development login, we go directly to the login form.
+ """
+ # Security check: only allow on localhost/127.0.0.1
+ if not _is_localhost(request):
+ raise Http404("Development login only available on localhost")
+
+ return reverse('auth@devlogin@login')
+
+def get_user_info_after_auth(request):
+ """
+ Called after authentication to get user information.
+ For dev login, we always return user@example.com
+ """
+ # Security check: only allow on localhost/127.0.0.1
+ if not _is_localhost(request):
+ raise Http404("Development login only available on localhost")
+
+ # Return fixed development user info
+ return {
+ 'type': 'devlogin',
+ 'user_id': 'user@example.com',
+ 'name': 'Development User',
+ 'info': {
+ 'email': 'user@example.com',
+ 'name': 'Development User'
+ },
+ 'token': None
+ }
+
+def do_logout(user):
+ """
+ Log out the user. Nothing special needed for dev login.
+ """
+ return None
+
+def update_status(token, message):
+ """
+ Update user status. Not applicable for dev login.
+ """
+ pass
+
+def send_message(user_id, name, user_info, subject, body):
+ """
+ Send a message to the user. For dev login, we just log it.
+ """
+ logging.info(f"Dev login message to {user_id}: {subject}")
+
+def check_constraint(constraint, user):
+ """
+ Check if user meets a constraint. Always true for dev login.
+ """
+ return True
+
+def can_create_election(user_id, user):
+ """
+ Check if user can create elections. Always true for dev login.
+ """
+ return True
+
+def devlogin_view(request):
+ """
+ Handle the development login form and process.
+ """
+ # Security check: only allow on localhost/127.0.0.1
+ if not _is_localhost(request):
+ raise Http404("Development login only available on localhost")
+
+ if request.method == 'POST':
+ # Process the login - always successful for dev
+ # Redirect to the auth completion URL
+ return HttpResponseRedirect(reverse(url_names.AUTH_AFTER))
+
+ # Show the login form
+ return render(request, 'auth/devlogin.html', {
+ 'dev_warning': True
+ })
+
+def _is_localhost(request):
+ """
+ Check if the request is coming from localhost.
+ Returns True if the host is localhost, 127.0.0.1, or in DEBUG mode.
+ """
+ if not settings.DEBUG:
+ return False
+
+ host = request.get_host().split(':')[0] # Remove port if present
+ localhost_hosts = ['localhost', '127.0.0.1', 'testserver']
+
+ # During testing, allow testserver explicitly or when TESTING flag is set
+ if getattr(settings, 'TESTING', False) or host == 'testserver':
+ return True
+
+ return host in localhost_hosts
+
+# URL patterns for this auth system
+urlpatterns = [
+ re_path(r'^devlogin/login$', devlogin_view, name='auth@devlogin@login'),
+]
\ No newline at end of file
diff --git a/helios_auth/auth_systems/facebook.py b/helios_auth/auth_systems/facebook.py
index 4c50c6594..a727c363a 100644
--- a/helios_auth/auth_systems/facebook.py
+++ b/helios_auth/auth_systems/facebook.py
@@ -2,8 +2,6 @@
Facebook Authentication
"""
-import logging
-
from django.conf import settings
from django.core.mail import send_mail
@@ -12,31 +10,32 @@
API_SECRET = settings.FACEBOOK_API_SECRET
#from facebookclient import Facebook
-import urllib, urllib2, cgi
+import urllib.request, urllib.error, urllib.parse
# some parameters to indicate that status updating is possible
STATUS_UPDATES = True
STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status"
from helios_auth import utils
+from helios_auth.utils import format_recipient
def facebook_url(url, params):
if params:
- return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params))
+ return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params))
else:
return "https://graph.facebook.com%s" % url
def facebook_get(url, params):
full_url = facebook_url(url,params)
try:
- return urllib2.urlopen(full_url).read()
- except urllib2.HTTPError:
+ return urllib.request.urlopen(full_url).read()
+ except urllib.error.HTTPError:
from helios_auth.models import AuthenticationExpired
raise AuthenticationExpired()
def facebook_post(url, params):
full_url = facebook_url(url, None)
- return urllib2.urlopen(full_url, urllib.urlencode(params)).read()
+ return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read()
def get_auth_url(request, redirect_url):
request.session['fb_redirect_uri'] = redirect_url
@@ -69,8 +68,8 @@ def update_status(user_id, user_info, token, message):
})
def send_message(user_id, user_name, user_info, subject, body):
- if user_info.has_key('email'):
- send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_info['email'])], fail_silently=False)
+ if 'email' in user_info:
+ send_mail(subject, body, settings.SERVER_EMAIL, [format_recipient(user_name, user_info['email'])], fail_silently=False)
##
diff --git a/helios_auth/auth_systems/facebookclient/__init__.py b/helios_auth/auth_systems/facebookclient/__init__.py
deleted file mode 100644
index 03264730c..000000000
--- a/helios_auth/auth_systems/facebookclient/__init__.py
+++ /dev/null
@@ -1,1431 +0,0 @@
-#! /usr/bin/env python
-#
-# pyfacebook - Python bindings for the Facebook API
-#
-# Copyright (c) 2008, Samuel Cormier-Iijima
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-# * Neither the name of the author nor the names of its contributors may
-# be used to endorse or promote products derived from this software
-# without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY
-# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-Python bindings for the Facebook API (pyfacebook - http://code.google.com/p/pyfacebook)
-
-PyFacebook is a client library that wraps the Facebook API.
-
-For more information, see
-
-Home Page: http://code.google.com/p/pyfacebook
-Developer Wiki: http://wiki.developers.facebook.com/index.php/Python
-Facebook IRC Channel: #facebook on irc.freenode.net
-
-PyFacebook can use simplejson if it is installed, which
-is much faster than XML and also uses less bandwith. Go to
-http://undefined.org/python/#simplejson to download it, or do
-apt-get install python-simplejson on a Debian-like system.
-"""
-
-import sys
-import time
-import struct
-import urllib
-import urllib2
-import httplib
-import hashlib
-import binascii
-import urlparse
-import mimetypes
-
-# try to use simplejson first, otherwise fallback to XML
-RESPONSE_FORMAT = 'JSON'
-
-import json
-
-# try:
-# import json as simplejson
-# except ImportError:
-# try:
-# import simplejson
-# except ImportError:
-# try:
-# from django.utils import simplejson
-# except ImportError:
-# try:
-# import jsonlib as simplejson
-# simplejson.loads
-# except (ImportError, AttributeError):
-# from xml.dom import minidom
-# RESPONSE_FORMAT = 'XML'
-
-# support Google App Engine. GAE does not have a working urllib.urlopen.
-try:
- from google.appengine.api import urlfetch
-
- def urlread(url, data=None, headers=None):
- if data is not None:
- if headers is None:
- headers = {"Content-type": "application/x-www-form-urlencoded"}
- method = urlfetch.POST
- else:
- if headers is None:
- headers = {}
- method = urlfetch.GET
-
- result = urlfetch.fetch(url, method=method,
- payload=data, headers=headers)
-
- if result.status_code == 200:
- return result.content
- else:
- raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code))
-
-except ImportError:
- def urlread(url, data=None):
- res = urllib2.urlopen(url, data=data)
- return res.read()
-
-__all__ = ['Facebook']
-
-VERSION = '0.1'
-
-FACEBOOK_URL = 'http://api.facebook.com/restserver.php'
-FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php'
-
-class json(object): pass
-
-# simple IDL for the Facebook API
-METHODS = {
- 'application': {
- 'getPublicInfo': [
- ('application_id', int, ['optional']),
- ('application_api_key', str, ['optional']),
- ('application_canvas_name', str,['optional']),
- ],
- },
-
- # admin methods
- 'admin': {
- 'getAllocation': [
- ('integration_point_name', str, []),
- ],
- },
-
- # auth methods
- 'auth': {
- 'revokeAuthorization': [
- ('uid', int, ['optional']),
- ],
- },
-
- # feed methods
- 'feed': {
- 'publishStoryToUser': [
- ('title', str, []),
- ('body', str, ['optional']),
- ('image_1', str, ['optional']),
- ('image_1_link', str, ['optional']),
- ('image_2', str, ['optional']),
- ('image_2_link', str, ['optional']),
- ('image_3', str, ['optional']),
- ('image_3_link', str, ['optional']),
- ('image_4', str, ['optional']),
- ('image_4_link', str, ['optional']),
- ('priority', int, ['optional']),
- ],
-
- 'publishActionOfUser': [
- ('title', str, []),
- ('body', str, ['optional']),
- ('image_1', str, ['optional']),
- ('image_1_link', str, ['optional']),
- ('image_2', str, ['optional']),
- ('image_2_link', str, ['optional']),
- ('image_3', str, ['optional']),
- ('image_3_link', str, ['optional']),
- ('image_4', str, ['optional']),
- ('image_4_link', str, ['optional']),
- ('priority', int, ['optional']),
- ],
-
- 'publishTemplatizedAction': [
- ('title_template', str, []),
- ('page_actor_id', int, ['optional']),
- ('title_data', json, ['optional']),
- ('body_template', str, ['optional']),
- ('body_data', json, ['optional']),
- ('body_general', str, ['optional']),
- ('image_1', str, ['optional']),
- ('image_1_link', str, ['optional']),
- ('image_2', str, ['optional']),
- ('image_2_link', str, ['optional']),
- ('image_3', str, ['optional']),
- ('image_3_link', str, ['optional']),
- ('image_4', str, ['optional']),
- ('image_4_link', str, ['optional']),
- ('target_ids', list, ['optional']),
- ],
-
- 'registerTemplateBundle': [
- ('one_line_story_templates', json, []),
- ('short_story_templates', json, ['optional']),
- ('full_story_template', json, ['optional']),
- ('action_links', json, ['optional']),
- ],
-
- 'deactivateTemplateBundleByID': [
- ('template_bundle_id', int, []),
- ],
-
- 'getRegisteredTemplateBundles': [],
-
- 'getRegisteredTemplateBundleByID': [
- ('template_bundle_id', str, []),
- ],
-
- 'publishUserAction': [
- ('template_bundle_id', int, []),
- ('template_data', json, ['optional']),
- ('target_ids', list, ['optional']),
- ('body_general', str, ['optional']),
- ('story_size', int, ['optional']),
- ],
- },
-
- # fql methods
- 'fql': {
- 'query': [
- ('query', str, []),
- ],
- },
-
- # friends methods
- 'friends': {
- 'areFriends': [
- ('uids1', list, []),
- ('uids2', list, []),
- ],
-
- 'get': [
- ('flid', int, ['optional']),
- ],
-
- 'getLists': [],
-
- 'getAppUsers': [],
- },
-
- # notifications methods
- 'notifications': {
- 'get': [],
-
- 'send': [
- ('to_ids', list, []),
- ('notification', str, []),
- ('email', str, ['optional']),
- ('type', str, ['optional']),
- ],
-
- 'sendRequest': [
- ('to_ids', list, []),
- ('type', str, []),
- ('content', str, []),
- ('image', str, []),
- ('invite', bool, []),
- ],
-
- 'sendEmail': [
- ('recipients', list, []),
- ('subject', str, []),
- ('text', str, ['optional']),
- ('fbml', str, ['optional']),
- ]
- },
-
- # profile methods
- 'profile': {
- 'setFBML': [
- ('markup', str, ['optional']),
- ('uid', int, ['optional']),
- ('profile', str, ['optional']),
- ('profile_action', str, ['optional']),
- ('mobile_fbml', str, ['optional']),
- ('profile_main', str, ['optional']),
- ],
-
- 'getFBML': [
- ('uid', int, ['optional']),
- ('type', int, ['optional']),
- ],
-
- 'setInfo': [
- ('title', str, []),
- ('type', int, []),
- ('info_fields', json, []),
- ('uid', int, []),
- ],
-
- 'getInfo': [
- ('uid', int, []),
- ],
-
- 'setInfoOptions': [
- ('field', str, []),
- ('options', json, []),
- ],
-
- 'getInfoOptions': [
- ('field', str, []),
- ],
- },
-
- # users methods
- 'users': {
- 'getInfo': [
- ('uids', list, []),
- ('fields', list, [('default', ['name'])]),
- ],
-
- 'getStandardInfo': [
- ('uids', list, []),
- ('fields', list, [('default', ['uid'])]),
- ],
-
- 'getLoggedInUser': [],
-
- 'isAppAdded': [],
-
- 'hasAppPermission': [
- ('ext_perm', str, []),
- ('uid', int, ['optional']),
- ],
-
- 'setStatus': [
- ('status', str, []),
- ('clear', bool, []),
- ('status_includes_verb', bool, ['optional']),
- ('uid', int, ['optional']),
- ],
- },
-
- # events methods
- 'events': {
- 'get': [
- ('uid', int, ['optional']),
- ('eids', list, ['optional']),
- ('start_time', int, ['optional']),
- ('end_time', int, ['optional']),
- ('rsvp_status', str, ['optional']),
- ],
-
- 'getMembers': [
- ('eid', int, []),
- ],
-
- 'create': [
- ('event_info', json, []),
- ],
- },
-
- # update methods
- 'update': {
- 'decodeIDs': [
- ('ids', list, []),
- ],
- },
-
- # groups methods
- 'groups': {
- 'get': [
- ('uid', int, ['optional']),
- ('gids', list, ['optional']),
- ],
-
- 'getMembers': [
- ('gid', int, []),
- ],
- },
-
- # marketplace methods
- 'marketplace': {
- 'createListing': [
- ('listing_id', int, []),
- ('show_on_profile', bool, []),
- ('listing_attrs', str, []),
- ],
-
- 'getCategories': [],
-
- 'getListings': [
- ('listing_ids', list, []),
- ('uids', list, []),
- ],
-
- 'getSubCategories': [
- ('category', str, []),
- ],
-
- 'removeListing': [
- ('listing_id', int, []),
- ('status', str, []),
- ],
-
- 'search': [
- ('category', str, ['optional']),
- ('subcategory', str, ['optional']),
- ('query', str, ['optional']),
- ],
- },
-
- # pages methods
- 'pages': {
- 'getInfo': [
- ('fields', list, [('default', ['page_id', 'name'])]),
- ('page_ids', list, ['optional']),
- ('uid', int, ['optional']),
- ],
-
- 'isAdmin': [
- ('page_id', int, []),
- ],
-
- 'isAppAdded': [
- ('page_id', int, []),
- ],
-
- 'isFan': [
- ('page_id', int, []),
- ('uid', int, []),
- ],
- },
-
- # photos methods
- 'photos': {
- 'addTag': [
- ('pid', int, []),
- ('tag_uid', int, [('default', 0)]),
- ('tag_text', str, [('default', '')]),
- ('x', float, [('default', 50)]),
- ('y', float, [('default', 50)]),
- ('tags', str, ['optional']),
- ],
-
- 'createAlbum': [
- ('name', str, []),
- ('location', str, ['optional']),
- ('description', str, ['optional']),
- ],
-
- 'get': [
- ('subj_id', int, ['optional']),
- ('aid', int, ['optional']),
- ('pids', list, ['optional']),
- ],
-
- 'getAlbums': [
- ('uid', int, ['optional']),
- ('aids', list, ['optional']),
- ],
-
- 'getTags': [
- ('pids', list, []),
- ],
- },
-
- # status methods
- 'status': {
- 'get': [
- ('uid', int, ['optional']),
- ('limit', int, ['optional']),
- ],
- 'set': [
- ('status', str, ['optional']),
- ('uid', int, ['optional']),
- ],
- },
-
- # fbml methods
- 'fbml': {
- 'refreshImgSrc': [
- ('url', str, []),
- ],
-
- 'refreshRefUrl': [
- ('url', str, []),
- ],
-
- 'setRefHandle': [
- ('handle', str, []),
- ('fbml', str, []),
- ],
- },
-
- # SMS Methods
- 'sms' : {
- 'canSend' : [
- ('uid', int, []),
- ],
-
- 'send' : [
- ('uid', int, []),
- ('message', str, []),
- ('session_id', int, []),
- ('req_session', bool, []),
- ],
- },
-
- 'data': {
- 'getCookies': [
- ('uid', int, []),
- ('string', str, ['optional']),
- ],
-
- 'setCookie': [
- ('uid', int, []),
- ('name', str, []),
- ('value', str, []),
- ('expires', int, ['optional']),
- ('path', str, ['optional']),
- ],
- },
-
- # connect methods
- 'connect': {
- 'registerUsers': [
- ('accounts', json, []),
- ],
-
- 'unregisterUsers': [
- ('email_hashes', json, []),
- ],
-
- 'getUnconnectedFriendsCount': [
- ],
- },
-
- #stream methods (beta)
- 'stream' : {
- 'addComment' : [
- ('post_id', int, []),
- ('comment', str, []),
- ('uid', int, ['optional']),
- ],
-
- 'addLike': [
- ('uid', int, ['optional']),
- ('post_id', int, ['optional']),
- ],
-
- 'get' : [
- ('viewer_id', int, ['optional']),
- ('source_ids', list, ['optional']),
- ('start_time', int, ['optional']),
- ('end_time', int, ['optional']),
- ('limit', int, ['optional']),
- ('filter_key', str, ['optional']),
- ],
-
- 'getComments' : [
- ('post_id', int, []),
- ],
-
- 'getFilters' : [
- ('uid', int, ['optional']),
- ],
-
- 'publish' : [
- ('message', str, ['optional']),
- ('attachment', json, ['optional']),
- ('action_links', json, ['optional']),
- ('target_id', str, ['optional']),
- ('uid', str, ['optional']),
- ],
-
- 'remove' : [
- ('post_id', int, []),
- ('uid', int, ['optional']),
- ],
-
- 'removeComment' : [
- ('comment_id', int, []),
- ('uid', int, ['optional']),
- ],
-
- 'removeLike' : [
- ('uid', int, ['optional']),
- ('post_id', int, ['optional']),
- ],
- }
-}
-
-class Proxy(object):
- """Represents a "namespace" of Facebook API calls."""
-
- def __init__(self, client, name):
- self._client = client
- self._name = name
-
- def __call__(self, method=None, args=None, add_session_args=True):
- # for Django templates
- if method is None:
- return self
-
- if add_session_args:
- self._client._add_session_args(args)
-
- return self._client('%s.%s' % (self._name, method), args)
-
-
-# generate the Facebook proxies
-def __generate_proxies():
- for namespace in METHODS:
- methods = {}
-
- for method in METHODS[namespace]:
- params = ['self']
- body = ['args = {}']
-
- for param_name, param_type, param_options in METHODS[namespace][method]:
- param = param_name
-
- for option in param_options:
- if isinstance(option, tuple) and option[0] == 'default':
- if param_type == list:
- param = '%s=None' % param_name
- body.append('if %s is None: %s = %s' % (param_name, param_name, repr(option[1])))
- else:
- param = '%s=%s' % (param_name, repr(option[1]))
-
- if param_type == json:
- # we only jsonify the argument if it's a list or a dict, for compatibility
- body.append('if isinstance(%s, list) or isinstance(%s, dict): %s = simplejson.dumps(%s)' % ((param_name,) * 4))
-
- if 'optional' in param_options:
- param = '%s=None' % param_name
- body.append('if %s is not None: args[\'%s\'] = %s' % (param_name, param_name, param_name))
- else:
- body.append('args[\'%s\'] = %s' % (param_name, param_name))
-
- params.append(param)
-
- # simple docstring to refer them to Facebook API docs
- body.insert(0, '"""Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s"""' % (namespace, method))
-
- body.insert(0, 'def %s(%s):' % (method, ', '.join(params)))
-
- body.append('return self(\'%s\', args)' % method)
-
- exec('\n '.join(body))
-
- methods[method] = eval(method)
-
- proxy = type('%sProxy' % namespace.title(), (Proxy, ), methods)
-
- globals()[proxy.__name__] = proxy
-
-
-__generate_proxies()
-
-
-class FacebookError(Exception):
- """Exception class for errors received from Facebook."""
-
- def __init__(self, code, msg, args=None):
- self.code = code
- self.msg = msg
- self.args = args
-
- def __str__(self):
- return 'Error %s: %s' % (self.code, self.msg)
-
-
-class AuthProxy(AuthProxy):
- """Special proxy for facebook.auth."""
-
- def getSession(self):
- """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession"""
- args = {}
- try:
- args['auth_token'] = self._client.auth_token
- except AttributeError:
- raise RuntimeError('Client does not have auth_token set.')
- result = self._client('%s.getSession' % self._name, args)
- self._client.session_key = result['session_key']
- self._client.uid = result['uid']
- self._client.secret = result.get('secret')
- self._client.session_key_expires = result['expires']
- return result
-
- def createToken(self):
- """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken"""
- token = self._client('%s.createToken' % self._name)
- self._client.auth_token = token
- return token
-
-
-class FriendsProxy(FriendsProxy):
- """Special proxy for facebook.friends."""
-
- def get(self, **kwargs):
- """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get"""
- if not kwargs.get('flid') and self._client._friends:
- return self._client._friends
- return super(FriendsProxy, self).get(**kwargs)
-
-
-class PhotosProxy(PhotosProxy):
- """Special proxy for facebook.photos."""
-
- def upload(self, image, aid=None, caption=None, size=(604, 1024), filename=None, callback=None):
- """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload
-
- size -- an optional size (width, height) to resize the image to before uploading. Resizes by default
- to Facebook's maximum display width of 604.
- """
- args = {}
-
- if aid is not None:
- args['aid'] = aid
-
- if caption is not None:
- args['caption'] = caption
-
- args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args))
-
- try:
- import cStringIO as StringIO
- except ImportError:
- import StringIO
-
- # check for a filename specified...if the user is passing binary data in
- # image then a filename will be specified
- if filename is None:
- try:
- import Image
- except ImportError:
- data = StringIO.StringIO(open(image, 'rb').read())
- else:
- img = Image.open(image)
- if size:
- img.thumbnail(size, Image.ANTIALIAS)
- data = StringIO.StringIO()
- img.save(data, img.format)
- else:
- # there was a filename specified, which indicates that image was not
- # the path to an image file but rather the binary data of a file
- data = StringIO.StringIO(image)
- image = filename
-
- content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)])
- urlinfo = urlparse.urlsplit(self._client.facebook_url)
- try:
- content_length = len(body)
- chunk_size = 4096
-
- h = httplib.HTTPConnection(urlinfo[1])
- h.putrequest('POST', urlinfo[2])
- h.putheader('Content-Type', content_type)
- h.putheader('Content-Length', str(content_length))
- h.putheader('MIME-Version', '1.0')
- h.putheader('User-Agent', 'PyFacebook Client Library')
- h.endheaders()
-
- if callback:
- count = 0
- while len(body) > 0:
- if len(body) < chunk_size:
- data = body
- body = ''
- else:
- data = body[0:chunk_size]
- body = body[chunk_size:]
-
- h.send(data)
- count += 1
- callback(count, chunk_size, content_length)
- else:
- h.send(body)
-
- response = h.getresponse()
-
- if response.status != 200:
- raise Exception('Error uploading photo: Facebook returned HTTP %s (%s)' % (response.status, response.reason))
- response = response.read()
- except:
- # sending the photo failed, perhaps we are using GAE
- try:
- from google.appengine.api import urlfetch
-
- try:
- response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'})
- except urllib2.URLError:
- raise Exception('Error uploading photo: Facebook returned %s' % (response))
- except ImportError:
- # could not import from google.appengine.api, so we are not running in GAE
- raise Exception('Error uploading photo.')
-
- return self._client._parse_response(response, 'facebook.photos.upload')
-
-
- def __encode_multipart_formdata(self, fields, files):
- """Encodes a multipart/form-data message to upload an image."""
- boundary = '-------tHISiStheMulTIFoRMbOUNDaRY'
- crlf = '\r\n'
- l = []
-
- for (key, value) in fields:
- l.append('--' + boundary)
- l.append('Content-Disposition: form-data; name="%s"' % str(key))
- l.append('')
- l.append(str(value))
- for (filename, value) in files:
- l.append('--' + boundary)
- l.append('Content-Disposition: form-data; filename="%s"' % (str(filename), ))
- l.append('Content-Type: %s' % self.__get_content_type(filename))
- l.append('')
- l.append(value.getvalue())
- l.append('--' + boundary + '--')
- l.append('')
- body = crlf.join(l)
- content_type = 'multipart/form-data; boundary=%s' % boundary
- return content_type, body
-
-
- def __get_content_type(self, filename):
- """Returns a guess at the MIME type of the file from the filename."""
- return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream'
-
-
-class Facebook(object):
- """
- Provides access to the Facebook API.
-
- Instance Variables:
-
- added
- True if the user has added this application.
-
- api_key
- Your API key, as set in the constructor.
-
- app_name
- Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if
- this is for an internal web application. Optional, but useful for automatic redirects
- to canvas pages.
-
- auth_token
- The auth token that Facebook gives you, either with facebook.auth.createToken,
- or through a GET parameter.
-
- callback_path
- The path of the callback set in the Facebook app settings. If your callback is set
- to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'.
- Optional, but useful for automatic redirects back to the same page after login.
-
- desktop
- True if this is a desktop app, False otherwise. Used for determining how to
- authenticate.
-
- ext_perms
- Any extended permissions that the user has granted to your application.
- This parameter is set only if the user has granted any.
-
- facebook_url
- The url to use for Facebook requests.
-
- facebook_secure_url
- The url to use for secure Facebook requests.
-
- in_canvas
- True if the current request is for a canvas page.
-
- in_iframe
- True if the current request is for an HTML page to embed in Facebook inside an iframe.
-
- is_session_from_cookie
- True if the current request session comes from a session cookie.
-
- in_profile_tab
- True if the current request is for a user's tab for your application.
-
- internal
- True if this Facebook object is for an internal application (one that can be added on Facebook)
-
- locale
- The user's locale. Default: 'en_US'
-
- page_id
- Set to the page_id of the current page (if any)
-
- profile_update_time
- The time when this user's profile was last updated. This is a UNIX timestamp. Default: None if unknown.
-
- secret
- Secret that is used after getSession for desktop apps.
-
- secret_key
- Your application's secret key, as set in the constructor.
-
- session_key
- The current session key. Set automatically by auth.getSession, but can be set
- manually for doing infinite sessions.
-
- session_key_expires
- The UNIX time of when this session key expires, or 0 if it never expires.
-
- uid
- After a session is created, you can get the user's UID with this variable. Set
- automatically by auth.getSession.
-
- ----------------------------------------------------------------------
-
- """
-
- def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None, proxy=None, facebook_url=None, facebook_secure_url=None):
- """
- Initializes a new Facebook object which provides wrappers for the Facebook API.
-
- If this is a desktop application, the next couple of steps you might want to take are:
-
- facebook.auth.createToken() # create an auth token
- facebook.login() # show a browser window
- wait_login() # somehow wait for the user to log in
- facebook.auth.getSession() # get a session key
-
- For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter.
- Then call:
-
- facebook.auth.getSession()
-
- """
- self.api_key = api_key
- self.secret_key = secret_key
- self.session_key = None
- self.session_key_expires = None
- self.auth_token = auth_token
- self.secret = None
- self.uid = None
- self.page_id = None
- self.in_canvas = False
- self.in_iframe = False
- self.is_session_from_cookie = False
- self.in_profile_tab = False
- self.added = False
- self.app_name = app_name
- self.callback_path = callback_path
- self.internal = internal
- self._friends = None
- self.locale = 'en_US'
- self.profile_update_time = None
- self.ext_perms = None
- self.proxy = proxy
- if facebook_url is None:
- self.facebook_url = FACEBOOK_URL
- else:
- self.facebook_url = facebook_url
- if facebook_secure_url is None:
- self.facebook_secure_url = FACEBOOK_SECURE_URL
- else:
- self.facebook_secure_url = facebook_secure_url
-
- for namespace in METHODS:
- self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace))
-
-
- def _hash_args(self, args, secret=None):
- """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest."""
- # @author: houyr
- # fix for UnicodeEncodeError
- hasher = hashlib.md5(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())]))
- if secret:
- hasher.update(secret)
- elif self.secret:
- hasher.update(self.secret)
- else:
- hasher.update(self.secret_key)
- return hasher.hexdigest()
-
-
- def _parse_response_item(self, node):
- """Parses an XML response node from Facebook."""
- if node.nodeType == node.DOCUMENT_NODE and \
- node.childNodes[0].hasAttributes() and \
- node.childNodes[0].hasAttribute('list') and \
- node.childNodes[0].getAttribute('list') == "true":
- return {node.childNodes[0].nodeName: self._parse_response_list(node.childNodes[0])}
- elif node.nodeType == node.ELEMENT_NODE and \
- node.hasAttributes() and \
- node.hasAttribute('list') and \
- node.getAttribute('list')=="true":
- return self._parse_response_list(node)
- elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0:
- return self._parse_response_dict(node)
- else:
- return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE)
-
-
- def _parse_response_dict(self, node):
- """Parses an XML dictionary response node from Facebook."""
- result = {}
- for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes):
- result[item.nodeName] = self._parse_response_item(item)
- if node.nodeType == node.ELEMENT_NODE and node.hasAttributes():
- if node.hasAttribute('id'):
- result['id'] = node.getAttribute('id')
- return result
-
-
- def _parse_response_list(self, node):
- """Parses an XML list response node from Facebook."""
- result = []
- for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes):
- result.append(self._parse_response_item(item))
- return result
-
-
- def _check_error(self, response):
- """Checks if the given Facebook response is an error, and then raises the appropriate exception."""
- if type(response) is dict and response.has_key('error_code'):
- raise FacebookError(response['error_code'], response['error_msg'], response['request_args'])
-
-
- def _build_post_args(self, method, args=None):
- """Adds to args parameters that are necessary for every call to the API."""
- if args is None:
- args = {}
-
- for arg in args.items():
- if type(arg[1]) == list:
- args[arg[0]] = ','.join(str(a) for a in arg[1])
- elif type(arg[1]) == unicode:
- args[arg[0]] = arg[1].encode("UTF-8")
- elif type(arg[1]) == bool:
- args[arg[0]] = str(arg[1]).lower()
-
- args['method'] = method
- args['api_key'] = self.api_key
- args['v'] = '1.0'
- args['format'] = RESPONSE_FORMAT
- args['sig'] = self._hash_args(args)
-
- return args
-
-
- def _add_session_args(self, args=None):
- """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions."""
- if args is None:
- args = {}
-
- if not self.session_key:
- return args
- #some calls don't need a session anymore. this might be better done in the markup
- #raise RuntimeError('Session key not set. Make sure auth.getSession has been called.')
-
- args['session_key'] = self.session_key
- args['call_id'] = str(int(time.time() * 1000))
-
- return args
-
-
- def _parse_response(self, response, method, format=None):
- """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'."""
- if not format:
- format = RESPONSE_FORMAT
-
- if format == 'JSON':
- result = simplejson.loads(response)
-
- self._check_error(result)
- elif format == 'XML':
- dom = minidom.parseString(response)
- result = self._parse_response_item(dom)
- dom.unlink()
-
- if 'error_response' in result:
- self._check_error(result['error_response'])
-
- result = result[method[9:].replace('.', '_') + '_response']
- else:
- raise RuntimeError('Invalid format specified.')
-
- return result
-
-
- def hash_email(self, email):
- """
- Hash an email address in a format suitable for Facebook Connect.
-
- """
- email = email.lower().strip()
- return "%s_%s" % (
- struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0],
- hashlib.md5(email).hexdigest(),
- )
-
-
- def unicode_urlencode(self, params):
- """
- @author: houyr
- A unicode aware version of urllib.urlencode.
- """
- if isinstance(params, dict):
- params = params.items()
- return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v)
- for k, v in params])
-
-
- def __call__(self, method=None, args=None, secure=False):
- """Make a call to Facebook's REST server."""
- # for Django templates, if this object is called without any arguments
- # return the object itself
- if method is None:
- return self
-
- # __init__ hard-codes into en_US
- if args is not None and not args.has_key('locale'):
- args['locale'] = self.locale
-
- # @author: houyr
- # fix for bug of UnicodeEncodeError
- post_data = self.unicode_urlencode(self._build_post_args(method, args))
-
- if self.proxy:
- proxy_handler = urllib2.ProxyHandler(self.proxy)
- opener = urllib2.build_opener(proxy_handler)
- if secure:
- response = opener.open(self.facebook_secure_url, post_data).read()
- else:
- response = opener.open(self.facebook_url, post_data).read()
- else:
- if secure:
- response = urlread(self.facebook_secure_url, post_data)
- else:
- response = urlread(self.facebook_url, post_data)
-
- return self._parse_response(response, method)
-
-
- # URL helpers
- def get_url(self, page, **args):
- """
- Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php).
- Named arguments are passed as GET query string parameters.
-
- """
- return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args))
-
-
- def get_app_url(self, path=''):
- """
- Returns the URL for this app's canvas page, according to app_name.
-
- """
- return 'http://apps.facebook.com/%s/%s' % (self.app_name, path)
-
-
- def get_add_url(self, next=None):
- """
- Returns the URL that the user should be redirected to in order to add the application.
-
- """
- args = {'api_key': self.api_key, 'v': '1.0'}
-
- if next is not None:
- args['next'] = next
-
- return self.get_url('install', **args)
-
-
- def get_authorize_url(self, next=None, next_cancel=None):
- """
- Returns the URL that the user should be redirected to in order to
- authorize certain actions for application.
-
- """
- args = {'api_key': self.api_key, 'v': '1.0'}
-
- if next is not None:
- args['next'] = next
-
- if next_cancel is not None:
- args['next_cancel'] = next_cancel
-
- return self.get_url('authorize', **args)
-
-
- def get_login_url(self, next=None, popup=False, canvas=True):
- """
- Returns the URL that the user should be redirected to in order to login.
-
- next -- the URL that Facebook should redirect to after login
-
- """
- args = {'api_key': self.api_key, 'v': '1.0'}
-
- if next is not None:
- args['next'] = next
-
- if canvas is True:
- args['canvas'] = 1
-
- if popup is True:
- args['popup'] = 1
-
- if self.auth_token is not None:
- args['auth_token'] = self.auth_token
-
- return self.get_url('login', **args)
-
-
- def login(self, popup=False):
- """Open a web browser telling the user to login to Facebook."""
- import webbrowser
- webbrowser.open(self.get_login_url(popup=popup))
-
-
- def get_ext_perm_url(self, ext_perm, next=None, popup=False):
- """
- Returns the URL that the user should be redirected to in order to grant an extended permission.
-
- ext_perm -- the name of the extended permission to request
- next -- the URL that Facebook should redirect to after login
-
- """
- args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'}
-
- if next is not None:
- args['next'] = next
-
- if popup is True:
- args['popup'] = 1
-
- return self.get_url('authorize', **args)
-
-
- def request_extended_permission(self, ext_perm, popup=False):
- """Open a web browser telling the user to grant an extended permission."""
- import webbrowser
- webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup))
-
-
- def check_session(self, request):
- """
- Checks the given Django HttpRequest for Facebook parameters such as
- POST variables or an auth token. If the session is valid, returns True
- and this object can now be used to access the Facebook API. Otherwise,
- it returns False, and the application should take the appropriate action
- (either log the user in or have him add the application).
-
- """
- self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1')
-
- if self.session_key and (self.uid or self.page_id):
- return True
-
-
- if request.method == 'POST':
- params = self.validate_signature(request.POST)
- else:
- if 'installed' in request.GET:
- self.added = True
-
- if 'fb_page_id' in request.GET:
- self.page_id = request.GET['fb_page_id']
-
- if 'auth_token' in request.GET:
- self.auth_token = request.GET['auth_token']
-
- try:
- self.auth.getSession()
- except FacebookError, e:
- self.auth_token = None
- return False
-
- return True
-
- params = self.validate_signature(request.GET)
-
- if not params:
- # first check if we are in django - to check cookies
- if hasattr(request, 'COOKIES'):
- params = self.validate_cookie_signature(request.COOKIES)
- self.is_session_from_cookie = True
- else:
- # if not, then we might be on GoogleAppEngine, check their request object cookies
- if hasattr(request,'cookies'):
- params = self.validate_cookie_signature(request.cookies)
- self.is_session_from_cookie = True
-
- if not params:
- return False
-
- if params.get('in_canvas') == '1':
- self.in_canvas = True
-
- if params.get('in_iframe') == '1':
- self.in_iframe = True
-
- if params.get('in_profile_tab') == '1':
- self.in_profile_tab = True
-
- if params.get('added') == '1':
- self.added = True
-
- if params.get('expires'):
- self.session_key_expires = int(params['expires'])
-
- if 'locale' in params:
- self.locale = params['locale']
-
- if 'profile_update_time' in params:
- try:
- self.profile_update_time = int(params['profile_update_time'])
- except ValueError:
- pass
-
- if 'ext_perms' in params:
- self.ext_perms = params['ext_perms']
-
- if 'friends' in params:
- if params['friends']:
- self._friends = params['friends'].split(',')
- else:
- self._friends = []
-
- if 'session_key' in params:
- self.session_key = params['session_key']
- if 'user' in params:
- self.uid = params['user']
- elif 'page_id' in params:
- self.page_id = params['page_id']
- else:
- return False
- elif 'profile_session_key' in params:
- self.session_key = params['profile_session_key']
- if 'profile_user' in params:
- self.uid = params['profile_user']
- else:
- return False
- elif 'canvas_user' in params:
- self.uid = params['canvas_user']
- elif 'uninstall' in params:
- self.uid = params['user']
- else:
- return False
-
- return True
-
-
- def validate_signature(self, post, prefix='fb_sig', timeout=None):
- """
- Validate parameters passed to an internal Facebook app from Facebook.
-
- """
- args = post.copy()
-
- if prefix not in args:
- return None
-
- del args[prefix]
-
- if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout:
- return None
-
- args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)])
-
- hash = self._hash_args(args)
-
- if hash == post[prefix]:
- return args
- else:
- return None
-
- def validate_cookie_signature(self, cookies):
- """
- Validate parameters passed by cookies, namely facebookconnect or js api.
- """
-
- api_key = self.api_key
- if api_key not in cookies:
- return None
-
- prefix = api_key + "_"
-
- params = {}
- vals = ''
- for k in sorted(cookies):
- if k.startswith(prefix):
- key = k.replace(prefix,"")
- value = cookies[k]
- params[key] = value
- vals += '%s=%s' % (key, value)
-
- hasher = hashlib.md5(vals)
-
- hasher.update(self.secret_key)
- digest = hasher.hexdigest()
- if digest == cookies[api_key]:
- params['is_session_from_cookie'] = True
- return params
- else:
- return False
-
-
-
-
-if __name__ == '__main__':
- # sample desktop application
-
- api_key = ''
- secret_key = ''
-
- facebook = Facebook(api_key, secret_key)
-
- facebook.auth.createToken()
-
- # Show login window
- # Set popup=True if you want login without navigational elements
- facebook.login()
-
- # Login to the window, then press enter
- print 'After logging in, press enter...'
- raw_input()
-
- facebook.auth.getSession()
- print 'Session Key: ', facebook.session_key
- print 'Your UID: ', facebook.uid
-
- info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0]
-
- print 'Your Name: ', info['name']
- print 'Your Birthday: ', info['birthday']
- print 'Your Gender: ', info['sex']
-
- friends = facebook.friends.get()
- friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status'])
-
- for friend in friends:
- print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status']
-
- arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']])
-
- photos = facebook.photos.getAlbums(facebook.uid)
-
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
deleted file mode 100644
index 68b1b27c3..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
+++ /dev/null
@@ -1,248 +0,0 @@
-import re
-import datetime
-import facebook
-
-from django.http import HttpResponse, HttpResponseRedirect
-from django.core.exceptions import ImproperlyConfigured
-from django.conf import settings
-from datetime import datetime
-
-try:
- from threading import local
-except ImportError:
- from django.utils._threading_local import local
-
-__all__ = ['Facebook', 'FacebookMiddleware', 'get_facebook_client', 'require_login', 'require_add']
-
-_thread_locals = local()
-
-class Facebook(facebook.Facebook):
- def redirect(self, url):
- """
- Helper for Django which redirects to another page. If inside a
- canvas page, writes a instead to achieve the same effect.
-
- """
- if self.in_canvas:
- return HttpResponse(' ' % (url, ))
- elif re.search("^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?", url.lower()):
- return HttpResponse('' % url)
- else:
- return HttpResponseRedirect(url)
-
-
-def get_facebook_client():
- """
- Get the current Facebook object for the calling thread.
-
- """
- try:
- return _thread_locals.facebook
- except AttributeError:
- raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
-
-
-def require_login(next=None, internal=None):
- """
- Decorator for Django views that requires the user to be logged in.
- The FacebookMiddleware must be installed.
-
- Standard usage:
- @require_login()
- def some_view(request):
- ...
-
- Redirecting after login:
- To use the 'next' parameter to redirect to a specific page after login, a callable should
- return a path relative to the Post-add URL. 'next' can also be an integer specifying how many
- parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None,
- settings.callback_path and settings.app_name are checked to redirect to the same page after logging
- in. (This is the default behavior.)
- @require_login(next=some_callable)
- def some_view(request):
- ...
- """
- def decorator(view):
- def newview(request, *args, **kwargs):
- next = newview.next
- internal = newview.internal
-
- try:
- fb = request.facebook
- except:
- raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
-
- if internal is None:
- internal = request.facebook.internal
-
- if callable(next):
- next = next(request.path)
- elif isinstance(next, int):
- next = '/'.join(request.path.split('/')[next + 1:])
- elif next is None and fb.callback_path and request.path.startswith(fb.callback_path):
- next = request.path[len(fb.callback_path):]
- elif not isinstance(next, str):
- next = ''
-
- if not fb.check_session(request):
- #If user has never logged in before, the get_login_url will redirect to the TOS page
- return fb.redirect(fb.get_login_url(next=next))
-
- if internal and request.method == 'GET' and fb.app_name:
- return fb.redirect('%s%s' % (fb.get_app_url(), next))
-
- return view(request, *args, **kwargs)
- newview.next = next
- newview.internal = internal
- return newview
- return decorator
-
-
-def require_add(next=None, internal=None, on_install=None):
- """
- Decorator for Django views that requires application installation.
- The FacebookMiddleware must be installed.
-
- Standard usage:
- @require_add()
- def some_view(request):
- ...
-
- Redirecting after installation:
- To use the 'next' parameter to redirect to a specific page after login, a callable should
- return a path relative to the Post-add URL. 'next' can also be an integer specifying how many
- parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None,
- settings.callback_path and settings.app_name are checked to redirect to the same page after logging
- in. (This is the default behavior.)
- @require_add(next=some_callable)
- def some_view(request):
- ...
-
- Post-install processing:
- Set the on_install parameter to a callable in order to handle special post-install processing.
- The callable should take a request object as the parameter.
- @require_add(on_install=some_callable)
- def some_view(request):
- ...
- """
- def decorator(view):
- def newview(request, *args, **kwargs):
- next = newview.next
- internal = newview.internal
-
- try:
- fb = request.facebook
- except:
- raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
-
- if internal is None:
- internal = request.facebook.internal
-
- if callable(next):
- next = next(request.path)
- elif isinstance(next, int):
- next = '/'.join(request.path.split('/')[next + 1:])
- elif next is None and fb.callback_path and request.path.startswith(fb.callback_path):
- next = request.path[len(fb.callback_path):]
- else:
- next = ''
-
- if not fb.check_session(request):
- if fb.added:
- if request.method == 'GET' and fb.app_name:
- return fb.redirect('%s%s' % (fb.get_app_url(), next))
- return fb.redirect(fb.get_login_url(next=next))
- else:
- return fb.redirect(fb.get_add_url(next=next))
-
- if not fb.added:
- return fb.redirect(fb.get_add_url(next=next))
-
- if 'installed' in request.GET and callable(on_install):
- on_install(request)
-
- if internal and request.method == 'GET' and fb.app_name:
- return fb.redirect('%s%s' % (fb.get_app_url(), next))
-
- return view(request, *args, **kwargs)
- newview.next = next
- newview.internal = internal
- return newview
- return decorator
-
-# try to preserve the argspecs
-try:
- import decorator
-except ImportError:
- pass
-else:
- def updater(f):
- def updated(*args, **kwargs):
- original = f(*args, **kwargs)
- def newdecorator(view):
- return decorator.new_wrapper(original(view), view)
- return decorator.new_wrapper(newdecorator, original)
- return decorator.new_wrapper(updated, f)
- require_login = updater(require_login)
- require_add = updater(require_add)
-
-class FacebookMiddleware(object):
- """
- Middleware that attaches a Facebook object to every incoming request.
- The Facebook object created can also be accessed from models for the
- current thread by using get_facebook_client().
-
- """
-
- def __init__(self, api_key=None, secret_key=None, app_name=None, callback_path=None, internal=None):
- self.api_key = api_key or settings.FACEBOOK_API_KEY
- self.secret_key = secret_key or settings.FACEBOOK_SECRET_KEY
- self.app_name = app_name or getattr(settings, 'FACEBOOK_APP_NAME', None)
- self.callback_path = callback_path or getattr(settings, 'FACEBOOK_CALLBACK_PATH', None)
- self.internal = internal or getattr(settings, 'FACEBOOK_INTERNAL', True)
- self.proxy = None
- if getattr(settings, 'USE_HTTP_PROXY', False):
- self.proxy = settings.HTTP_PROXY
-
- def process_request(self, request):
- _thread_locals.facebook = request.facebook = Facebook(self.api_key, self.secret_key, app_name=self.app_name, callback_path=self.callback_path, internal=self.internal, proxy=self.proxy)
- if not self.internal:
- if 'fb_sig_session_key' in request.GET and 'fb_sig_user' in request.GET:
- request.facebook.session_key = request.session['facebook_session_key'] = request.GET['fb_sig_session_key']
- request.facebook.uid = request.session['fb_sig_user'] = request.GET['fb_sig_user']
- elif request.session.get('facebook_session_key', None) and request.session.get('facebook_user_id', None):
- request.facebook.session_key = request.session['facebook_session_key']
- request.facebook.uid = request.session['facebook_user_id']
-
- def process_response(self, request, response):
- if not self.internal and request.facebook.session_key and request.facebook.uid:
- request.session['facebook_session_key'] = request.facebook.session_key
- request.session['facebook_user_id'] = request.facebook.uid
-
- if request.facebook.session_key_expires:
- expiry = datetime.datetime.fromtimestamp(request.facebook.session_key_expires)
- request.session.set_expiry(expiry)
-
- try:
- fb = request.facebook
- except:
- return response
-
- if not fb.is_session_from_cookie:
- # Make sure the browser accepts our session cookies inside an Iframe
- response['P3P'] = 'CP="NOI DSP COR NID ADMa OPTa OUR NOR"'
- fb_cookies = {
- 'expires': fb.session_key_expires,
- 'session_key': fb.session_key,
- 'user': fb.uid,
- }
-
- expire_time = None
- if fb.session_key_expires:
- expire_time = datetime.utcfromtimestamp(fb.session_key_expires)
-
- for k in fb_cookies:
- response.set_cookie(self.api_key + '_' + k, fb_cookies[k], expires=expire_time)
- response.set_cookie(self.api_key , fb._hash_args(fb_cookies), expires=expire_time)
-
- return response
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py b/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
deleted file mode 100644
index 6f9543973..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def messages(request):
- """Returns messages similar to ``django.core.context_processors.auth``."""
- if hasattr(request, 'facebook') and request.facebook.uid is not None:
- from models import Message
- messages = Message.objects.get_and_delete_all(uid=request.facebook.uid)
- return {'messages': messages}
\ No newline at end of file
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py
deleted file mode 100644
index 666ccd3f3..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.db import models
-
-# get_facebook_client lets us get the current Facebook object
-# from outside of a view, which lets us have cleaner code
-from facebook.djangofb import get_facebook_client
-
-class UserManager(models.Manager):
- """Custom manager for a Facebook User."""
-
- def get_current(self):
- """Gets a User object for the logged-in Facebook user."""
- facebook = get_facebook_client()
- user, created = self.get_or_create(id=int(facebook.uid))
- if created:
- # we could do some custom actions for new users here...
- pass
- return user
-
-class User(models.Model):
- """A simple User model for Facebook users."""
-
- # We use the user's UID as the primary key in our database.
- id = models.IntegerField(primary_key=True)
-
- # TODO: The data that you want to store for each user would go here.
- # For this sample, we let users let people know their favorite progamming
- # language, in the spirit of Extended Info.
- language = models.CharField(maxlength=64, default='Python')
-
- # Add the custom manager
- objects = UserManager()
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml b/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml
deleted file mode 100644
index 6734dd17c..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml
+++ /dev/null
@@ -1,22 +0,0 @@
-
- {% comment %}
- We can use {{ fbuser }} to get at the current user.
- {{ fbuser.id }} will be the user's UID, and {{ fbuser.language }}
- is his/her favorite language (Python :-).
- {% endcomment %}
- Welcome, !
-
-
-
- Your favorite language is {{ fbuser.language|escape }}.
-
-
-
-
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
deleted file mode 100644
index 850184440..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from django.conf.urls.defaults import *
-
-urlpatterns = patterns('{{ project }}.{{ app }}.views',
- (r'^$', 'canvas'),
- # Define other pages you want to create here
-)
-
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
deleted file mode 100644
index 609314fe0..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from django.http import HttpResponse
-# from django.views.generic.simple import direct_to_template
-#uncomment the following two lines and the one below
-#if you dont want to use a decorator instead of the middleware
-#from django.utils.decorators import decorator_from_middleware
-#from facebook.djangofb import FacebookMiddleware
-
-# Import the Django helpers
-import facebook.djangofb as facebook
-
-# The User model defined in models.py
-from models import User
-
-# We'll require login for our canvas page. This
-# isn't necessarily a good idea, as we might want
-# to let users see the page without granting our app
-# access to their info. See the wiki for details on how
-# to do this.
-#@decorator_from_middleware(FacebookMiddleware)
-@facebook.require_login()
-def canvas(request):
- # Get the User object for the currently logged in user
- user = User.objects.get_current()
-
- # Check if we were POSTed the user's new language of choice
- if 'language' in request.POST:
- user.language = request.POST['language'][:64]
- user.save()
-
- # User is guaranteed to be logged in, so pass canvas.fbml
- # an extra 'fbuser' parameter that is the User object for
- # the currently logged in user.
- #return direct_to_template(request, 'canvas.fbml', extra_context={'fbuser': user})
- return None
-
-@facebook.require_login()
-def ajax(request):
- return HttpResponse('hello world')
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/models.py b/helios_auth/auth_systems/facebookclient/djangofb/models.py
deleted file mode 100644
index b5d2c6222..000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/models.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from django.db import models
-from django.utils.html import escape
-from django.utils.safestring import mark_safe
-
-FB_MESSAGE_STATUS = (
- (0, 'Explanation'),
- (1, 'Error'),
- (2, 'Success'),
-)
-
-class MessageManager(models.Manager):
- def get_and_delete_all(self, uid):
- messages = []
- for m in self.filter(uid=uid):
- messages.append(m)
- m.delete()
- return messages
-
-class Message(models.Model):
- """Represents a message for a Facebook user."""
- uid = models.CharField(max_length=25)
- status = models.IntegerField(choices=FB_MESSAGE_STATUS)
- message = models.CharField(max_length=300)
- objects = MessageManager()
-
- def __unicode__(self):
- return self.message
-
- def _fb_tag(self):
- return self.get_status_display().lower()
-
- def as_fbml(self):
- return mark_safe(u' ' % (
- self._fb_tag(),
- escape(self.message),
- ))
diff --git a/helios_auth/auth_systems/facebookclient/webappfb.py b/helios_auth/auth_systems/facebookclient/webappfb.py
deleted file mode 100644
index 5fdf77af5..000000000
--- a/helios_auth/auth_systems/facebookclient/webappfb.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#
-# webappfb - Facebook tools for Google's AppEngine "webapp" Framework
-#
-# Copyright (c) 2009, Max Battcher
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-# * Neither the name of the author nor the names of its contributors may
-# be used to endorse or promote products derived from this software
-# without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY
-# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-from google.appengine.api import memcache
-from google.appengine.ext.webapp import RequestHandler
-from facebook import Facebook
-import yaml
-
-"""
-Facebook tools for Google AppEngine's object-oriented "webapp" framework.
-"""
-
-# This global configuration dictionary is for configuration variables
-# for Facebook requests such as the application's API key and secret
-# key. Defaults to loading a 'facebook.yaml' YAML file. This should be
-# useful and familiar for most AppEngine development.
-FACEBOOK_CONFIG = yaml.load(file('facebook.yaml', 'r'))
-
-class FacebookRequestHandler(RequestHandler):
- """
- Base class for request handlers for Facebook apps, providing useful
- Facebook-related tools: a local
- """
-
- def _fbconfig_value(self, name, default=None):
- """
- Checks the global config dictionary and then for a class/instance
- variable, using a provided default if no value is found.
- """
- if name in FACEBOOK_CONFIG:
- default = FACEBOOK_CONFIG[name]
-
- return getattr(self, name, default)
-
- def initialize(self, request, response):
- """
- Initialize's this request's Facebook client.
- """
- super(FacebookRequestHandler, self).initialize(request, response)
-
- app_name = self._fbconfig_value('app_name', '')
- api_key = self._fbconfig_value('api_key', None)
- secret_key = self._fbconfig_value('secret_key', None)
-
- self.facebook = Facebook(api_key, secret_key,
- app_name=app_name)
-
- require_app = self._fbconfig_value('require_app', False)
- require_login = self._fbconfig_value('require_login', False)
- need_session = self._fbconfig_value('need_session', False)
- check_session = self._fbconfig_value('check_session', True)
-
- self._messages = None
- self.redirecting = False
-
- if require_app or require_login:
- if not self.facebook.check_session(request):
- self.redirect(self.facebook.get_login_url(next=request.path))
- self.redirecting = True
- return
- elif check_session:
- self.facebook.check_session(request) # ignore response
-
- # NOTE: require_app is deprecated according to modern Facebook login
- # policies. Included for completeness, but unnecessary.
- if require_app and not self.facebook.added:
- self.redirect(self.facebook.get_add_url(next=request.path))
- self.redirecting = True
- return
-
- if not (require_app or require_login) and need_session:
- self.facebook.auth.getSession()
-
- def redirect(self, url, **kwargs):
- """
- For Facebook canvas pages we should use instead of
- a normal redirect.
- """
- if self.facebook.in_canvas:
- self.response.clear()
- self.response.out.write(' ' % (url, ))
- else:
- super(FacebookRequestHandler, self).redirect(url, **kwargs)
-
- def add_user_message(self, kind, msg, detail='', time=15 * 60):
- """
- Add a message to the current user to memcache.
- """
- if self.facebook.uid:
- key = 'messages:%s' % self.facebook.uid
- self._messages = memcache.get(key)
- message = {
- 'kind': kind,
- 'message': msg,
- 'detail': detail,
- }
- if self._messages is not None:
- self._messages.append(message)
- else:
- self._messages = [message]
- memcache.set(key, self._messages, time=time)
-
- def get_and_delete_user_messages(self):
- """
- Get all of the messages for the current user; removing them.
- """
- if self.facebook.uid:
- key = 'messages:%s' % self.facebook.uid
- if not hasattr(self, '_messages') or self._messages is None:
- self._messages = memcache.get(key)
- memcache.delete(key)
- return self._messages
- return None
-
-class FacebookCanvasHandler(FacebookRequestHandler):
- """
- Request handler for Facebook canvas (FBML application) requests.
- """
-
- def canvas(self, *args, **kwargs):
- """
- This will be your handler to deal with Canvas requests.
- """
- raise NotImplementedError()
-
- def get(self, *args):
- """
- All valid canvas views are POSTS.
- """
- # TODO: Attempt to auto-redirect to Facebook canvas?
- self.error(404)
-
- def post(self, *args, **kwargs):
- """
- Check a couple of simple safety checks and then call the canvas
- handler.
- """
- if self.redirecting: return
-
- if not self.facebook.in_canvas:
- self.error(404)
- return
-
- self.canvas(*args, **kwargs)
-
-# vim: ai et ts=4 sts=4 sw=4
diff --git a/helios_auth/auth_systems/facebookclient/wsgi.py b/helios_auth/auth_systems/facebookclient/wsgi.py
deleted file mode 100644
index f6a790db1..000000000
--- a/helios_auth/auth_systems/facebookclient/wsgi.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""This is some simple helper code to bridge the Pylons / PyFacebook gap.
-
-There's some generic WSGI middleware, some Paste stuff, and some Pylons
-stuff. Once you put FacebookWSGIMiddleware into your middleware stack,
-you'll have access to ``environ["pyfacebook.facebook"]``, which is a
-``facebook.Facebook`` object. If you're using Paste (which includes
-Pylons users), you can also access this directly using the facebook
-global in this module.
-
-"""
-
-# Be careful what you import. Don't expect everyone to have Pylons,
-# Paste, etc. installed. Degrade gracefully.
-
-from facebook import Facebook
-
-__docformat__ = "restructuredtext"
-
-
-# Setup Paste, if available. This needs to stay in the same module as
-# FacebookWSGIMiddleware below.
-
-try:
- from paste.registry import StackedObjectProxy
- from webob.exc import _HTTPMove
- from paste.util.quoting import strip_html, html_quote, no_quote
-except ImportError:
- pass
-else:
- facebook = StackedObjectProxy(name="PyFacebook Facebook Connection")
-
-
- class CanvasRedirect(_HTTPMove):
-
- """This is for canvas redirects."""
-
- title = "See Other"
- code = 200
- template = ' '
-
- def html(self, environ):
- """ text/html representation of the exception """
- body = self.make_body(environ, self.template, html_quote, no_quote)
- return body
-
-class FacebookWSGIMiddleware(object):
-
- """This is WSGI middleware for Facebook."""
-
- def __init__(self, app, config, facebook_class=Facebook):
- """Initialize the Facebook middleware.
-
- ``app``
- This is the WSGI application being wrapped.
-
- ``config``
- This is a dict containing the keys "pyfacebook.apikey" and
- "pyfacebook.secret".
-
- ``facebook_class``
- If you want to subclass the Facebook class, you can pass in
- your replacement here. Pylons users will want to use
- PylonsFacebook.
-
- """
- self.app = app
- self.config = config
- self.facebook_class = facebook_class
-
- def __call__(self, environ, start_response):
- config = self.config
- real_facebook = self.facebook_class(config["pyfacebook.apikey"],
- config["pyfacebook.secret"])
- registry = environ.get('paste.registry')
- if registry:
- registry.register(facebook, real_facebook)
- environ['pyfacebook.facebook'] = real_facebook
- return self.app(environ, start_response)
-
-
-# The remainder is Pylons specific.
-
-try:
- import pylons
- from pylons.controllers.util import redirect_to as pylons_redirect_to
- from routes import url_for
-except ImportError:
- pass
-else:
-
-
- class PylonsFacebook(Facebook):
-
- """Subclass Facebook to add Pylons goodies."""
-
- def check_session(self, request=None):
- """The request parameter is now optional."""
- if request is None:
- request = pylons.request
- return Facebook.check_session(self, request)
-
- # The Django request object is similar enough to the Paste
- # request object that check_session and validate_signature
- # should *just work*.
-
- def redirect_to(self, url):
- """Wrap Pylons' redirect_to function so that it works in_canvas.
-
- By the way, this won't work until after you call
- check_session().
-
- """
- if self.in_canvas:
- raise CanvasRedirect(url)
- pylons_redirect_to(url)
-
- def apps_url_for(self, *args, **kargs):
- """Like url_for, but starts with "http://apps.facebook.com"."""
- return "http://apps.facebook.com" + url_for(*args, **kargs)
-
-
- def create_pylons_facebook_middleware(app, config):
- """This is a simple wrapper for FacebookWSGIMiddleware.
-
- It passes the correct facebook_class.
-
- """
- return FacebookWSGIMiddleware(app, config,
- facebook_class=PylonsFacebook)
diff --git a/helios_auth/auth_systems/github.py b/helios_auth/auth_systems/github.py
new file mode 100644
index 000000000..5ef293675
--- /dev/null
+++ b/helios_auth/auth_systems/github.py
@@ -0,0 +1,116 @@
+"""
+Github Authentication
+
+"""
+
+from django.conf import settings
+from django.core.mail import send_mail
+from requests_oauthlib import OAuth2Session
+
+from helios_auth.utils import format_recipient
+
+# some parameters to indicate that status updating is not possible
+STATUS_UPDATES = False
+
+# GitHub usernames are case-insensitive
+CASE_INSENSITIVE_USER_ID = True
+
+# display tweaks
+LOGIN_MESSAGE = "Log in with GitHub"
+
+AUTHORIZATION_URL = "https://github.com/login/oauth/authorize"
+TOKEN_URL = "https://github.com/login/oauth/access_token"
+
+def get_oauth_session(redirect_url=None):
+ return OAuth2Session(
+ settings.GH_CLIENT_ID,
+ redirect_uri=redirect_url,
+ scope='read:user,user:email',
+ )
+
+def get_auth_url(request, redirect_url):
+ oauth = get_oauth_session(redirect_url)
+ authorization_url, state = oauth.authorization_url(AUTHORIZATION_URL)
+ request.session['gh_redirect_uri'] = redirect_url
+ request.session['gh_oauth_state'] = state
+ return authorization_url
+
+def get_user_info_after_auth(request):
+ if 'code' not in request.GET:
+ return None
+
+ # Verify OAuth state to prevent CSRF attacks
+ expected_state = request.session.get('gh_oauth_state')
+ actual_state = request.GET.get('state')
+ if not expected_state or expected_state != actual_state:
+ raise Exception("OAuth state mismatch - possible CSRF attack")
+
+ redirect_uri = request.session.get('gh_redirect_uri')
+
+ # Clean up session data
+ for key in ['gh_redirect_uri', 'gh_oauth_state']:
+ request.session.pop(key, None)
+
+ oauth = get_oauth_session(redirect_uri)
+ oauth.fetch_token(
+ TOKEN_URL,
+ client_secret=settings.GH_CLIENT_SECRET,
+ code=request.GET['code'],
+ )
+
+ # Get user info
+ response = oauth.get("https://api.github.com/user")
+ try:
+ response.raise_for_status()
+ except Exception as e:
+ raise Exception("GitHub user API request failed") from e
+ user_data = response.json()
+ user_id = user_data['login']
+ user_name = user_data.get('name', user_id)
+
+ # Get user emails
+ response = oauth.get("https://api.github.com/user/emails")
+ try:
+ response.raise_for_status()
+ except Exception as e:
+ raise Exception("GitHub user emails API request failed") from e
+ emails = response.json()
+ user_email = None
+ for email in emails:
+ if email['verified'] and email['primary']:
+ user_email = email['email']
+ break
+ if not user_email:
+ raise Exception("email address with GitHub not verified")
+
+ return {
+ 'type': 'github',
+ 'user_id': user_id,
+ 'name': '%s (%s)' % (user_id, user_name),
+ 'info': {'email': user_email},
+ 'token': {},
+ }
+
+def do_logout(user):
+ return None
+
+def update_status(token, message):
+ pass
+
+def send_message(user_id, name, user_info, subject, body):
+ send_mail(
+ subject,
+ body,
+ settings.SERVER_EMAIL,
+ [format_recipient(user_id, user_info['email'])],
+ fail_silently=False,
+ )
+
+def check_constraint(eligibility, user_info):
+ pass
+
+#
+# Election Creation
+#
+def can_create_election(user_id, user_info):
+ return True
diff --git a/helios_auth/auth_systems/gitlab.py b/helios_auth/auth_systems/gitlab.py
new file mode 100644
index 000000000..bc1d61ff9
--- /dev/null
+++ b/helios_auth/auth_systems/gitlab.py
@@ -0,0 +1,99 @@
+"""
+Gitlab Authentication
+
+"""
+
+from django.conf import settings
+from django.core.mail import send_mail
+from requests_oauthlib import OAuth2Session
+
+from helios_auth.utils import format_recipient
+
+# some parameters to indicate that status updating is not possible
+STATUS_UPDATES = False
+
+# display tweaks
+LOGIN_MESSAGE = "Log in with Gitlab"
+
+AUTHORIZATION_URL = "https://gitlab.com/oauth/authorize"
+TOKEN_URL = "https://gitlab.com/oauth/token"
+
+def get_oauth_session(redirect_url=None):
+ return OAuth2Session(
+ settings.GITLAB_CLIENT_ID,
+ redirect_uri=redirect_url,
+ scope=['read_user'],
+ )
+
+def get_auth_url(request, redirect_url):
+ oauth = get_oauth_session(redirect_url)
+ authorization_url, state = oauth.authorization_url(AUTHORIZATION_URL)
+ request.session['gl_redirect_uri'] = redirect_url
+ request.session['gl_oauth_state'] = state
+ return authorization_url
+
+def get_user_info_after_auth(request):
+ if 'code' not in request.GET:
+ return None
+
+ # Verify OAuth state to prevent CSRF attacks
+ expected_state = request.session.get('gl_oauth_state')
+ actual_state = request.GET.get('state')
+ if not expected_state or expected_state != actual_state:
+ raise Exception("OAuth state mismatch - possible CSRF attack")
+
+ redirect_uri = request.session.get('gl_redirect_uri')
+
+ # Clean up session data
+ for key in ['gl_redirect_uri', 'gl_oauth_state']:
+ request.session.pop(key, None)
+
+ oauth = get_oauth_session(redirect_uri)
+ oauth.fetch_token(
+ TOKEN_URL,
+ client_secret=settings.GITLAB_CLIENT_SECRET,
+ code=request.GET['code'],
+ )
+
+ # Get user info
+ response = oauth.get("https://gitlab.com/api/v4/user")
+ try:
+ response.raise_for_status()
+ except Exception as e:
+ raise Exception("GitLab user API request failed") from e
+ user_data = response.json()
+ user_id = user_data['username']
+ user_name = user_data['name']
+ user_email = user_data['email']
+
+ return {
+ 'type': 'gitlab',
+ 'user_id': user_id,
+ 'name': '%s (%s)' % (user_id, user_name),
+ 'info': {'email': user_email},
+ 'token': {},
+ }
+
+def do_logout(user):
+ return None
+
+def update_status(token, message):
+ pass
+
+def send_message(user_id, name, user_info, subject, body):
+ send_mail(
+ subject,
+ body,
+ settings.SERVER_EMAIL,
+ [format_recipient(user_id, user_info['email'])],
+ fail_silently=False,
+ )
+
+def check_constraint(eligibility, user_info):
+ pass
+
+#
+# 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 b6eb57c43..600d720fc 100644
--- a/helios_auth/auth_systems/google.py
+++ b/helios_auth/auth_systems/google.py
@@ -3,15 +3,12 @@
"""
-from django.http import *
-from django.core.mail import send_mail
+import requests
from django.conf import settings
+from django.core.mail import send_mail
+from google_auth_oauthlib.flow import Flow
-import httplib2,json
-
-import sys, os, cgi, urllib, urllib2, re
-
-from oauth2client.client import OAuth2WebServerFlow
+from helios_auth.utils import format_recipient
# some parameters to indicate that status updating is not possible
STATUS_UPDATES = False
@@ -20,53 +17,90 @@
LOGIN_MESSAGE = "Log in with my Google Account"
def get_flow(redirect_url=None):
- return OAuth2WebServerFlow(client_id=settings.GOOGLE_CLIENT_ID,
- client_secret=settings.GOOGLE_CLIENT_SECRET,
- scope='profile email',
- redirect_uri=redirect_url)
+ client_config = {
+ "web": {
+ "client_id": settings.GOOGLE_CLIENT_ID,
+ "client_secret": settings.GOOGLE_CLIENT_SECRET,
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ }
+ }
+ flow = Flow.from_client_config(
+ client_config,
+ scopes=['openid', 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'],
+ redirect_uri=redirect_url
+ )
+ return flow
def get_auth_url(request, redirect_url):
flow = get_flow(redirect_url)
-
request.session['google-redirect-url'] = redirect_url
- return flow.step1_get_authorize_url()
+ authorization_url, state = flow.authorization_url(
+ access_type='offline',
+ include_granted_scopes='true'
+ )
+ request.session['google-oauth-state'] = state
+ return authorization_url
def get_user_info_after_auth(request):
- flow = get_flow(request.session['google-redirect-url'])
-
- if not request.GET.has_key('code'):
+ if 'code' not in request.GET:
return None
-
- code = request.GET['code']
- credentials = flow.step2_exchange(code)
-
- # the email address is in the credentials, that's how we make sure it's verified
- id_token = credentials.id_token
- if not id_token['email_verified']:
- raise Exception("email address with Google not verified")
-
- email = id_token['email']
-
- # get the nice name
- http = httplib2.Http(".cache")
- http = credentials.authorize(http)
- (resp_headers, content) = http.request("https://www.googleapis.com/plus/v1/people/me", "GET")
-
- response = json.loads(content)
-
- name = response['displayName']
-
- # 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' : 'google', 'user_id': email, 'name': name , 'info': {'email': email}, 'token':{}}
-
+
+ # Verify OAuth state to prevent CSRF attacks
+ expected_state = request.session.get('google-oauth-state')
+ actual_state = request.GET.get('state')
+ if not expected_state or expected_state != actual_state:
+ raise Exception("OAuth state mismatch - possible CSRF attack")
+
+ redirect_url = request.session.get('google-redirect-url')
+
+ # Clean up session data
+ for key in ['google-redirect-url', 'google-oauth-state']:
+ request.session.pop(key, None)
+
+ flow = get_flow(redirect_url)
+
+ # Exchange the authorization code for credentials
+ flow.fetch_token(code=request.GET['code'])
+ credentials = flow.credentials
+
+ # Verify the ID token and get user info
+ # Use the userinfo endpoint instead of decoding id_token manually
+ headers = {'Authorization': f'Bearer {credentials.token}'}
+ try:
+ userinfo_response = requests.get(
+ 'https://www.googleapis.com/oauth2/v3/userinfo',
+ headers=headers
+ )
+ userinfo_response.raise_for_status()
+ except requests.RequestException as e:
+ raise Exception("Failed to retrieve user info from Google.") from e
+
+ try:
+ userinfo = userinfo_response.json()
+ except ValueError as e:
+ raise Exception("Received invalid user info response from Google.") from e
+
+ # Check email_verified (v3) or verified_email (v2) - Google uses different field names
+ email_verified = userinfo.get('email_verified', userinfo.get('verified_email'))
+ if email_verified is None:
+ raise Exception("Google did not provide email verification status")
+ if not email_verified:
+ raise Exception("Email verification failed: the email address associated with your Google account is not verified. Please verify your email in your Google account settings and try again.")
+
+ email = userinfo.get('email')
+ if not email:
+ raise Exception("email address not provided by Google")
+ name = userinfo.get('name', email)
+
+ return {'type': 'google', 'user_id': email, 'name': name, 'info': {'email': email}, 'token': {}}
+
def do_logout(user):
"""
logout of Google
"""
return None
-
+
def update_status(token, message):
"""
simple update
@@ -77,8 +111,8 @@ def send_message(user_id, name, user_info, subject, body):
"""
send email to google users. user_id is the email for google.
"""
- send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, user_id)], fail_silently=False)
-
+ send_mail(subject, body, settings.SERVER_EMAIL, [format_recipient(name, user_id)], fail_silently=False)
+
def check_constraint(constraint, user_info):
"""
for eligibility
diff --git a/helios_auth/auth_systems/ldapauth.py b/helios_auth/auth_systems/ldapauth.py
new file mode 100644
index 000000000..ffc5d9360
--- /dev/null
+++ b/helios_auth/auth_systems/ldapauth.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+"""
+LDAP Authentication
+Author : shirlei@gmail.com
+Version: 2.0
+LDAP authentication relies on django-auth-ldap (https://django-auth-ldap.readthedocs.io/)
+"""
+
+from django import forms
+from django.conf import settings
+from django.urls import re_path
+from django.core.mail import send_mail
+from django.http import HttpResponseRedirect
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from helios_auth.utils import format_recipient
+
+# some parameters to indicate that status updating is possible
+STATUS_UPDATES = False
+
+LDAP_LOGIN_URL_NAME = "auth@ldap@login"
+LOGIN_MESSAGE = _("Log in with my LDAP Account")
+
+class LoginForm(forms.Form):
+ username = forms.CharField(max_length=250)
+ password = forms.CharField(widget=forms.PasswordInput(), max_length=100)
+
+
+def ldap_login_view(request):
+ from helios_auth.view_utils import render_template
+ from helios_auth.views import after
+ from helios_auth.auth_systems.ldapbackend import backend
+
+ error = None
+
+ if request.method == "GET":
+ form = LoginForm()
+ else:
+ form = LoginForm(request.POST)
+
+ request.session['auth_system_name'] = 'ldap'
+
+ if 'return_url' in request.POST:
+ request.session['auth_return_url'] = request.POST.get('return_url')
+
+ if form.is_valid():
+ username = form.cleaned_data['username'].strip()
+ password = form.cleaned_data['password'].strip()
+
+ auth = backend.CustomLDAPBackend()
+ user = auth.authenticate( None, username=username, password=password)
+
+ if user:
+ request.session['ldap_user'] = {
+ 'username': user.username,
+ 'email': user.email,
+ 'name': f'{user.first_name} {user.last_name}',
+ }
+ return HttpResponseRedirect(reverse(after))
+ else:
+ error = 'Bad Username or Password'
+
+ return render_template(request, 'ldapauth/login', {
+ 'form': form,
+ 'error': error,
+ 'enabled_auth_systems': settings.AUTH_ENABLED_SYSTEMS,
+ })
+
+
+def get_user_info_after_auth(request):
+ return {
+ 'type': 'ldap',
+ 'user_id' : request.session['ldap_user']['username'],
+ 'name': request.session['ldap_user']['name'],
+ 'info': {'email': request.session['ldap_user']['email']},
+ 'token': None
+ }
+
+
+def get_auth_url(request, redirect_url = None):
+ return reverse(ldap_login_view)
+
+
+def send_message(user_id, name, user_info, subject, body):
+ send_mail(
+ subject,
+ body,
+ settings.SERVER_EMAIL,
+ [format_recipient(name, user_info['email'])],
+ fail_silently=False,
+ html_message=body,
+ )
+
+
+def check_constraint(constraint, user_info):
+ """
+ for eligibility
+ """
+ pass
+
+
+def can_create_election(user_id, user_info):
+ return True
+
+urlpatterns = [re_path(r'^ldap/login', ldap_login_view, name=LDAP_LOGIN_URL_NAME)]
diff --git a/helios/south_migrations/__init__.py b/helios_auth/auth_systems/ldapbackend/__init__.py
similarity index 100%
rename from helios/south_migrations/__init__.py
rename to helios_auth/auth_systems/ldapbackend/__init__.py
diff --git a/helios_auth/auth_systems/ldapbackend/backend.py b/helios_auth/auth_systems/ldapbackend/backend.py
new file mode 100644
index 000000000..c6253eeab
--- /dev/null
+++ b/helios_auth/auth_systems/ldapbackend/backend.py
@@ -0,0 +1,35 @@
+"""
+LDAP Authentication
+Author : shirlei@gmail.com
+Version: 1.0
+Requires:
+- libldap2-dev
+- django-auth-ldap 1.2.6
+Technical support from IFSC - Instituto Federal de Santa Catarina
+http://dtic.ifsc.edu.br/sistemas/sistema-de-votacao-on-line-helios/
+"""
+
+from django.core.exceptions import ImproperlyConfigured
+
+from django_auth_ldap.backend import LDAPBackend
+
+
+class CustomLDAPBackend(LDAPBackend):
+
+ def authenticate_ldap_user(self, username, password):
+ """
+ Some ldap servers allow anonymous search but naturally return just a set of user attributes. So, here we re-perform search and populate user methods. For now, just in cases where AUTH_LDAP_BIND_PASSWORD is empty
+ """
+ user = super(CustomLDAPBackend, self).authenticate_ldap_user(username, password)
+
+ if user and self.settings.BIND_PASSWORD == '' :
+ search = self.settings.USER_SEARCH
+ if not isinstance(search, LDAPSearch):
+ raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.')
+ results = search.execute(user.ldap_user.connection, {'user': user.username})
+ if results is not None and len(results) == 1:
+ (user.ldap_user._user_dn, user.ldap_user.user_attrs) = results[0]
+ user.ldap_user._load_user_attrs()
+ user.ldap_user._populate_user_from_attributes()
+ user.save()
+ return user
diff --git a/helios_auth/auth_systems/linkedin.py b/helios_auth/auth_systems/linkedin.py
index 32b0033c1..5842498f3 100644
--- a/helios_auth/auth_systems/linkedin.py
+++ b/helios_auth/auth_systems/linkedin.py
@@ -1,98 +1,110 @@
"""
-LinkedIn Authentication
+LinkedIn Authentication using OAuth 2.0
+
"""
-from oauthclient import client
+from django.conf import settings
+from django.core.mail import send_mail
+from requests_oauthlib import OAuth2Session
-from django.core.urlresolvers import reverse
-from django.http import HttpResponseRedirect
+from helios_auth.utils import format_recipient
-from helios_auth import utils
+# some parameters to indicate that status updating is not possible
+STATUS_UPDATES = False
-from xml.etree import ElementTree
+# display tweaks
+LOGIN_MESSAGE = "Log in with LinkedIn"
-import logging
+AUTHORIZATION_URL = "https://www.linkedin.com/oauth/v2/authorization"
+TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"
-from django.conf import settings
-API_KEY = settings.LINKEDIN_API_KEY
-API_SECRET = settings.LINKEDIN_API_SECRET
+def get_oauth_session(redirect_url=None):
+ return OAuth2Session(
+ settings.LINKEDIN_CLIENT_ID,
+ redirect_uri=redirect_url,
+ scope='email,openid,profile',
+ )
-# some parameters to indicate that status updating is possible
-STATUS_UPDATES = False
-STATUS_UPDATE_WORDING_TEMPLATE = "Tweet %s"
+def get_auth_url(request, redirect_url):
+ oauth = get_oauth_session(redirect_url)
+ authorization_url, state = oauth.authorization_url(AUTHORIZATION_URL)
+ request.session['linkedin_redirect_uri'] = redirect_url
+ request.session['linkedin_oauth_state'] = state
+ return authorization_url
-OAUTH_PARAMS = {
- 'root_url' : 'https://api.linkedin.com/uas',
- 'request_token_path' : '/oauth/requestToken',
- 'authorize_path' : '/oauth/authorize',
- 'authenticate_path' : '/oauth/authenticate',
- 'access_token_path': '/oauth/accessToken'
-}
+def get_user_info_after_auth(request):
+ if 'code' not in request.GET:
+ return None
-def _get_new_client(token=None, token_secret=None):
- if token:
- return client.LoginOAuthClient(API_KEY, API_SECRET, OAUTH_PARAMS, token, token_secret)
- else:
- return client.LoginOAuthClient(API_KEY, API_SECRET, OAUTH_PARAMS)
+ # Verify OAuth state to prevent CSRF attacks
+ expected_state = request.session.get('linkedin_oauth_state')
+ actual_state = request.GET.get('state')
+ if not expected_state or expected_state != actual_state:
+ raise Exception("OAuth state mismatch - possible CSRF attack")
-def _get_client_by_token(token):
- return _get_new_client(token['oauth_token'], token['oauth_token_secret'])
+ redirect_uri = request.session.get('linkedin_redirect_uri')
-def get_auth_url(request, redirect_url):
- client = _get_new_client()
+ # Clean up session data
+ for key in ['linkedin_redirect_uri', 'linkedin_oauth_state']:
+ request.session.pop(key, None)
+
+ oauth = get_oauth_session(redirect_uri)
+ oauth.fetch_token(
+ TOKEN_URL,
+ client_secret=settings.LINKEDIN_CLIENT_SECRET,
+ code=request.GET['code'],
+ include_client_id=True,
+ )
+
+ # Get user info from LinkedIn's OpenID Connect userinfo endpoint
+ response = oauth.get("https://api.linkedin.com/v2/userinfo")
try:
- tok = client.get_request_token()
- except:
- return None
-
- request.session['request_token'] = tok
- url = client.get_authenticate_url(tok['oauth_token'])
- return url
-
-def get_user_info_after_auth(request):
- tok = request.session['request_token']
- login_client = _get_client_by_token(tok)
- access_token = login_client.get_access_token(verifier = request.GET.get('oauth_verifier', None))
- request.session['access_token'] = access_token
-
- user_info_xml = ElementTree.fromstring(login_client.oauth_request('http://api.linkedin.com/v1/people/~:(id,first-name,last-name)', args={}, method='GET'))
-
- user_id = user_info_xml.findtext('id')
- first_name = user_info_xml.findtext('first-name')
- last_name = user_info_xml.findtext('last-name')
-
- return {'type': 'linkedin', 'user_id' : user_id, 'name': "%s %s" % (first_name, last_name), 'info': {}, 'token': access_token}
-
+ response.raise_for_status()
+ except Exception as e:
+ raise Exception("LinkedIn user API request failed") from e
+
+ user_data = response.json()
+ user_id = user_data['sub']
+ user_name = user_data.get('name', user_id)
+ user_email = user_data.get('email')
+
+ if not user_email:
+ raise Exception("Email address not available from LinkedIn")
+
+ return {
+ 'type': 'linkedin',
+ 'user_id': user_id,
+ 'name': user_name,
+ 'info': {'email': user_email},
+ 'token': {},
+ }
+
+def do_logout(user):
+ return None
def user_needs_intervention(user_id, user_info, token):
"""
- check to see if user is following the users we need
+ check to see if user needs intervention
"""
return None
-def _get_client_by_request(request):
- access_token = request.session['access_token']
- return _get_client_by_token(access_token)
-
-def update_status(user_id, user_info, token, message):
- """
- post a message to the auth system's update stream, e.g. twitter stream
- """
- return
- #twitter_client = _get_client_by_token(token)
- #result = twitter_client.oauth_request('http://api.twitter.com/1/statuses/update.json', args={'status': message}, method='POST')
-
-def send_message(user_id, user_name, user_info, subject, body):
- pass
-
-def send_notification(user_id, user_info, message):
+def update_status(token, message):
pass
+def send_message(user_id, name, user_info, subject, body):
+ send_mail(
+ subject,
+ body,
+ settings.SERVER_EMAIL,
+ [format_recipient(name, user_info['email'])],
+ fail_silently=False,
+ )
+def check_constraint(eligibility, user_info):
+ 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 9f34a2783..348d02ff3 100644
--- a/helios_auth/auth_systems/live.py
+++ b/helios_auth/auth_systems/live.py
@@ -5,14 +5,14 @@
# NOT WORKING YET because Windows Live documentation and status is unclear. Is it in beta? I think it is.
"""
-import logging
+import urllib.parse
+import urllib.request
from django.conf import settings
+
APP_ID = settings.LIVE_APP_ID
APP_SECRET = settings.LIVE_APP_SECRET
-import urllib, urllib2, cgi
-
# some parameters to indicate that status updating is possible
STATUS_UPDATES = False
# STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status"
@@ -21,17 +21,17 @@
def live_url(url, params):
if params:
- return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params))
+ return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params))
else:
return "https://graph.facebook.com%s" % url
def live_get(url, params):
full_url = live_url(url,params)
- return urllib2.urlopen(full_url).read()
+ return urllib.request.urlopen(full_url).read()
def live_post(url, params):
full_url = live_url(url, None)
- return urllib2.urlopen(full_url, urllib.urlencode(params)).read()
+ return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read()
def get_auth_url(request, redirect_url):
request.session['live_redirect_uri'] = redirect_url
@@ -41,16 +41,16 @@ def get_auth_url(request, redirect_url):
'scope': 'publish_stream'})
def get_user_info_after_auth(request):
- args = facebook_get('/oauth/access_token', {
+ args = live_get('/oauth/access_token', {
'client_id' : APP_ID,
'redirect_uri' : request.session['fb_redirect_uri'],
- 'client_secret' : API_SECRET,
+ 'client_secret' : APP_SECRET,
'code' : request.GET['code']
})
- access_token = cgi.parse_qs(args)['access_token'][0]
+ access_token = urllib.parse.parse_qs(args)['access_token'][0]
- info = utils.from_json(facebook_get('/me', {'access_token':access_token}))
+ info = utils.from_json(live_get('/me', {'access_token':access_token}))
return {'type': 'facebook', 'user_id' : info['id'], 'name': info['name'], 'info': info, 'token': {'access_token': access_token}}
@@ -58,7 +58,7 @@ def update_status(user_id, user_info, token, message):
"""
post a message to the auth system's update stream, e.g. twitter stream
"""
- result = facebook_post('/me/feed', {
+ result = live_post('/me/feed', {
'access_token': token['access_token'],
'message': message
})
diff --git a/helios_auth/auth_systems/oauthclient/README b/helios_auth/auth_systems/oauthclient/README
deleted file mode 100644
index 10deb9377..000000000
--- a/helios_auth/auth_systems/oauthclient/README
+++ /dev/null
@@ -1,56 +0,0 @@
-Python Oauth client for Twitter
----------
-
-I built this so that i didn't have to keep looking for an oauth client for twitter to use in python.
-
-It is based off of the PHP work from abrah.am (http://github.com/poseurtech/twitteroauth/tree/master).
-It was very helpful.
-
-I am using the OAuth lib that is from google gdata. I figure it is a working client and is in production use - so it should be solid. You can find it at:
-http://gdata-python-client.googlecode.com/svn/trunk/src/gdata/oauth
-
-With a bit of modification this client should work with other publishers.
-
-btw, i am a python n00b. so feel free to help out.
-
-Thanks,
-harper - harper@nata2.org (email and xmpp)
-
-
------------
-Links:
-
-Google Code Project: http://code.google.com/p/twitteroauth-python/
-Issue Tracker: http://code.google.com/p/twitteroauth-python/issues/list
-Wiki: http://wiki.github.com/harperreed/twitteroauth-python
-
------------
-
-The example client is included in the client.py. It is:
-
-if __name__ == '__main__':
- consumer_key = ''
- consumer_secret = ''
- while not consumer_key:
- consumer_key = raw_input('Please enter consumer key: ')
- while not consumer_secret:
- consumer_secret = raw_input('Please enter consumer secret: ')
- auth_client = TwitterOAuthClient(consumer_key,consumer_secret)
- tok = auth_client.get_request_token()
- token = tok['oauth_token']
- token_secret = tok['oauth_token_secret']
- url = auth_client.get_authorize_url(token)
- webbrowser.open(url)
- print "Visit this URL to authorize your app: " + url
- response_token = raw_input('What is the oauth_token from twitter: ')
- response_client = TwitterOAuthClient(consumer_key, consumer_secret,token, token_secret)
- tok = response_client.get_access_token()
- print "Making signed request"
- #verify user access
- content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST')
- #make an update
- #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST')
- print content
-
- print 'Done.'
-
diff --git a/helios_auth/auth_systems/oauthclient/__init__.py b/helios_auth/auth_systems/oauthclient/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/helios_auth/auth_systems/oauthclient/client.py b/helios_auth/auth_systems/oauthclient/client.py
deleted file mode 100644
index c9e6b829b..000000000
--- a/helios_auth/auth_systems/oauthclient/client.py
+++ /dev/null
@@ -1,158 +0,0 @@
-'''
-Python Oauth client for Twitter
-modified to work with other oAuth logins like LinkedIn (Ben Adida)
-
-Used the SampleClient from the OAUTH.org example python client as basis.
-
-props to leahculver for making a very hard to use but in the end usable oauth lib.
-
-'''
-import httplib
-import urllib, urllib2
-import time
-import webbrowser
-import oauth as oauth
-from urlparse import urlparse
-
-class LoginOAuthClient(oauth.OAuthClient):
-
- #set api urls
- def request_token_url(self):
- return self.server_params['root_url'] + self.server_params['request_token_path']
- def authorize_url(self):
- return self.server_params['root_url'] + self.server_params['authorize_path']
- def authenticate_url(self):
- return self.server_params['root_url'] + self.server_params['authenticate_path']
- def access_token_url(self):
- return self.server_params['root_url'] + self.server_params['access_token_path']
-
- #oauth object
- def __init__(self, consumer_key, consumer_secret, server_params, oauth_token=None, oauth_token_secret=None):
- """
- params should be a dictionary including
- root_url, request_token_path, authorize_path, authenticate_path, access_token_path
- """
- self.server_params = server_params
-
- self.sha1_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
- self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
- if ((oauth_token != None) and (oauth_token_secret!=None)):
- self.token = oauth.OAuthConsumer(oauth_token, oauth_token_secret)
- else:
- self.token = None
-
- def oauth_request(self,url, args = {}, method=None):
- if (method==None):
- if args=={}:
- method = "GET"
- else:
- method = "POST"
- req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args)
- req.sign_request(self.sha1_method, self.consumer,self.token)
- if (method=="GET"):
- return self.http_wrapper(req.to_url())
- elif (method == "POST"):
- return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata())
-
- #this is barely working. (i think. mostly it is that everyone else is using httplib)
- def http_wrapper(self, url, postdata={}):
- try:
- if (postdata != {}):
- f = urllib.urlopen(url, postdata)
- else:
- f = urllib.urlopen(url)
- response = f.read()
- except:
- import traceback
- import logging, sys
- cla, exc, tb = sys.exc_info()
- logging.error(url)
- if postdata:
- logging.error("with post data")
- else:
- logging.error("without post data")
- logging.error(exc.args)
- logging.error(traceback.format_tb(tb))
- response = ""
- return response
-
-
- def get_request_token(self):
- response = self.oauth_request(self.request_token_url())
- token = self.oauth_parse_response(response)
- try:
- self.token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret'])
- return token
- except:
- raise oauth.OAuthError('Invalid oauth_token')
-
- def oauth_parse_response(self, response_string):
- r = {}
- for param in response_string.split("&"):
- pair = param.split("=")
- if (len(pair)!=2):
- break
-
- r[pair[0]]=pair[1]
- return r
-
- def get_authorize_url(self, token):
- return self.authorize_url() + '?oauth_token=' +token
-
- def get_authenticate_url(self, token):
- return self.authenticate_url() + '?oauth_token=' +token
-
- def get_access_token(self,token=None,verifier=None):
- if verifier:
- r = self.oauth_request(self.access_token_url(), args={'oauth_verifier': verifier})
- else:
- r = self.oauth_request(self.access_token_url())
- token = self.oauth_parse_response(r)
- self.token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret'])
- return token
-
- def oauth_request(self, url, args={}, method=None):
- if (method==None):
- if args=={}:
- method = "GET"
- else:
- method = "POST"
- req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args)
- req.sign_request(self.sha1_method, self.consumer,self.token)
- if (method=="GET"):
- return self.http_wrapper(req.to_url())
- elif (method == "POST"):
- return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata())
-
-
-##
-## the code below needs to be updated to take into account not just Twitter
-##
-
-if __name__ == '__main__':
- consumer_key = ''
- consumer_secret = ''
- while not consumer_key:
- consumer_key = raw_input('Please enter consumer key: ')
- while not consumer_secret:
- consumer_secret = raw_input('Please enter consumer secret: ')
- auth_client = LoginOAuthClient(consumer_key,consumer_secret)
- tok = auth_client.get_request_token()
- token = tok['oauth_token']
- token_secret = tok['oauth_token_secret']
- url = auth_client.get_authorize_url(token)
- webbrowser.open(url)
- print "Visit this URL to authorize your app: " + url
- response_token = raw_input('What is the oauth_token from twitter: ')
- response_client = LoginOAuthClient(consumer_key, consumer_secret,token, token_secret, server_params={})
- tok = response_client.get_access_token()
- print "Making signed request"
- #verify user access
- content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST')
- #make an update
- #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST')
- print content
-
- print 'Done.'
-
-
diff --git a/helios_auth/auth_systems/oauthclient/oauth/CHANGES.txt b/helios_auth/auth_systems/oauthclient/oauth/CHANGES.txt
deleted file mode 100755
index 7c2b92cd9..000000000
--- a/helios_auth/auth_systems/oauthclient/oauth/CHANGES.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-1. Moved oauth.py to __init__.py
-
-2. Refactored __init__.py for compatibility with python 2.2 (Issue 59)
-
-3. Refactored rsa.py for compatibility with python 2.2 (Issue 59)
-
-4. Refactored OAuthRequest.from_token_and_callback since the callback url was
-getting double url-encoding the callback url in place of single. (Issue 43)
-
-5. Added build_signature_base_string method to rsa.py since it used the
-implementation of this method from oauth.OAuthSignatureMethod_HMAC_SHA1 which
-was incorrect since it enforced the presence of a consumer secret and a token
-secret. Also, changed its super class from oauth.OAuthSignatureMethod_HMAC_SHA1
-to oauth.OAuthSignatureMethod (Issue 64)
-
-6. Refactored .to_header method since it returned non-oauth params
-as well which was incorrect. (Issue 31)
\ No newline at end of file
diff --git a/helios_auth/auth_systems/oauthclient/oauth/__init__.py b/helios_auth/auth_systems/oauthclient/oauth/__init__.py
deleted file mode 100755
index baf543ed4..000000000
--- a/helios_auth/auth_systems/oauthclient/oauth/__init__.py
+++ /dev/null
@@ -1,524 +0,0 @@
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-# Generic exception class
-class OAuthError(RuntimeError):
- def __init__(self, message='OAuth error occured.'):
- self.message = message
-
-# optional WWW-Authenticate header (401 error)
-def build_authenticate_header(realm=''):
- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-# url escape
-def escape(s):
- # escape '/' too
- return urllib.quote(s, safe='~')
-
-# util function: current timestamp
-# seconds since epoch (UTC)
-def generate_timestamp():
- return int(time.time())
-
-# util function: nonce
-# pseudorandom number
-def generate_nonce(length=8):
- return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-# OAuthConsumer is a data type that represents the identity of the Consumer
-# via its shared secret with the Service Provider.
-class OAuthConsumer(object):
- key = None
- secret = None
-
- def __init__(self, key, secret):
- self.key = key
- self.secret = secret
-
-# OAuthToken is a data type that represents an End User via either an access
-# or request token.
-class OAuthToken(object):
- # access tokens and request tokens
- key = None
- secret = None
-
- '''
- key = the token
- secret = the token secret
- '''
- def __init__(self, key, secret):
- self.key = key
- self.secret = secret
-
- def to_string(self):
- return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
-
- # return a token from something like:
- # oauth_token_secret=digg&oauth_token=digg
- def from_string(s):
- params = cgi.parse_qs(s, keep_blank_values=False)
- key = params['oauth_token'][0]
- secret = params['oauth_token_secret'][0]
- return OAuthToken(key, secret)
- from_string = staticmethod(from_string)
-
- def __str__(self):
- return self.to_string()
-
-# OAuthRequest represents the request and can be serialized
-class OAuthRequest(object):
- '''
- OAuth parameters:
- - oauth_consumer_key
- - oauth_token
- - oauth_signature_method
- - oauth_signature
- - oauth_timestamp
- - oauth_nonce
- - oauth_version
- ... any additional parameters, as defined by the Service Provider.
- '''
- parameters = None # oauth parameters
- http_method = HTTP_METHOD
- http_url = None
- version = VERSION
-
- def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
- self.http_method = http_method
- self.http_url = http_url
- self.parameters = parameters or {}
-
- def set_parameter(self, parameter, value):
- self.parameters[parameter] = value
-
- def get_parameter(self, parameter):
- try:
- return self.parameters[parameter]
- except:
- raise OAuthError('Parameter not found: %s' % parameter)
-
- def _get_timestamp_nonce(self):
- return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
-
- # get any non-oauth parameters
- def get_nonoauth_parameters(self):
- parameters = {}
- for k, v in self.parameters.iteritems():
- # ignore oauth parameters
- if k.find('oauth_') < 0:
- parameters[k] = v
- return parameters
-
- # serialize as a header for an HTTPAuth request
- def to_header(self, realm=''):
- auth_header = 'OAuth realm="%s"' % realm
- # add the oauth parameters
- if self.parameters:
- for k, v in self.parameters.iteritems():
- if k[:6] == 'oauth_':
- auth_header += ', %s="%s"' % (k, escape(str(v)))
- return {'Authorization': auth_header}
-
- # serialize as post data for a POST request
- def to_postdata(self):
- return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()])
-
- # serialize as a url for a GET request
- def to_url(self):
- return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
-
- # return a string that consists of all the parameters that need to be signed
- def get_normalized_parameters(self):
- params = self.parameters
- try:
- # exclude the signature if it exists
- del params['oauth_signature']
- except:
- pass
- key_values = params.items()
- # sort lexicographically, first after key, then after value
- key_values.sort()
- # combine key value pairs in string and escape
- return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values])
-
- # just uppercases the http method
- def get_normalized_http_method(self):
- return self.http_method.upper()
-
- # parses the url and rebuilds it to be scheme://host/path
- def get_normalized_http_url(self):
- parts = urlparse.urlparse(self.http_url)
- url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
- return url_string
-
- # set the signature parameter to the result of build_signature
- def sign_request(self, signature_method, consumer, token):
- # set the signature method
- self.set_parameter('oauth_signature_method', signature_method.get_name())
- # set the signature
- self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
-
- def build_signature(self, signature_method, consumer, token):
- # call the build signature method within the signature method
- return signature_method.build_signature(self, consumer, token)
-
- def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
- # combine multiple parameter sources
- if parameters is None:
- parameters = {}
-
- # headers
- if headers and 'Authorization' in headers:
- auth_header = headers['Authorization']
- # check that the authorization header is OAuth
- if auth_header.index('OAuth') > -1:
- try:
- # get the parameters from the header
- header_params = OAuthRequest._split_header(auth_header)
- parameters.update(header_params)
- except:
- raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
-
- # GET or POST query string
- if query_string:
- query_params = OAuthRequest._split_url_string(query_string)
- parameters.update(query_params)
-
- # URL parameters
- param_str = urlparse.urlparse(http_url)[4] # query
- url_params = OAuthRequest._split_url_string(param_str)
- parameters.update(url_params)
-
- if parameters:
- return OAuthRequest(http_method, http_url, parameters)
-
- return None
- from_request = staticmethod(from_request)
-
- def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
- if not parameters:
- parameters = {}
-
- defaults = {
- 'oauth_consumer_key': oauth_consumer.key,
- 'oauth_timestamp': generate_timestamp(),
- 'oauth_nonce': generate_nonce(),
- 'oauth_version': OAuthRequest.version,
- }
-
- defaults.update(parameters)
- parameters = defaults
-
- if token:
- parameters['oauth_token'] = token.key
-
- return OAuthRequest(http_method, http_url, parameters)
- from_consumer_and_token = staticmethod(from_consumer_and_token)
-
- def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
- if not parameters:
- parameters = {}
-
- parameters['oauth_token'] = token.key
-
- if callback:
- parameters['oauth_callback'] = callback
-
- return OAuthRequest(http_method, http_url, parameters)
- from_token_and_callback = staticmethod(from_token_and_callback)
-
- # util function: turn Authorization: header into parameters, has to do some unescaping
- def _split_header(header):
- params = {}
- parts = header.split(',')
- for param in parts:
- # ignore realm parameter
- if param.find('OAuth realm') > -1:
- continue
- # remove whitespace
- param = param.strip()
- # split key-value
- param_parts = param.split('=', 1)
- # remove quotes and unescape the value
- params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
- return params
- _split_header = staticmethod(_split_header)
-
- # util function: turn url string into parameters, has to do some unescaping
- def _split_url_string(param_str):
- parameters = cgi.parse_qs(param_str, keep_blank_values=False)
- for k, v in parameters.iteritems():
- parameters[k] = urllib.unquote(v[0])
- return parameters
- _split_url_string = staticmethod(_split_url_string)
-
-# OAuthServer is a worker to check a requests validity against a data store
-class OAuthServer(object):
- timestamp_threshold = 300 # in seconds, five minutes
- version = VERSION
- signature_methods = None
- data_store = None
-
- def __init__(self, data_store=None, signature_methods=None):
- self.data_store = data_store
- self.signature_methods = signature_methods or {}
-
- def set_data_store(self, oauth_data_store):
- self.data_store = data_store
-
- def get_data_store(self):
- return self.data_store
-
- def add_signature_method(self, signature_method):
- self.signature_methods[signature_method.get_name()] = signature_method
- return self.signature_methods
-
- # process a request_token request
- # returns the request token on success
- def fetch_request_token(self, oauth_request):
- try:
- # get the request token for authorization
- token = self._get_token(oauth_request, 'request')
- except OAuthError:
- # no token required for the initial token request
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- self._check_signature(oauth_request, consumer, None)
- # fetch a new token
- token = self.data_store.fetch_request_token(consumer)
- return token
-
- # process an access_token request
- # returns the access token on success
- def fetch_access_token(self, oauth_request):
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- # get the request token
- token = self._get_token(oauth_request, 'request')
- self._check_signature(oauth_request, consumer, token)
- new_token = self.data_store.fetch_access_token(consumer, token)
- return new_token
-
- # verify an api call, checks all the parameters
- def verify_request(self, oauth_request):
- # -> consumer and token
- version = self._get_version(oauth_request)
- consumer = self._get_consumer(oauth_request)
- # get the access token
- token = self._get_token(oauth_request, 'access')
- self._check_signature(oauth_request, consumer, token)
- parameters = oauth_request.get_nonoauth_parameters()
- return consumer, token, parameters
-
- # authorize a request token
- def authorize_token(self, token, user):
- return self.data_store.authorize_request_token(token, user)
-
- # get the callback url
- def get_callback(self, oauth_request):
- return oauth_request.get_parameter('oauth_callback')
-
- # optional support for the authenticate header
- def build_authenticate_header(self, realm=''):
- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
- # verify the correct version request for this server
- def _get_version(self, oauth_request):
- try:
- version = oauth_request.get_parameter('oauth_version')
- except:
- version = VERSION
- if version and version != self.version:
- raise OAuthError('OAuth version %s not supported.' % str(version))
- return version
-
- # figure out the signature with some defaults
- def _get_signature_method(self, oauth_request):
- try:
- signature_method = oauth_request.get_parameter('oauth_signature_method')
- except:
- signature_method = SIGNATURE_METHOD
- try:
- # get the signature method object
- signature_method = self.signature_methods[signature_method]
- except:
- signature_method_names = ', '.join(self.signature_methods.keys())
- raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
-
- return signature_method
-
- def _get_consumer(self, oauth_request):
- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- if not consumer_key:
- raise OAuthError('Invalid consumer key.')
- consumer = self.data_store.lookup_consumer(consumer_key)
- if not consumer:
- raise OAuthError('Invalid consumer.')
- return consumer
-
- # try to find the token for the provided request token key
- def _get_token(self, oauth_request, token_type='access'):
- token_field = oauth_request.get_parameter('oauth_token')
- token = self.data_store.lookup_token(token_type, token_field)
- if not token:
- raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
- return token
-
- def _check_signature(self, oauth_request, consumer, token):
- timestamp, nonce = oauth_request._get_timestamp_nonce()
- self._check_timestamp(timestamp)
- self._check_nonce(consumer, token, nonce)
- signature_method = self._get_signature_method(oauth_request)
- try:
- signature = oauth_request.get_parameter('oauth_signature')
- except:
- raise OAuthError('Missing signature.')
- # validate the signature
- valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
- if not valid_sig:
- key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
- raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
- built = signature_method.build_signature(oauth_request, consumer, token)
-
- def _check_timestamp(self, timestamp):
- # verify that timestamp is recentish
- timestamp = int(timestamp)
- now = int(time.time())
- lapsed = now - timestamp
- if lapsed > self.timestamp_threshold:
- raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
-
- def _check_nonce(self, consumer, token, nonce):
- # verify that the nonce is uniqueish
- nonce = self.data_store.lookup_nonce(consumer, token, nonce)
- if nonce:
- raise OAuthError('Nonce already used: %s' % str(nonce))
-
-# OAuthClient is a worker to attempt to execute a request
-class OAuthClient(object):
- consumer = None
- token = None
-
- def __init__(self, oauth_consumer, oauth_token):
- self.consumer = oauth_consumer
- self.token = oauth_token
-
- def get_consumer(self):
- return self.consumer
-
- def get_token(self):
- return self.token
-
- def fetch_request_token(self, oauth_request):
- # -> OAuthToken
- raise NotImplementedError
-
- def fetch_access_token(self, oauth_request):
- # -> OAuthToken
- raise NotImplementedError
-
- def access_resource(self, oauth_request):
- # -> some protected resource
- raise NotImplementedError
-
-# OAuthDataStore is a database abstraction used to lookup consumers and tokens
-class OAuthDataStore(object):
-
- def lookup_consumer(self, key):
- # -> OAuthConsumer
- raise NotImplementedError
-
- def lookup_token(self, oauth_consumer, token_type, token_token):
- # -> OAuthToken
- raise NotImplementedError
-
- def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
- # -> OAuthToken
- raise NotImplementedError
-
- def fetch_request_token(self, oauth_consumer):
- # -> OAuthToken
- raise NotImplementedError
-
- def fetch_access_token(self, oauth_consumer, oauth_token):
- # -> OAuthToken
- raise NotImplementedError
-
- def authorize_request_token(self, oauth_token, user):
- # -> OAuthToken
- raise NotImplementedError
-
-# OAuthSignatureMethod is a strategy class that implements a signature method
-class OAuthSignatureMethod(object):
- def get_name(self):
- # -> str
- raise NotImplementedError
-
- def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
- # -> str key, str raw
- raise NotImplementedError
-
- def build_signature(self, oauth_request, oauth_consumer, oauth_token):
- # -> str
- raise NotImplementedError
-
- def check_signature(self, oauth_request, consumer, token, signature):
- built = self.build_signature(oauth_request, consumer, token)
- return built == signature
-
-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
-
- def get_name(self):
- return 'HMAC-SHA1'
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- sig = (
- escape(oauth_request.get_normalized_http_method()),
- escape(oauth_request.get_normalized_http_url()),
- escape(oauth_request.get_normalized_parameters()),
- )
-
- key = '%s&' % escape(consumer.secret)
- if token:
- key += escape(token.secret)
- raw = '&'.join(sig)
- return key, raw
-
- def build_signature(self, oauth_request, consumer, token):
- # build the base signature string
- key, raw = self.build_signature_base_string(oauth_request, consumer, token)
-
- # hmac object
- try:
- import hashlib # 2.5
- hashed = hmac.new(key, raw, hashlib.sha1)
- except:
- import sha # deprecated
- hashed = hmac.new(key, raw, sha)
-
- # calculate the digest base 64
- return binascii.b2a_base64(hashed.digest())[:-1]
-
-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
-
- def get_name(self):
- return 'PLAINTEXT'
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- # concatenate the consumer key and secret
- sig = escape(consumer.secret) + '&'
- if token:
- sig = sig + escape(token.secret)
- return sig
-
- def build_signature(self, oauth_request, consumer, token):
- return self.build_signature_base_string(oauth_request, consumer, token)
diff --git a/helios_auth/auth_systems/oauthclient/oauth/rsa.py b/helios_auth/auth_systems/oauthclient/oauth/rsa.py
deleted file mode 100755
index f8d9b8503..000000000
--- a/helios_auth/auth_systems/oauthclient/oauth/rsa.py
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/python
-
-"""
-requires tlslite - http://trevp.net/tlslite/
-
-"""
-
-import binascii
-
-from gdata.tlslite.utils import keyfactory
-from gdata.tlslite.utils import cryptomath
-
-# XXX andy: ugly local import due to module name, oauth.oauth
-import gdata.oauth as oauth
-
-class OAuthSignatureMethod_RSA_SHA1(oauth.OAuthSignatureMethod):
- def get_name(self):
- return "RSA-SHA1"
-
- def _fetch_public_cert(self, oauth_request):
- # not implemented yet, ideas are:
- # (1) do a lookup in a table of trusted certs keyed off of consumer
- # (2) fetch via http using a url provided by the requester
- # (3) some sort of specific discovery code based on request
- #
- # either way should return a string representation of the certificate
- raise NotImplementedError
-
- def _fetch_private_cert(self, oauth_request):
- # not implemented yet, ideas are:
- # (1) do a lookup in a table of trusted certs keyed off of consumer
- #
- # either way should return a string representation of the certificate
- raise NotImplementedError
-
- def build_signature_base_string(self, oauth_request, consumer, token):
- sig = (
- oauth.escape(oauth_request.get_normalized_http_method()),
- oauth.escape(oauth_request.get_normalized_http_url()),
- oauth.escape(oauth_request.get_normalized_parameters()),
- )
- key = ''
- raw = '&'.join(sig)
- return key, raw
-
- def build_signature(self, oauth_request, consumer, token):
- key, base_string = self.build_signature_base_string(oauth_request,
- consumer,
- token)
-
- # Fetch the private key cert based on the request
- cert = self._fetch_private_cert(oauth_request)
-
- # Pull the private key from the certificate
- privatekey = keyfactory.parsePrivateKey(cert)
-
- # Convert base_string to bytes
- #base_string_bytes = cryptomath.createByteArraySequence(base_string)
-
- # Sign using the key
- signed = privatekey.hashAndSign(base_string)
-
- return binascii.b2a_base64(signed)[:-1]
-
- def check_signature(self, oauth_request, consumer, token, signature):
- decoded_sig = base64.b64decode(signature);
-
- key, base_string = self.build_signature_base_string(oauth_request,
- consumer,
- token)
-
- # Fetch the public key cert based on the request
- cert = self._fetch_public_cert(oauth_request)
-
- # Pull the public key from the certificate
- publickey = keyfactory.parsePEMKey(cert, public=True)
-
- # Check the signature
- ok = publickey.hashAndVerify(decoded_sig, base_string)
-
- return ok
-
-
-class TestOAuthSignatureMethod_RSA_SHA1(OAuthSignatureMethod_RSA_SHA1):
- def _fetch_public_cert(self, oauth_request):
- cert = """
------BEGIN CERTIFICATE-----
-MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0
-IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV
-BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
-gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY
-zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb
-mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3
-DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d
-4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb
-WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J
------END CERTIFICATE-----
-"""
- return cert
-
- def _fetch_private_cert(self, oauth_request):
- cert = """
------BEGIN PRIVATE KEY-----
-MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V
-A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d
-7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ
-hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H
-X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm
-uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw
-rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z
-zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn
-qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG
-WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno
-cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+
-3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8
-AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54
-Lw03eHTNQghS0A==
------END PRIVATE KEY-----
-"""
- return cert
diff --git a/helios_auth/auth_systems/openid/util.py b/helios_auth/auth_systems/openid/util.py
index 277b92ca1..2a2e12a42 100644
--- a/helios_auth/auth_systems/openid/util.py
+++ b/helios_auth/auth_systems/openid/util.py
@@ -3,20 +3,15 @@
Utility code for the Django example consumer and server.
"""
-from urlparse import urljoin
-
-from django.db import connection
-from django.template.context import RequestContext
-from django.template import loader
-from django import http
-from django.core.exceptions import ImproperlyConfigured
-from django.core.urlresolvers import reverse as reverseURL
+from urllib.parse import urljoin
from django.conf import settings
-
-from openid.store.filestore import FileOpenIDStore
+from django.core.exceptions import ImproperlyConfigured
+from django.db import connection
+from django.urls import reverse as reverseURL
from openid.store import sqlstore
-from openid.yadis.constants import YADIS_CONTENT_TYPE
+from openid.store.filestore import FileOpenIDStore
+
def getOpenIDStore(filestore_path, table_prefix):
"""
@@ -69,14 +64,13 @@ def getOpenIDStore(filestore_path, table_prefix):
s = types[db_engine](connection.connection,
**tablenames)
except KeyError:
- raise ImproperlyConfigured, \
- "Database engine %s not supported by OpenID library" % \
- (db_engine,)
+ raise ImproperlyConfigured("Database engine %s not supported by OpenID library" % \
+ (db_engine,))
try:
s.createTables()
- except (SystemExit, KeyboardInterrupt, MemoryError), e:
- raise
+ except (SystemExit, KeyboardInterrupt, MemoryError) as e:
+ raise e
except:
# XXX This is not the Right Way to do this, but because the
# underlying database implementation might differ in behavior
@@ -138,5 +132,5 @@ def normalDict(request_data):
values are lists, because in OpenID, each key in the query arg set
can have at most one value.
"""
- return dict((k, v[0]) for k, v in request_data.iteritems())
+ return dict((k, v[0]) for k, v in request_data.items())
diff --git a/helios_auth/auth_systems/openid/view_helpers.py b/helios_auth/auth_systems/openid/view_helpers.py
index 06eef8a28..de79b451a 100644
--- a/helios_auth/auth_systems/openid/view_helpers.py
+++ b/helios_auth/auth_systems/openid/view_helpers.py
@@ -1,14 +1,8 @@
-
-from django import http
-from django.http import HttpResponseRedirect
-
from openid.consumer import consumer
from openid.consumer.discover import DiscoveryFailure
from openid.extensions import ax, pape, sreg
-from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE
-from openid.server.trustroot import RP_RETURN_TO_URL_TYPE
-import util
+from . import util
PAPE_POLICIES = [
'AUTH_PHISHING_RESISTANT',
@@ -56,16 +50,12 @@ def start_openid(session, openid_url, trust_root, return_to):
# Start OpenID authentication.
c = get_consumer(session)
- error = None
try:
auth_request = c.begin(openid_url)
- except DiscoveryFailure, e:
+ except DiscoveryFailure as e:
# Some other protocol-level failure occurred.
- error = "OpenID discovery error: %s" % (str(e),)
-
- if error:
- raise Exception("error in openid")
+ raise Exception("error in openid: OpenID discovery error") from e
# Add Simple Registration request information. Some fields
# are optional, some are required. It's possible that the
@@ -80,7 +70,7 @@ def start_openid(session, openid_url, trust_root, return_to):
# XXX - uses myOpenID-compatible schema values, which are
# not those listed at axschema.org.
- for k, v in AX_REQUIRED_FIELDS.iteritems():
+ for k, v in AX_REQUIRED_FIELDS.items():
ax_request.add(ax.AttrInfo(v, required=True))
auth_request.addExtension(ax_request)
@@ -123,12 +113,12 @@ def finish_openid(session, request_args, return_to):
ax_response = ax.FetchResponse.fromSuccessResponse(response)
if ax_response:
- for k, v in AX_REQUIRED_FIELDS.iteritems():
+ for k, v in AX_REQUIRED_FIELDS.items():
"""
the values are the URIs, they are the key into the data
the key is the shortname
"""
- if ax_response.data.has_key(v):
+ if v in ax_response.data:
ax_items[k] = ax_response.get(v)
# Map different consumer status codes to template contexts.
@@ -141,7 +131,7 @@ def finish_openid(session, request_args, return_to):
consumer.SUCCESS:
{'url': response.getDisplayIdentifier(),
- 'sreg': sreg_response and sreg_response.items(),
+ 'sreg': sreg_response and list(sreg_response.items()),
'ax': ax_items}
}
diff --git a/helios_auth/auth_systems/password.py b/helios_auth/auth_systems/password.py
index f78d9f6a1..883b7e9f1 100644
--- a/helios_auth/auth_systems/password.py
+++ b/helios_auth/auth_systems/password.py
@@ -2,24 +2,32 @@
Username/Password Authentication
"""
-from django.core.urlresolvers import reverse
+from django.urls import reverse
from django import forms
from django.core.mail import send_mail
from django.conf import settings
from django.http import HttpResponseRedirect
+from django.urls import re_path
+
+from helios_auth import url_names
+from helios_auth.utils import format_recipient
import logging
# some parameters to indicate that status updating is possible
STATUS_UPDATES = False
-
+PASSWORD_LOGIN_URL_NAME = "auth@password@login"
+PASSWORD_FORGOTTEN_URL_NAME = "auth@password@forgotten"
def create_user(username, password, name = None):
from helios_auth.models import User
-
- user = User.get_by_type_and_id('password', username)
- if user:
- raise Exception('user exists')
+
+ try:
+ User.get_by_type_and_id('password', username)
+ except User.DoesNotExist:
+ pass
+ else:
+ raise ValueError(f"user '{username}' already exists")
info = {'password' : password, 'name': name}
user = User.update_or_create(user_type='password', user_id=username, info = info)
@@ -48,7 +56,7 @@ def password_login_view(request):
# set this in case we came here straight from the multi-login chooser
# and thus did not have a chance to hit the "start/password" URL
request.session['auth_system_name'] = 'password'
- if request.POST.has_key('return_url'):
+ if 'return_url' in request.POST:
request.session['auth_return_url'] = request.POST.get('return_url')
if form.is_valid():
@@ -58,7 +66,7 @@ def password_login_view(request):
user = User.get_by_type_and_id('password', username)
if password_check(user, password):
request.session['password_user_id'] = user.user_id
- return HttpResponseRedirect(reverse(after))
+ return HttpResponseRedirect(reverse(url_names.AUTH_AFTER))
except User.DoesNotExist:
pass
error = 'Bad Username or Password'
@@ -96,12 +104,12 @@ def password_forgotten_view(request):
""" % (user.user_id, user.info['password'], settings.SITE_TITLE)
# FIXME: make this a task
- send_mail('password reminder', body, settings.SERVER_EMAIL, ["%s <%s>" % (user.info['name'], user.info['email'])], fail_silently=False)
+ send_mail('password reminder', body, settings.SERVER_EMAIL, [format_recipient(user.info['name'], user.info['email'])], fail_silently=False)
return HttpResponseRedirect(return_url)
def get_auth_url(request, redirect_url = None):
- return reverse(password_login_view)
+ return reverse(PASSWORD_LOGIN_URL_NAME)
def get_user_info_after_auth(request):
from helios_auth.models import User
@@ -115,8 +123,8 @@ def update_status(token, message):
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)
+ name = user_name or email
+ send_mail(subject, body, settings.SERVER_EMAIL, [format_recipient(name, email)], fail_silently=False)
#
@@ -125,3 +133,9 @@ def send_message(user_id, user_name, user_info, subject, body):
def can_create_election(user_id, user_info):
return True
+
+
+urlpatterns = [
+ re_path(r'^password/login', password_login_view, name=PASSWORD_LOGIN_URL_NAME),
+ re_path(r'^password/forgot', password_forgotten_view, name=PASSWORD_FORGOTTEN_URL_NAME)
+]
diff --git a/helios_auth/auth_systems/twitter.py b/helios_auth/auth_systems/twitter.py
deleted file mode 100644
index 9963f9121..000000000
--- a/helios_auth/auth_systems/twitter.py
+++ /dev/null
@@ -1,127 +0,0 @@
-"""
-Twitter Authentication
-"""
-
-from oauthclient import client
-
-from django.core.urlresolvers import reverse
-from django.http import HttpResponseRedirect
-
-from helios_auth import utils
-
-import logging
-
-from django.conf import settings
-API_KEY = settings.TWITTER_API_KEY
-API_SECRET = settings.TWITTER_API_SECRET
-USER_TO_FOLLOW = settings.TWITTER_USER_TO_FOLLOW
-REASON_TO_FOLLOW = settings.TWITTER_REASON_TO_FOLLOW
-DM_TOKEN = settings.TWITTER_DM_TOKEN
-
-# some parameters to indicate that status updating is possible
-STATUS_UPDATES = True
-STATUS_UPDATE_WORDING_TEMPLATE = "Tweet %s"
-
-OAUTH_PARAMS = {
- 'root_url' : 'https://twitter.com',
- 'request_token_path' : '/oauth/request_token',
- 'authorize_path' : '/oauth/authorize',
- 'authenticate_path' : '/oauth/authenticate',
- 'access_token_path': '/oauth/access_token'
-}
-
-def _get_new_client(token=None, token_secret=None):
- if token:
- return client.LoginOAuthClient(API_KEY, API_SECRET, OAUTH_PARAMS, token, token_secret)
- else:
- return client.LoginOAuthClient(API_KEY, API_SECRET, OAUTH_PARAMS)
-
-def _get_client_by_token(token):
- return _get_new_client(token['oauth_token'], token['oauth_token_secret'])
-
-def get_auth_url(request, redirect_url):
- client = _get_new_client()
- try:
- tok = client.get_request_token()
- except:
- return None
-
- request.session['request_token'] = tok
- url = client.get_authenticate_url(tok['oauth_token'])
- return url
-
-def get_user_info_after_auth(request):
- tok = request.session['request_token']
- twitter_client = _get_client_by_token(tok)
- access_token = twitter_client.get_access_token()
- request.session['access_token'] = access_token
-
- user_info = utils.from_json(twitter_client.oauth_request('http://api.twitter.com/1/account/verify_credentials.json', args={}, method='GET'))
-
- return {'type': 'twitter', 'user_id' : user_info['screen_name'], 'name': user_info['name'], 'info': user_info, 'token': access_token}
-
-
-def user_needs_intervention(user_id, user_info, token):
- """
- check to see if user is following the users we need
- """
- twitter_client = _get_client_by_token(token)
- friendship = utils.from_json(twitter_client.oauth_request('http://api.twitter.com/1/friendships/exists.json', args={'user_a': user_id, 'user_b': USER_TO_FOLLOW}, method='GET'))
- if friendship:
- return None
-
- return HttpResponseRedirect(reverse(follow_view))
-
-def _get_client_by_request(request):
- access_token = request.session['access_token']
- return _get_client_by_token(access_token)
-
-def update_status(user_id, user_info, token, message):
- """
- post a message to the auth system's update stream, e.g. twitter stream
- """
- twitter_client = _get_client_by_token(token)
- result = twitter_client.oauth_request('http://api.twitter.com/1/statuses/update.json', args={'status': message}, method='POST')
-
-def send_message(user_id, user_name, user_info, subject, body):
- pass
-
-def public_url(user_id):
- return "http://twitter.com/%s" % user_id
-
-def send_notification(user_id, user_info, message):
- twitter_client = _get_client_by_token(DM_TOKEN)
- result = twitter_client.oauth_request('http://api.twitter.com/1/direct_messages/new.json', args={'screen_name': user_id, 'text': message}, method='POST')
-
-##
-## views
-##
-
-def follow_view(request):
- if request.method == "GET":
- from helios_auth.view_utils import render_template
- from helios_auth.views import after
-
- return render_template(request, 'twitter/follow', {'user_to_follow': USER_TO_FOLLOW, 'reason_to_follow' : REASON_TO_FOLLOW})
-
- if request.method == "POST":
- follow_p = bool(request.POST.get('follow_p',False))
-
- if follow_p:
- from helios_auth.security import get_user
-
- user = get_user(request)
- twitter_client = _get_client_by_token(user.token)
- result = twitter_client.oauth_request('http://api.twitter.com/1/friendships/create.json', args={'screen_name': USER_TO_FOLLOW}, method='POST')
-
- from helios_auth.views import after_intervention
- 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 16bc0343c..fd31e9100 100644
--- a/helios_auth/auth_systems/yahoo.py
+++ b/helios_auth/auth_systems/yahoo.py
@@ -3,14 +3,11 @@
"""
-from django.http import *
-from django.core.mail import send_mail
from django.conf import settings
+from django.core.mail import send_mail
-import sys, os, cgi, urllib, urllib2, re
-from xml.etree import ElementTree
-
-from openid import view_helpers
+from helios_auth.utils import format_recipient
+from .openid import view_helpers
# some parameters to indicate that status updating is not possible
STATUS_UPDATES = False
@@ -45,7 +42,7 @@ def send_message(user_id, user_name, user_info, subject, body):
"""
send email to yahoo user, user_id is email for yahoo and other openID logins.
"""
- send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_id)], fail_silently=False)
+ send_mail(subject, body, settings.SERVER_EMAIL, [format_recipient(user_name, user_id)], fail_silently=False)
def check_constraint(constraint, user_info):
"""
diff --git a/helios_auth/jsonfield.py b/helios_auth/jsonfield.py
index 0104ce496..ab66d5d5d 100644
--- a/helios_auth/jsonfield.py
+++ b/helios_auth/jsonfield.py
@@ -4,11 +4,13 @@
http://www.djangosnippets.org/snippets/377/
"""
-import datetime, json
-from django.db import models
-from django.db.models import signals
-from django.conf import settings
+import json
+
from django.core.serializers.json import DjangoJSONEncoder
+from django.db import models
+
+from . import utils
+
class JSONField(models.TextField):
"""
@@ -18,9 +20,6 @@ class JSONField(models.TextField):
deserialization_params added on 2011-01-09 to provide additional hints at deserialization time
"""
- # Used so to_python() is called
- __metaclass__ = models.SubfieldBase
-
def __init__(self, json_type=None, deserialization_params=None, **kwargs):
self.json_type = json_type
self.deserialization_params = deserialization_params
@@ -36,17 +35,17 @@ def to_python(self, value):
if isinstance(value, dict) or isinstance(value, list):
return value
- if value == "" or value == None:
- return None
+ return self.from_db_value(value)
- try:
- parsed_value = json.loads(value)
- except:
- raise Exception("not JSON")
+ # noinspection PyUnusedLocal
+ def from_db_value(self, value, *args, **kwargs):
+ parsed_value = utils.from_json(value)
+ if parsed_value is None:
+ return None
if self.json_type and parsed_value:
parsed_value = self.json_type.fromJSONDict(parsed_value, **self.deserialization_params)
-
+
return parsed_value
# we should never look up by JSON field anyways.
@@ -54,10 +53,10 @@ def to_python(self, value):
def get_prep_value(self, value):
"""Convert our JSON object to a string before we save"""
- if isinstance(value, basestring):
+ if isinstance(value, str):
return value
- if value == None:
+ if value is None:
return None
if self.json_type and isinstance(value, self.json_type):
@@ -70,5 +69,4 @@ def get_prep_value(self, value):
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
- return self.get_db_prep_value(value)
-
+ return self.get_db_prep_value(value, None)
diff --git a/helios_auth/media/login-icons/devlogin.png b/helios_auth/media/login-icons/devlogin.png
new file mode 100644
index 000000000..7ab335af8
Binary files /dev/null and b/helios_auth/media/login-icons/devlogin.png differ
diff --git a/helios_auth/media/login-icons/github.png b/helios_auth/media/login-icons/github.png
new file mode 100644
index 000000000..182a1a3f7
Binary files /dev/null and b/helios_auth/media/login-icons/github.png differ
diff --git a/helios_auth/media/login-icons/gitlab.png b/helios_auth/media/login-icons/gitlab.png
new file mode 100644
index 000000000..9954bd79c
Binary files /dev/null and b/helios_auth/media/login-icons/gitlab.png differ
diff --git a/helios_auth/media/login-icons/ldap.png b/helios_auth/media/login-icons/ldap.png
new file mode 100644
index 000000000..589c3cc67
Binary files /dev/null and b/helios_auth/media/login-icons/ldap.png differ
diff --git a/helios_auth/migrations/0001_initial.py b/helios_auth/migrations/0001_initial.py
index fc54ff2aa..002068a63 100644
--- a/helios_auth/migrations/0001_initial.py
+++ b/helios_auth/migrations/0001_initial.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
from django.db import models, migrations
+
import helios_auth.jsonfield
diff --git a/helios_auth/models.py b/helios_auth/models.py
index b9179958e..1fbc0fee7 100644
--- a/helios_auth/models.py
+++ b/helios_auth/models.py
@@ -6,13 +6,11 @@
Ben Adida
(ben@adida.net)
"""
-
from django.db import models
-from jsonfield import JSONField
-import datetime, logging
+from .auth_systems import can_check_constraint, uses_case_insensitive_user_id, AUTH_SYSTEMS
+from .jsonfield import JSONField
-from auth_systems import AUTH_SYSTEMS, can_check_constraint, can_list_categories
# an exception to catch when a user is no longer authenticated
class AuthenticationExpired(Exception):
@@ -35,7 +33,8 @@ class User(models.Model):
class Meta:
unique_together = (('user_type', 'user_id'),)
-
+ app_label = 'helios_auth'
+
@classmethod
def _get_type_and_id(cls, user_type, user_id):
return "%s:%s" % (user_type, user_id)
@@ -46,26 +45,41 @@ def type_and_id(self):
@classmethod
def get_by_type_and_id(cls, user_type, user_id):
- return cls.objects.get(user_type = user_type, user_id = user_id)
+ if uses_case_insensitive_user_id(user_type):
+ return cls.objects.get(user_type=user_type, user_id__iexact=user_id)
+ return cls.objects.get(user_type=user_type, user_id=user_id)
@classmethod
def update_or_create(cls, user_type, user_id, name=None, info=None, token=None):
- obj, created_p = cls.objects.get_or_create(user_type = user_type, user_id = user_id, defaults = {'name': name, 'info':info, 'token':token})
-
+ case_insensitive = uses_case_insensitive_user_id(user_type)
+
+ if case_insensitive:
+ try:
+ obj = cls.objects.get(user_type=user_type, user_id__iexact=user_id)
+ created_p = False
+ except cls.DoesNotExist:
+ obj = cls.objects.create(user_type=user_type, user_id=user_id, name=name, info=info, token=token)
+ created_p = True
+ else:
+ obj, created_p = cls.objects.get_or_create(user_type=user_type, user_id=user_id, defaults={'name': name, 'info': info, 'token': token})
+
if not created_p:
# special case the password: don't replace it if it exists
- if obj.info.has_key('password'):
+ if 'password' in obj.info:
info['password'] = obj.info['password']
obj.info = info
obj.name = name
obj.token = token
+ # For case-insensitive systems, update user_id to preserve the current case
+ if case_insensitive:
+ obj.user_id = user_id
obj.save()
return obj
def can_update_status(self):
- if not AUTH_SYSTEMS.has_key(self.user_type):
+ if self.user_type not in AUTH_SYSTEMS:
return False
return AUTH_SYSTEMS[self.user_type].STATUS_UPDATES
@@ -75,7 +89,7 @@ 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):
+ if self.user_type not in AUTH_SYSTEMS:
return False
return AUTH_SYSTEMS[self.user_type].can_create_election(self.user_id, self.info)
@@ -87,16 +101,16 @@ def update_status_template(self):
return AUTH_SYSTEMS[self.user_type].STATUS_UPDATE_WORDING_TEMPLATE
def update_status(self, status):
- if AUTH_SYSTEMS.has_key(self.user_type):
+ if self.user_type in AUTH_SYSTEMS:
AUTH_SYSTEMS[self.user_type].update_status(self.user_id, self.info, self.token, status)
def send_message(self, subject, body):
- if AUTH_SYSTEMS.has_key(self.user_type):
+ if self.user_type in AUTH_SYSTEMS:
subject = subject.split("\n")[0]
AUTH_SYSTEMS[self.user_type].send_message(self.user_id, self.name, self.info, subject, body)
def send_notification(self, message):
- if AUTH_SYSTEMS.has_key(self.user_type):
+ if self.user_type in AUTH_SYSTEMS:
if hasattr(AUTH_SYSTEMS[self.user_type], 'send_notification'):
AUTH_SYSTEMS[self.user_type].send_notification(self.user_id, self.info, message)
@@ -111,17 +125,17 @@ def is_eligible_for(self, eligibility_case):
return False
# no constraint? Then eligible!
- if not eligibility_case.has_key('constraint'):
+ if 'constraint' not in eligibility_case:
return True
# from here on we know we match the auth system, but do we match one of the constraints?
- auth_system = AUTH_SYSTEMS[self.user_type]
-
# does the auth system allow for checking a constraint?
- if not hasattr(auth_system, 'check_constraint'):
+ if not can_check_constraint(self.user_type):
return False
-
+
+ auth_system = AUTH_SYSTEMS[self.user_type]
+
for constraint in eligibility_case['constraint']:
# do we match on this constraint?
if auth_system.check_constraint(constraint=constraint, user = self):
@@ -142,14 +156,14 @@ def pretty_name(self):
if self.name:
return self.name
- if self.info.has_key('name'):
+ if 'name' in self.info:
return self.info['name']
return self.user_id
@property
def public_url(self):
- if AUTH_SYSTEMS.has_key(self.user_type):
+ if self.user_type in AUTH_SYSTEMS:
if hasattr(AUTH_SYSTEMS[self.user_type], 'public_url'):
return AUTH_SYSTEMS[self.user_type].public_url(self.user_id)
diff --git a/helios_auth/security/__init__.py b/helios_auth/security/__init__.py
index b036067d9..1fa30e1cf 100644
--- a/helios_auth/security/__init__.py
+++ b/helios_auth/security/__init__.py
@@ -4,18 +4,16 @@
Ben Adida (ben@adida.net)
"""
+import uuid
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponseNotAllowed
+from django.http import HttpResponseRedirect
# nicely update the wrapper function
from functools import update_wrapper
-from django.http import HttpResponse, Http404, HttpResponseRedirect
-from django.core.exceptions import *
-from django.conf import settings
-
-import oauth
-
-import uuid
-
-from helios_auth.models import *
+from . import oauth
+from helios_auth.models import User
FIELDS_TO_SAVE = 'FIELDS_TO_SAVE'
@@ -95,10 +93,10 @@ def get_user(request):
# request.session.set_expiry(settings.SESSION_COOKIE_AGE)
# set up CSRF protection if needed
- if not request.session.has_key('csrf_token') or (type(request.session['csrf_token']) != str and type(request.session['csrf_token']) != unicode):
+ if 'csrf_token' not in request.session or not isinstance(request.session['csrf_token'], str):
request.session['csrf_token'] = str(uuid.uuid4())
- if request.session.has_key('user'):
+ if 'user' in request.session:
user = request.session['user']
# find the user
@@ -111,7 +109,7 @@ def check_csrf(request):
if request.method != "POST":
return HttpResponseNotAllowed("only a POST for this URL")
- if (not request.POST.has_key('csrf_token')) or (request.POST['csrf_token'] != request.session['csrf_token']):
+ if ('csrf_token' not in request.POST) or (request.POST['csrf_token'] != request.session['csrf_token']):
raise Exception("A CSRF problem was detected")
def save_in_session_across_logouts(request, field_name, field_value):
diff --git a/helios_auth/security/oauth.py b/helios_auth/security/oauth.py
index 4addf2267..568272bd8 100644
--- a/helios_auth/security/oauth.py
+++ b/helios_auth/security/oauth.py
@@ -6,15 +6,12 @@
- access tokens are looked up with an extra param of consumer
"""
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
import base64
+import hmac
import logging
-import hashlib
+import random
+import time
+import urllib.parse
VERSION = '1.0' # Hi Blaine!
HTTP_METHOD = 'GET'
@@ -32,7 +29,7 @@ def build_authenticate_header(realm=''):
# url escape
def escape(s):
# escape '/' too
- return urllib.quote(s, safe='~')
+ return urllib.parse.quote(s, safe='~')
# util function: current timestamp
# seconds since epoch (UTC)
@@ -55,7 +52,7 @@ def __init__(self, key, secret):
self.secret = secret
# OAuthToken is a data type that represents an End User via either an access
-# or request token.
+# or request token.
class OAuthToken(object):
# access tokens and request tokens
key = None
@@ -70,13 +67,13 @@ def __init__(self, key, secret):
self.secret = secret
def to_string(self):
- return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+ return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
# return a token from something like:
# oauth_token_secret=digg&oauth_token=digg
- @staticmethod
+ @staticmethod
def from_string(s):
- params = cgi.parse_qs(s, keep_blank_values=False)
+ params = urllib.parse.parse_qs(s, keep_blank_values=False)
key = params['oauth_token'][0]
secret = params['oauth_token_secret'][0]
return OAuthToken(key, secret)
@@ -88,11 +85,11 @@ def __str__(self):
class OAuthRequest(object):
'''
OAuth parameters:
- - oauth_consumer_key
+ - oauth_consumer_key
- oauth_token
- oauth_signature_method
- - oauth_signature
- - oauth_timestamp
+ - oauth_signature
+ - oauth_timestamp
- oauth_nonce
- oauth_version
... any additional parameters, as defined by the Service Provider.
@@ -101,7 +98,7 @@ class OAuthRequest(object):
http_method = HTTP_METHOD
http_url = None
version = VERSION
-
+
# added by Ben to filter out extra params from header
OAUTH_PARAMS = ['oauth_consumer_key', 'oauth_token', 'oauth_signature_method', 'oauth_signature', 'oauth_timestamp', 'oauth_nonce', 'oauth_version']
@@ -125,7 +122,7 @@ def _get_timestamp_nonce(self):
# get any non-oauth parameters
def get_nonoauth_parameters(self):
parameters = {}
- for k, v in self.parameters.iteritems():
+ for k, v in self.parameters.items():
# ignore oauth parameters
if k.find('oauth_') < 0:
parameters[k] = v
@@ -136,7 +133,7 @@ def to_header(self, realm=''):
auth_header = 'OAuth realm="%s"' % realm
# add the oauth parameters
if self.parameters:
- for k, v in self.parameters.iteritems():
+ for k, v in self.parameters.items():
# only if it's a standard OAUTH param (Ben)
if k in self.OAUTH_PARAMS:
auth_header += ', %s="%s"' % (k, escape(str(v)))
@@ -144,7 +141,7 @@ def to_header(self, realm=''):
# serialize as post data for a POST request
def to_postdata(self):
- return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
+ return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items())
# serialize as a url for a GET request
def to_url(self):
@@ -158,7 +155,7 @@ def get_normalized_parameters(self):
del params['oauth_signature']
except:
pass
- key_values = params.items()
+ key_values = list(params.items())
# sort lexicographically, first after key, then after value
key_values.sort()
# combine key value pairs in string and escape
@@ -170,10 +167,10 @@ def get_normalized_http_method(self):
# parses the url and rebuilds it to be scheme://host/path
def get_normalized_http_url(self):
- parts = urlparse.urlparse(self.http_url)
+ parts = urllib.parse.urlparse(self.http_url)
url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
return url_string
-
+
# set the signature parameter to the result of build_signature
def sign_request(self, signature_method, consumer, token):
# set the signature method
@@ -209,7 +206,7 @@ def from_request(http_method, http_url, headers=None, parameters=None, query_str
parameters.update(query_params)
# URL parameters
- param_str = urlparse.urlparse(http_url)[4] # query
+ param_str = urllib.parse.urlparse(http_url)[4] # query
url_params = OAuthRequest._split_url_string(param_str)
parameters.update(url_params)
@@ -264,15 +261,15 @@ def _split_header(header):
# split key-value
param_parts = param.split('=', 1)
# remove quotes and unescape the value
- params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+ params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
return params
-
+
# util function: turn url string into parameters, has to do some unescaping
@staticmethod
def _split_url_string(param_str):
- parameters = cgi.parse_qs(param_str, keep_blank_values=False)
- for k, v in parameters.iteritems():
- parameters[k] = urllib.unquote(v[0])
+ parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False)
+ for k, v in parameters.items():
+ parameters[k] = urllib.parse.unquote(v[0])
return parameters
# OAuthServer is a worker to check a requests validity against a data store
@@ -287,7 +284,7 @@ def __init__(self, data_store=None, signature_methods=None):
self.signature_methods = signature_methods or {}
def set_data_store(self, oauth_data_store):
- self.data_store = data_store
+ self.data_store = oauth_data_store
def get_data_store(self):
return self.data_store
@@ -336,12 +333,12 @@ def verify_request(self, oauth_request):
# authorize a request token
def authorize_token(self, token, user):
return self.data_store.authorize_request_token(token, user)
-
+
# get the callback url
def get_callback(self, oauth_request):
return oauth_request.get_parameter('oauth_callback')
- # optional support for the authenticate header
+ # optional support for the authenticate header
def build_authenticate_header(self, realm=''):
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
@@ -365,7 +362,7 @@ def _get_signature_method(self, oauth_request):
# get the signature method object
signature_method = self.signature_methods[signature_method]
except:
- signature_method_names = ', '.join(self.signature_methods.keys())
+ signature_method_names = ', '.join(list(self.signature_methods.keys()))
raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
return signature_method
@@ -495,7 +492,7 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
def get_name(self):
return 'HMAC-SHA1'
-
+
def build_signature_base_string(self, oauth_request, consumer, token):
sig = (
escape(oauth_request.get_normalized_http_method()),
@@ -514,7 +511,12 @@ def build_signature(self, oauth_request, consumer, token):
key, raw = self.build_signature_base_string(oauth_request, consumer, token)
# hmac object
- hashed = hmac.new(key, raw, hashlib.sha1)
+ try:
+ from Crypto.Hash import SHA1
+ hashed = hmac.new(key, raw, SHA1)
+ except:
+ import hashlib
+ hashed = hmac.new(key, raw, hashlib.sha1)
# calculate the digest base 64
return base64.b64encode(hashed.digest())
diff --git a/helios_auth/south_migrations/0001_initial.py b/helios_auth/south_migrations/0001_initial.py
deleted file mode 100644
index 4ba2a3a2b..000000000
--- a/helios_auth/south_migrations/0001_initial.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
-
- # Adding model 'User'
- db.create_table('helios_auth_user', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('user_type', self.gf('django.db.models.fields.CharField')(max_length=50)),
- ('user_id', self.gf('django.db.models.fields.CharField')(max_length=100)),
- ('name', self.gf('django.db.models.fields.CharField')(max_length=200, null=True)),
- ('info', self.gf('helios_auth.jsonfield.JSONField')()),
- ('token', self.gf('helios_auth.jsonfield.JSONField')(null=True)),
- ('admin_p', self.gf('django.db.models.fields.BooleanField')(default=False)),
- ))
- db.send_create_signal('helios_auth', ['User'])
-
- # Adding unique constraint on 'User', fields ['user_type', 'user_id']
- db.create_unique('helios_auth_user', ['user_type', 'user_id'])
-
-
- def backwards(self, orm):
-
- # Removing unique constraint on 'User', fields ['user_type', 'user_id']
- db.delete_unique('helios_auth_user', ['user_type', 'user_id'])
-
- # Deleting model 'User'
- db.delete_table('helios_auth_user')
-
-
- models = {
- 'helios_auth.user': {
- 'Meta': {'unique_together': "(('user_type', 'user_id'),)", 'object_name': 'User'},
- 'admin_p': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'info': ('helios_auth.jsonfield.JSONField', [], {}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
- 'token': ('helios_auth.jsonfield.JSONField', [], {'null': 'True'}),
- 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'user_type': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- }
- }
-
- complete_apps = ['helios_auth']
diff --git a/helios_auth/south_migrations/__init__.py b/helios_auth/south_migrations/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/helios_auth/templates/auth/devlogin.html b/helios_auth/templates/auth/devlogin.html
new file mode 100644
index 000000000..3c99b0fac
--- /dev/null
+++ b/helios_auth/templates/auth/devlogin.html
@@ -0,0 +1,25 @@
+
+
+
+ Dev Login
+
+
+
+
+
+
Dev Login
+
user@example.com
+
+ {% csrf_token %}
+ Log in
+
+
+
+
diff --git a/helios_auth/templates/index.html b/helios_auth/templates/index.html
index f16a7b80c..3c2e9f8d7 100644
--- a/helios_auth/templates/index.html
+++ b/helios_auth/templates/index.html
@@ -8,7 +8,7 @@ Authentication
You are currently logged in as{{user.user_id}} via {{user.user_type}} .
- logout
+ logout
{% else %}
diff --git a/helios_auth/templates/ldapauth/login.html b/helios_auth/templates/ldapauth/login.html
new file mode 100644
index 000000000..5e7a007ca
--- /dev/null
+++ b/helios_auth/templates/ldapauth/login.html
@@ -0,0 +1,29 @@
+{% extends TEMPLATE_BASE %}
+{% load i18n %}
+
+{% block content %}
+
+{% endblock content %}
+{% block footer %}
+
+{% endblock footer %}
diff --git a/helios_auth/templates/login_box.html b/helios_auth/templates/login_box.html
index 2e89e94e4..7273675bc 100644
--- a/helios_auth/templates/login_box.html
+++ b/helios_auth/templates/login_box.html
@@ -1,14 +1,14 @@
{% if default_auth_system %}
-Log in
+Log in
{% else %}
{% for auth_system in enabled_auth_systems %}
-{% ifequal auth_system "password" %}
+{% if auth_system == "password" %}
{% else %}
-
+
{{auth_system}}
-{% endifequal %}
+{% endif %}
{% endfor %}
diff --git a/helios_auth/templates/password/forgot.html b/helios_auth/templates/password/forgot.html
index 043a66789..ac756888c 100644
--- a/helios_auth/templates/password/forgot.html
+++ b/helios_auth/templates/password/forgot.html
@@ -15,6 +15,6 @@ {{ settings.SITE_TITLE }} — Password Forgotten
-Your Username :
+Your Username :
{% endblock %}
diff --git a/helios_auth/templates/password/login.html b/helios_auth/templates/password/login.html
index c92b8f1d6..691d14967 100644
--- a/helios_auth/templates/password/login.html
+++ b/helios_auth/templates/password/login.html
@@ -20,7 +20,7 @@ {{ settings.SITE_TITLE }} — Login
{{form.as_table}}
-
+
diff --git a/helios_auth/templates/perms_why.html b/helios_auth/templates/perms_why.html
index 179d103c2..f072d6690 100644
--- a/helios_auth/templates/perms_why.html
+++ b/helios_auth/templates/perms_why.html
@@ -13,8 +13,8 @@ Why we need your information
-
-
+
+
nope, get me out of here .
diff --git a/helios_auth/templates/twitter/follow.html b/helios_auth/templates/twitter/follow.html
deleted file mode 100644
index bbda5da3d..000000000
--- a/helios_auth/templates/twitter/follow.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends TEMPLATE_BASE %}
-
-{% block header %}
-{{ settings.SITE_TITLE }} — Twitter Follow?
-{% endblock %}
-
-
-{% block content %}
-
-
-We recommend that you follow @{{user_to_follow}} .
-That way, {{reason_to_follow}}. We will not abuse of this friendship: only messages pertinent to you will be sent to you directly, and general updates happen only sporadically.
-
-
-
-
-
-
-follow @{{user_to_follow}}
-
-
-
-
-
-{% endblock %}
diff --git a/helios_auth/tests.py b/helios_auth/tests.py
index f07f309fb..6f29fcb59 100644
--- a/helios_auth/tests.py
+++ b/helios_auth/tests.py
@@ -3,16 +3,47 @@
"""
import unittest
-import models
+from django.core import mail
from django.db import IntegrityError, transaction
+from django.test import TestCase, override_settings
+from django.urls import reverse
-from django.test.client import Client
-from django.test import TestCase
+from . import models, views
+from .auth_systems import AUTH_SYSTEMS, password as password_views
+from .utils import format_recipient
-from django.core import mail
-from auth_systems import AUTH_SYSTEMS
+class FormatRecipientTests(unittest.TestCase):
+ """Tests for the format_recipient helper function"""
+
+ def test_basic_formatting(self):
+ """Test basic name and email formatting"""
+ result = format_recipient("John Doe", "john@example.com")
+ self.assertEqual(result, "\"John Doe\" ")
+
+ def test_truncates_long_name(self):
+ """Test that names longer than 70 characters are truncated"""
+ long_name = "A" * 100
+ result = format_recipient(long_name, "test@example.com")
+ self.assertEqual(result, "\"%s\" " % ("A" * 70))
+
+ def test_empty_name_uses_email(self):
+ """Test that empty name falls back to email"""
+ result = format_recipient("", "test@example.com")
+ self.assertEqual(result, "\"test@example.com\" ")
+
+ def test_none_name_uses_email(self):
+ """Test that None name falls back to email"""
+ result = format_recipient(None, "test@example.com")
+ self.assertEqual(result, "\"test@example.com\" ")
+
+# Import devlogin for testing if available
+try:
+ from .auth_systems import devlogin
+except ImportError:
+ devlogin = None
+
class UserModelTests(unittest.TestCase):
@@ -23,12 +54,12 @@ def test_unique_users(self):
"""
there should not be two users with the same user_type and user_id
"""
- for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+ for auth_system, auth_system_module in AUTH_SYSTEMS.items():
models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name':'Foo Bar'})
-
+
def double_insert():
models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name': 'Foo2 Bar'})
-
+
self.assertRaises(IntegrityError, double_insert)
transaction.rollback()
@@ -36,39 +67,38 @@ def test_create_or_update(self):
"""
shouldn't create two users, and should reset the password
"""
- for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+ for auth_system, auth_system_module in AUTH_SYSTEMS.items():
u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name':'Foo Bar'})
def double_update_or_create():
new_name = 'Foo2 Bar'
u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name': new_name})
- self.assertEquals(u.id, u2.id)
- self.assertEquals(u2.info['name'], new_name)
+ self.assertEqual(u.id, u2.id)
+ self.assertEqual(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():
+ for auth_system, auth_system_module in AUTH_SYSTEMS.items():
assert(hasattr(auth_system_module, 'can_create_election'))
- if auth_system != 'clever':
- assert(auth_system_module.can_create_election('foobar', {}))
-
+ 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,
and otherwise does not report it
"""
- for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+ for auth_system, auth_system_module in AUTH_SYSTEMS.items():
u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'})
if hasattr(auth_system_module, 'send_message'):
- self.assertNotEquals(u.update_status_template, None)
+ self.assertNotEqual(u.update_status_template, None)
else:
- self.assertEquals(u.update_status_template, None)
+ self.assertEqual(u.update_status_template, None)
def test_eligibility(self):
"""
@@ -76,22 +106,121 @@ def test_eligibility(self):
FIXME: also test constraints on eligibility
"""
- for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+ for auth_system, auth_system_module in AUTH_SYSTEMS.items():
u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'})
self.assertTrue(u.is_eligible_for({'auth_system': auth_system}))
def test_eq(self):
- for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+ for auth_system, auth_system_module in AUTH_SYSTEMS.items():
u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'})
u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'})
- self.assertEquals(u, u2)
+ self.assertEqual(u, u2)
+
+class GitHubUserTests(TestCase):
+ """
+ Tests specific to GitHub authentication, particularly case-insensitive username matching.
+ GitHub usernames are case-insensitive, so 'JohnDoe', 'johndoe', and 'JOHNDOE' should all
+ refer to the same user.
+ """
+
+ def test_github_case_insensitive_update_or_create(self):
+ """
+ Test that update_or_create matches GitHub users case-insensitively
+ """
+ # Create a user with mixed case
+ u1 = models.User.update_or_create(
+ user_type='github',
+ user_id='JohnDoe',
+ name='John Doe (JohnDoe)',
+ info={'email': 'john@example.com'}
+ )
+
+ # Login again with lowercase - should return the same user
+ u2 = models.User.update_or_create(
+ user_type='github',
+ user_id='johndoe',
+ name='John Doe (johndoe)',
+ info={'email': 'john@example.com'}
+ )
+
+ # Should be the same database record
+ self.assertEqual(u1.id, u2.id)
+
+ # The user_id should be updated to the new case
+ u2.refresh_from_db()
+ self.assertEqual(u2.user_id, 'johndoe')
+
+ def test_github_case_insensitive_get_by_type_and_id(self):
+ """
+ Test that get_by_type_and_id finds GitHub users case-insensitively
+ """
+ # Create a user with uppercase
+ models.User.update_or_create(
+ user_type='github',
+ user_id='TESTUSER',
+ name='Test User (TESTUSER)',
+ info={'email': 'test@example.com'}
+ )
+
+ # Should find the user with lowercase
+ u = models.User.get_by_type_and_id('github', 'testuser')
+ self.assertEqual(u.user_id, 'TESTUSER')
+
+ # Should find the user with mixed case
+ u = models.User.get_by_type_and_id('github', 'TestUser')
+ self.assertEqual(u.user_id, 'TESTUSER')
+
+ def test_github_preserves_display_case(self):
+ """
+ Test that the username case is preserved from the most recent login
+ """
+ # First login with lowercase
+ u1 = models.User.update_or_create(
+ user_type='github',
+ user_id='myuser',
+ name='My User (myuser)',
+ info={'email': 'my@example.com'}
+ )
+ self.assertEqual(u1.user_id, 'myuser')
+
+ # Second login with mixed case - should update stored case
+ u2 = models.User.update_or_create(
+ user_type='github',
+ user_id='MyUser',
+ name='My User (MyUser)',
+ info={'email': 'my@example.com'}
+ )
+ self.assertEqual(u2.user_id, 'MyUser')
+
+ # Verify it's the same user
+ self.assertEqual(u1.id, u2.id)
+
+ def test_password_auth_still_case_sensitive(self):
+ """
+ Test that password auth remains case-sensitive (not affected by GitHub changes)
+ """
+ # Create a user with mixed case
+ u1 = models.User.update_or_create(
+ user_type='password',
+ user_id='TestUser@example.com',
+ name='Test User',
+ info={'password': 'hashed_password'}
+ )
+
+ # Create another user with different case - should be a new user
+ u2 = models.User.update_or_create(
+ user_type='password',
+ user_id='testuser@example.com',
+ name='Test User 2',
+ info={'password': 'hashed_password2'}
+ )
+
+ # Should be different users
+ self.assertNotEqual(u1.id, u2.id)
-import views
-import auth_systems.password as password_views
-from django.core.urlresolvers import reverse
# FIXME: login CSRF should make these tests more complicated
# and should be tested for
@@ -116,8 +245,8 @@ def test_password_login(self):
# self.assertContains(response, "Foobar User")
def test_logout(self):
- response = self.client.post(reverse(views.logout), follow=True)
-
+ response = self.client.post(reverse("auth@logout"), follow=True)
+
self.assertContains(response, "not logged in")
self.assertNotContains(response, "Foobar User")
@@ -125,6 +254,162 @@ def test_email(self):
"""using the test email backend"""
self.test_user.send_message("testing subject", "testing body")
- self.assertEquals(len(mail.outbox), 1)
- self.assertEquals(mail.outbox[0].subject, "testing subject")
- self.assertEquals(mail.outbox[0].to[0], "\"Foobar User\" ")
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, "testing subject")
+ self.assertEqual(mail.outbox[0].to[0], "\"Foobar User\" ")
+
+# LDAP auth tests
+from .auth_systems import ldapauth as ldap_views
+class LDAPAuthTests(TestCase):
+ """
+ These tests relies on OnLine LDAP Test Server, provided by forum Systems:
+ http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
+ """
+
+ def setUp(self):
+ """ set up necessary django-auth-ldap settings """
+ self.password = 'password'
+ self.username = 'euclid'
+
+ def test_backend_login(self):
+ """ test if authenticates using the backend """
+ from helios_auth.auth_systems.ldapbackend import backend
+ auth = backend.CustomLDAPBackend()
+ user = auth.authenticate(None, username=self.username, password=self.password)
+ if user is None:
+ self.skipTest("LDAP server unavailable - skipping test")
+ self.assertEqual(user.username, 'euclid')
+
+ def test_ldap_view_login(self):
+ """ test if authenticates using the auth system login view """
+ resp = self.client.post(reverse(ldap_views.ldap_login_view), {
+ 'username' : self.username,
+ 'password': self.password
+ }, follow=True)
+ self.assertEqual(resp.status_code, 200)
+
+ def test_logout(self):
+ """ test if logs out using the auth system logout view """
+ response = self.client.post(reverse(views.logout), follow=True)
+ print(response.content)
+ self.assertContains(response, "not logged in")
+ self.assertNotContains(response, "euclid")
+
+
+# Development Login Tests
+@unittest.skipIf(devlogin is None, "devlogin not available (not in DEBUG mode)")
+@override_settings(DEBUG=True, AUTH_ENABLED_SYSTEMS=['devlogin', 'password'], ALLOWED_HOSTS=['localhost', '127.0.0.1', 'testserver'], TEMPLATE_BASE='server_ui/templates/base.html')
+class DevLoginTests(TestCase):
+ """Tests for the development-only authentication system."""
+
+ def test_full_devlogin_flow(self):
+ """Test the complete devlogin authentication flow"""
+ # Start auth, submit form, verify logged in
+ response = self.client.get(reverse('auth@start', kwargs={'system_name': 'devlogin'}))
+ response = self.client.post(response.url, follow=True)
+
+ self.assertIn('user', self.client.session)
+ self.assertEqual(self.client.session['user']['type'], 'devlogin')
+ self.assertEqual(self.client.session['user']['user_id'], 'user@example.com')
+
+ def test_devlogin_blocked_when_not_localhost(self):
+ """Test that devlogin is blocked for non-localhost requests"""
+ from django.test import RequestFactory
+ from django.http import Http404
+
+ with self.settings(DEBUG=False):
+ factory = RequestFactory()
+ request = factory.get('/', HTTP_HOST='example.com')
+
+ with self.assertRaises(Http404):
+ devlogin.get_auth_url(request)
+
+
+class OAuthAuthSystemTests(TestCase):
+ """
+ Tests for OAuth-based authentication systems (Google, GitHub, GitLab, LinkedIn)
+ """
+
+ def test_oauth_systems_have_required_interface(self):
+ """Verify OAuth auth systems implement required interface methods"""
+ oauth_systems = ['google', 'github', 'gitlab', 'linkedin']
+ required_methods = [
+ 'get_auth_url',
+ 'get_user_info_after_auth',
+ 'do_logout',
+ 'send_message',
+ 'can_create_election',
+ ]
+
+ for system_name in oauth_systems:
+ if system_name not in AUTH_SYSTEMS:
+ continue
+ system = AUTH_SYSTEMS[system_name]
+ for method in required_methods:
+ self.assertTrue(
+ hasattr(system, method),
+ f"{system_name} missing required method: {method}"
+ )
+
+ def test_oauth_state_verification_rejects_missing_state(self):
+ """Verify OAuth state verification rejects requests with missing state"""
+ from django.test import RequestFactory
+ from .auth_systems import google, github, gitlab, linkedin
+
+ factory = RequestFactory()
+
+ for system, session_key in [
+ (google, 'google-oauth-state'),
+ (github, 'gh_oauth_state'),
+ (gitlab, 'gl_oauth_state'),
+ (linkedin, 'linkedin_oauth_state'),
+ ]:
+ # Create request with code but no state
+ request = factory.get('/auth/after/', {'code': 'test_code'})
+ request.session = {}
+
+ with self.assertRaises(Exception) as context:
+ system.get_user_info_after_auth(request)
+ self.assertIn('state mismatch', str(context.exception).lower())
+
+ def test_oauth_state_verification_rejects_wrong_state(self):
+ """Verify OAuth state verification rejects requests with wrong state"""
+ from django.test import RequestFactory
+ from .auth_systems import google, github, gitlab, linkedin
+
+ factory = RequestFactory()
+
+ test_cases = [
+ (google, 'google-oauth-state', 'google-redirect-url'),
+ (github, 'gh_oauth_state', 'gh_redirect_uri'),
+ (gitlab, 'gl_oauth_state', 'gl_redirect_uri'),
+ (linkedin, 'linkedin_oauth_state', 'linkedin_redirect_uri'),
+ ]
+
+ for system, state_key, redirect_key in test_cases:
+ # Create request with code and wrong state
+ request = factory.get('/auth/after/', {
+ 'code': 'test_code',
+ 'state': 'wrong_state'
+ })
+ request.session = {
+ state_key: 'correct_state',
+ redirect_key: 'http://example.com/callback'
+ }
+
+ with self.assertRaises(Exception) as context:
+ system.get_user_info_after_auth(request)
+ self.assertIn('state mismatch', str(context.exception).lower())
+
+ def test_oauth_returns_none_without_code(self):
+ """Verify OAuth auth returns None when no code is provided"""
+ from django.test import RequestFactory
+ from .auth_systems import google, github, gitlab, linkedin
+
+ factory = RequestFactory()
+ request = factory.get('/auth/after/')
+ request.session = {}
+
+ for system in [google, github, gitlab, linkedin]:
+ result = system.get_user_info_after_auth(request)
+ self.assertIsNone(result)
diff --git a/helios_auth/url_names.py b/helios_auth/url_names.py
new file mode 100644
index 000000000..29f7b228f
--- /dev/null
+++ b/helios_auth/url_names.py
@@ -0,0 +1,6 @@
+AUTH_INDEX="auth@index"
+AUTH_LOGOUT="auth@logout"
+AUTH_START="auth@start"
+AUTH_AFTER="auth@after"
+AUTH_WHY="auth@why"
+AUTH_AFTER_INTERVENTION="auth@after-intervention"
diff --git a/helios_auth/urls.py b/helios_auth/urls.py
index e4dca3985..a90ba35f6 100644
--- a/helios_auth/urls.py
+++ b/helios_auth/urls.py
@@ -1,31 +1,37 @@
+
"""
Authentication URLs
Ben Adida (ben@adida.net)
"""
-from django.conf.urls import *
+from django.urls import path, re_path
-from views import *
-from auth_systems.password import password_login_view, password_forgotten_view
-from auth_systems.twitter import follow_view
+from settings import AUTH_ENABLED_SYSTEMS
+from . import views, url_names
-urlpatterns = patterns('',
+urlpatterns = [
# basic static stuff
- (r'^$', index),
- (r'^logout$', logout),
- (r'^start/(?P.*)$', start),
+ path('', views.index, name=url_names.AUTH_INDEX),
+ path('logout', views.logout, name=url_names.AUTH_LOGOUT),
+ re_path(r'^start/(?P.*)$', views.start, name=url_names.AUTH_START),
# weird facebook constraint for trailing slash
- (r'^after/$', after),
- (r'^why$', perms_why),
- (r'^after_intervention$', after_intervention),
-
- ## should make the following modular
-
- # password auth
- (r'^password/login', password_login_view),
- (r'^password/forgot', password_forgotten_view),
-
- # twitter
- (r'^twitter/follow', follow_view),
-)
+ path('after/', views.after, name=url_names.AUTH_AFTER),
+ path('why', views.perms_why, name=url_names.AUTH_WHY),
+ path('after_intervention', views.after_intervention, name=url_names.AUTH_AFTER_INTERVENTION),
+]
+
+# password auth
+if 'password' in AUTH_ENABLED_SYSTEMS:
+ from .auth_systems.password import urlpatterns as password_patterns
+ urlpatterns.extend(password_patterns)
+
+# ldap
+if 'ldap' in AUTH_ENABLED_SYSTEMS:
+ from .auth_systems.ldapauth import urlpatterns as ldap_patterns
+ urlpatterns.extend(ldap_patterns)
+
+# devlogin (development only)
+if 'devlogin' in AUTH_ENABLED_SYSTEMS:
+ from .auth_systems.devlogin import urlpatterns as devlogin_patterns
+ urlpatterns.extend(devlogin_patterns)
\ No newline at end of file
diff --git a/helios_auth/utils.py b/helios_auth/utils.py
index f57dedf6e..2dda71ad5 100644
--- a/helios_auth/utils.py
+++ b/helios_auth/utils.py
@@ -7,23 +7,40 @@
import json
+
## JSON
def to_json(d):
- return json.dumps(d, sort_keys=True)
-
-def from_json(json_str):
- if not json_str: return None
- return json.loads(json_str)
-
-def JSONtoDict(json):
- x=json.loads(json)
- return x
-
+ return json.dumps(d, sort_keys=True)
+
+
+def from_json(value):
+ if value == "" or value is None:
+ return None
+
+ if isinstance(value, str):
+ try:
+ return json.loads(value)
+ except Exception as e:
+ # import ast
+ # try:
+ # parsed_value = ast.literal_eval(parsed_value)
+ # except Exception as e1:
+ raise Exception("value is not JSON parseable, that's bad news") from e
+
+ return value
+
+
def JSONFiletoDict(filename):
- f = open(filename, 'r')
- content = f.read()
- f.close()
- return JSONtoDict(content)
-
+ with open(filename, 'r') as f:
+ content = f.read()
+ return from_json(content)
+def format_recipient(name, email):
+ """
+ Format an email recipient as "name" .
+ Truncates name to 70 characters to avoid issues with Python3's email module.
+ Quotes the name per RFC 5322 to handle special characters.
+ """
+ truncated_name = name[:70] if name else email
+ return "\"%s\" <%s>" % (truncated_name, email)
diff --git a/helios_auth/view_utils.py b/helios_auth/view_utils.py
index 1355ba450..9a4f9ab24 100644
--- a/helios_auth/view_utils.py
+++ b/helios_auth/view_utils.py
@@ -4,15 +4,13 @@
Ben Adida (12-30-2008)
"""
-from django.template import Context, Template, loader
-from django.http import HttpResponse, Http404
-from django.shortcuts import render_to_response
-
-from helios_auth.security import get_user
+from django.conf import settings
+from django.http import HttpResponse
+from django.shortcuts import render
+from django.template import loader
import helios_auth
-
-from django.conf import settings
+from helios_auth.security import get_user
##
## BASICS
@@ -24,37 +22,33 @@
## template abstraction
##
-def prepare_vars(request, vars):
- vars_with_user = vars.copy()
-
+def prepare_vars(request, values):
+ vars_with_user = values.copy()
+
if request:
vars_with_user['user'] = get_user(request)
vars_with_user['csrf_token'] = request.session['csrf_token']
vars_with_user['SECURE_URL_HOST'] = settings.SECURE_URL_HOST
-
+
vars_with_user['STATIC'] = '/static/auth'
vars_with_user['MEDIA_URL'] = '/static/auth/'
vars_with_user['TEMPLATE_BASE'] = helios_auth.TEMPLATE_BASE
-
+
vars_with_user['settings'] = settings
-
+
return vars_with_user
-
-def render_template(request, template_name, vars = {}):
- t = loader.get_template(template_name + '.html')
-
- vars_with_user = prepare_vars(request, vars)
-
- return render_to_response('helios_auth/templates/%s.html' % template_name, vars_with_user)
-def render_template_raw(request, template_name, vars={}):
- t = loader.get_template(template_name + '.html')
-
- vars_with_user = prepare_vars(request, vars)
- c = Context(vars_with_user)
- return t.render(c)
-def render_json(json_txt):
- return HttpResponse(json_txt)
+def render_template(request, template_name, values=None):
+ vars_with_user = prepare_vars(request, values or {})
+
+ return render(request, 'helios_auth/templates/%s.html' % template_name, vars_with_user)
+
+
+def render_template_raw(request, template_name, values=None):
+ t = loader.get_template(template_name + '.html')
+ values = values or {}
+ vars_with_user = prepare_vars(request, values)
+ return t.render(context=vars_with_user, request=request)
diff --git a/helios_auth/views.py b/helios_auth/views.py
index b85f82e79..73f23050b 100644
--- a/helios_auth/views.py
+++ b/helios_auth/views.py
@@ -5,22 +5,20 @@
2009-07-05
"""
-from django.http import *
-from django.core.urlresolvers import reverse
+from urllib.parse import urlencode
-from view_utils import *
-from helios_auth.security import get_user
-
-import auth_systems
-from auth_systems import AUTH_SYSTEMS
-from auth_systems import password
-import helios_auth
+from django.http import HttpResponseRedirect, HttpResponse
+from django.urls import reverse
-import copy, urllib
-
-from models import User
+import settings
+from helios_auth import DEFAULT_AUTH_SYSTEM, ENABLED_AUTH_SYSTEMS
+from helios_auth.security import get_user
+from helios_auth.url_names import AUTH_INDEX, AUTH_START, AUTH_AFTER, AUTH_WHY, AUTH_AFTER_INTERVENTION
+from .auth_systems import AUTH_SYSTEMS, password
+from .models import User
+from .security import FIELDS_TO_SAVE
+from .view_utils import render_template, render_template_raw
-from security import FIELDS_TO_SAVE
def index(request):
"""
@@ -30,42 +28,42 @@ def index(request):
user = get_user(request)
# single auth system?
- if len(helios_auth.ENABLED_AUTH_SYSTEMS) == 1 and not user:
- return HttpResponseRedirect(reverse(start, args=[helios_auth.ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', ''))
+ if len(ENABLED_AUTH_SYSTEMS) == 1 and not user:
+ return HttpResponseRedirect(reverse(AUTH_START, args=[ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', ''))
- #if helios_auth.DEFAULT_AUTH_SYSTEM and not user:
- # return HttpResponseRedirect(reverse(start, args=[helios_auth.DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', ''))
+ #if DEFAULT_AUTH_SYSTEM and not user:
+ # return HttpResponseRedirect(reverse(start, args=[DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', ''))
default_auth_system_obj = None
- if helios_auth.DEFAULT_AUTH_SYSTEM:
- default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM]
+ if DEFAULT_AUTH_SYSTEM:
+ default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM]
#form = password.LoginForm()
- return render_template(request,'index', {'return_url' : request.GET.get('return_url', '/'),
- 'enabled_auth_systems' : helios_auth.ENABLED_AUTH_SYSTEMS,
- 'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM,
- 'default_auth_system_obj': default_auth_system_obj})
+ return render_template(request, 'index', {'return_url' : request.GET.get('return_url', '/'),
+ 'enabled_auth_systems' : ENABLED_AUTH_SYSTEMS,
+ 'default_auth_system': DEFAULT_AUTH_SYSTEM,
+ 'default_auth_system_obj': default_auth_system_obj})
def login_box_raw(request, return_url='/', auth_systems = None):
"""
a chunk of HTML that shows the various login options
"""
default_auth_system_obj = None
- if helios_auth.DEFAULT_AUTH_SYSTEM:
- default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM]
+ if DEFAULT_AUTH_SYSTEM:
+ default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM]
# make sure that auth_systems includes only available and enabled auth systems
- if auth_systems != None:
- enabled_auth_systems = set(auth_systems).intersection(set(helios_auth.ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys()))
+ if auth_systems is not None:
+ enabled_auth_systems = set(auth_systems).intersection(set(ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys()))
else:
- enabled_auth_systems = set(helios_auth.ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys()))
+ enabled_auth_systems = set(ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys()))
form = password.LoginForm()
return render_template_raw(request, 'login_box', {
'enabled_auth_systems': enabled_auth_systems, 'return_url': return_url,
- 'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj,
+ 'default_auth_system': DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj,
'form' : form})
def do_local_logout(request):
@@ -76,7 +74,7 @@ def do_local_logout(request):
user = None
- if request.session.has_key('user'):
+ if 'user' in request.session:
user = request.session['user']
# 2010-08-14 be much more aggressive here
@@ -87,7 +85,7 @@ def do_local_logout(request):
# let's clean up the self-referential issue:
field_names_to_save = set(field_names_to_save)
- field_names_to_save = field_names_to_save - set([FIELDS_TO_SAVE])
+ field_names_to_save = field_names_to_save - {FIELDS_TO_SAVE}
field_names_to_save = list(field_names_to_save)
fields_to_save = dict([(name, request.session.get(name, None)) for name in field_names_to_save])
@@ -144,7 +142,7 @@ def _do_auth(request):
system = AUTH_SYSTEMS[system_name]
# where to send the user to?
- redirect_url = "%s%s" % (settings.SECURE_URL_HOST,reverse(after))
+ redirect_url = settings.SECURE_URL_HOST + reverse(AUTH_AFTER)
auth_url = system.get_auth_url(request, redirect_url=redirect_url)
if auth_url:
@@ -153,8 +151,8 @@ def _do_auth(request):
return HttpResponse("an error occurred trying to contact " + system_name +", try again later")
def start(request, system_name):
- if not (system_name in helios_auth.ENABLED_AUTH_SYSTEMS):
- return HttpResponseRedirect(reverse(index))
+ if not (system_name in ENABLED_AUTH_SYSTEMS):
+ return HttpResponseRedirect(reverse(AUTH_INDEX))
# why is this here? Let's try without it
# request.session.save()
@@ -175,7 +173,7 @@ def perms_why(request):
def after(request):
# which auth system were we using?
- if not request.session.has_key('auth_system_name'):
+ if 'auth_system_name' not in request.session:
do_local_logout(request)
return HttpResponseRedirect("/")
@@ -190,7 +188,7 @@ def after(request):
request.session['user'] = user
else:
- return HttpResponseRedirect("%s?%s" % (reverse(perms_why), urllib.urlencode({'system_name' : request.session['auth_system_name']})))
+ return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urlencode({'system_name' : request.session['auth_system_name']})))
# does the auth system want to present an additional view?
# this is, for example, to prompt the user to follow @heliosvoting
@@ -201,12 +199,12 @@ def after(request):
return intervention_response
# go to the after intervention page. This is for modularity
- return HttpResponseRedirect(reverse(after_intervention))
+ return HttpResponseRedirect(reverse(AUTH_AFTER_INTERVENTION))
def after_intervention(request):
return_url = "/"
- if request.session.has_key('auth_return_url'):
+ if 'auth_return_url' in request.session:
return_url = request.session['auth_return_url']
del request.session['auth_return_url']
- return HttpResponseRedirect("%s%s" % (settings.URL_HOST, return_url))
+ return HttpResponseRedirect(settings.URL_HOST + return_url)
diff --git a/heliosbooth/boothworker-single.js b/heliosbooth/boothworker-single.js
index edb1debb5..1c87986ca 100644
--- a/heliosbooth/boothworker-single.js
+++ b/heliosbooth/boothworker-single.js
@@ -49,12 +49,16 @@ function do_encrypt(message) {
// receive either
// a) an election and an integer position of the question
// that this worker will be used to encrypt
-// {'type': 'setup', 'question_num' : 2, 'election' : election_json}
+// {'type': 'setup', 'election': election_json}
//
// b) an answer that needs encrypting
-// {'type': 'encrypt', 'answer' : answer_json}
+// {'type': 'encrypt', 'q_num': 2, 'id': id, 'answer': answer_json}
//
self.onmessage = function(event) {
// dispatch to method
- self['do_' + event.data.type](event.data);
-}
+ if (event.data.type === "setup") {
+ do_setup(event.data);
+ } else if (event.data.type === "encrypt") {
+ do_encrypt(event.data);
+ }
+};
diff --git a/heliosbooth/css/booth.css b/heliosbooth/css/booth.css
index 716ada2b6..4d4eefca4 100644
--- a/heliosbooth/css/booth.css
+++ b/heliosbooth/css/booth.css
@@ -1,65 +1,68 @@
-
body {
- font-family: Arial;
+ font-family: sans-serif;
background: white;
- padding: 0px;
- margin: 0px;
+ padding: 0;
+ margin: 0;
}
#wrapper {
- position: absolute;
- padding: 0px;
- background: white;
+ padding: 0;
+ background: #ffffff;
border: 1px solid #666;
- top: 20px;
- margin-left: 100px;
- margin-top: 0px;
- width: 1000px;
+ margin: 20px auto;
+ max-width: 1000px;
+}
+
+@media screen and (max-width: 1000px) {
+ #wrapper {
+ margin: 0;
+ }
}
#content {
- padding: 20px 30px 20px 30px;
+ padding: 20px;
}
#header {
- padding-top: 0px;
+ padding-top: 0;
text-align: center;
padding-bottom: 5px;
}
#header h1 {
font-size: 28pt;
- padding: 0px;
- margin: 0px;
+ padding: 0;
+ margin: 0;
line-height: 120%;
}
#header h2 {
font-size: 20pt;
- padding: 0px;
- margin: 0px;
+ padding: 0;
+ margin: 0;
line-height: 100%;
- font-weight: none;
}
-#banner {
+.edge {
width: 100%;
+ display: flex;
+ padding: 5px 0;
text-align: center;
- padding: 2px 0px 2px 0px;
background: #ccc;
font-size: 18pt;
- border-bottom: 1px solid #666;
}
#progress_div {
- width: 100%;
+ margin: auto;
+ max-width: 500px;
font-size: 14pt;
}
#progress_div table {
+ width: 100%;
border-collapse: collapse;
text-align: center;
- border: 0px;
+ border: 0;
}
#progress_div td.unselected {
@@ -76,20 +79,14 @@ body {
}
#footer {
- position: relative;
- bottom: 0px;
- width: 100%;
- text-align: center;
- margin-top: 10px;
- padding: 2px 0px 2px 0px;
- background: #ddd;
- border-top: 1px solid #666;
+ font-size: 12pt;
+ word-break: break-all;
}
#page h2 {
background: #fc9;
border-bottom: 1px solid #f90;
- padding: 5px 0px 2px 5px;
+ padding: 5px 0 2px 5px;
}
h3 {
@@ -101,9 +98,9 @@ h3 {
}
#election_hash {
- font-family: courier;
+ font-family: monospace;
}
#loading_div {
display: none;
-}
\ No newline at end of file
+}
diff --git a/heliosbooth/css/forms.css b/heliosbooth/css/forms.css
index ee1a141a5..c89a0f5a7 100644
--- a/heliosbooth/css/forms.css
+++ b/heliosbooth/css/forms.css
@@ -1,4 +1,3 @@
-
form.prettyform {
font-size: 1.4em;
}
@@ -7,7 +6,7 @@ form.prettyform label,input,textarea,select {
line-height: 1.8;
}
-form.prettyform label {
+form.prettyform label:not(.answer) {
display: block;
text-align: right;
float: left;
@@ -19,12 +18,19 @@ form.prettyform br {
clear: left;
}
-form.prettyform input, form.prettyform textarea, button {
+form.prettyform input, form.prettyform textarea {
border: 1px solid black;
margin: 5px;
font-size: 16pt;
}
+button {
+ background: lightblue;
+ margin: 5px;
+ padding: 5px 20px 5px 20px;
+ font-size: 18pt;
+}
+
input.pretty {
font-size: 16pt;
border: 1px solid black;
@@ -35,7 +41,6 @@ input.prettysmall {
border: 1px solid black;
}
-
table.pretty {
margin: 1em 1em 1em 2em;
background: whitesmoke;
@@ -48,11 +53,10 @@ table.pretty th, td {
padding: 0.3em;
}
-
#answers {
padding-left: 210px;
- padding-top: 0px;
- margin-top: 0px;
+ padding-top: 0;
+ margin-top: 0;
}
#questions_div {
@@ -62,4 +66,3 @@ table.pretty th, td {
div.selected {
background: lightblue;
}
-
diff --git a/heliosbooth/css/style.css b/heliosbooth/css/style.css
index 8834be848..166c65f6f 100644
--- a/heliosbooth/css/style.css
+++ b/heliosbooth/css/style.css
@@ -4,13 +4,16 @@ a:visited {color:#00a;}
a:active {color:blue;}
-a:hover, a.subfoot:hover, a.linknav:hover, a.sublinknav:hover {color:blue;text-decoration:underline;}
+a:hover, a.subfoot:hover, a.linknav:hover, a.sublinknav:hover {
+ color:blue;
+ text-decoration:underline;
+}
body {
- margin:0px;
- padding: 0px;
+ margin:0;
+ padding: 0;
background:#ddd;
- font-family: "Trebuchet MS",verdana,sans-serif;
+ font-family: sans-serif;
font-size: 14px;
color:#333;
}
@@ -19,16 +22,9 @@ body {
margin-top:20px;
margin-right:50px;
margin-left:50px;
- background:white;
- border-top:1px solid #666;
- border-left:1px solid #666;
- border-right:1px solid #666;
- border-bottom:1px solid #666;
- background-color: white;
- padding-top: 0px;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
+ background: #ffffff;
+ border: 1px solid #666;
+ padding: 0;
}
#content {
@@ -43,7 +39,7 @@ body {
h1, h2, h3, h4, pre {
line-height:120%;
- margin: 0px;
+ margin: 0;
}
p {
@@ -94,7 +90,6 @@ h3 {
border-bottom: 1px dotted #888;
}
-
.question_selected {
padding-top: 15px;
padding-bottom: 10px;
@@ -114,6 +109,5 @@ h3 {
}
.data {
- font-family: courier;
+ font-family: monospace;
}
-
diff --git a/heliosbooth/election.json b/heliosbooth/election.json
deleted file mode 100644
index ee8512524..000000000
--- a/heliosbooth/election.json
+++ /dev/null
@@ -1 +0,0 @@
-{"cast_url": "http://localhost:8000/cast/", "uuid": "82cd836a-65f4-11de-8c90-001b63948875", "name": "Distributed Proofreaders Foundation", "openreg": true, "public_key": {"g": "68111451286792593845145063691659993410221812806874234365854504719057401858372594942893291581957322023471947260828209362467690671421429979048643907159864269436501403220400197614308904460547529574693875218662505553938682573554719632491024304637643868603338114042760529545510633271426088675581644231528918421974", "p": "169989719781940995935039590956086833929670733351333885026079217526937746166790934510618940073906514429409914370072173967782198129423558224854191320917329420870526887804017711055077916007496804049206725568956610515399196848621653907978580213217522397058071043503404700268425750722626265208099856407306527012763", "q": "84994859890970497967519795478043416964835366675666942513039608763468873083395467255309470036953257214704957185036086983891099064711779112427095660458664710435263443902008855527538958003748402024603362784478305257699598424310826953989290106608761198529035521751702350134212875361313132604049928203653263506381", "y": "166381348774608583478438068541900979308284008775524113662477325596101906734254387151724992928319196422498626340355054333287202185267035983610044362456796963319559762922405260105538812887763994476127275234333452789887229585512484617401407171125923170313708217690722245099929673448997254171816215353834880048567"}, "questions": [{"answer_urls": ["http:\/\/www.pgdp.net\/phpBB2\/viewtopic.php?p=539704#539704", "http:\/\/www.pgdp.net\/phpBB2\/viewtopic.php?p=539706#539706", "http:\/\/www.pgdp.net\/phpBB2\/viewtopic.php?p=539708#539708", "http:\/\/www.pgdp.net\/phpBB2\/viewtopic.php?p=539709#539709", "http:\/\/www.pgdp.net\/phpBB2\/viewtopic.php?p=539710#539710"], "answers": ["fvandrog: Frank van Drogen", "jhutch: Joshua Hutchinson", "rfrank: Roger Frank", "Simple Simon: David Jones", "TheEileen: Eileen Gormly"], "max": 3, "question": "Please choose 3 of the following 5 people to serve as Board members of the Distributed Proofreaders Foundation. Names are in alphabetical order by DP ID.", "short_name": "Choose new DPF Board Members"}]}
\ No newline at end of file
diff --git a/heliosbooth/js/jscrypto/random.js b/heliosbooth/js/jscrypto/random.js
index dd69f9d4f..8e363a6d0 100644
--- a/heliosbooth/js/jscrypto/random.js
+++ b/heliosbooth/js/jscrypto/random.js
@@ -26,10 +26,9 @@ Random.getRandomInteger = function(max) {
var bit_length = max.bitLength();
Random.setupGenerator();
var random;
- random = sjcl.random.randomWords(Math.ceil(bit_length / 32)+2, 0);
+ random = sjcl.random.randomWords(Math.ceil(bit_length / 32) + 2, 6);
// we get a bit array instead of a BigInteger in this case
var rand_bi = new BigInt(sjcl.codec.hex.fromBits(random), 16);
return rand_bi.mod(max);
- return BigInt._from_java_object(random).mod(max);
};
diff --git a/heliosbooth/single-ballot-verify.html b/heliosbooth/single-ballot-verify.html
index 113484fc7..1f7bff5a2 100644
--- a/heliosbooth/single-ballot-verify.html
+++ b/heliosbooth/single-ballot-verify.html
@@ -1,7 +1,9 @@
-
+
-
+
+
+
Helios Voting System
@@ -110,9 +112,7 @@
-
- Helios Single-Ballot Verifier
-
+
Helios Single-Ballot Verifier
diff --git a/heliosbooth/templates/audit.html b/heliosbooth/templates/audit.html
index 9165d52d9..f8ddd4111 100644
--- a/heliosbooth/templates/audit.html
+++ b/heliosbooth/templates/audit.html
@@ -25,9 +25,9 @@
Your audited ballot
Even if you post your audited ballot, you must go back to voting and choose "cast" if you want your vote to count.
-
+
back to voting
-
+
post audited ballot to tracking center
diff --git a/heliosbooth/templates/election.html b/heliosbooth/templates/election.html
index 680c4e06a..7139b0e57 100644
--- a/heliosbooth/templates/election.html
+++ b/heliosbooth/templates/election.html
@@ -1,29 +1,24 @@
- To cast a vote, you will be led through the following steps.
- If you have not yet logged in, you will be asked to do so at the very end of the process.
+ To vote, follow these steps:
- Select your preferred options.
-You can easily navigate forwards and backwards.
+ Select your preferred options.
- Review & Confirm your choices.
-
- Your choices are encrypted safely inside your browser, and you get a smart ballot tracker.
-
-
+ Review your choices, which are then encrypted.
- Submit your encrypted ballot.
-
-You will be asked to log in to submit your encrypted ballot for tallying.
-
-
+ Submit your encrypted ballot and authenticate to verify your eligibility.
+
Start
+
+ You can email for help .
+
+
diff --git a/heliosbooth/templates/footer.html b/heliosbooth/templates/footer.html
index 750fa4328..d907fbbae 100644
--- a/heliosbooth/templates/footer.html
+++ b/heliosbooth/templates/footer.html
@@ -1,9 +1,10 @@
-
-help!
-
-{#if $T.election.BOGUS_P}
-The public key for this election is not yet ready. This election is in preview mode only.
-{#else}
-Election Fingerprint:
{$T.election.hash}
-{#/if}
+
+
+ {#if $T.election.BOGUS_P}
+ The public key for this election is not yet ready. This election is in preview mode only.
+ {#else}
+ Election Fingerprint: {$T.election.hash}
+ {#/if}
+
+
diff --git a/heliosbooth/templates/header.html b/heliosbooth/templates/header.html
index 32873964e..07da45114 100644
--- a/heliosbooth/templates/header.html
+++ b/heliosbooth/templates/header.html
@@ -1,2 +1 @@
{$T.election.name}
-
diff --git a/heliosbooth/templates/question.html b/heliosbooth/templates/question.html
index 776eefdf5..0ac7a2d39 100644
--- a/heliosbooth/templates/question.html
+++ b/heliosbooth/templates/question.html
@@ -4,35 +4,40 @@
-{$T.question.question}
+{$T.question.question}
#{$T.question_num + 1} of {$T.last_question_num + 1} —
- vote for
+ vote for
{#if $T.question.min && $T.question.min > 0}
{#if $T.question.max}
-{$T.question.min} to {$T.question.max}
+ {$T.question.min} to {$T.question.max}
{#else}
-at least {$T.question.min}
+ at least {$T.question.min}
{#/if}
{#else}
{#if $T.question.max}
-{#if $T.question.max > 1}up to {#/if}{$T.question.max}
+{#if $T.question.max > 1} up to{#/if} {$T.question.max}
{#else}
-as many as you approve of
+ as many as you approve of
{#/if}
{#/if}
{#foreach $T.question.answers as answer}
-
{$T.question.answers[$T.answer_ordering[$T.answer$index]]}
+
+
-{#if $T.question.answer_urls && $T.question.answer_urls[$T.answer_ordering[$T.answer$index]] && $T.question.answer_urls[$T.answer_ordering[$T.answer$index]] != ""}
-
-
-[more info ]
-
-{#/if}
+
+ {$T.question.answers[$T.answer_ordering[$T.answer$index]]}
+
+ {#if $T.question.answer_urls && $T.question.answer_urls[$T.answer_ordering[$T.answer$index]] && $T.question.answer_urls[$T.answer_ordering[$T.answer$index]] != ""}
+
+
+ [more info ]
+
+ {#/if}
+
{#/for}
@@ -42,17 +47,17 @@
{#if $T.show_reviewall}
-
+Proceed
{#/if}
{#if $T.question_num != 0}
-
+
Previous
{#/if}
{#if $T.question_num < $T.last_question_num}
-
+
Next
{#/if}
diff --git a/heliosbooth/templates/seal.html b/heliosbooth/templates/seal.html
index a17de4bca..c8bd24be1 100644
--- a/heliosbooth/templates/seal.html
+++ b/heliosbooth/templates/seal.html
@@ -1,50 +1,28 @@
-
-{#if $T.election_metadata.use_advanced_audit_features}
-
-
Audit [optional]
-
-
-If you choose, you can audit your ballot and reveal how your choices were encrypted.
-
-
-You will then be guided to re-encrypt your choices for final casting.
-
-
-
-
-
-{#/if}
-
Review your Ballot
-
-
+
{#foreach $T.questions as question}
-
Question #{$T.question$index + 1}: {$T.question.short_name}
+Question #{$T.question$index + 1}: {$T.question.short_name}
{#if $T.choices[$T.question$index].length == 0}
-
☐ No choice selected
+
☐ No choice selected
{#/if}
{#foreach $T.choices[$T.question$index] as choice}
-
✓ {$T.choice}
+
✓ {$T.choice}
{#/for}
{#if $T.choices[$T.question$index].length < $T.question.max}
-[you under-voted: you may select up to {$T.question.max}]
+[{$T.choices[$T.question$index].length} selections out of possible {$T.question.min}-{$T.question.max}]
{#/if}
-[
edit responses ]
+[
change ]
{#if !$T.question$last}
{#/if}
{#/for}
+
Your ballot tracker is {$T.encrypted_vote_hash} .
-
Your ballot tracker is {$T.encrypted_vote_hash} , and you can print it.
-
-
-Once you click "Submit", the unencrypted version of your ballot will be destroyed, and only the encrypted version will remain. The encrypted version will be submitted to the Helios server.
-
-
Submit this Vote!
-
-
+
+ Proceed to Login
+
@@ -54,3 +32,22 @@ Review your Ballot
{$T.encrypted_vote_json}
+
+{#if $T.election_metadata.use_advanced_audit_features}
+
+
+
+
+If you choose, you can spoil this ballot and reveal how your choices were encrypted. This is an optional auditing process.
+
+
+You will then be guided to re-encrypt your choices for final casting.
+
+
Spoil & Audit
+
+
+
+{#/if}
+
+
+
diff --git a/heliosbooth/verifier.js b/heliosbooth/verifier.js
index 96acbbe0f..f38fcd19e 100644
--- a/heliosbooth/verifier.js
+++ b/heliosbooth/verifier.js
@@ -10,7 +10,7 @@ function verify_ballot(election_raw_json, encrypted_vote_json, status_cb) {
// display ballot fingerprint
encrypted_vote = HELIOS.EncryptedVote.fromJSONObject(encrypted_vote_json, election);
- status_cb("smart ballot tracker is " + encrypted_vote.get_hash());
+ status_cb("ballot tracker is " + encrypted_vote.get_hash());
// check the hash
if (election_hash == encrypted_vote.election_hash) {
diff --git a/heliosbooth/vote.html b/heliosbooth/vote.html
index 7ac836be7..b8ce20dc0 100644
--- a/heliosbooth/vote.html
+++ b/heliosbooth/vote.html
@@ -1,7 +1,8 @@
-
-
+
+
-
+
+
Helios Voting Booth
@@ -26,10 +27,14 @@
+
-
[exit ]
-
Helios Voting Booth
+