* Add authentication and authorization

* pr self review

* βœ…. Pylint Corrections

* βœ… mypy fixes

* βœ… Fix mypy around scopeto

* βœ… LGTM fixes surrounding __all__

Co-authored-by: Keith Fung <keith.robert.fung@gmail.com>
Co-authored-by: Keith Fung <keithrfung@users.noreply.github.com>
This commit is contained in:
Matt Wilhelm 2021-11-19 09:40:01 -05:00 ΠΊΠΎΠΌΠΌΠΈΡ‚ ΠΏΡ€ΠΎΠΈΠ·Π²Ρ‘Π» GitHub
Π ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒ 806fe06a82
ΠšΠΎΠΌΠΌΠΈΡ‚ c5753f4e50
НС Π½Π°ΠΉΠ΄Π΅Π½ ΠΊΠ»ΡŽΡ‡, ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ Π΄Π°Π½Π½ΠΎΠΉ подписи
Π˜Π΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΊΠ»ΡŽΡ‡Π° GPG: 4AEE18F83AFDEB23
29 ΠΈΠ·ΠΌΠ΅Π½Ρ‘Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ²: 1044 Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠΉ ΠΈ 61 ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΉ

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1 @@
from .routes import router

153
app/api/v1/auth/auth.py Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,153 @@
from typing import Any, List, Optional
from datetime import datetime, timedelta
from fastapi import (
params,
APIRouter,
Depends,
HTTPException,
Request,
Security,
status,
)
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from pydantic import ValidationError
from app.api.v1.models.user import UserScope
from app.core import Settings
from app.core.user import get_user_info
from ..models import Token, TokenData
from ....core import AuthenticationContext
from ..tags import AUTHORIZE
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
UserScope.admin: "The admin role can execute administrative functions.",
UserScope.auditor: "The auditor role is a readonly role that can observe the election",
UserScope.guardian: "The guardian role can excute guardian functions.",
UserScope.voter: "The voter role can execute voting functions such as encrypt ballot.",
},
)
class ScopedTo(params.Depends):
"""Define a dependency on particular scope."""
def __init__(self, scopes: List[UserScope]) -> None:
super().__init__(self.__call__)
self._scopes = scopes
def __call__(
self,
request: Request,
settings: Settings = Settings(),
token: str = Security(oauth2_scheme),
) -> TokenData:
"""Check scopes and return the current user."""
data = validate_access_token(settings, token)
validate_access_token_authorization(data, self._scopes)
return data
def validate_access_token_authorization(
token_data: TokenData, scopes: List[UserScope]
) -> None:
"""Validate that the access token is authorized to the requested resource."""
if any(scopes):
scope_str = ",".join(scopes)
authenticate_value = f'Bearer scope="{scope_str}"'
else:
authenticate_value = "Bearer"
for scope in scopes:
if scope in token_data.scopes:
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
def create_access_token(
data: dict,
expires_delta: Optional[timedelta] = None,
settings: Settings = Settings(),
) -> Any:
"""Create an access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.AUTH_ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode, settings.AUTH_SECRET_KEY, algorithm=settings.AUTH_ALGORITHM
)
return encoded_jwt
def validate_access_token(
settings: Settings = Settings(), token: str = Depends(oauth2_scheme)
) -> TokenData:
"""validate the token contains a username and scopes"""
try:
payload = jwt.decode(
token,
settings.AUTH_SECRET_KEY,
algorithms=[settings.AUTH_ALGORITHM],
)
username: str = payload.get("sub")
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token_scopes = payload.get("scopes")
token_data = TokenData(username=username, scopes=token_scopes)
except (JWTError, ValidationError) as internal_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credential scopes",
headers={"WWW-Authenticate": "Bearer"},
) from internal_error
return token_data
@router.post("/login", response_model=Token, tags=[AUTHORIZE])
async def login_for_access_token(
request: Request, form_data: OAuth2PasswordRequestForm = Depends()
) -> Token:
"""Log in using the provided username and password."""
authenticated = AuthenticationContext(
request.app.state.settings
).authenticate_credential(form_data.username, form_data.password)
if not authenticated:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# get the database cached user info
user_info = get_user_info(form_data.username, request.app.state.settings)
access_token_expires = timedelta(
minutes=request.app.state.settings.AUTH_ACCESS_TOKEN_EXPIRE_MINUTES
)
access_token = create_access_token(
data={"sub": form_data.username, "scopes": user_info.scopes},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
# TODO: add refresh support

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,8 @@
from fastapi import APIRouter
from . import auth
from . import user
router = APIRouter()
router.include_router(auth.router, prefix="/auth")
router.include_router(user.router, prefix="/user")

116
app/api/v1/auth/user.py Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,116 @@
from typing import Any
from base64 import b64encode, b16decode
from fastapi import APIRouter, Body, HTTPException, Request, status
from electionguard.group import rand_q
from .auth import ScopedTo
from ..models import (
AuthenticationCredential,
UserInfo,
UserScope,
)
from ....core import (
AuthenticationContext,
get_user_info,
set_user_info,
filter_user_info,
get_auth_credential,
set_auth_credential,
update_auth_credential,
)
from ..tags import USER
router = APIRouter()
@router.get(
"/me",
response_model=UserInfo,
tags=[USER],
)
async def me(
request: Request,
scopedTo: ScopedTo = ScopedTo(
[UserScope.admin, UserScope.auditor, UserScope.guardian, UserScope.voter]
),
) -> UserInfo:
"""
Get user info for the current logged in user.
"""
token_data = scopedTo(request)
if token_data.username is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="User not specified"
)
current_user = get_user_info(token_data.username, request.app.state.settings)
if current_user.disabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"
)
return current_user
@router.post(
"/create",
dependencies=[ScopedTo([UserScope.admin])],
tags=[USER],
)
async def create_user(request: Request, user_info: UserInfo = Body(...)) -> Any:
"""Create a new user."""
if any(
filter_user_info(
filter={"username": user_info.username},
skip=0,
limit=1,
settings=request.app.state.settings,
)
):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User already exists"
)
# TODO: generate passwords differently
new_password = b64encode(b16decode(rand_q().to_hex()[0:16]))
hashed_password = AuthenticationContext(
request.app.state.settings
).get_password_hash(new_password)
credential = AuthenticationCredential(
username=user_info.username, hashed_password=hashed_password
)
set_auth_credential(credential, request.app.state.settings)
set_user_info(user_info, request.app.state.settings)
return {"user_info": user_info, "password": new_password}
@router.post(
"/reset_password",
dependencies=[ScopedTo([UserScope.admin])],
tags=[USER],
)
async def reset_password(request: Request, username: str) -> Any:
"""Reset a user's password."""
credential = get_auth_credential(
username,
settings=request.app.state.settings,
)
# TODO: generate passwords differently
new_password = b64encode(b16decode(rand_q().to_hex()[0:16]))
credential.hashed_password = AuthenticationContext(
request.app.state.settings
).get_password_hash(new_password)
update_auth_credential(credential, request.app.state.settings)
return {"username": username, "password": new_password}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -17,7 +17,7 @@ from electionguard.group import ElementModP
from ....core.client import get_client_id from ....core.client import get_client_id
from ....core.key_guardian import get_key_guardian from ....core.key_guardian import get_key_guardian
from ....core.key_ceremony import ( from ....core.key_ceremony import (
from_query, key_ceremony_from_query,
get_key_ceremony, get_key_ceremony,
update_key_ceremony, update_key_ceremony,
update_key_ceremony_state, update_key_ceremony_state,
@ -141,7 +141,7 @@ def find_ceremonies(
cursor = repository.find(filter, skip, limit) cursor = repository.find(filter, skip, limit)
key_ceremonies: List[KeyCeremony] = [] key_ceremonies: List[KeyCeremony] = []
for item in cursor: for item in cursor:
key_ceremonies.append(from_query(item)) key_ceremonies.append(key_ceremony_from_query(item))
return KeyCeremonyQueryResponse(key_ceremonies=key_ceremonies) return KeyCeremonyQueryResponse(key_ceremonies=key_ceremonies)
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,4 @@
from .auth import *
from .ballot import * from .ballot import *
from .base import * from .base import *
from .decrypt import * from .decrypt import *
@ -9,3 +10,4 @@ from .key_guardian import *
from .manifest import * from .manifest import *
from .tally import * from .tally import *
from .tally_decrypt import * from .tally_decrypt import *
from .user import *

32
app/api/v1/models/auth.py Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,32 @@
from typing import List, Optional
from pydantic import BaseModel
from app.api.v1.models.user import UserScope
__all__ = [
"AuthenticationCredential",
"Token",
"TokenData",
]
class AuthenticationCredential(BaseModel):
"""Authentication credential used to authenticate users."""
username: str
hashed_password: str
class Token(BaseModel):
"""An access token and its type."""
access_token: str
token_type: str
class TokenData(BaseModel):
"""The payload of an access token."""
username: Optional[str] = None
scopes: List[UserScope] = []

32
app/api/v1/models/user.py Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,32 @@
from typing import List, Optional
from enum import Enum
from pydantic import BaseModel
__all__ = [
"UserScope",
"UserInfo",
]
class UserScope(str, Enum):
admin = "admin"
"""The admin role can execute administrative functions."""
auditor = "auditor"
"""The auditor role is a readonly role that can observe the election."""
guardian = "guardian"
"""The guardian role can excute guardian functions."""
voter = "voter"
"""
The voter role can execute voting functions such as encrypt ballot.
The voting endpoints are useful for testing only and are not recommended for production.
"""
class UserInfo(BaseModel):
"""A specific user in the system"""
username: str
scopes: List[UserScope] = []
email: Optional[str] = None
disabled: Optional[bool] = None

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -3,11 +3,14 @@ from app.core.settings import ApiMode, Settings
from . import common from . import common
from . import guardian from . import guardian
from . import mediator from . import mediator
from . import auth
def get_routes(settings: Settings) -> APIRouter: def get_routes(settings: Settings) -> APIRouter:
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(auth.router)
if settings.API_MODE == ApiMode.GUARDIAN: if settings.API_MODE == ApiMode.GUARDIAN:
api_router.include_router(guardian.router) api_router.include_router(guardian.router)
elif settings.API_MODE == ApiMode.MEDIATOR: elif settings.API_MODE == ApiMode.MEDIATOR:

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,4 @@
AUTHORIZE = "Authentication & Authorization"
ELECTION = "Configure Election" ELECTION = "Configure Election"
MANIFEST = "Election Manifest" MANIFEST = "Election Manifest"
GUARDIAN = "Guardian" GUARDIAN = "Guardian"
@ -10,3 +11,4 @@ TALLY = "Tally Results"
TALLY_DECRYPT = "Tally Decrypt" TALLY_DECRYPT = "Tally Decrypt"
PUBLISH = "Publish Results" PUBLISH = "Publish Results"
UTILITY = "Utility Functions" UTILITY = "Utility Functions"
USER = "User Information"

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1 +1,16 @@
from .auth import *
from .ballot import *
from .client import *
from .election import *
from .guardian import *
from .key_ceremony import *
from .key_guardian import *
from .manifest import *
from .queue import *
from .repository import * from .repository import *
from .scheduler import *
from .schema import *
from .settings import *
from .tally_decrypt import *
from .tally import *
from .user import *

109
app/core/auth.py Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,109 @@
import sys
from typing import Any, Union
from fastapi import HTTPException, status
from passlib.context import CryptContext
from .client import get_client_id
from .repository import get_repository, DataCollection
from .settings import Settings
from ..api.v1.models import BaseResponse, AuthenticationCredential
__all__ = [
"AuthenticationContext",
"get_auth_credential",
"set_auth_credential",
"update_auth_credential",
]
class AuthenticationContext:
"""
An Authentication context object wrapper
encapsulating the crypto context used to verify crdentials
"""
def __init__(self, settings: Settings = Settings()):
self.context = CryptContext(schemes=["bcrypt"], deprecated="auto")
self.settings = settings
def authenticate_credential(self, username: str, password: str) -> Any:
credential = get_auth_credential(username, self.settings)
return self.verify_password(password, credential.hashed_password)
def verify_password(self, plain_password: str, hashed_password: str) -> Any:
return self.context.verify(plain_password, hashed_password)
def get_password_hash(self, password: Union[bytes, str]) -> Any:
return self.context.hash(password)
def get_auth_credential(
username: str, settings: Settings = Settings()
) -> AuthenticationCredential:
"""Get an authenitcation credential from the repository."""
try:
with get_repository(
get_client_id(), DataCollection.AUTHENTICATION, settings
) as repository:
query_result = repository.get({"username": username})
if not query_result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find credential for {username}",
)
return AuthenticationCredential(**query_result)
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{username} not found",
) from error
def set_auth_credential(
credential: AuthenticationCredential, settings: Settings = Settings()
) -> None:
"""Set an authentication credential in the repository."""
try:
with get_repository(
get_client_id(), DataCollection.AUTHENTICATION, settings
) as repository:
query_result = repository.get({"username": credential.username})
if not query_result:
repository.set(credential.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Already exists {credential.username}",
)
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="set auth credential failed",
) from error
def update_auth_credential(
credential: AuthenticationCredential, settings: Settings = Settings()
) -> BaseResponse:
"""Update an authentication credential in the repository."""
try:
with get_repository(
get_client_id(), DataCollection.AUTHENTICATION, settings
) as repository:
query_result = repository.get({"username": credential.username})
if not query_result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find credential {credential.username}",
)
repository.update({"username": credential.username}, credential.dict())
return BaseResponse()
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="update auth credential failed",
) from error

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -11,6 +11,15 @@ from .settings import Settings
from ..api.v1.models import BaseResponse, BallotInventory from ..api.v1.models import BaseResponse, BallotInventory
__all__ = [
"get_ballot",
"set_ballots",
"filter_ballots",
"get_ballot_inventory",
"upsert_ballot_inventory",
]
def get_ballot( def get_ballot(
election_id: str, ballot_id: str, settings: Settings = Settings() election_id: str, ballot_id: str, settings: Settings = Settings()
) -> SubmittedBallot: ) -> SubmittedBallot:
@ -28,8 +37,8 @@ def get_ballot(
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get ballot failed", detail=f"{ballot_id} not found",
) from error ) from error
@ -72,8 +81,8 @@ def filter_ballots(
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="filter ballots failed", detail="provided filter not found",
) from error ) from error

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,6 +1,10 @@
# TODO: multi-tenancy # TODO: multi-tenancy
DEFAULT_CLIENT_ID = "electionguard-default-client-id" DEFAULT_CLIENT_ID = "electionguard-default-client-id"
__all__ = [
"get_client_id",
]
def get_client_id() -> str: def get_client_id() -> str:
return DEFAULT_CLIENT_ID return DEFAULT_CLIENT_ID

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -10,8 +10,16 @@ from .repository import get_repository, DataCollection
from .settings import Settings from .settings import Settings
from ..api.v1.models import BaseResponse, Election, ElectionState from ..api.v1.models import BaseResponse, Election, ElectionState
__all__ = [
"election_from_query",
"get_election",
"set_election",
"filter_elections",
"update_election_state",
]
def from_query(query_result: Any) -> Election:
def election_from_query(query_result: Any) -> Election:
return Election( return Election(
election_id=query_result["election_id"], election_id=query_result["election_id"],
key_name=query_result["key_name"], key_name=query_result["key_name"],
@ -32,14 +40,14 @@ def get_election(election_id: str, settings: Settings = Settings()) -> Election:
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find election {election_id}", detail=f"Could not find election {election_id}",
) )
election = from_query(query_result) election = election_from_query(query_result)
return election return election
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get election failed", detail=f"{election_id} not found",
) from error ) from error
@ -70,7 +78,7 @@ def filter_elections(
cursor = repository.find(filter, skip, limit) cursor = repository.find(filter, skip, limit)
elections: List[Election] = [] elections: List[Election] = []
for item in cursor: for item in cursor:
elections.append(from_query(item)) elections.append(election_from_query(item))
return elections return elections
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
@ -93,7 +101,7 @@ def update_election_state(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find election {election_id}", detail=f"Could not find election {election_id}",
) )
election = from_query(query_result) election = election_from_query(query_result)
election.state = new_state election.state = new_state
repository.update({"election_id": election_id}, election.dict()) repository.update({"election_id": election_id}, election.dict())
return BaseResponse() return BaseResponse()

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -12,8 +12,14 @@ from ..api.v1.models import (
Guardian, Guardian,
) )
__all__ = [
"guardian_from_query",
"get_guardian",
"update_guardian",
]
def from_query(query_result: Any) -> Guardian:
def guardian_from_query(query_result: Any) -> Guardian:
return Guardian( return Guardian(
guardian_id=query_result["guardian_id"], guardian_id=query_result["guardian_id"],
sequence_order=query_result["sequence_order"], sequence_order=query_result["sequence_order"],
@ -40,13 +46,13 @@ def get_guardian(guardian_id: str, settings: Settings = Settings()) -> Guardian:
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find guardian {guardian_id}", detail=f"Could not find guardian {guardian_id}",
) )
guardian = from_query(query_result) guardian = guardian_from_query(query_result)
return guardian return guardian
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get guardian failed", detail=f"{guardian_id} not found",
) from error ) from error

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -13,7 +13,16 @@ from ..api.v1.models import (
) )
def from_query(query_result: Any) -> KeyCeremony: __all__ = [
"key_ceremony_from_query",
"get_key_ceremony",
"update_key_ceremony",
"update_key_ceremony_state",
"validate_can_publish",
]
def key_ceremony_from_query(query_result: Any) -> KeyCeremony:
return KeyCeremony( return KeyCeremony(
key_name=query_result["key_name"], key_name=query_result["key_name"],
state=query_result["state"], state=query_result["state"],
@ -37,13 +46,13 @@ def get_key_ceremony(key_name: str, settings: Settings = Settings()) -> KeyCerem
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find key ceremony {key_name}", detail=f"Could not find key ceremony {key_name}",
) )
key_ceremony = from_query(query_result) key_ceremony = key_ceremony_from_query(query_result)
return key_ceremony return key_ceremony
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get key ceremony failed", detail=f"{key_name} not found",
) from error ) from error
@ -83,7 +92,7 @@ def update_key_ceremony_state(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find key ceremony {key_name}", detail=f"Could not find key ceremony {key_name}",
) )
key_ceremony = from_query(query_result) key_ceremony = key_ceremony_from_query(query_result)
key_ceremony.state = new_state key_ceremony.state = new_state
repository.update({"key_name": key_name}, key_ceremony.dict()) repository.update({"key_name": key_name}, key_ceremony.dict())

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -9,6 +9,11 @@ from ..api.v1.models import (
KeyCeremonyGuardian, KeyCeremonyGuardian,
) )
__all__ = [
"get_key_guardian",
"update_key_guardian",
]
def get_key_guardian( def get_key_guardian(
key_name: str, guardian_id: str, settings: Settings = Settings() key_name: str, guardian_id: str, settings: Settings = Settings()
@ -41,8 +46,8 @@ def get_key_guardian(
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get key ceremony guardian failed", detail=f"{key_name} {guardian_id} not found",
) from error ) from error

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -13,8 +13,15 @@ from .repository import get_repository, DataCollection
from .settings import Settings from .settings import Settings
from ..api.v1.models import Manifest, ManifestSubmitResponse, ManifestQueryResponse from ..api.v1.models import Manifest, ManifestSubmitResponse, ManifestQueryResponse
__all__ = [
"from_manifest_query",
"get_manifest",
"set_manifest",
"filter_manifests",
]
# TODO: rework the caching mechanism to reduce the amount of object conversions # TODO: rework the caching mechanism to reduce the amount of object conversions
def from_query(query_result: Any) -> Manifest: def from_manifest_query(query_result: Any) -> Manifest:
sdk_manifest = electionguard.manifest.Manifest.from_json_object( sdk_manifest = electionguard.manifest.Manifest.from_json_object(
query_result["manifest"] query_result["manifest"]
) )
@ -38,12 +45,12 @@ def get_manifest(
detail=f"Could not find manifest {manifest_hash.to_hex()}", detail=f"Could not find manifest {manifest_hash.to_hex()}",
) )
return from_query(query_result) return from_manifest_query(query_result)
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get manifest failed", detail=f"{manifest_hash} not found",
) from error ) from error
@ -79,7 +86,7 @@ def filter_manifests(
cursor = repository.find(filter, skip, limit) cursor = repository.find(filter, skip, limit)
manifests: List[Manifest] = [] manifests: List[Manifest] = []
for item in cursor: for item in cursor:
manifests.append(from_query(item)) manifests.append(from_manifest_query(item))
return ManifestQueryResponse(manifests=manifests) return ManifestQueryResponse(manifests=manifests)
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -54,6 +54,7 @@ class IRepository(Protocol):
class DataCollection: class DataCollection:
AUTHENTICATION = "authenticationContext"
GUARDIAN = "guardian" GUARDIAN = "guardian"
KEY_GUARDIAN = "keyGuardian" KEY_GUARDIAN = "keyGuardian"
KEY_CEREMONY = "keyCeremony" KEY_CEREMONY = "keyCeremony"
@ -64,6 +65,7 @@ class DataCollection:
CIPHERTEXT_TALLY = "ciphertextTally" CIPHERTEXT_TALLY = "ciphertextTally"
PLAINTEXT_TALLY = "plaintextTally" PLAINTEXT_TALLY = "plaintextTally"
DECRYPTION_SHARES = "decryptionShares" DECRYPTION_SHARES = "decryptionShares"
USER_INFO = "userInfo"
class LocalRepository(IRepository): class LocalRepository(IRepository):

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -3,6 +3,13 @@ from enum import Enum
from pydantic import AnyHttpUrl, BaseSettings from pydantic import AnyHttpUrl, BaseSettings
from pydantic.fields import Field from pydantic.fields import Field
__all__ = [
"ApiMode",
"QueueMode",
"StorageMode",
"Settings",
]
class ApiMode(str, Enum): class ApiMode(str, Enum):
GUARDIAN = "guardian" GUARDIAN = "guardian"
@ -25,6 +32,7 @@ class Settings(BaseSettings):
API_MODE: ApiMode = ApiMode.MEDIATOR API_MODE: ApiMode = ApiMode.MEDIATOR
QUEUE_MODE: QueueMode = QueueMode.LOCAL QUEUE_MODE: QueueMode = QueueMode.LOCAL
STORAGE_MODE: StorageMode = StorageMode.MEMORY STORAGE_MODE: StorageMode = StorageMode.MEMORY
API_V1_STR: str = "/api/v1" API_V1_STR: str = "/api/v1"
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field( BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field(
default=[ default=[
@ -39,5 +47,11 @@ class Settings(BaseSettings):
MONGODB_URI: str = "mongodb://root:example@localhost:27017" MONGODB_URI: str = "mongodb://root:example@localhost:27017"
MESSAGEQUEUE_URI: str = "amqp://guest:guest@localhost:5672" MESSAGEQUEUE_URI: str = "amqp://guest:guest@localhost:5672"
AUTH_ALGORITHM = "HS256"
AUTH_SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
AUTH_ACCESS_TOKEN_EXPIRE_MINUTES = 30
DEFAULT_ADMIN_USERNAME = "default"
DEFAULT_ADMIN_PASSWORD = "Elect1onGu4rd!"
class Config: class Config:
case_sensitive = True case_sensitive = True

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -6,6 +6,18 @@ from .repository import get_repository, DataCollection
from .settings import Settings from .settings import Settings
from ..api.v1.models import BaseResponse, CiphertextTally, PlaintextTally from ..api.v1.models import BaseResponse, CiphertextTally, PlaintextTally
__all__ = [
"ciphertext_tally_from_query",
"plaintext_tally_from_query",
"get_ciphertext_tally",
"set_ciphertext_tally",
"filter_ciphertext_tallies",
"get_plaintext_tally",
"set_plaintext_tally",
"update_plaintext_tally",
"filter_plaintext_tallies",
]
def ciphertext_tally_from_query(query_result: Any) -> CiphertextTally: def ciphertext_tally_from_query(query_result: Any) -> CiphertextTally:
return CiphertextTally( return CiphertextTally(
@ -45,8 +57,8 @@ def get_ciphertext_tally(
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get ciphertext tally failed", detail=f"{election_id} {tally_name} not found",
) from error ) from error

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -6,8 +6,17 @@ from .repository import get_repository, DataCollection
from .settings import Settings from .settings import Settings
from ..api.v1.models import BaseResponse, CiphertextTallyDecryptionShare from ..api.v1.models import BaseResponse, CiphertextTallyDecryptionShare
__all__ = [
"from_tally_decryption_share_query",
"get_decryption_share",
"set_decryption_share",
"filter_decryption_shares",
]
def from_query(query_result: Any) -> CiphertextTallyDecryptionShare:
def from_tally_decryption_share_query(
query_result: Any,
) -> CiphertextTallyDecryptionShare:
return CiphertextTallyDecryptionShare( return CiphertextTallyDecryptionShare(
election_id=query_result["election_id"], election_id=query_result["election_id"],
tally_name=query_result["tally_name"], tally_name=query_result["tally_name"],
@ -36,12 +45,12 @@ def get_decryption_share(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find decryption share {election_id} {tally_name} {guardian_id}", detail=f"Could not find decryption share {election_id} {tally_name} {guardian_id}",
) )
return from_query(query_result) return from_tally_decryption_share_query(query_result)
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_404_NOT_FOUND,
detail="get decryption share failed", detail=f"{election_id} {tally_name} {guardian_id} not found",
) from error ) from error
@ -76,7 +85,7 @@ def filter_decryption_shares(
cursor = repository.find(filter, skip, limit) cursor = repository.find(filter, skip, limit)
decryption_shares: List[CiphertextTallyDecryptionShare] = [] decryption_shares: List[CiphertextTallyDecryptionShare] = []
for item in cursor: for item in cursor:
decryption_shares.append(from_query(item)) decryption_shares.append(from_tally_decryption_share_query(item))
return decryption_shares return decryption_shares
except Exception as error: except Exception as error:
print(sys.exc_info()) print(sys.exc_info())

92
app/core/user.py Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,92 @@
from typing import Any, List
import sys
from fastapi import HTTPException, status
from .client import get_client_id
from .repository import get_repository, DataCollection
from .settings import Settings
from ..api.v1.models import BaseResponse, UserInfo
__all__ = ["get_user_info", "filter_user_info", "set_user_info", "update_user_info"]
def get_user_info(username: str, settings: Settings = Settings()) -> UserInfo:
try:
with get_repository(
get_client_id(), DataCollection.USER_INFO, settings
) as repository:
query_result = repository.get({"username": username})
if not query_result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find user {username}",
)
return UserInfo(**query_result)
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{username} not found",
) from error
def filter_user_info(
filter: Any, skip: int = 0, limit: int = 1000, settings: Settings = Settings()
) -> List[UserInfo]:
try:
with get_repository(
get_client_id(), DataCollection.ELECTION, settings
) as repository:
cursor = repository.find(filter, skip, limit)
results: List[UserInfo] = []
for item in cursor:
results.append(item)
return results
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="filter users failed",
) from error
def set_user_info(user: UserInfo, settings: Settings = Settings()) -> None:
try:
with get_repository(
get_client_id(), DataCollection.USER_INFO, settings
) as repository:
query_result = repository.get({"username": user.username})
if not query_result:
repository.set(user.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Already exists {user.username}",
)
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="set user info failed",
) from error
def update_user_info(user: UserInfo, settings: Settings = Settings()) -> BaseResponse:
try:
with get_repository(
get_client_id(), DataCollection.GUARDIAN, settings
) as repository:
query_result = repository.get({"username": user.username})
if not query_result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find user {user.username}",
)
repository.update({"username": user.username}, user.dict())
return BaseResponse()
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="update user info failed",
) from error

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,15 +1,40 @@
from logging import getLogger from logging import getLogger
from typing import Optional from typing import Optional
from fastapi import FastAPI from fastapi import FastAPI, HTTPException
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from app.api.v1.models.auth import AuthenticationCredential
from app.api.v1.routes import get_routes from app.api.v1.routes import get_routes
from app.core.settings import Settings from app.core.settings import Settings
from app.core.scheduler import get_scheduler from app.core.scheduler import get_scheduler
from app.api.v1.models import UserInfo, UserScope
from app.core import AuthenticationContext, set_auth_credential, set_user_info
logger = getLogger(__name__) logger = getLogger(__name__)
def seed_default_user(settings: Settings = Settings()) -> None:
# TODO: a more secure way to set the default auth credential
hashed_password = AuthenticationContext(settings).get_password_hash(
settings.DEFAULT_ADMIN_PASSWORD
)
credential = AuthenticationCredential(
username=settings.DEFAULT_ADMIN_USERNAME, hashed_password=hashed_password
)
user_info = UserInfo(username=credential.username, scopes=[UserScope.admin])
try:
set_auth_credential(credential, settings)
except HTTPException:
pass
try:
set_user_info(user_info, settings)
except HTTPException:
pass
def get_app(settings: Optional[Settings] = None) -> FastAPI: def get_app(settings: Optional[Settings] = None) -> FastAPI:
if not settings: if not settings:
settings = Settings() settings = Settings()
@ -34,6 +59,8 @@ def get_app(settings: Optional[Settings] = None) -> FastAPI:
allow_headers=["*"], allow_headers=["*"],
) )
seed_default_user(settings)
routes = get_routes(settings) routes = get_routes(settings)
web_app.include_router(routes, prefix=settings.API_V1_STR) web_app.include_router(routes, prefix=settings.API_V1_STR)
@ -45,7 +72,7 @@ app = get_app()
@app.on_event("startup") @app.on_event("startup")
def on_startup() -> None: def on_startup() -> None:
... pass
@app.on_event("shutdown") @app.on_event("shutdown")

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,4 @@
db.createCollection('authenticationContext');
db.createCollection('guardian'); db.createCollection('guardian');
db.createCollection('keyGuardian'); db.createCollection('keyGuardian');
db.createCollection('keyCeremony') db.createCollection('keyCeremony')
@ -8,3 +9,4 @@ db.createCollection('submittedBallots');
db.createCollection('ciphertextTally'); db.createCollection('ciphertextTally');
db.createCollection('plaintextTally'); db.createCollection('plaintextTally');
db.createCollection('decryptionShares'); db.createCollection('decryptionShares');
db.createCollection('userInfo');

