summaryrefslogtreecommitdiff
path: root/src/login
Commit message (Collapse)AuthorAge
* Split `user` into a chat-facing entity and an authentication-facing entity.Owen Jacobson2025-08-26
| | | | | | | | | | | | | | The taxonomy is now as follows: * A _login_ is someone's identity for the purposes of authenticating to the service. Logins are not synchronized, and in fact are not published anywhere in the current API. They have a login ID, a name and a password. * A _user_ is someone's identity for the purpose of participating in conversations. Users _are_ synchronized, as before. They have a user ID, a name, and a creation instant for the purposes of synchronization. In practice, a user exists for every login - in fact, users' names are stored in the login table and are joined in, rather than being stored redundantly in the user table. A login ID and its corresponding user ID are always equal, and the user and login ID types support conversion and comparison to facilitate their use in this context. Tokens are now associated with logins, not users. The currently-acting identity is passed down into app types as a login, not a user, and then resolved to a user where appropriate within the app methods. As a side effect, the `GET /api/boot` method now returns a `login` key instead of a `user` key. The structure of the nested value is unchanged.
* Rename the `login` module to `user`.Owen Jacobson2025-03-23
|
* Rename `user` to `login` at the database.Owen Jacobson2025-03-23
|
* Ensure `must_use` warnings fire even after results are unwrapped.Owen Jacobson2025-02-21
|
* Upgrade to Rust 1.85 and Rust 2024 edition.Owen Jacobson2025-02-20
| | | | | | | | There are a couple of migration suggestions from `cargo fix --edition` that I have deliberately skipped, which are intended to make sure that the changes to `if let` scoping don't bite us. They don't, I'm pretty sure, and if I turn out to be wrong, I'd rather fix the scoping issues (as they arise) than use `match` (`cargo fix --edition`'s suggestion). This change also includes a bulk reformat and a clippy cleanup. NOTA BENE: As this requires a new Rust toolchain, you'll need to update Rust (`rustup update`, normally) or the server won't build. This also applies to the Debian builder Docker image; it'll need to be rebuilt (from scratch, pulling its base image again) as well.
* Resume points are no longer optional.Owen Jacobson2024-10-30
| | | | This is an inconsequential change for actual clients, since "resume from the beginning" was never a preferred mode of operation, and it simplifies some internals. It should also mean we get better query plans where `coalesce(cond, true)` was previously being used.
* Remove `hi-recanonicalize`.Owen Jacobson2024-10-30
| | | | This utility was needed to support a database migration with existing data. I have it on good authority that no further databases exist that are in the state that made this tool necessary.
* Add `change password` UI + API.Owen Jacobson2024-10-29
| | | | The protocol here re-checks the caller's password, as a "I left myself logged in" anti-pranking check.
* fixup! Restrict login names.Owen Jacobson2024-10-29
|
* Create a dedicated workflow type for creating logins.Owen Jacobson2024-10-29
| | | | | | | | | | | | | | | | | Nasty design corner. Logins need to be created in three places: 1. In tests, using app.logins().create(…); 2. On initial setup, using app.setup().initial(…); and 3. When accepting invites, using app.invites().accept(…). These three places do the same thing with respect to logins, but also do a varying mix of other things. Testing is the simplest and _only_ creates a login. Initial setup and invite acceptance both issue a token for the newly-created login. Accepting an invite also invalidates the invite. Previously, those three functions have been copy-pasted variations on a theme. Now that we have validation, the copy-paste approach is no longer tenable; it will become increasingly hard to ensure that the three functions (plus any future functions) remain in synch. To accommodate the variations while consolidating login creation, I've added a typestate-based state machine, which is driven by method calls: * A creation attempt begins with `let create = Create::begin()`. This always succeeds; it packages up arguments used in later steps, but does nothing else. * A creation attempt can be validated using `let validated = create.validate()?`. This may fail. Input validation and password hashing are carried out at this stage, making it potentially expensive. * A validated attempt can be stored in the DB, using `let stored = validated.store(&mut tx).await?`. This may fail. The login will be written to the DB; the caller is responsible for transaction demarcation, to allow other things to take place in the same transaction. * A fully-stored attempt can be used to publish events, using `let login = stored.publish(self.events)`. This always succeeds, and unwraps the state machine to its final product (a `login::History`).
* Restrict login names.Owen Jacobson2024-10-29
| | | | | | | | There's no good reason to use an empty string as your login name, or to use one so long as to annoy others. Names beginning or ending with whitespace, or containing runs of whitespace, are also a technical problem, so they're also prohibited. This change does not implement [UTS #39], as I haven't yet fully understood how to do so. [UTS #39]: https://www.unicode.org/reports/tr39/
* Tests for accepting invitesOwen Jacobson2024-10-24
|
* Make sure (most) queries avoid table scans.Owen Jacobson2024-10-23
| | | | | | | | | | | | I've exempted inserts (they never scan in the first place), queries on `event_sequence` (at most one row), and the coalesce()s used for event replay (for now; these are obviously a performance risk area and need addressing). Method: ``` find .sqlx -name 'query-*.json' -exec jq -r '"explain query plan " + .query + ";"' {} + > explain.sql ``` Then go query by query through the resulting file.
* Sort out the naming of the various parts of an identity.Owen Jacobson2024-10-22
| | | | | | | | | * A `cookie::Identity` (`IdentityCookie`) is a specialized CookieJar for working with identities. * An `Identity` is a token/login pair. I hope for this to be a bit more legible. In service of this, `Login` is no longer extractable. You have to get an identity.
* Provide `hi-recanonicalize` to recover from canonicalized-name problems.Owen Jacobson2024-10-22
|
* Canonicalize login and channel names.Owen Jacobson2024-10-22
| | | | | | | | | | | | | | | Canonicalization does two things: * It prevents duplicate names that differ only by case or only by normalization/encoding sequence; and * It makes certain name-based comparisons "case-insensitive" (generalizing via Unicode's case-folding rules). This change is complicated, as it means that every name now needs to be stored in two forms. Unfortunately, this is _very likely_ a breaking schema change. The migrations in this commit perform a best-effort attempt to canonicalize existing channel or login names, but it's likely any existing channels or logins with non-ASCII characters will not be canonicalize correctly. Since clients look at all channel names and all login names on boot, and since the code in this commit verifies canonicalization when reading from the database, this will effectively make the server un-usuable until any incorrectly-canonicalized values are either manually canonicalized, or removed It might be possible to do better with [the `icu` sqlite3 extension][icu], but (a) I'm not convinced of that and (b) this commit is already huge; adding database extension support would make it far larger. [icu]: https://sqlite.org/src/dir/ext/icu For some references on why it's worth storing usernames this way, see <https://www.b-list.org/weblog/2018/nov/26/case/> and the refernced talk, as well as <https://www.b-list.org/weblog/2018/feb/11/usernames/>. Bennett's treatment of this issue is, to my eye, much more readable than the referenced Unicode technical reports, and I'm inclined to trust his opinion given that he maintains a widely-used, internet-facing user registration library for Django.
* Unicode normalization on input.Owen Jacobson2024-10-21
| | | | | | | | | | | | | | | | | | This normalizes the following values: * login names * passwords * channel names * message bodies, because why not The goal here is to have a canonical representation of these values, so that, for example, the service does not inadvertently host two channels whose names are semantically identical but differ in the specifics of how diacritics are encoded, or two users whose names are identical. Normalization is done on input from the wire, using Serde hooks, and when reading from the database. The `crate::nfc::String` type implements these normalizations (as well as normalizing whenever converted from a `std::string::String` generally). This change does not cover: * Trying to cope with passwords that were created as non-normalized strings, which are now non-verifiable as all the paths to verify passwords normalize the input. * Trying to ensure that non-normalized data in the database compares reasonably to normalized data. Fortunately, we don't _do_ very many string comparisons (I think only login names), so this isn't a huge deal at this stage. Login names will probably have to Get Fixed later on, when we figure out how to handle case folding for login name verification.
* Make the responses for various data creation requests more consistent.Owen Jacobson2024-10-19
| | | | | | | | | | | | | | | | | | | | In general: * If the client can only assume the response is immediately valid (mostly, login creation, where the client cannot monitor the event stream), then 200 Okay, with data describing the server's view of the request. * If the client can monitor for completion by watching the event stream, then 202 Accepted, with data describing the server's view of the request. This comes on the heels of a comment I made on Discord: > hrm > > creating a login: 204 No Content, no body > sending a message: 202 Accepted, no body > creating a channel: 200 Okay, has a body > > past me, what were you on There wasn't any principled reason for this inconsistency; it happened as the endpoints were written at different times and with different states of mind.
* Retain deleted messages and channels temporarily, to preserve events for replay.Owen Jacobson2024-10-17
| | | | | | | | | | | | Previously, when a channel (message) was deleted, `hi` would send events to all _connected_ clients to inform them of the deletion, then delete all memory of the channel (message). Any disconnected client, on reconnecting, would not receive the deletion event, and would de-synch with the service. The creation events were also immediately retconned out of the event stream, as well. With this change, `hi` keeps a record of deleted channels (messages). When replaying events, these records are used to replay the deletion event. After 7 days, the retained data is deleted, both to keep storage under control and to conform to users' expectations that deleted means gone. To match users' likely intuitions about what deletion does, deleting a channel (message) _does_ immediately delete some of its associated data. Channels' names are blanked, and messages' bodies are also blanked. When the event stream is replayed, the original channel.created (message.sent) event is "tombstoned", with an additional `deleted_at` field to inform clients. The included client does not use this field, at least yet. The migration is, once again, screamingingly complicated due to sqlite's limited ALTER TABLE … ALTER COLUMN support. This change also contains capabilities that would allow the API to return 410 Gone for deleted channels or messages, instead of 404. I did experiment with this, but it's tricky to do pervasively, especially since most app-level interfaces return an `Option<Channel>` or `Option<Message>`. Redesigning these to return either `Ok(Channel)` (`Ok(Message)`) or `Err(Error::NotFound)` or `Err(Error::Deleted)` is more work than I wanted to take on for this change, and the utility of 410 Gone responses is not obvious to me. We have other, more pressing API design warts to address.
* Organizational pass on endpoints and routes.Owen Jacobson2024-10-16
|
* Split the login transaction, to reduce database contention during loginOwen Jacobson2024-10-11
|
* Stop creating accounts during login.Owen Jacobson2024-10-11
|
* Blind debug output for StoredHash.Owen Jacobson2024-10-11
| | | | This is a little excessive, as PasswordHash (which StoredHash converts to) _does_ derive Debug and exposes the hash, but I'll feel better if the hash never ends up in logs.
* Provide a view of logins to clients.Owen Jacobson2024-10-09
|
* Separate `/api/boot` into its own module.Owen Jacobson2024-10-05
|
* Use `/api/boot` to bootstrap the client.Owen Jacobson2024-10-05
| | | | | | | | | | | The client now takes an initial snapshot from the response to `/api/boot`, then picks up the event stream at the immediately-successive event to the moment the snapshot was taken. This commit removes the following unused endpoints: * `/api/channels` (GET) * `/api/channels/:channel/messages` (GET) The information therein is now part of the boot response. We can always add 'em back, but I wanted to clear the deck for designing something more capable, for dealing with client needs.
* Retire top-level `repo`.Owen Jacobson2024-10-02
| | | | This helped me discover an organizational scheme I like more.
* Split login and token handling.Owen Jacobson2024-10-02
|
* First pass on reorganizing the backend.Owen Jacobson2024-10-02
| | | | This is primarily renames and repackagings.
* Organize IDs into top-level namespaces.Owen Jacobson2024-10-01
| | | | (This is part of a larger reorganization.)
* Provide a resume point to bridge clients from state snapshots to the event ↵Owen Jacobson2024-10-01
| | | | sequence.
* Prevent racing between `limit_stream` and logging out.Owen Jacobson2024-10-01
|
* Reimplement the logout machinery in terms of token IDs, not token secrets.Owen Jacobson2024-09-29
| | | | | | This (a) reduces the amount of passing secrets around that's needed, and (b) allows tests to log out in a more straightforwards manner. Ish. The fixtures are a mess, but so is the nomenclature. Fix the latter and the former will probably follow.
* Shut down the `/api/events` stream when the user logs out or their token ↵Owen Jacobson2024-09-29
| | | | | | | | expires. When tokens are revoked (logout or expiry), the server now publishes an internal event via the new `logins` event broadcaster. These events are used to guard the `/api/events` stream. When a token revocation event arrives for the token used to subscribe to the stream, the stream is cut short, disconnecting the client. In service of this, tokens now have IDs, which are non-confidential values that can be used to discuss tokens without their secrets being passed around unnecessarily. These IDs are not (at this time) exposed to clients, but they could be.
* Wrap credential and credential-holding types to prevent `Debug` leaks.Owen Jacobson2024-09-28
| | | | | | | | | | | | The following values are considered confidential, and should never be logged, even by accident: * `Password`, which is a durable bearer token for a specific Login; * `IdentitySecret`, which is an ephemeral but potentially long-lived bearer token for a specific Login; or * `IdentityToken`, which may hold cookies containing an `IdentitySecret`. These values are now wrapped in types whose `Debug` impls output opaque values, so that they can be included in structs that `#[derive(Debug)]` without requiring any additional care. The wrappers also avoid implementing `Display`, to prevent inadvertent `to_string()`s. We don't bother obfuscating `IdentitySecret`s in memory or in the `.hi` database. There's no point: we'd also need to store the information needed to de-obfuscate them, and they can be freely invalidated and replaced by blanking that table and asking everyone to log in again. Passwords _are_ obfuscated for storage, as they're intended to be durable.
* Expire channels, too.Owen Jacobson2024-09-28
|
* Browsers default Path= to the directory part of the request URI.Owen Jacobson2024-09-27
| | | | This change makes the identity cookie available throughout `/api`.
* Retire `fixtures::error::expected!`.Owen Jacobson2024-09-25
| | | | I had no idea `std` included a `matches!` macro, and I feel we're better off using it.
* Crank up the Clippy warnings.Owen Jacobson2024-09-25
| | | | This'll catch style issues, mostly.
* rustdoc comment for the (very limited) public API of the crate.Owen Jacobson2024-09-25
| | | | | | | | This silences some `-Wclippy::pedantic` warning, and it's just a good thing to do. I've made the choice to have the docs comment face programmers, and to provide `hi --help` and `hi -h` content via Clap attributes instead of inferring it from the docs comment. Internal (private) "rustdoc" comments have been converted to regular comments until I learn how to write better rustdoc.
* Write tests.Owen Jacobson2024-09-20
|
* Pass dates around by ref more consistentlyOwen Jacobson2024-09-20
|
* Remove the HTML client, and expose a JSON API.Owen Jacobson2024-09-20
| | | | | | | | | | | | | This API structure fell out of a conversation with Kit. Described loosely: kit: ok kit: Here's what I'm picturing in a client kit: list channels, make-new-channel, zero to one active channels, post-to-active. kit: login/sign-up, logout owen: you will likely also want "am I logged in" here kit: sure, whoami
* Expire messages after 90 days.Owen Jacobson2024-09-20
| | | | | | | | | | This is intended to manage storage growth. A community with broadly steady traffic will now reach a steady state (ish) where the amount of storage in use stays within a steady band. The 90 day threshold is a spitball; this should be made configurable for the community's needs. I've also hoisted expiry out into the `app` classes, to reduce the amount of non-database work repo types are doing. This should make it easier to make expiry configurable later on. Includes incidental cleanup and style changes.
* Less Option calisthenicOwen Jacobson2024-09-20
|
* Most pass-through errors do not need additional message textOwen Jacobson2024-09-18
|
* Return 404s when resources are not found.Owen Jacobson2024-09-18
| | | | This is implemented by making the return values, in most cases, idiosyncratic ad-hoc types that then convert to the approprate error response. This also should make endpoints more testable, since the return values can now be inspected to check their properties without having to process or parse an HTTP response.
* App methods now return errors that allow not-found cases to be distinguished.Owen Jacobson2024-09-18
|
* Express record dependencies through types.Owen Jacobson2024-09-17
| | | | This provides a convenient place to _stick_ "not found" errors, though actually introducing them will come in a later commit.
* Consolidate most repository types into a repo module.Owen Jacobson2024-09-16
| | | | | | | | | | | | Having them contained in the individual endpoint groups conveyed an unintended sense that their intended scope was _only_ that endpoint group. It also made most repo-related import paths _quite_ long. This splits up the repos as follows: * "General applicability" repos - those that are only loosely connected to a single task, and are likely to be shared between tasks - go in crate::repo. * Specialized repos - those tightly connected to a specific task - go in the module for that task, under crate::PATH::repo. In both cases, each repo goes in its own submodule, to make it easier to use the module name as a namespace. Which category a repo goes in is a judgment call. `crate::channel::repo::broadcast` (formerly `channel::repo::messages`) is used outside of `crate::channel`, for example, but its main purpose is to support channel message broadcasts. It could arguably live under `crate::event::repo::channel`, but the resulting namespace is less legible to me.