feat: authorize and audit recap time logging (Closes #589)
This commit is contained in:
1 parent
53bf89bd47
commit
379f2b8203
4 files changed
+242
-5
No files matched your search
+51
-1
@@ -279,6 +279,7 @@ StepUpAction = Literal[
|
||||
"merge_pull",
|
||||
"submit_pull_review",
|
||||
"close_issue",
|
||||
"log_recap_time",
|
||||
"revoke_device",
|
||||
"revoke_all_sessions",
|
||||
"enroll_passkey",
|
||||
@@ -430,6 +431,19 @@ class TodayRecapTimeLog(TodayRecap):
|
||||
log_identities: list[str] = Field(min_length=1, max_length=20)
|
||||
|
||||
|
||||
def _recap_time_log_target(payload: TodayRecapTimeLog) -> str:
|
||||
selected_minutes = {
|
||||
item.identity: item.actual_minutes
|
||||
for item in payload.items
|
||||
if item.identity in payload.log_identities
|
||||
}
|
||||
material = "\0".join(
|
||||
[payload.session_id]
|
||||
+ [f"{identity}\0{selected_minutes.get(identity, '')}" for identity in sorted(payload.log_identities)]
|
||||
)
|
||||
return f"today-recap:{hashlib.sha256(material.encode()).hexdigest()[:32]}"
|
||||
|
||||
|
||||
class LaterOperation(BaseModel):
|
||||
operation_id: str = Field(min_length=1, max_length=100)
|
||||
action: Literal["defer", "restore"]
|
||||
@@ -1921,7 +1935,14 @@ async def save_today_recap(payload: TodayRecap, response: Response):
|
||||
|
||||
|
||||
@app.post("/api/v1/today/recaps/log-time")
|
||||
async def save_today_recap_and_log_time(payload: TodayRecapTimeLog, response: Response):
|
||||
async def save_today_recap_and_log_time(
|
||||
payload: TodayRecapTimeLog,
|
||||
request: Request,
|
||||
response: Response,
|
||||
step_up_grant: str | None = Header(
|
||||
default=None, alias="X-Step-Up-Grant", max_length=128
|
||||
),
|
||||
):
|
||||
login = await _confirmed_login()
|
||||
if len(set(payload.log_identities)) != len(payload.log_identities):
|
||||
raise HTTPException(status_code=422, detail="time log targets must be unique")
|
||||
@@ -1936,6 +1957,12 @@ async def save_today_recap_and_log_time(payload: TodayRecapTimeLog, response: Re
|
||||
if requested.actual_minutes <= 0:
|
||||
raise ValueError("only non-zero recap time can be logged")
|
||||
selected.append(requested)
|
||||
await _require_step_up(
|
||||
request,
|
||||
step_up_grant,
|
||||
action="log_recap_time",
|
||||
target=_recap_time_log_target(payload),
|
||||
)
|
||||
recap = await asyncio.to_thread(
|
||||
_today_store().save_recap,
|
||||
login,
|
||||
@@ -1959,6 +1986,21 @@ async def save_today_recap_and_log_time(payload: TodayRecapTimeLog, response: Re
|
||||
_today_store().begin_time_log, login, payload.session_id, item.identity, item.actual_minutes
|
||||
)
|
||||
if state == "claimed":
|
||||
repository, number = gitea_proxy.issue_time_target(item.identity)
|
||||
journal = _security_event_store()
|
||||
try:
|
||||
operation_id = await asyncio.to_thread(
|
||||
journal.reserve,
|
||||
"gitea_time_logged",
|
||||
target=f"{repository}#{number}",
|
||||
)
|
||||
except SecurityEventStoreError:
|
||||
await asyncio.to_thread(
|
||||
_today_store().finish_time_log,
|
||||
login, payload.session_id, item.identity, succeeded=False,
|
||||
)
|
||||
results.append({"identity": item.identity, "status": "retry"})
|
||||
continue
|
||||
try:
|
||||
await gitea_proxy.log_issue_time(item.identity, item.actual_minutes * 60)
|
||||
except Exception as error:
|
||||
@@ -1967,6 +2009,10 @@ async def save_today_recap_and_log_time(payload: TodayRecapTimeLog, response: Re
|
||||
_today_store().finish_time_log,
|
||||
login, payload.session_id, item.identity, succeeded=False,
|
||||
)
|
||||
try:
|
||||
await asyncio.to_thread(journal.discard, operation_id)
|
||||
except SecurityEventStoreError:
|
||||
pass
|
||||
status = "retry"
|
||||
else:
|
||||
status = "verify"
|
||||
@@ -1976,6 +2022,10 @@ async def save_today_recap_and_log_time(payload: TodayRecapTimeLog, response: Re
|
||||
_today_store().finish_time_log,
|
||||
login, payload.session_id, item.identity, succeeded=True,
|
||||
)
|
||||
try:
|
||||
await asyncio.to_thread(journal.finalize, operation_id)
|
||||
except SecurityEventStoreError:
|
||||
pass
|
||||
state = "logged"
|
||||
results.append({
|
||||
"identity": item.identity,
|
||||
|
||||
Reference in new issue
Block a user