160
poetry.lock сгСнСрированный
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -40,6 +40,22 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
name = "bcrypt"
version = "3.2.0"
description = "Modern password hashing for your software and your servers"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cffi = ">=1.1"
six = ">=1.4.1"
[package.extras]
tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"]
[[package]] [[package]]
name = "black" name = "black"
version = "20.8b1" version = "20.8b1"
@ -124,6 +140,21 @@ sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]]
name = "ecdsa"
version = "0.17.0"
description = "ECDSA cryptographic signature library (pure python)"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[package.dependencies]
six = ">=1.9.0"
[package.extras]
gmpy = ["gmpy"]
gmpy2 = ["gmpy2"]
[[package]] [[package]]
name = "electionguard" name = "electionguard"
version = "1.2.2" version = "1.2.2"
@ -386,6 +417,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
[[package]]
name = "passlib"
version = "1.7.4"
description = "comprehensive password hashing framework supporting over 30 schemes"
category = "main"
optional = false
python-versions = "*"
[package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"]
build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"]
totp = ["cryptography"]
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.8.1" version = "0.8.1"
@ -437,6 +482,14 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyasn1"
version = "0.4.8"
description = "ASN.1 types and codecs"
category = "main"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.20" version = "2.20"
@ -541,6 +594,35 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
[[package]]
name = "python-jose"
version = "3.3.0"
description = "JOSE implementation in Python"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
ecdsa = "!=0.15"
pyasn1 = "*"
rsa = "*"
[package.extras]
cryptography = ["cryptography (>=3.4.0)"]
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"]
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"]
[[package]]
name = "python-multipart"
version = "0.0.5"
description = "A streaming multipart parser for Python"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.4.0"
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "5.4.1" version = "5.4.1"
@ -586,6 +668,17 @@ urllib3 = ">=1.21.1,<1.27"
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "rsa"
version = "4.7.2"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=3.5, <4"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -726,7 +819,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "~=3.8" python-versions = "~=3.8"
content-hash = "d60544ff746f841e958e7e4d78b6de88b9549b0b006f77f63630597b22b85ca1" content-hash = "9459a6e996e4403c98d26fc16b5bec080516970db65a55259ce6cce2246d1375"
[metadata.files] [metadata.files]
appdirs = [ appdirs = [
@ -745,6 +838,15 @@ attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
] ]
bcrypt = [
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"},
{file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"},
{file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"},
{file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"},
]
black = [ black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
] ]
@ -825,12 +927,14 @@ cryptography = [
{file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3"},
{file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"},
] ]
ecdsa = [
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
]
electionguard = [ electionguard = [
{file = "electionguard-1.2.2-py3-none-any.whl", hash = "sha256:ae4ff2696813f09db670f0363a25352c05c1283e781c2e293db6681716e4401d"}, {file = "electionguard-1.2.2-py3-none-any.whl", hash = "sha256:ae4ff2696813f09db670f0363a25352c05c1283e781c2e293db6681716e4401d"},
{file = "electionguard-1.2.2.tar.gz", hash = "sha256:c020e2e4efee3afbca3c2b672e092d783dbd6085e02559d04eb6d32b4e7e7e4e"}, {file = "electionguard-1.2.2.tar.gz", hash = "sha256:c020e2e4efee3afbca3c2b672e092d783dbd6085e02559d04eb6d32b4e7e7e4e"},
@ -931,22 +1035,12 @@ markdown = [
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
] ]
markupsafe = [ markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
@ -955,21 +1049,14 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
@ -979,9 +1066,6 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
@ -1022,6 +1106,10 @@ packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
] ]
passlib = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
]
pathspec = [ pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
@ -1068,6 +1156,21 @@ py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
] ]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pycparser = [ pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
@ -1181,6 +1284,13 @@ python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
] ]
python-jose = [
{file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"},
{file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"},
]
python-multipart = [
{file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
]
pyyaml = [ pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@ -1263,6 +1373,10 @@ requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
] ]
rsa = [
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
{file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
]
six = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -16,6 +16,10 @@ uvicorn = "~0.11"
pika = "1.2.0" pika = "1.2.0"
pymongo = "~3.11.4" pymongo = "~3.11.4"
electionguard = "^1.2.2" electionguard = "^1.2.2"
python-jose = "^3.3.0"
passlib = "^1.7.4"
bcrypt = "^3.2.0"
python-multipart = "^0.0.5"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -9,6 +9,78 @@
{ {
"name": "Mediator", "name": "Mediator",
"item": [ "item": [
{
"name": "Auth",
"item": [
{
"name": "Login",
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"pm.environment.set(\"token\", jsonData.access_token);"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "username",
"value": "default",
"type": "text"
},
{
"key": "password",
"value": "Elect1onGu4rd!",
"type": "text"
},
{
"key": "grant_type",
"value": "password",
"type": "text"
},
{
"key": "scope",
"value": "admin",
"type": "text"
},
{
"key": "client_id",
"value": "electionguard-default-client-id",
"type": "text"
},
{
"key": "client_secret",
"value": "electionguard-default-client-secret",
"type": "text"
}
]
},
"url": {
"raw": "{{mediator-url}}/api/{{version}}/auth/login",
"host": [
"{{mediator-url}}"
],
"path": [
"api",
"{{version}}",
"auth",
"login"
]
}
},
"response": []
}
]
},
{ {
"name": "Configure Election", "name": "Configure Election",
"item": [ "item": [
@ -1415,6 +1487,116 @@
} }
] ]
}, },
{
"name": "User",
"item": [
{
"name": "me",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{mediator-url}}/api/{{version}}/user/me",
"host": [
"{{mediator-url}}"
],
"path": [
"api",
"{{version}}",
"user",
"me"
]
}
},
"response": []
},
{
"name": "Create User",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"username\": \"some-user\",\n \"scopes\": [\n \"admin\"\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{mediator-url}}/api/{{version}}/user/create",
"host": [
"{{mediator-url}}"
],
"path": [
"api",
"{{version}}",
"user",
"create"
]
}
},
"response": []
},
{
"name": "Reset Password",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"url": {
"raw": "{{mediator-url}}/api/{{version}}/user/reset_password?username=some-user",
"host": [
"{{mediator-url}}"
],
"path": [
"api",
"{{version}}",
"user",
"reset_password"
],
"query": [
{
"key": "username",
"value": "some-user"
}
]
}
},
"response": []
}
]
},
{ {
"name": "Utility", "name": "Utility",
"item": [ "item": [
@ -1905,6 +2087,10 @@
{ {
"key": "version", "key": "version",
"value": "v1" "value": "v1"
},
{
"key": "token",
"value": ""
} }
] ]
} }