-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #66 from elekto-io/migration
Security updates and migration code for 0.6
- Loading branch information
Showing
8 changed files
with
258 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,19 +11,26 @@ | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
# | ||
# Author(s): Manish Sahani <[email protected]> | ||
|
||
import uuid | ||
import sqlalchemy as S | ||
|
||
from sqlalchemy.orm import sessionmaker, scoped_session | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.types import TypeDecorator, CHAR | ||
from sqlalchemy import event | ||
|
||
|
||
BASE = declarative_base() | ||
|
||
""" | ||
schema version, remember to update this | ||
whenever you make changes to the schema | ||
""" | ||
schema_version = 2 | ||
|
||
|
||
def create_session(url): | ||
""" | ||
|
@@ -46,6 +53,7 @@ def create_session(url): | |
def migrate(url): | ||
""" | ||
Create the tables in the database using the url | ||
Check if we need to upgrade the schema, and do that as well | ||
Args: | ||
url (string): the URL used to connect the application to the | ||
|
@@ -54,12 +62,74 @@ def migrate(url): | |
ie: <engine>://<user>:<password>@<host>/<dbname> | ||
""" | ||
engine = S.create_engine(url) | ||
update_schema(engine, schema_version) | ||
BASE.metadata.create_all(bind=engine) | ||
|
||
|
||
session = scoped_session( | ||
sessionmaker(bind=engine, autocommit=False, autoflush=False) | ||
) | ||
return session | ||
|
||
|
||
def update_schema(engine, schema_version): | ||
""" | ||
Primitive database schema upgrade facility, designed to work | ||
with production Elekto databases | ||
Currently only works with PostgreSQL due to requiring transaction | ||
support for DDL statements. MySQL, SQLite backends will error. | ||
Start by figuring out our schema version, and then upgrade | ||
stepwise until we match | ||
""" | ||
db_version = 1 | ||
db_schema = S.inspect(engine) | ||
|
||
if db_schema.has_table("election"): | ||
if db_schema.has_table("schema_version"): | ||
db_version = engine.execute('select version from schema_version').scalar() | ||
if db_version is None: | ||
""" intialize the table, if necessary """ | ||
engine.execute('insert into schema_version ( version ) values ( 2 )') | ||
else: | ||
""" new, empty db """ | ||
return schema_version | ||
|
||
while db_version < schema_version: | ||
if engine.dialect.name != "postgresql": | ||
raise RuntimeError('Upgrading the schema is required, but the database is not PostgreSQL. You will need to upgrade manually.') | ||
|
||
if db_version < 2: | ||
db_version = update_schema_2(engine) | ||
continue | ||
|
||
return db_version; | ||
|
||
|
||
def update_schema_2(engine): | ||
""" | ||
update from schema version 1 to schema version 2 | ||
as a set of raw SQL statements | ||
currently only works for PostgreSQL | ||
written this way because SQLalchemy can't handle the | ||
steps involved without data loss | ||
""" | ||
session = scoped_session(sessionmaker(bind=engine)) | ||
|
||
session.execute('CREATE TABLE schema_version ( version INT PRIMARY KEY);') | ||
session.execute('INSERT INTO schema_version VALUES ( 2 );') | ||
session.execute('ALTER TABLE voter ADD COLUMN salt BYTEA, ADD COLUMN ballot_id BYTEA;') | ||
session.execute('CREATE INDEX voter_election_id ON voter(election_id);') | ||
session.execute('ALTER TABLE ballot DROP COLUMN created_at, DROP COLUMN updated_at;') | ||
session.execute('ALTER TABLE ballot DROP CONSTRAINT ballot_pkey;') | ||
session.execute("ALTER TABLE ballot ALTER COLUMN id TYPE CHAR(32) USING to_char(id , 'FM00000000000000000000000000000000');") | ||
session.execute('ALTER TABLE ballot ALTER COLUMN id DROP DEFAULT;') | ||
session.execute('ALTER TABLE ballot ADD CONSTRAINT ballot_pkey PRIMARY KEY ( id );') | ||
session.execute('CREATE INDEX ballot_election_id ON ballot(election_id);') | ||
session.commit() | ||
|
||
return 2 | ||
|
||
|
||
class UUID(TypeDecorator): | ||
|
@@ -94,6 +164,19 @@ def process_result_value(self, value, dialect): | |
return value | ||
|
||
|
||
class Version(BASE): | ||
""" | ||
Stores Elekto schema version in the database for ad-hoc upgrades | ||
""" | ||
__tablename__ = "schema_version" | ||
|
||
# Attributes | ||
version = S.Column(S.Integer, default=schema_version, primary_key=True) | ||
|
||
@event.listens_for(Version.__table__, 'after_create') | ||
def create_version(target, connection, **kwargs): | ||
connection.execute(f"INSERT INTO schema_version ( version ) VALUES ( {schema_version} )") | ||
|
||
class User(BASE): | ||
""" | ||
User Schema - registered from the oauth external application - github | ||
|
@@ -185,11 +268,11 @@ class Voter(BASE): | |
|
||
id = S.Column(S.Integer, primary_key=True) | ||
user_id = S.Column(S.Integer, S.ForeignKey("user.id", ondelete="CASCADE")) | ||
election_id = S.Column(S.Integer, S.ForeignKey("election.id", ondelete="CASCADE")) | ||
election_id = S.Column(S.Integer, S.ForeignKey("election.id", ondelete="CASCADE"), index=True) | ||
created_at = S.Column(S.DateTime, default=S.func.now()) | ||
updated_at = S.Column(S.DateTime, default=S.func.now()) | ||
salt = S.Column(S.LargeBinary, nullable=False) | ||
ballot_id = S.Column(S.LargeBinary, nullable=False) # encrypted | ||
salt = S.Column(S.LargeBinary) | ||
ballot_id = S.Column(S.LargeBinary) # encrypted | ||
|
||
# Relationships | ||
|
||
|
@@ -227,7 +310,7 @@ class Ballot(BASE): | |
|
||
# Attributes | ||
id = S.Column(UUID(), primary_key=True, default=uuid.uuid4) | ||
election_id = S.Column(S.Integer, S.ForeignKey("election.id", ondelete="CASCADE")) | ||
election_id = S.Column(S.Integer, S.ForeignKey("election.id", ondelete="CASCADE"), index=True) | ||
rank = S.Column(S.Integer, default=100000000) | ||
candidate = S.Column(S.String(255), nullable=False) | ||
voter = S.Column(S.String(255), nullable=False) # uuid | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
{% extends 'layouts/app.html' %} | ||
|
||
{% block header %} | ||
<h1>{% block title %}{{ election['name'] }}{% endblock %}</h1> | ||
{% endblock %} | ||
|
||
{% block breadcrums %} | ||
<a href="{{ url_for('elections') }}" class="breadcrums">elections</a> | ||
<a href="{{ url_for('elections_single', eid=election['key']) }}" class="breadcrums breadcrums-active">{{ | ||
election['name'] }}</a> | ||
{% endblock %} | ||
|
||
{% block content %} | ||
|
||
|
||
<div class=""> | ||
<div class="space--md pb-0"> | ||
<h1 class="banner-title space-lr"> | ||
{{ election['name'] }} | ||
</h1> | ||
<p class="banner-subtitle space-lr mb-2rem"> | ||
<span class="mr-5px">{{ election['organization'] }}</span> | ||
<span class="text-muted mr-5px">|</span> | ||
<small class="badge mr-5px badge-{{ election['status'] }} ">{{ election['status'] }}</small> | ||
<span class="text-muted mr-5px">|</span> | ||
{% if g.user.username in voters['eligible_voters'] %} | ||
<small class="badge badge-blue ">eligible</small> | ||
{% else %} | ||
<small class="badge badge-blue">Not eligible</small> | ||
{% endif %} | ||
</p> | ||
<div class="description space-lr pb-0"> | ||
{{ election['description'] | safe }} | ||
</div> | ||
</div> | ||
<div class="space--md pt-0"> | ||
<h4 class="title space-lr mb-1rem"> | ||
Your Ballot | ||
</h4> | ||
<div class="space-lr"> | ||
{% for ballot in ballots %} | ||
<div class="boxed-hover row" style="border: 1px solid #80808012;"> | ||
<div class="col-10 pt-5px pl-0"> | ||
<h6 class="title mt-5px pb-0 mb-0"> | ||
{{ ballot.candidate }} | ||
</h6> | ||
</div> | ||
<div class="col-2 text--right"> | ||
<h6 class="title mt-5px pb-0 mb-0"> | ||
{{ ballot.rank }} | ||
</h6> | ||
</div> | ||
</div> | ||
{% endfor %} | ||
</div> | ||
<p class="disclaimer space-lr mt-1rem"> | ||
{% if election['status'] == 'running' and g.user.username in voters['eligible_voters'] %} | ||
{% if g.user.id in voted %} | ||
You have cast your vote. | ||
{% else %} | ||
You have not yet voted in this election. | ||
{% endif %} | ||
{% endif %} | ||
Voting starts at <b>{{ election['start_datetime'] }} UTC</b> and ends at | ||
<b>{{ election['end_datetime'] }} UTC</b>. | ||
{% if g.user.username not in voters['eligible_voters'] %} | ||
If you wish to participate in the election, please fill the | ||
<a href="{{ url_for('elections_exception', eid=election['key']) }}"><b>exception form</b></a> | ||
before <b>{{ election['exception_due'] }}</b>. | ||
{% endif %} | ||
</p> | ||
</div> | ||
|
||
<div class="space--md pt-0"> | ||
<div class="space-lr row"> | ||
{% if election['status'] == 'running' and g.user.username in voters['eligible_voters'] %} | ||
{% if g.user.id not in voted %} | ||
<div class="col-md-2 pr-0"> | ||
<a href="{{ url_for('elections_voting_page', eid=election['key'])}}" class="btn btn-dark pl-3rem pr-3rem">Vote</a> | ||
</div> | ||
{% else %} | ||
<div class="col-md-6"> | ||
<form action="{{ url_for('elections_edit', eid=election['key']) }}" method="post"> | ||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> | ||
<div class="input-group"> | ||
<input type="password" name="password" class="form-control" placeholder="Enter the passphrase" id=""> | ||
<div class="input-group-append"> | ||
<button type="submit" class="btn btn-dark">Revoke Ballot</button> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
{% endif %} | ||
{% endif %} | ||
<div class="col-md-6 | ||
{% if election['status'] == 'running' and g.user.username in voters['eligible_voters'] %} | ||
pl-0 | ||
{% endif %} | ||
"> | ||
{% if election['status'] == 'completed' %} | ||
{% if election['results'] %} | ||
<a href="{{ url_for('elections_results', eid=election['key'])}}" class="btn btn-dark">Results</a> | ||
{% else %} | ||
<button class="btn btn-dark" disabled>Results (not published)</button> | ||
{% endif %} | ||
{% endif %} | ||
|
||
{% if g.user.username in election['election_officers'] %} | ||
<a href="{{ url_for('elections_admin', eid=election['key'])}}" class="btn btn-dark">Admin</a> | ||
{% endif %} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters