| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
| |
`GET /auth/whoami` lists the user's capabilities (`caps`) and its token's `limit`, if
any, side by side; what the request may do is what both give, as endpoints decide it.
It says so now (`may`): the capabilities, or with a limit each capability met with each
of the limit's (`Capability::meet`): at the deeper of the two paths, a `*` of one
taking the other's key there, with the rights both name; nothing where the paths part,
no right is shared, or the path is protocol data one of them doesn't name.
|
| |
|
|
|
|
|
|
|
| |
Docs and comments outside future-work.md and review-rejections.md say what the code
does, and point nowhere else for what it doesn't: the architecture page's map, the
transports' forwarders, keys with `/`, a strict swarm's registry, trust.md's "Not yet"
(now saying what isn't protected, in a sentence), the topology bench, and the comments
of the change benchmarks, capabilities' grantor, tokens, dissemination, refusals and
topology.
|
| |
|
|
|
|
|
| |
Review 4, 1250's note: a key's comment was kept as given, unbounded but by the request
limit (2 MiB), in protocol data every node holds and the dashboard shows. It says whose
and where the key is: at most 256 bytes now (`sshsig::MAX_COMMENT`), a longer one
refused with the key.
|
| |
|
|
|
|
|
|
|
|
|
| |
Review 4, F12: `Screening::of` started screening over when the document was swapped (a
drop, a switch, a rejoin), the root pin alone carried on, so whether the document had
held a policy (`policed`, 1211) was forgotten there too, not only across a restart as
documented: after a drop with the policy removed meanwhile, late changes written under
it went in unjudged again.
It is carried on now, as the root pin is; what belongs to the old document (changes
waiting, refusals, drops held) still starts over. access.md says so.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Review 4, F11: only streams answered to a token ended with it (1273); one opened by
password (basic authentication) or a proxy's header went on after its user was
disabled or removed.
`streams_end` now wraps those too, by the user `basic_auth` leaves on the answer: such
a stream ends once the document no longer has its user, enabled
(`token::user_taken`, the part of a token's check about its user, which
`still_taken` uses too), looked at as the stream begins and as protocol data changes.
No expiry: a password has none.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Review 4, F7: `DELETE /auth/session` recorded the session as ended until the
`session_exp` of the token it came with. A token handed to another member's dashboard
has its `session_exp` cut to its own end (five minutes), and so has the session that
dashboard keeps with it: logging out there recorded an end minutes away, the next
remake's snapshot left it out, and the original browser's tokens of the same session,
refreshable for days, were taken again.
A token now keeps the login's end as it logged in (`Claims::login_exp`), which no
refresh changes, and an ended session is recorded until then (or the token's
`session_exp`, if later, as for a token from before the field).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A token was checked when a request came, and a stream answered to it (the dashboard's
events, a subscription's links and messages, the store's streams) then went on for as
long as the client kept it open: past the token's expiry, past its user's tokens being
revoked, its user disabled or removed, its session ended.
Now a middleware inside `login::bearer` (`login::streams_end`) ends a streaming answer
(`text/event-stream`, `application/x-ndjson`) to a request with a token at the token's
deadline, its expiry, or as soon as the document no longer takes it
(`token::still_taken`, the checks `verify` makes of the document, now its own
function): looked at when a change touches protocol data (the root, a switch, among
it), and when a session ends on the node itself in a swarm without a registry
(`AppState::session_ended`). No polling: a timer for the expiry, and the changes as
they come. The client reconnects with the token it has then; a refreshed token is a
new one, so a stream lasts a token's lifetime at most (12 hours by default).
`futures` is a dependency of every build now (for `take_until`), not only of libp2p's
and Tor's.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Logging out of the dashboard (`DELETE /auth/session`) only cleared the cookie: the
session's tokens stayed taken until they expired, by every node. Ending them took
`POST /auth/logout`, which ends all of a user's sessions.
Now `DELETE /auth/session` ends the session of the token it comes with (the cookie, or
a bearer token): where the swarm has a registry, it is recorded as
`_continuum/sessions/<user>/<session>` (`registry::session_path`), its value until when
the session would have lasted, and every node refuses the session's tokens once the
change reaches it (`token::Refused::Ended`): the page's own, its narrow tokens, and one
handed to another member's dashboard, all refreshed from it and keeping its ID. The
rules take the entry from the user itself or one who manages users, as a logout. A
remake's snapshot leaves out entries past that time, a user's map where none is left:
their tokens are expired anyway.
Without a registry the document can't record it: the node ending it refuses the
session's tokens itself (`AppState::ended_sessions`, until it restarts), others take
them until they expire, as a logout there is refused.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Setting a node's log filter took `write` on `_continuum`, all of the protocol data:
whoever could change one node's logging could change the swarm's members, remakes and
users too. Now what a node holds alone has paths of its own for capabilities, under
`_continuum/local/<node ID>/`: `log` (`/admin/log`), `config` (`/admin/config`) and
`debug` (`/debug`, always `write`), `read` to look and `write` to act
(`api::access::needed`, which takes the node's ID). So `_continuum/local/<node ID>` is
everything on one node, `_continuum/local/*/log` the log filter on every node, and
holders of `_continuum` keep it all.
Nothing is stored there: the store refuses writes under `_continuum/local`, and the
rules refuse a change touching it, as for epoch metadata. The quarantine stays with
`_continuum`: its entries are everyone's writes, which a node-local capability
mustn't show.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A capability's path covered by prefix only, so what is the same at every key of a map
(each node's log filter, say, at `_continuum/local/<id>/log` from the next patch on)
took a capability per key. Now a `*` segment stands for any one key, as it already does
in signed paths and sealed places (`envelope`, `sealed`, which now share one `agree`):
* covering (`Capability::covers`): the pattern no longer than the path, and agreeing
with it segment by segment;
* listings (`reaches`): pattern and path agreeing on the segments both have, so every
key on the way shows, nothing past where the pattern differs;
* narrowing (`Capability::holds`, for a token's limit narrowed again and a refresh's):
at or below, a `*` only under a `*`: a limit naming one key never widens to all.
A `*` first is outside protocol data, so it never covers `_continuum`, as `[]` doesn't.
A key that is `*` itself can't be named alone. Every node judges changes by
capabilities, so all must read them alike: they run the same version.
|
| |
|
|
|
|
|
|
| |
A key's line was stored as the key alone (`ssh-ed25519 <base64>`), whatever comment it
came with, both from `PUT /auth/keys/<name>` and from `--bootstrap-keys`. Now its
comment is kept after it (`PublicKey::kept_line`), the spaces between its words made
one. The comment checks nothing: every use of a key parses the line, and the parse
reads the key alone.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The credentials dialog listed a user's keys whole, to add one or
remove one but the last, and any failure replaced the dialog with the
error, the form lost.
* A key is shown short: its type, the first and last four characters
of what tells it apart (an Ed25519 key's base64 after the part all of
them share), its comment; the whole line in its tooltip.
* A key can be edited, its name and its line: saved under the name, a
new name written first and the old one removed.
* A user with a password on this node (`--htpasswd`) may remove its
last key: it logs in by the password. GET /auth/keys says so
(`password`), and DELETE /auth/keys/{id} takes it then; the rules no
longer keep the last key on every node (no other node can tell who
has a password where; under a strict policy the signed entry keeps
one still).
* What fails says why beside where it was asked (the add form, the
edited row, the removed one), which stays as it was, to be set right
and tried again.
|
| |
|
|
|
|
|
| |
How many changes apply_bytes applied was read only by the handler of
SIGNATURES, which re-screened the changes waiting for them and
published those that went in. With SIGNATURES gone (1219), nothing
reads it: Outcome loses the field.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 3, R4: whether a batch was judged followed the receiving
document's policy now (and whether the batch writes one). After the
policy was removed, or replaced by a version the node doesn't know,
changes written under it and received late went in unjudged, while
nodes that had them earlier judged them; REFUSALS gave up likewise.
Screening now remembers that the document held a policy, known or not
(Screening::policed), and judges what it receives while it does, each
change by the policy at its dependencies; REFUSALS judges then too
(not under a policy the node doesn't know, as before). Not across a
restart, which access.md says.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 3, R2: the root pin was set by the first HELLO_REPLY, and the
first sync, a round trip later, took that root; a peer naming another
afterwards was refused as another history. So the seed that answered
first decided, not "none while they disagree", and a malicious seed
makes itself fastest.
A node joining with an empty document of the first epoch now takes no
root (RootPin::Awaiting, roots left for later) until each of its seeds
named one, or ROOT_WAIT (10 s) passed since the first did; then the
one they name, none while they differ, as before. With --genesis-root
it takes that one alone, at once. access.md, the option's help and
example.toml say so; switching roots later, while the joiner holds
nothing of its own, is postponed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 3, R1: what a node received was vouched for only while its own
document's policy required signatures. A joiner's first sync, and
every node's message bringing `required`, went in by the rules alone:
a malicious first peer could plant changes under other members' actors
at their next seqs, their real ones then failing as duplicates for
good.
The signatures a message carries are always offered now, and the
screen always has them: a change waits for one where the policy at the
heads it is judged at requires it (rules::Reads::required, read once
per heads), whatever the receiving document holds. Changes written
under a policy not requiring signatures wait for none, as on every
other node; the check for forged changes runs always. Changes waiting
for a signature are judged again whenever signatures come.
The test of signatures required now takes the history into an empty
document; a new one takes changes written under a policy not requiring
them without any.
|
| |
|
|
|
|
|
|
| |
Each change taken but not yet applied got its own set of the heads it
is judged at, built in an ordered set and copied into a vector, and
kept so for those building on it. Most build on one change alone and
are judged at its heads: share them instead (reference counted), and
build a set only for a change with several dependencies.
|
| |
|
|
|
|
|
| |
Screening copied every change it took, and every one left waiting for
a signature, bytes and all, from the batch it had ordered and then
dropped. Move them out of the batch instead; only a refused change,
recorded with why, is still copied.
|
| |
|
|
|
|
|
| |
in_order is about a seventh of screening in the profile, most of it
hashing change hashes: each change went into and out of three maps,
and each dependency into two. Look each hash up once, in a map from
hash to place, and keep the rest by place in vectors.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The profile of bench_screening puts screening at about 21% of the
samples, half of it Automerge applying the changes. Of the other half,
reading the policy for every change taken is the largest part (3.4%):
reaches_signed reads it, and what users sign, to tell whether the
change must go in before the next is judged.
Read them once, and again only after the document took the accepted
ones; a change to the policy or to what users sign is relevant, taken
at once. A change touching nothing reaches nothing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
With the policy's reads kept, judging a change still costs about one
read (some 3.5 µs), and so does judging without a policy: what is left
is At::new, which asks get_change_by_hash for each head. Automerge
builds the change again from the document's operations to answer, at
a cost growing with its history. Screening asks it again for every
candidate and every dependency, and sync for heads.
Ask by the missing dependencies instead (changes::holds), which look
the hash up: one the document holds is not among them. A change queued
in the document, waiting for its dependencies, isn't either; but then
some are missing, and only then is it asked the slow way. Every check
of whether a document holds a change goes through it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
bench_screening shows judging at about 17 µs of a change's 25: some
two and a half reads at its heads (the policy, bans, its node's member
entry, whether there is a registry, the sealed declarations), each
costing as much as working out what the heads cover. Screening judges
most of a batch at the same heads, and those reads are the same for
every change judged there.
Keep them (rules::Reads), by node and by user where they depend on
one, while the heads stay the same; screening keeps one for a batch.
What the heads hold doesn't change as the document takes changes, so
nothing goes stale but by other heads. The benchmark breakdown shows
judging so and afresh.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N7, the care it raised: a joining node's empty document took
as the first epoch's root whatever dependency-less change came first,
from whichever peer it joined through; the first epoch's ID is all
zeros, so nothing pinned it. A remake's epoch has its ID, the snapshot
change's hash, which the rules hold its root to.
* Every node names its epoch's root in HELLO and HELLO_REPLY (the
epoch's ID for a remake's, its genesis change for the first; wire
version 45). A node whose document starts from another root of the
same epoch refuses the handshake, logging it: another history under
the swarm's ID, a forged root or a genesis started again.
* A joining node with an empty document takes as its root only the one
it was given (--genesis-root, new), else the one its peers name, and
none while they name different ones (access::screen::RootPin, kept
in Screening across documents): one peer hands it another root only
if it is all the joiner hears, or never, with --genesis-root. A root
refused is listed with the refused, what builds on it with it.
* The genesis node logs the root (`The swarm's root`); GET
/admin/epochs shows it (`root`).
A genesis started again under a remembered swarm ID used to merge with
the nodes of it still running; they now refuse it (another root), and
the docs say to join them with seeds instead.
The epoch's identity stays as it is (generation 0 all zeros): records
and epoch metadata name it so, and every comparison of epochs with
them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N7: a node screened received changes only while its own
document had a policy it enforces. A joiner's empty document has none,
so its first sync, as much history as the node it joins through chose,
was applied unjudged, and under signatures required unvouched; the same
for any node while the policy's own change arrived with later ones.
Sync messages, deltas and downloaded changes are now always screened,
each change judged by the policy at its dependencies (1135), so the
verdicts are the swarm's. While neither the document nor the changes
hold a policy (none touches a "policy" key: inspect::touch_key), only
an epoch's root is judged and nothing is placed: bench_screening, after
1188, measured placing everything at about as much again as applying
(564 ms + 543 ms for 100 000 changes), judging by the rules at about
five times (+2.5 s), which a document with a policy pays anyway.
access.md no longer lists the joiner's first sync, nor nodes agreeing
only as the policy reaches them (judged at the dependencies, they
agree), as left to do; changes::load_delta is for tests now.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
bench_screening showed screening growing quadratically: 1 000 changes
took 29 ms on top of applying them, 10 000 took 2.8 s, and 100 000
didn't finish in minutes. in_order placed changes in rounds, each
taking those, in hash order, whose dependencies were placed: a chain
received in one delta, its hashes in no order of their own, took about
as many rounds as changes, each over all of them.
It now counts, for each change, its dependencies among those received
not placed yet, and places the smallest hash of those with none left,
then those building on it as their counts reach zero (Kahn's
algorithm): n log n, and still the same order on every node, whatever
order the changes came in. One with a dependency neither in the
document nor among them, and what builds on it, still wait.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N3: 1150 let any member write an abort decision once a remake
had expired a grace period ago, judged by the change's own time, the
writer's word. A member could race the coordinator's legitimate commit
with an abort dated as it liked; both taken, Automerge shows
one, and nodes that acted on the commit split from those that didn't.
The two writers of such aborts are operator requests whose endpoints
already need write on _continuum/remakes: aborting an expired remake
(DELETE /admin/remakes/{id}) and superseding one (POST
/admin/remakes?force=supersede). Both now commit as the user who asks
(the proposal and its vote with them, for a supersede), which the rules
take as one who may write the decision. The clause judging an abort by
its time goes, and with it the time the rules carried. An expired
remake is still aborted through any member, by an operator.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N1: a strict manifest wasn't bound to its base. Any member
could propose an old, legitimately signed one again at the current
heads; nodes would verify it and vote, and every value at a signed
place changed or added since (a ban, a new user, a rotated key, a
changed userSigned or sealed declaration) would be deleted, nothing
after the base being left to replay.
The manifest now names the epoch and the heads it was made at, and a
node takes it (proposing or voting) only for a remake of that epoch
with exactly that base. GET /admin/strict-manifest makes it so;
continuum-signed strict proposes at those heads already.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N12: a strict remake kept only the values at the signed
places its manifest names, and copied what holds them as it was. A
member that replaced _continuum/users by a scalar before the turn left
the strict epoch without a registry, where any member may do anything.
The holders of the registry, `_continuum/access` and the bans are now
made anew as maps, with only what the manifest keeps copied into them;
and a manifest keeping no user is refused (at proposal, vote and build
alike), as a strict swarm without one would trust every member.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N6: finding 2's class survived for two reads the frontier
didn't cover: whether a map a change makes is there (maps made on the
way, 1137; holders made where none, 1147, for any path), and
_continuum/votes, which `relevant` didn't list. A crafted pair, the
second making a map the first made, was taken by nodes receiving both
in one batch (judged at the frontier, where the map isn't yet) and
refused by nodes receiving them apart.
The screen now keeps the paths the accepted, not applied yet, touch;
a change making a map at one of them is judged after they go in. Votes
join what `relevant` lists. A crafted pair needs hand-made changes,
which the tests don't build.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N5: under a strict policy a user's signed PUT of its own
entry, keys changed (continuum-signed key-add), passed the API and
envelope::check's own_keys_only, and was then refused by the rules'
path check: _continuum/users/<name> wanted write on the registry, so the
node judged it as every node would and answered 403. 1168 worked only
without strict.
Under a strict policy the rules now take a write of a user's own entry
from that user, leaving what it may be to signed_values, which runs
envelope::check: an envelope by the user, with only its keys changed,
one left at least. Strict only: under another policy the entry isn't an
envelope, and the user would rewrite its own capabilities.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Until a swarm is strict, the user a change names is its node's word, so
any member could have planted registry entries, policy, bans or
user-signed values as any user, plain or signed by a key it put there
itself; writing the strict policy trusted all of it as it was.
A remake of a new kind, `strict`, turns a swarm strict instead:
* Its content is a manifest (access::strict): the swarm, the strict
policy, and each value kept at the places values are signed, by the
SHA-256 of its JSON, signed by an operator (ssh-keygen -Y sign -n
continuum-strict).
* Every node builds the next epoch as compaction does, keeping at the
signed places only the values the manifest names, as they were at
the base, and writing its policy (RemakeKind::protocol, the one kind
that changes protocol data). Writes after the base reach the new
epoch only by replays, judged by the strict policy.
* A node votes for one only with its manifest signed by one of
--strict-keys, keys given out of the document; without any, for none
(logged once, `Not voting for a strict remake`), unless
--follow-strict-remakes. A coordinator proposes only what it would
vote for (403). Once a majority commits one, every node switches, as
with any remake: refusing to switch would only cut a node off.
* GET /admin/strict-manifest gives the manifest keeping everything held
now, the values for review and the heads they are at;
continuum-signed strict shows them, signs once confirmed and proposes
at those heads.
The sim a_swarm_turning_strict_through_a_remake: a member plants a
registry entry as a manager; the operator's manifest leaves it out; a
and b, given the key, commit; c, without, follows; the entry is gone
and the policy strict everywhere.
|
| |
|
|
|
|
|
|
| |
A user managing its own keys (1168) could remove the last, and then
neither sign nor log in by key. It can't now: DELETE /auth/keys/{id}
of the last answers 409, the rules take a user's change of its own keys
only with one left after it, and under a strict policy its own entry
signed only with one key at least. One who may manage users still may.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A node told (REFUSALS) that it holds a change the swarm's rules refuse,
and whose own rules refuse it too, dropped it and everything built on
it on one peer's word, however much that was; its own writes among them
went to the quarantine, the others' nowhere.
* A drop taking more than 100 changes, or more than ten and a tenth of
the document, now
waits until a second member names the same change: the node's rules
and one peer's word could both be off. Logged as an error, counted in
/admin/stats (sync.dropsHeld).
* The others' changes dropped are listed with the refused (GET
/admin/refused, "taken unjudged, dropped"), for operators to see.
The cheap half of asking the others (future-work.md, Telling a
malicious change from a wrong policy).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Only a user who may manage users could change a user's keys: a user
couldn't add a new laptop's key, or remove a lost one.
A user now manages its own keys, whatever else it may do:
* GET /auth/keys: its keys, its entry, and the envelope holding it
under a strict policy (`signed`), to sign against.
* PUT /auth/keys/{id} with an authorized_keys line, DELETE
/auth/keys/{id}: one change, as the user. The rules take a write of
_continuum/users/<name>/keys/… from the user it names.
* Under a strict policy, the user signs its entry with only its keys
changed, with a key it holds already (envelope::check takes that from
the user itself), PUT to its own entry even without write on the
registry; continuum-signed key-add and key-remove do so.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Review 6: a node remembers the challenges used until they expire, 10 000
at most, and refused every login by key once full. One user holding a
key could make 10 000 logins within five minutes and keep everyone else
out.
A user may now log in by key at most --key-logins-per-user times (100)
on a node within a challenge's lifetime, past which that user waits and
others don't; the 10 000 stay as the cap for all users together. The
lifetime, five minutes until now, is --challenge-lifetime.
|
| |
|
|
|
|
|
|
|
|
|
| |
A change waiting for a dependency that hadn't come stayed waiting when
another of its dependencies was refused meanwhile, until the bound
pushed it out. Changes building on one refused before were refused one
generation per batch.
Building on a refused change is now followed through at once, before
judging and again after (refuse_building): whatever waits on a change
refused in the batch, or on one of those, is refused then and there.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Review section 7: Automerge takes the changes judged in a batch all or
none, so one it won't take (a fork of an actor's changes, under a
policy without signatures) fails the whole batch, and every batch
carrying it. The log said who sent the message (`Failed to process sync
message`, `Delta download failed`, with the peer) but not what failed.
The error now names the changes it was given, the first eight, each
with its seq and the node it says made it, so whoever sends the
offending one shows in the log beside what it sent.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Review 3.1, part of finding 12: a signed value's payload bound path,
user, time and prev, but not the swarm. A user with the same name and
key in two swarms could have an envelope replayed from one into the
other, wherever the path holds no envelope yet (prev "").
The payload now names the swarm (`swarm`), and a node takes an envelope
only for its own. /auth/whoami says the swarm, and continuum-signed and
the example in api.md sign it. Wire version 44: nodes before this
refuse such envelopes, and those after refuse the old ones.
|
| |
|
|
|
|
|
|
|
| |
Review finding 11: token::verify checked only that the issuer is a
member, and a ban leaves the member entry. A compromised node's tokens
stayed valid after it was banned, until it was forgotten too.
A token issued by a banned node is now refused (BannedIssuer), as soon
as the ban reaches the node judging it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review finding 9: once 4096 signatures waited for their changes, new
ones were dropped, and the waiting changes were cut at 10 000 keeping
the oldest. A member could send junk once (signatures of changes that
never come, changes no signature will vouch for) and fill both for
good. Every honest signature arriving before its change was then lost,
so under signatures required its changes waited for the actor's next
signature: a stall for quiet actors.
Both now keep the newest: the oldest go. A share per sender, so a member
sending junk without pause can't push out what others sent either, is
in future-work.md.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Under a policy, a remake's decision was taken from its coordinator only.
A node aborting an expired remake whose coordinator is gone
(DELETE /admin/remakes, after abortableAt) was refused by its own
judgement (403), so such a remake could never be decided.
The decision is now also taken from a user who may write it (the
protocol writers), and an abort from any member once the remake expired
a grace period ago (abortableAt, by the coordinator's grace period in
the proposal). The rules have no clock of their own, so that is judged
by the time the change says it was made: the writer's word, so a member
could abort a remake early, as it could ask its coordinator to, but
never commit one. Before then, an abort stays a request the coordinator
applies.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review finding 7, second part: a record carried into a new epoch whole
(proposal and decision in one change) was taken from any member, for
any remake ID. A member could write a forged record, decision included,
for a fresh ID in a single change.
The snapshot's epoch metadata now names the remake's coordinator, as
the quorum built it. The carried-record exception holds only in a
later epoch, only for the remake that made it, and only from the
coordinator its snapshot names, where there is no record yet. Wire
version 43: the snapshot, and so the epoch ID, changes.
The review suggested carrying the record inside the snapshot instead.
The snapshot can't hold the decision, which names the snapshot's own
hash. So the snapshot vouches for who may carry the record instead.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Review finding 7, first part: the protocol-map rules looked at paths,
not actions. Any member could delete _continuum/members, /votes or
/remakes whole, or rewrite another remake's proposal (its coordinator)
and then write its decision.
What holds the members' entries, votes and remake records, and a
remake's record itself, are now made by a member only where there is
none; a proposal is written only where there is none. Replacing or
removing any of them is left to a user who may write there (the
protocol writers). An abort asked for stays any member's.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review finding 5: vouching by (actor, seq) isn't bound to a hash. A
member can make a change under another member's actor at a seq that
member's signature covers. The change is then taken as vouched, and if
it breaks a rule, the honest member is refused as a whole and logged as
the one whose own change broke the rules.
A signature now vouches for its change and the changes of its actor it
builds on: in the document, as before; among received changes, those
it reaches through the batch's dependencies. A received change under an
actor at a seq whose change the document holds (a signature verified up
to it) is a fork: refused as forged, never as its actor's node's, not
told back in REFUSALS, and logged with the peer that brought it.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A node refusing a member as a whole sent it nothing, and lifted that
only when a change of the member's was taken again. But a member that
dropped what was refused (REFUSALS) starts its sync sessions over, and
a fresh session sends no changes until the other side answers: the two
waited for each other, and the member stayed refused.
The refusal now notes the member's run and epoch_seq when it began; a
member with another document since (it dropped what was refused, or
rejoined) is synced with again.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The API checks the path a request asks for; the rules judge everything
the change does. Where they disagreed, the honest node's change was
refused by every other node, and with signatures required the node was
refused as a whole. The plan had both gates run the same judgement.
Under a policy, each change a node makes (its own writes and those
through the API) is now judged by rules::judge as receivers will judge
it, before it is signed or sent; one every node would refuse is rolled
back (the document forked at the heads before it, same actor: the
change never left) and the request refused with 403 (StoreError::
Refused). rules::judge_held, judging a change the document holds,
moves from swarm::refusals to access::rules for both.
The simulations that play a member past its own checks write with a
helper that bypasses them (forge).
Review finding 3 (the same judgement at both gates).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two writes the API lets a user make were refused by the rules:
* DELETE /peers/{pk}, with write on _continuum/forgotten alone, writes
the forget record and removes the member's entry in one change; the
rules wanted write on the entry. Removing an entry is now also taken
in a change forgetting that member, from a user who may forget it.
* DELETE /admin/users/{name}/tokens by a user who manages users writes
_continuum/logouts/<name>; the rules wanted write there. One who may
write _continuum/users may now write anyone's logouts entry.
Review finding 3 (DELETE /peers, revoking tokens).
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A write makes the maps on its way where there are none
(store::write_path); the rules judged each made map as a write at its
own path, so a user whose capability lies below a map not made yet
(write on servers/eu, a first PUT at servers/eu/0) had its first write
taken by its node and refused by every other. A map made where there
was none, in a change writing within it, is now judged by what is
written within it.
Review finding 3 (parent maps created on the way).
|
| |
|
|
|
|
|
|
|
|
|
| |
Under a strict policy the rules read user data: the value at a signed
place (prev) and what holds one. A batch judged at its frontier missed
a signed write earlier in the same batch, so two successive signed
writes at one path received together were refused on some nodes and
taken on others. A change at, within or above a signed path now goes
in before the next is judged.
Review finding 2.
|
| |
|
|
|
|
|
|
|
|
| |
The rules read the policy at the change's dependencies only for strict
mode, so history from before the policy was judged by today's rules
when a late node caught up, or when REFUSALS had it judged again. A
change written while the document had no policy, or one this node
doesn't know, is now judged by none.
Review finding 6.
|