Π·Π΅ΡΠΊΠ°Π»ΠΎ ΠΈΠ· https://github.com/microsoft/electionguard-api-python.git
π‘. Add authentication and authorization (#172)
* 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:
Π ΠΎΠ΄ΠΈΡΠ΅Π»Ρ
806fe06a82
ΠΠΎΠΌΠΌΠΈΡ
c5753f4e50
|
@ -0,0 +1 @@
|
|||
from .routes import router
|
|
@ -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")
|
|
@ -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 *
|
||||
|
|
|
@ -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] = []
|
|
@ -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 *
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
31
app/main.py
31
app/main.py
|
@ -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')
|
||||
|
@ -7,4 +8,5 @@ db.createCollection('ballotInventory');
|
|||
db.createCollection('submittedBallots');
|
||||
db.createCollection('ciphertextTally');
|
||||
db.createCollection('plaintextTally');
|
||||
db.createCollection('decryptionShares');
|
||||
db.createCollection('decryptionShares');
|
||||
db.createCollection('userInfo');
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
ΠΠ°Π³ΡΡΠ·ΠΊΠ°β¦
Π‘ΡΡΠ»ΠΊΠ° Π² Π½ΠΎΠ²ΠΎΠΉ Π·Π°Π΄Π°ΡΠ΅