Skip to content

Commit 72104e1

Browse files
authored
Added: auth_for_result property to make result private/only accessible by admin (#64)
1 parent 5704dfc commit 72104e1

File tree

5 files changed

+76
-10
lines changed

5 files changed

+76
-10
lines changed

app/crud.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ def update_election(
330330
"date_end",
331331
"hide_results",
332332
"force_close",
333+
"auth_for_result",
333334
]:
334335
if getattr(election, key) is None:
335336
continue
@@ -513,11 +514,20 @@ def get_ballot(db: Session, token: str) -> schemas.BallotGet:
513514
return schemas.BallotGet(token=token, votes=votes_get, election=election)
514515

515516

516-
def get_results(db: Session, election_ref: str) -> schemas.ResultsGet:
517+
def get_results(db: Session, election_ref: str, token: t.Optional[str]) -> schemas.ResultsGet:
517518
db_election = get_election(db, election_ref)
518519
if db_election is None:
519520
raise errors.NotFoundError("elections")
520521

522+
if db_election.auth_for_result:
523+
if token is None:
524+
raise errors.UnauthorizedError("Election require auth for result, you need to set Authentication header")
525+
526+
payload = jws_verify(token)
527+
528+
if payload["election"] != election_ref:
529+
raise errors.UnauthorizedError("Wrong authentication for this election")
530+
521531
if (
522532
db_election.hide_results
523533
and (db_election.date_end is not None and db_election.date_end > datetime.now())

app/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,6 @@ def get_ballot(authorization: str = Header(), db: Session = Depends(get_db)):
142142

143143

144144
@app.get("/results/{election_ref}", response_model=schemas.ResultsGet)
145-
def get_results(election_ref: str, db: Session = Depends(get_db)):
146-
return crud.get_results(db=db, election_ref=election_ref)
145+
def get_results(election_ref: str, authorization: t.Optional[str] = Header(default=None), db: Session = Depends(get_db)):
146+
token = authorization.split("Bearer ")[1] if authorization else None
147+
return crud.get_results(db=db, token=token, election_ref=election_ref)

app/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Election(Base):
1919
hide_results = Column(Boolean, default=False)
2020
restricted = Column(Boolean, default=False)
2121
force_close = Column(Boolean, default=False)
22+
auth_for_result = Column(Boolean, default=False)
2223

2324
grades = relationship("Grade", back_populates="election")
2425
candidates = relationship("Candidate", back_populates="election")

app/schemas.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class ElectionBase(BaseModel):
119119
date_end: datetime | int | str | None = Field(default_factory=_in_a_long_time)
120120
hide_results: bool = True
121121
restricted: bool = False
122+
auth_for_result: bool = False
122123

123124
@field_validator("date_end", "date_start", mode="before")
124125
@classmethod
@@ -226,6 +227,7 @@ class ElectionUpdate(BaseModel):
226227
num_voters: int | None = None
227228
force_close: bool | None = None
228229
candidates: list[CandidateUpdate] | None = None
230+
auth_for_result: bool | None = None
229231

230232
@field_validator("date_end", "date_start", mode="before")
231233
@classmethod

app/tests/test_api.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class RandomElection(t.TypedDict):
5959
hide_results: bool
6060
num_voters: int
6161
date_end: t.Optional[str]
62+
auth_for_result: bool
6263

6364

6465
def _random_election(num_candidates: int, num_grades: int) -> RandomElection:
@@ -78,6 +79,7 @@ def _random_election(num_candidates: int, num_grades: int) -> RandomElection:
7879
"hide_results": False,
7980
"num_voters": 0,
8081
"date_end": None,
82+
"auth_for_result": False,
8183
}
8284

8385

@@ -537,6 +539,47 @@ def test_get_results_with_hide_results():
537539
assert response.status_code == 200, response.text
538540

539541

542+
def test_get_results_with_auth_for_result():
543+
# Create a random election
544+
body = _random_election(10, 5)
545+
body["auth_for_result"] = True
546+
body["date_end"] = (datetime.now() + timedelta(days=1)).isoformat()
547+
response = client.post("/elections", json=body)
548+
assert response.status_code == 200, response.content
549+
data = response.json()
550+
election_ref = data["ref"]
551+
admin_token = data["admin"]
552+
553+
# We create votes using the ID
554+
votes = _generate_votes_from_response("id", data)
555+
response = client.post(
556+
f"/ballots", json={"votes": votes, "election_ref": election_ref}
557+
)
558+
assert response.status_code == 200, data
559+
560+
response = client.put(
561+
f"/elections", json=data, headers={"Authorization": f"Bearer {admin_token}"}
562+
)
563+
564+
assert response.status_code == 200, response.text
565+
566+
# But, we can't get the results
567+
response = client.get(f"/results/{election_ref}")
568+
assert response.status_code == 401, data
569+
570+
# Now, we can access to the results
571+
response = client.get(f"/results/{election_ref}", headers={"Authorization": f"Bearer {admin_token}"})
572+
assert response.status_code == 200, response.text
573+
574+
# Ensure other admin tokens can't access the results
575+
response = client.post("/elections", json=body)
576+
assert response.status_code == 200, response.content
577+
data2 = response.json()
578+
admin_token2 = data2["admin"]
579+
580+
response = client.get(f"/results/{election_ref}", headers={"Authorization": f"Bearer {admin_token2}"})
581+
assert response.status_code == 401, data
582+
540583
def test_update_election():
541584
# Create a random election
542585
body = _random_election(10, 5)
@@ -545,21 +588,30 @@ def test_update_election():
545588
data = response.json()
546589
new_name = f'{data["name"]}_MODIFIED'
547590
data["name"] = new_name
548-
ballot_token = data["admin"]
591+
admin_token = data["admin"]
549592

550593
# Check we can not update without the ballot_token
551594
response = client.put("/elections", json=data)
552595
assert response.status_code == 422, response.content
553596

554597
# Check that the request fails with a wrong ballot_token
555598
response = client.put(
556-
f"/elections", json=data, headers={"Authorization": f"Bearer {ballot_token}WRONG"}
599+
f"/elections", json=data, headers={"Authorization": f"Bearer {admin_token}WRONG"}
557600
)
558-
assert response.status_code == 401, response.text
601+
assert response.status_code == 401, response.text
602+
603+
# Check that the request fails with a admnin token of other election
604+
response2 = client.post("/elections", json=body)
605+
data2 = response2.json()
606+
admin_token2 = data2["admin"]
607+
response = client.put(
608+
f"/elections", json=data, headers={"Authorization": f"Bearer {admin_token2}"}
609+
)
610+
assert response.status_code == 403, response.text
559611

560612
# But it works with the right ballot_token
561613
response = client.put(
562-
f"/elections", json=data, headers={"Authorization": f"Bearer {ballot_token}"}
614+
f"/elections", json=data, headers={"Authorization": f"Bearer {admin_token}"}
563615
)
564616
assert response.status_code == 200, response.text
565617
response2 = client.get(f"/elections/{data['ref']}")
@@ -575,7 +627,7 @@ def test_update_election():
575627
data["grades"][0]["description"] += "MODIFIED"
576628
data["grades"][0]["value"] += 10
577629
response = client.put(
578-
f"/elections", json=data, headers={"Authorization": f"Bearer {ballot_token}"}
630+
f"/elections", json=data, headers={"Authorization": f"Bearer {admin_token}"}
579631
)
580632
assert response.status_code == 200, response.text
581633
data = response.json()
@@ -586,14 +638,14 @@ def test_update_election():
586638
data2 = copy.deepcopy(data)
587639
del data2["candidates"][-1]
588640
response = client.put(
589-
f"/elections", json=data2, headers={"Authorization": f"Bearer {ballot_token}"}
641+
f"/elections", json=data2, headers={"Authorization": f"Bearer {admin_token}"}
590642
)
591643
assert response.status_code == 403, response.text
592644

593645
data2 = copy.deepcopy(data)
594646
data2["grades"][0]["id"] += 100
595647
response = client.put(
596-
f"/elections", json=data2, headers={"Authorization": f"Bearer {ballot_token}"}
648+
f"/elections", json=data2, headers={"Authorization": f"Bearer {admin_token}"}
597649
)
598650
assert response.status_code == 403, response.text
599651

0 commit comments

Comments
 (0)