steid

@jamesgill /

steid/plans/progress.md
4.4 KBCode·Blame·Raw
ab7fea9chore: plans setup1mo
1# Progress
2
3## This attempt (#3, Topcoat)
4
5Nothing shipped yet. Milestone 0 in progress — see [current.md]current.md.
6
7---
8
9## Reference: what attempt #2 proved
10
11Not this repo's progress. This is a catalogue of what was built and **verified working**
12in `steid-backup-2026-07-31`, so the rebuild can crib rather than rediscover.
13
14Final state: single crate, ~4,200 LOC, 60 passing tests, five milestones.
15
16### Identity
17
18Domain model (User, Organization, Membership, Actor, Role), `Email` and
19`PasswordHash` value objects, typed IDs, `DomainError`, four repository ports with
20in-memory and SQLite implementations each. `RegistrationPolicy` (Personal / Invite /
21Open) driving which routes exist. Argon2 hashing behind a `PasswordHasher` port with a
22stub for tests. Use cases: `bootstrap_owner`, `register_user`, `login`, `create_invite`.
23Signed-cookie sessions via an `AuthUser` extractor.
24
25**Gotcha:** organizations must be saved before users — the FK runs that direction.
26Both `bootstrap_owner` and `register_user` had to be fixed for this.
27
28### Repo model
29
30`Repository` entity, `RepoId`, `Visibility` (Public/Private), `RepoRepository` port,
31migration `006_create_repositories.sql`. `create_repo` use case validates the name,
32rejects duplicates, and initialises the bare repo on disk in the same call. Bare repos
33live at `{data_dir}/{org}/{repo}.git`, `data_dir` defaulting to `./data`. Repos are
34created empty, no initial commit, like GitHub.
35
36### Git over SSH
37
38`GitStorage` port (`init_bare`, `repo_path`) with `DiskGitStorage` shelling out to
39`git init --bare`. `GitProtocolServer` port (`upload_pack`, `receive_pack`) with
40`GitBinary` spawning `git upload-pack` / `git receive-pack` via
41`tokio::process::Command` and pumping stdio with `tokio::io::copy`.
42
43`serve_clone` and `serve_push` use cases enforce visibility and actor checks **before
44any protocol byte flows** — that ordering is the whole point of putting them in the
45application layer.
46
47#### SSH channel bridging
48
49The fiddly part, and worth re-reading before Milestone 3:
50
51- Store `Channel<Msg>` per `ChannelId` in the handler's map on `channel_open_session`
52- On `exec_request`, take the channel, split it with `into_stream()` +
53 `tokio::io::split`
54- Take stderr via `make_writer_ext(Some(1))` **before** `into_stream()` — that call
55 consumes the channel, so the order is not optional
56- No `data()` or `channel_eof()` handlers needed once the streams are split
57
58An earlier iteration used mpsc channels, custom `ChannelReader`/`ChannelWriter`, and a
59`spawn_blocking` thread. All of it was deleted and the result was simpler.
60
61### SSH key auth and authorization
62
63`SshKey { id, user_id, name, fingerprint, openssh }` + port, migration
64`007_create_ssh_keys.sql` (`fingerprint` UNIQUE). Fingerprints are SHA256 via
65`russh::keys::ssh_key::PublicKey::fingerprint(HashAlg::Sha256)`, stored as `SHA256:…`.
66`add_ssh_key` parses the openssh blob, dedupes on fingerprint, and re-encodes to a
67canonical form before storing.
68
69`auth_none` rejects. `auth_publickey` fingerprints the offered key, looks it up, and
70on a match stores `user_id` on the handler; `exec_request` builds the real `Actor`
71from it.
72
73Authorization rules as shipped:
74
75| Operation | Requirement |
76|---|---|
77| Clone, public repo | open |
78| Clone, private repo | any membership in the repo's org |
79| Push | `Role::Owner` membership in the repo's org |
80
81Web UI at `/{owner}/keys` — owner-only, lists fingerprints, accepts openssh via
82textarea, revokes per-row.
83
84**Verified end-to-end:** clone with an unregistered key → `Permission denied` (exit
85128); register via web UI → clone and push both succeed; second unregistered key →
86rejected at auth; revoke via web UI → subsequent clone rejected at auth.
87
88### Security notes
89
90Attempt #2 ran with a **named, deliberate backdoor** between milestones: SSH accepted
91any connection and passed a placeholder `Actor` (`UserId("ssh-anonymous")`), leaving
92push open to anyone who could reach the port. It was recorded with an explicit
93tightening point (`ssh.rs::exec_request`) and a closing milestone, and it did close.
94
95That practice is worth keeping. When this attempt opens a hole to make progress, name
96it, name the line that closes it, and name the milestone.
97
98### Never built
99
100Repo browsing (tree/blob/log), HTTP smart protocol, personal access tokens, flash
101messages, issues, PRs, blogs, pages, project showcases. Milestones 5–8 in
102[ROADMAP.md]ROADMAP.md are all greenfield.