mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Merge branch 'fix/v1393-review-security' into release/v1.39.3
This commit is contained in:
commit
3cbfe7f7f9
76 files changed
+2751
-409
No files matched your search
+151
-51
@@ -864,7 +864,11 @@ paths:
|
||||
`selection` is REQUIRED and carries the leaf inclusion list — membership is inclusion, absence is exclusion, and
|
||||
there is no server-side default. An unknown leaf id is a 422 that names it; the retired grouped `sections` shape
|
||||
is a 422 too, because accepting it would mean choosing a scope the caller did not express. Auth via cookie or
|
||||
Bearer; shared `export:<userId>` rate bucket (10/h). Strict validation: unknown keys 422."
|
||||
Bearer; shared `export:<userId>` rate bucket (10/h). Strict validation: unknown keys 422. A browser session
|
||||
needs a fresh proof as well as a live one: a sign-in or `POST /api/auth/reproof` within the last five minutes
|
||||
(with a second factor, on an account that has one). Without it the answer is 401 `auth.reproof.required`, with
|
||||
`meta.methods` naming the proofs the account can give. When a manager exports a record they manage, it is their
|
||||
own session that is asked. A Bearer caller is not asked for more than its token."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1489,7 +1493,10 @@ paths:
|
||||
Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport (present when the
|
||||
window holds a vital sign) — flattened into one `searchset` Bundle. Scoped to the owner's SAVED report
|
||||
selection; an account that never saved one gets nothing. Auth: `fhir:read` scope. Offset paging via `_count`
|
||||
(≤200) / `_offset`."
|
||||
(≤200) / `_offset`. A browser session needs a fresh proof as well as a live one: a sign-in or `POST
|
||||
/api/auth/reproof` within the last five minutes (with a second factor, on an account that has one). Without it
|
||||
the answer is 401 `auth.reproof.required`, with `meta.methods` naming the proofs the account can give. A token,
|
||||
including a `fhir:read` one, is not asked for more."
|
||||
parameters:
|
||||
- in: query
|
||||
name: _count
|
||||
@@ -1524,7 +1531,13 @@ paths:
|
||||
notes ride their exported column. Shares the `export:<userId>` bucket with the encrypted variant (10 per hour)
|
||||
and audits every attempt as `export.download`. Cookie or Bearer auth; the caller is always resolved as
|
||||
themselves, so this download cannot be delegated to a shared record. Not module-gated: a disabled module's rows
|
||||
are still exported, because this is the account's own data leaving on request."
|
||||
are still exported, because this is the account's own data leaving on request. `type=all`, the default, is the
|
||||
whole record and asks for a fresh proof like the full backup; the single-type exports do not. A browser session
|
||||
needs a fresh proof as well as a live one: a sign-in or `POST /api/auth/reproof` within the last five minutes
|
||||
(with a second factor, on an account that has one). Without it the answer is 401 `auth.reproof.required`, with
|
||||
`meta.methods` naming the proofs the account can give. A Bearer caller asking for `type=all` presents an
|
||||
`X-Step-Up` elevation from `POST /api/auth/step-up` (a second-factor one on an account with a second factor) and
|
||||
receives 401 `auth.stepup.required` without it."
|
||||
parameters:
|
||||
- in: query
|
||||
name: format
|
||||
@@ -1852,6 +1865,15 @@ paths:
|
||||
200 with `data: null, error: null` and `meta.mfaRequired: true` plus a single-use `meta.mfaTicket` and the
|
||||
`meta.methods` list. The client must POST the ticket + a code to `/api/auth/mfa/verify` to obtain the token
|
||||
bundle. Accounts without MFA are unchanged.
|
||||
|
||||
|
||||
Two limits apply. Per source address, five attempts in fifteen minutes. Per account, failed passwords from a
|
||||
place the account has not signed in from before make the next attempt wait: five failures a day are free, then
|
||||
each one doubles the wait, from thirty seconds up to at most fifteen minutes. A place the account has signed in
|
||||
from is not counted and not held back: a remembered browser (trusted-device cookie), a phone that sends the
|
||||
`X-Device-Id` a login of this account was issued to, or an address a session or device login of this account
|
||||
came from in the last thirty days. Both answer 429 with `Retry-After`. An identifier that names no account is
|
||||
limited the same way, so a 429 says nothing about whether it exists.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1871,7 +1893,9 @@ paths:
|
||||
"403":
|
||||
description: "Password login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`; sign in
|
||||
through SSO instead. The one exception is a browser the SSO callback sent back with a pending link to
|
||||
confirm an existing account: it may sign in with that one account's password, which links the account."
|
||||
confirm an existing account: it may sign in with that one account's password, which links the account. A
|
||||
sign-in to any other account on that browser is refused before its password is checked, so the answer is the
|
||||
same whether the password was right or wrong."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -2172,14 +2196,18 @@ paths:
|
||||
/api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused.
|
||||
|
||||
|
||||
Adding a factor needs a fresh proof, not just a session. On the Bearer path the elevation is that proof (a
|
||||
password-proved one is enough). On the cookie path the request passes when the session signed in, or completed a
|
||||
second factor, within the last five minutes; otherwise the body must carry a proof in the shape POST
|
||||
/api/auth/step-up takes (`password`, a `totp` code from an already confirmed authenticator, or a passkey /
|
||||
security-key assertion begun at POST /api/auth/passkey/register-options). Without one the answer is 401
|
||||
`auth.reproof.required`; a proof that does not verify is 401 `auth.reproof.failed` and is audited. Proofs draw
|
||||
on the account's shared re-proof budget of five per fifteen minutes (429 once spent), the same budget as the
|
||||
step-up mint.
|
||||
Adding a factor needs a fresh proof, not just a session. On the Bearer path the elevation is that proof: a
|
||||
password-proved one is enough on an account without a second factor, and on an account that already has one the
|
||||
elevation must come from that factor or a passkey. On the cookie path, for an account without a second factor,
|
||||
the request passes when the session signed in, or completed a passkey sign-in, within the last five minutes;
|
||||
otherwise the body must carry a proof in the shape POST /api/auth/step-up takes (`password`, or a passkey
|
||||
assertion begun at POST /api/auth/passkey/register-options). For an account that already has a second factor,
|
||||
only that factor counts: a second factor completed within the last five minutes, or a `totp` code, security-key
|
||||
or passkey assertion in the body. A password or a young session (for example one that skipped the factor on a
|
||||
remembered browser) is not enough there. Without one the answer is 401 `auth.reproof.required` with
|
||||
`meta.methods` naming the proofs the account can give; a proof that does not verify is 401 `auth.reproof.failed`
|
||||
and is audited. Proofs draw on the account's shared re-proof budget of five per fifteen minutes (429 once
|
||||
spent), the same budget as the step-up mint.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@@ -2316,14 +2344,18 @@ paths:
|
||||
refused.
|
||||
|
||||
|
||||
Adding a factor needs a fresh proof, not just a session. On the Bearer path the elevation is that proof (a
|
||||
password-proved one is enough). On the cookie path the request passes when the session signed in, or completed a
|
||||
second factor, within the last five minutes; otherwise the body must carry a proof in the shape POST
|
||||
/api/auth/step-up takes (`password`, a `totp` code from an already confirmed authenticator, or a passkey /
|
||||
security-key assertion begun at POST /api/auth/passkey/register-options). Without one the answer is 401
|
||||
`auth.reproof.required`; a proof that does not verify is 401 `auth.reproof.failed` and is audited. Proofs draw
|
||||
on the account's shared re-proof budget of five per fifteen minutes (429 once spent), the same budget as the
|
||||
step-up mint.
|
||||
Adding a factor needs a fresh proof, not just a session. On the Bearer path the elevation is that proof: a
|
||||
password-proved one is enough on an account without a second factor, and on an account that already has one the
|
||||
elevation must come from that factor or a passkey. On the cookie path, for an account without a second factor,
|
||||
the request passes when the session signed in, or completed a passkey sign-in, within the last five minutes;
|
||||
otherwise the body must carry a proof in the shape POST /api/auth/step-up takes (`password`, or a passkey
|
||||
assertion begun at POST /api/auth/passkey/register-options). For an account that already has a second factor,
|
||||
only that factor counts: a second factor completed within the last five minutes, or a `totp` code, security-key
|
||||
or passkey assertion in the body. A password or a young session (for example one that skipped the factor on a
|
||||
remembered browser) is not enough there. Without one the answer is 401 `auth.reproof.required` with
|
||||
`meta.methods` naming the proofs the account can give; a proof that does not verify is 401 `auth.reproof.failed`
|
||||
and is audited. Proofs draw on the account's shared re-proof budget of five per fifteen minutes (429 once
|
||||
spent), the same budget as the step-up mint.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@@ -2600,7 +2632,10 @@ paths:
|
||||
(the OAuth connection, so it cannot mint a new access token), every programmatic token (connector, measurement
|
||||
and document tokens), and every clinician share link. Share links are revoked by default because a link made by
|
||||
whoever held a lost session or device would otherwise keep opening the record; send `keepShareLinks=1` to keep
|
||||
them. A Bearer caller keeps its own device login."
|
||||
them. Record-sharing invitations this account offered that nobody has accepted yet are withdrawn too, because an
|
||||
invitation sent from a stolen session would otherwise wait for the thief's own account. Accepted grants are not
|
||||
ended: the response lists them as `grantsKept` so the client can show who can still read the record and offer to
|
||||
end each one. A Bearer caller keeps its own device login."
|
||||
parameters:
|
||||
- in: query
|
||||
name: keepShareLinks
|
||||
@@ -3455,7 +3490,9 @@ paths:
|
||||
|
||||
**Enrollment arm.** Any other body must be a full factor proof (the same shape POST /api/auth/step-up takes): a
|
||||
password, a TOTP code, or a completed passkey / security-key assertion. Only then are registration options
|
||||
issued.
|
||||
issued. On an account that already has a second factor a password is refused before it is checked, with 422
|
||||
`auth.reproof.too_weak` and `meta.methods` naming the proofs that work: a passkey added on the password alone
|
||||
would sign in without the second factor from then on.
|
||||
|
||||
|
||||
Which factor you prove changes the session, and the direction is not the obvious one: a strong proof stamps the
|
||||
@@ -3502,7 +3539,13 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a4
|
||||
"422":
|
||||
description: "`auth.reproof.too_weak`: a password on an account that already has a second factor. `meta.methods` names
|
||||
the proofs that would be accepted. Refused before the password is checked, so it spends no attempt."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429": *a5
|
||||
/api/auth/passkey/register-verify:
|
||||
post:
|
||||
@@ -19367,7 +19410,12 @@ paths:
|
||||
(`requireFreshMfaIfEnrolled`, cookie-only): an enrolled account without a fresh factor gets 401
|
||||
`auth.stepup.required`, and a Bearer caller gets 403 `sharing.invite.manage_browser_only` before anything else
|
||||
happens, because the gate it would hit resolves through the session cookie and would otherwise answer 'not
|
||||
authenticated' to a caller it had just authenticated."
|
||||
authenticated' to a caller it had just authenticated. Every invitation, at any level, also needs a fresh proof:
|
||||
a browser session must have signed in or used `POST /api/auth/reproof` within the last five minutes (with the
|
||||
second factor, on an account that has one), or it gets 401 `auth.reproof.required` with `meta.methods`; a Bearer
|
||||
caller presents an `X-Step-Up` elevation from `POST /api/auth/step-up` (a second-factor one on an account with a
|
||||
second factor) and gets 401 `auth.stepup.required` without it. `DELETE /api/auth/me/sessions` withdraws the
|
||||
invitations nobody has accepted yet."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -19382,8 +19430,10 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AccountGrantEnvelope"
|
||||
"401":
|
||||
description: "Offering MANAGE from an MFA-enrolled account without a recent second-factor proof (`meta.errorCode:
|
||||
auth.stepup.required`). Re-prove the factor and retry; READ and WRITE invitations are never gated."
|
||||
description: "No recent proof. A browser session that has not signed in or re-proved within five minutes gets
|
||||
`meta.errorCode: auth.reproof.required` with `meta.methods`; re-prove at POST /api/auth/reproof and retry. A
|
||||
Bearer caller without a valid `X-Step-Up` elevation gets `auth.stepup.required`, as does offering MANAGE
|
||||
from an MFA-enrolled browser session without a recent second-factor proof."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -22971,8 +23021,12 @@ components:
|
||||
a second factor in the last five minutes needs nothing more, otherwise this field must carry the current
|
||||
password. Without an accepted proof the address is not changed and comes back in `rejectedFields` as
|
||||
`reproof_required` or `reproof_failed` (or, when the address was the only field sent, 401
|
||||
`auth.reproof.required` / `auth.reproof.failed`). Attempts draw on the account's shared re-proof budget of
|
||||
five per fifteen minutes. Never stored."
|
||||
`auth.reproof.required` / `auth.reproof.failed`). On an account with a second factor the password is not
|
||||
accepted and not checked: only a second factor or passkey completed in the last five minutes counts (at
|
||||
sign-in or at POST /api/auth/reproof), and without one the address comes back as `second_factor_required`
|
||||
(or 401 `profile.update.emailSecondFactorRequired` when it was the only field). A token cannot change the
|
||||
address of such an account. Attempts draw on the account's shared re-proof budget of five per fifteen
|
||||
minutes. Never stored."
|
||||
type: string
|
||||
maxLength: 512
|
||||
description: Partial profile update. Every field is optional and an omitted field is left untouched. An explicit null
|
||||
@@ -31482,29 +31536,29 @@ components:
|
||||
onboarding.disclaimer.invalid, onboarding.restart.invalid, onboarding.restart.rateLimited,
|
||||
onboarding.tour.invalid; personal_records — personal_records.invalid_query; practitioner —
|
||||
practitioner.invalid; profile — profile.update.emailInUse, profile.update.emailRateLimited,
|
||||
profile.update.invalidBody, profile.update.nothingSaved; record_write — record_write.rate_limited;
|
||||
report-selection — report-selection.body.invalid_json, report-selection.body.invalid_shape,
|
||||
report-selection.leaves.unknown; route — route.retired; share-link — share-link.document.decryptFailed,
|
||||
share-link.document.notFound, share-link.document.rateLimited, share-link.documents.invalid,
|
||||
share-link.report.notFound, share-link.report.rateLimited, share-link.selection.forbidden_leaf,
|
||||
share-link.selection.unknown_leaf; sharing — sharing.accept.expired, sharing.accept.not_found,
|
||||
sharing.accept.not_pending, sharing.accept.revoked, sharing.access.denied, sharing.invite.duplicate,
|
||||
sharing.invite.invalid, sharing.invite.invalid_scope, sharing.invite.manage_browser_only,
|
||||
sharing.invite.refused, sharing.invite.self, sharing.not_permitted, sharing.renounce.already_ended,
|
||||
sharing.renounce.not_found, sharing.revoke.already_ended, sharing.revoke.not_found,
|
||||
sharing.session.changed, sharing.switch.invalid, sharing.switch.wrong_transport; source-priority —
|
||||
source-priority.body.invalid_json, source-priority.body.invalid_shape; tokens —
|
||||
tokens.documents.ceiling_reached, tokens.measurements.ceiling_reached; vaccination —
|
||||
vaccination.booster-invalid, vaccination.booster-no-antigen, vaccination.booster-out-of-scope,
|
||||
vaccination.encounter-not-found, vaccination.identity-required, vaccination.invalid,
|
||||
vaccination.link.invalid, vaccination.not-found, vaccination.practitioner-not-found; workout —
|
||||
workout.batch.invalid, workout.batch.payload_too_large, workout.batch.too_large; (unprefixed) —
|
||||
about_me_conflict, ai_response_truncated, coach_prefs_conflict, dashboard_layout_conflict, empty_file,
|
||||
health_score_config_conflict, insights_layout_conflict, invalid_base_updated_at,
|
||||
json_carries_no_intake_time, json_not_an_array, medication_layout_conflict, missing_required_columns,
|
||||
modules_conflict, mood_tag_layout_conflict, not_configured, notification_prefs_conflict, oidc_only,
|
||||
private_origin_not_approved, private_origin_not_grantable, rate_limited_self, too_many_rows,
|
||||
unreadable_json, upstream_invalid_json, vapid_not_configured"
|
||||
profile.update.emailSecondFactorRequired, profile.update.invalidBody, profile.update.nothingSaved;
|
||||
record_write — record_write.rate_limited; report-selection — report-selection.body.invalid_json,
|
||||
report-selection.body.invalid_shape, report-selection.leaves.unknown; route — route.retired; share-link
|
||||
— share-link.document.decryptFailed, share-link.document.notFound, share-link.document.rateLimited,
|
||||
share-link.documents.invalid, share-link.report.notFound, share-link.report.rateLimited,
|
||||
share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf; sharing —
|
||||
sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending, sharing.accept.revoked,
|
||||
sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid, sharing.invite.invalid_scope,
|
||||
sharing.invite.manage_browser_only, sharing.invite.refused, sharing.invite.self, sharing.not_permitted,
|
||||
sharing.renounce.already_ended, sharing.renounce.not_found, sharing.revoke.already_ended,
|
||||
sharing.revoke.not_found, sharing.session.changed, sharing.switch.invalid,
|
||||
sharing.switch.wrong_transport; source-priority — source-priority.body.invalid_json,
|
||||
source-priority.body.invalid_shape; tokens — tokens.documents.ceiling_reached,
|
||||
tokens.measurements.ceiling_reached; vaccination — vaccination.booster-invalid,
|
||||
vaccination.booster-no-antigen, vaccination.booster-out-of-scope, vaccination.encounter-not-found,
|
||||
vaccination.identity-required, vaccination.invalid, vaccination.link.invalid, vaccination.not-found,
|
||||
vaccination.practitioner-not-found; workout — workout.batch.invalid, workout.batch.payload_too_large,
|
||||
workout.batch.too_large; (unprefixed) — about_me_conflict, ai_response_truncated, coach_prefs_conflict,
|
||||
dashboard_layout_conflict, empty_file, health_score_config_conflict, insights_layout_conflict,
|
||||
invalid_base_updated_at, json_carries_no_intake_time, json_not_an_array, medication_layout_conflict,
|
||||
missing_required_columns, modules_conflict, mood_tag_layout_conflict, not_configured,
|
||||
notification_prefs_conflict, oidc_only, private_origin_not_approved, private_origin_not_grantable,
|
||||
rate_limited_self, too_many_rows, unreadable_json, upstream_invalid_json, vapid_not_configured"
|
||||
type: string
|
||||
capability:
|
||||
description: "On an AI refusal: the capability that is unavailable, one of the keys of `AiCapabilities` (`coach`,
|
||||
@@ -39004,11 +39058,57 @@ components:
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Clinician share links revoked; 0 when `keepShareLinks=1` was sent.
|
||||
pendingInvitesRevoked:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: "Record-sharing invitations this account offered that nobody had accepted yet, now withdrawn (`revokedBy:
|
||||
GRANTOR`)."
|
||||
grantsKept:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The grant id, for DELETE /api/account/grants/{id}.
|
||||
account:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
displayName:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
- displayName
|
||||
additionalProperties: false
|
||||
access:
|
||||
type: string
|
||||
enum:
|
||||
- READ
|
||||
- WRITE
|
||||
- MANAGE
|
||||
required:
|
||||
- id
|
||||
- account
|
||||
- access
|
||||
additionalProperties: false
|
||||
description: Accepted grants this account gave that are still live. Signing out does not end them, because each is a
|
||||
person the owner chose and ending one silently could cut off a carer; they are listed so a client can show
|
||||
who can still read the record and offer to end each with DELETE /api/account/grants/{id}.
|
||||
required:
|
||||
- sessionsRevoked
|
||||
- accessTokensRevoked
|
||||
- connectorsRevoked
|
||||
- shareLinksRevoked
|
||||
- pendingInvitesRevoked
|
||||
- grantsKept
|
||||
additionalProperties: false
|
||||
SessionRevokeEnvelope:
|
||||
type: object
|
||||
|
||||
Reference in new issue
Block a user