* 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.key_guardian import get_key_guardian
from ....core.key_ceremony import (
from_query,
key_ceremony_from_query,
get_key_ceremony,
update_key_ceremony,
update_key_ceremony_state,
@ -141,7 +141,7 @@ def find_ceremonies(
cursor = repository.find(filter, skip, limit)
key_ceremonies: List[KeyCeremony] = []
for item in cursor:
key_ceremonies.append(from_query(item))
key_ceremonies.append(key_ceremony_from_query(item))
return KeyCeremonyQueryResponse(key_ceremonies=key_ceremonies)
except Exception as error:
print(sys.exc_info())

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

@ -1,3 +1,4 @@
from .auth import *
from .ballot import *
from .base import *
from .decrypt import *
@ -9,3 +10,4 @@ from .key_guardian import *
from .manifest import *
from .tally 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 guardian
from . import mediator
from . import auth
def get_routes(settings: Settings) -> APIRouter:
api_router = APIRouter()
api_router.include_router(auth.router)
if settings.API_MODE == ApiMode.GUARDIAN:
api_router.include_router(guardian.router)
elif settings.API_MODE == ApiMode.MEDIATOR:

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

@ -1,3 +1,4 @@
AUTHORIZE = "Authentication & Authorization"
ELECTION = "Configure Election"
MANIFEST = "Election Manifest"
GUARDIAN = "Guardian"
@ -10,3 +11,4 @@ TALLY = "Tally Results"
TALLY_DECRYPT = "Tally Decrypt"
PUBLISH = "Publish Results"
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 .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
__all__ = [
"get_ballot",
"set_ballots",
"filter_ballots",
"get_ballot_inventory",
"upsert_ballot_inventory",
]
def get_ballot(
election_id: str, ballot_id: str, settings: Settings = Settings()
) -> SubmittedBallot:
@ -28,8 +37,8 @@ def get_ballot(
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="get ballot failed",
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{ballot_id} not found",
) from error
@ -72,8 +81,8 @@ def filter_ballots(
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="filter ballots failed",
status_code=status.HTTP_404_NOT_FOUND,
detail="provided filter not found",
) from error

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

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

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

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

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

@ -12,8 +12,14 @@ from ..api.v1.models import (
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(
guardian_id=query_result["guardian_id"],
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,
detail=f"Could not find guardian {guardian_id}",
)
guardian = from_query(query_result)
guardian = guardian_from_query(query_result)
return guardian
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="get guardian failed",
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{guardian_id} not found",
) 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(
key_name=query_result["key_name"],
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,
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
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="get key ceremony failed",
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{key_name} not found",
) from error
@ -83,7 +92,7 @@ def update_key_ceremony_state(
status_code=status.HTTP_404_NOT_FOUND,
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
repository.update({"key_name": key_name}, key_ceremony.dict())

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

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

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

@ -13,8 +13,15 @@ from .repository import get_repository, DataCollection
from .settings import Settings
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
def from_query(query_result: Any) -> Manifest:
def from_manifest_query(query_result: Any) -> Manifest:
sdk_manifest = electionguard.manifest.Manifest.from_json_object(
query_result["manifest"]
)
@ -38,12 +45,12 @@ def get_manifest(
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:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="get manifest failed",
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{manifest_hash} not found",
) from error
@ -79,7 +86,7 @@ def filter_manifests(
cursor = repository.find(filter, skip, limit)
manifests: List[Manifest] = []
for item in cursor:
manifests.append(from_query(item))
manifests.append(from_manifest_query(item))
return ManifestQueryResponse(manifests=manifests)
except Exception as error:
print(sys.exc_info())

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

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

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

@ -3,6 +3,13 @@ from enum import Enum
from pydantic import AnyHttpUrl, BaseSettings
from pydantic.fields import Field
__all__ = [
"ApiMode",
"QueueMode",
"StorageMode",
"Settings",
]
class ApiMode(str, Enum):
GUARDIAN = "guardian"
@ -25,6 +32,7 @@ class Settings(BaseSettings):
API_MODE: ApiMode = ApiMode.MEDIATOR
QUEUE_MODE: QueueMode = QueueMode.LOCAL
STORAGE_MODE: StorageMode = StorageMode.MEMORY
API_V1_STR: str = "/api/v1"
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field(
default=[
@ -39,5 +47,11 @@ class Settings(BaseSettings):
MONGODB_URI: str = "mongodb://root:example@localhost:27017"
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:
case_sensitive = True

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

@ -6,6 +6,18 @@ from .repository import get_repository, DataCollection
from .settings import Settings
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:
return CiphertextTally(
@ -45,8 +57,8 @@ def get_ciphertext_tally(
except Exception as error:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="get ciphertext tally failed",
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{election_id} {tally_name} not found",
) from error

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

@ -6,8 +6,17 @@ from .repository import get_repository, DataCollection
from .settings import Settings
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(
election_id=query_result["election_id"],
tally_name=query_result["tally_name"],
@ -36,12 +45,12 @@ def get_decryption_share(
status_code=status.HTTP_404_NOT_FOUND,
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:
print(sys.exc_info())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="get decryption share failed",
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{election_id} {tally_name} {guardian_id} not found",
) from error
@ -76,7 +85,7 @@ def filter_decryption_shares(
cursor = repository.find(filter, skip, limit)
decryption_shares: List[CiphertextTallyDecryptionShare] = []
for item in cursor:
decryption_shares.append(from_query(item))
decryption_shares.append(from_tally_decryption_share_query(item))
return decryption_shares
except Exception as error:
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 typing import Optional
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from starlette.middleware.cors import CORSMiddleware
from app.api.v1.models.auth import AuthenticationCredential
from app.api.v1.routes import get_routes
from app.core.settings import Settings
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__)
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:
if not settings:
settings = Settings()
@ -34,6 +59,8 @@ def get_app(settings: Optional[Settings] = None) -> FastAPI:
allow_headers=["*"],
)
seed_default_user(settings)
routes = get_routes(settings)
web_app.include_router(routes, prefix=settings.API_V1_STR)
@ -45,7 +72,7 @@ app = get_app()
@app.on_event("startup")
def on_startup() -> None:
...
pass
@app.on_event("shutdown")

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

@ -1,3 +1,4 @@
db.createCollection('authenticationContext');
db.createCollection('guardian');
db.createCollection('keyGuardian');
db.createCollection('keyCeremony')
@ -8,3 +9,4 @@ db.createCollection('submittedBallots');
db.createCollection('ciphertextTally');
db.createCollection('plaintextTally');
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_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]]
name = "black"
version = "20.8b1"
@ -124,6 +140,21 @@ sdist = ["setuptools-rust (>=0.11.4)"]
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)"]
[[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]]
name = "electionguard"
version = "1.2.2"
@ -386,6 +417,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
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]]
name = "pathspec"
version = "0.8.1"
@ -437,6 +482,14 @@ category = "dev"
optional = false
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]]
name = "pycparser"
version = "2.20"
@ -541,6 +594,35 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
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]]
name = "pyyaml"
version = "5.4.1"
@ -586,6 +668,17 @@ urllib3 = ">=1.21.1,<1.27"
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
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]]
name = "six"
version = "1.16.0"
@ -726,7 +819,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata]
lock-version = "1.1"
python-versions = "~=3.8"
content-hash = "d60544ff746f841e958e7e4d78b6de88b9549b0b006f77f63630597b22b85ca1"
content-hash = "9459a6e996e4403c98d26fc16b5bec080516970db65a55259ce6cce2246d1375"
[metadata.files]
appdirs = [
@ -745,6 +838,15 @@ attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{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 = [
{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-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-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-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"},
]
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 = [
{file = "electionguard-1.2.2-py3-none-any.whl", hash = "sha256:ae4ff2696813f09db670f0363a25352c05c1283e781c2e293db6681716e4401d"},
{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"},
]
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-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-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-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-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{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_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-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-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-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-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-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-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{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_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-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-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{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.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 = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{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.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 = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{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-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 = [
{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"},
@ -1263,6 +1373,10 @@ requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{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 = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},

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

@ -16,6 +16,10 @@ uvicorn = "~0.11"
pika = "1.2.0"
pymongo = "~3.11.4"
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]

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

@ -9,6 +9,78 @@
{
"name": "Mediator",
"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",
"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",
"item": [
@ -1905,6 +2087,10 @@
{
"key": "version",
"value": "v1"
},
{
"key": "token",
"value": ""
}
]
}