diff --git a/packages/stack-encrypt-derive/src/lib.rs b/packages/stack-encrypt-derive/src/lib.rs index fcf7bc40f..6db556952 100644 --- a/packages/stack-encrypt-derive/src/lib.rs +++ b/packages/stack-encrypt-derive/src/lib.rs @@ -131,9 +131,14 @@ //! checked at compile time: one index (`Encrypted`), several //! (`Encrypted<(EqualityTerm, MatchTerms)>`, each operator answered by the //! one index that answers it), or a nullable field of either -//! (`Option>`). A record field's marker reaches the -//! inner record's markers under its own context: -//! `EncryptedAccount::ADDRESS.field(EncryptedAddress::STREET)`. +//! (`Option>`). +//! +//! To query a field of a record that is itself a field, call `field` on +//! the outer field's marker: +//! `EncryptedAccount::ADDRESS.field(EncryptedAddress::STREET)`. A nullable +//! record field, `Option`, has the same method. If the +//! inner record lists a scope, the query needs each record's scope, +//! outermost first. //! //! `#[stash(query = String)]` on a field states the type its queries take, //! so each of its marker's methods takes that type alone. The derive checks diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index aa1618d93..fad30ab0e 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -9,13 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Field markers** (#1486, #1590, #1596). `#[derive(EncryptTarget)]` on +- **Field markers** (#1486, #1590, #1596, #1600). `#[derive(EncryptTarget)]` on a `struct = ..` record emits one `Marker` per derived field. It is an associated constant named after the field in upper case: `EncryptedUser::EMAIL`. A marker is `Marker`. `W` is where the field sits: `marker::Root` for a field of the record `R`, or `marker::Nested` for one reached through a record field. `T` is the - field's target type. `C` is the scope the record's caller supplies. + field's target type. `C` is the type of the scopes a query takes: + `NoScope`, one `Scope`, or `target::Scopes` for a field of a nested + record. `P` is the type a query takes: `marker::TakesAny`, or `marker::Takes` for a field marked `#[stash(query = V)]`. - Its methods are one per operator the field's indexes answer, checked @@ -26,15 +28,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `op::OperatorAnswer`. An operator no index answers, or two answer, has no method. An `Option<..>` field has the methods a present value has. - `EncryptedAccount::ADDRESS.field(EncryptedAddress::STREET)` is the - inner field's marker under the outer field's context. It takes the - outer record's scope. `field` accepts only a marker of the inner record. + inner field's marker under the outer field's context. `field` accepts + only a marker of the inner record. + - A nullable record field, `Option`, has the same + `field` (`marker::Holds`). A row whose field is `None` stores nothing + to match. + - An inner record may list a scope of its own, such as + `scope(field = "tenant")`. The query then takes one scope for each + record that lists one, outermost first. The new `Scope::scope` + chains them: `.scope(Scope::new(outer).scope(inner))` + (`marker::JoinScope`). The chain returns the new `target::Scopes`, + and any number of records can list a scope. Where one record lists a + scope, `scope` takes that scope alone. - `#[stash(query = String)]` states the type a field's queries take. The derive checks it against the plaintext struct's field. - - A record that lists a caller scope, `scope(tenant: u64)`, gives a - query with no `encrypt` until `.scope(..)` is given once, with a - `Scope`. A record with no caller scope gives a query that is - ready at once. The query has no `bind` method, and ends with - `.encrypt(&keyset)`. The output is the index's query term. + - A record that lists a scope, `scope(tenant: u64)` or a stored + `scope(field = "tenant")` over a `u64`, gives a query with no `encrypt` + until `.scope(..)` is given once, with a `Scope`. A record with no + scope gives a query that is ready at once. The query has no `bind` + method, and ends with `.encrypt(&keyset)`. The output is the index's + query term. - A query from a marker derives under the declaration the derive wrote, so it is the stored term. No plan is written by hand beside the record. A method takes its value by value, and a string literal as diff --git a/packages/stack-encrypt/src/marker.rs b/packages/stack-encrypt/src/marker.rs index 9439e4840..0c41db284 100644 --- a/packages/stack-encrypt/src/marker.rs +++ b/packages/stack-encrypt/src/marker.rs @@ -17,14 +17,24 @@ //! a method for each operator one of its indexes answers. A nullable field, //! `Option>`, has the methods a present value has. //! -//! A record that lists a caller scope, `scope(tenant: u64)`, gives its -//! markers that scope's type, and the query has no -//! [`encrypt`](MarkedQuery::encrypt) until [`scope`](MarkedQuery::scope) is -//! given exactly once, with a value of that type. A record that lists none -//! gives a query that is ready at once and takes no scope. A binding is in -//! no query context, so the query type has no `bind` method. The query ends -//! with `encrypt`, whose output is the query term the field's index sends -//! for the operator ([`Supports::Query`]). +//! A record can list a scope. A scope is a value, such as a tenant ID, +//! that separates one group of rows from another. A record lists a scope +//! in one of two ways: +//! +//! - `scope(tenant: u64)`: the caller passes the scope when it encrypts. +//! - `scope(field = "tenant")`: each row stores the scope in a field. +//! +//! A query cannot read a row, so you pass the scope to the query in both +//! cases. Call [`scope`](MarkedQuery::scope) once, with a value of the +//! scope's type. Until you do, the query has no +//! [`encrypt`](MarkedQuery::encrypt) method. A query on a record that +//! lists no scope needs none, so you can encrypt it at once. +//! +//! A binding, such as a row ID, is not part of any query context. The +//! query type therefore has no `bind` method. +//! +//! `encrypt` ends the query. The method returns the query term that the +//! field's index sends for the operator ([`Supports::Query`]). //! //! ``` //! use stack_encrypt::kms::FakeDataKeySource; @@ -78,26 +88,67 @@ //! //! # A field that is a record //! -//! A field whose type is itself a record, `address: EncryptedAddress`, is -//! sealed under the outer field's context: each inner field's context is -//! its own, `addresses/street`, then the outer field's, `accounts/address`, -//! then the outer record's caller scope. The marker of the outer field -//! reaches the inner record's markers with that context applied: +//! The type of a field can be another record, for example +//! `address: EncryptedAddress`. To query a field of the inner record, +//! start from the outer field's marker and call [`field`](Marker::field): //! //! ```text //! EncryptedAccount::ADDRESS.field(EncryptedAddress::STREET).eq("Main St") //! ``` //! -//! [`field`](Marker::field) takes a marker of the record the field holds, -//! and no other, so a marker of another record does not compile -//! (`tests/ui/marker_field_of_another_record.rs`). The result is a marker -//! of the inner field, with the outer record's caller scope, and its -//! [`contexts`](Marker::contexts) are the outer record's, narrowed to the -//! inner field. A record three deep is reached one step at a time: +//! `field` returns a marker of the inner field. The derive seals each +//! inner field under a context with these parts, in this order: +//! +//! 1. the inner field's own label, `addresses/street` +//! 2. the outer field's label, `accounts/address` +//! 3. the outer record's scope, if the outer record lists one +//! +//! A query from the returned marker uses the same context, so the query +//! term matches the stored term. Call [`contexts`](Marker::contexts) on +//! the returned marker to read that context. +//! +//! `field` accepts only a marker of the record that is the field's type. +//! A marker of any other record does not compile +//! (`tests/ui/marker_field_of_another_record.rs`). +//! +//! For a record nested three deep, call `field` once for each level: //! `Outer::ACCOUNT.field(EncryptedAccount::ADDRESS).field(EncryptedAddress::STREET)`. -//! The inner record must list no scope of its own, stored or supplied: a -//! query of it would take two scopes, and `field` takes a marker whose -//! scope is [`NoScope`] ([`NestedScope`]). +//! +//! A nullable record field, `address: Option`, has the +//! same `field` method ([`Holds`]). A row whose field is `None` stores no +//! term, so no query matches that row. +//! +//! ## An inner record that lists a scope +//! +//! The inner record can list a scope too, for example +//! `scope(field = "tenant")`. The derive puts that scope into the context +//! after the inner field's label and before the outer field's label. +//! +//! The query then needs one scope for each record that lists one. Start +//! with the outermost record's scope, and chain +//! [`Scope::scope`] for each record further in. Pass the result to +//! [`scope`](MarkedQuery::scope): +//! +//! ```text +//! EncryptedCompany::OFFICE +//! .field(EncryptedOffice::STREET) +//! .eq("1 Main St") +//! .scope(Scope::new(company_tenant).scope(office_tenant)) +//! ``` +//! +//! If only one of the records lists a scope, pass that scope alone: +//! `.scope(Scope::new(tenant))`. [`JoinScope`] works out the type that +//! `scope` accepts. +//! +//! These mistakes do not compile +//! (`tests/ui/marker_nested_query_needs_each_scope.rs`): +//! +//! - leaving out a scope +//! - passing a scope of the wrong type +//! - passing two scopes of different types in the wrong order +//! +//! Two scopes of the same type in the wrong order do compile. The query +//! then derives a term that matches no row. //! //! # The type a query takes //! @@ -125,19 +176,33 @@ use crate::op::{ Answers, Between, Eq, Gt, Gte, Input, LabelledTarget, Lt, Lte, Matches, Operator, QueryToken, Supports, }; -use crate::target::{DeclaredContext, NoScope, Pending, Scope}; +use crate::target::{DeclaredContext, NoScope, Pending, Scope, Scopes}; use crate::{KeysetCipher, Label, NonEmpty}; -/// A field of a derived record, as a value: the field's name, the context -/// the derive sealed it under, its target type `T`, the type `C` of the -/// scope the record's caller supplies (`Scope` for `scope(tenant: -/// u64)`, [`NoScope`] for a record that lists none), and the type `P` a -/// query of it takes ([`TakesAny`], or [`Takes`] for a field marked -/// `#[stash(query = String)]`). +/// A field of a derived record, as a value that a query starts from. +/// +/// A marker records the field's name and the context the derive sealed the +/// field under. The type parameters are: +/// +/// - `W`: where the field is. [`Root`] is a field of the record `R`, +/// and every marker the derive emits has it. [`Nested`] is a field of +/// a record that is itself a field, and [`field`](Self::field) returns +/// it. +/// - `T`: the field's target type. +/// - `C`: the scopes that a query from this marker needs. +/// - `P`: the value type that a query from this marker accepts. `P` is +/// [`TakesAny`], or [`Takes`] for a field marked +/// `#[stash(query = String)]`. +/// +/// `C` is one of these: +/// +/// - [`NoScope`], for a record that lists no scope. +/// - `Scope`, for a record that lists `scope(tenant: u64)`, or +/// `scope(field = "tenant")` where the field is a `u64`. +/// - [`Scopes`], which lists two or more scopes with the outermost +/// record's first. `field` returns a marker with `Scopes` when more +/// than one record lists a scope ([`JoinScope`]). /// -/// `W` is where the field sits: [`Root`] for a field of the record `R`, -/// which is every marker the derive emits, and [`Nested`] for a field of -/// a record that is itself a field, reached with [`field`](Self::field). /// See the [module docs](self). pub struct Marker { name: &'static str, @@ -164,7 +229,7 @@ impl std::fmt::Debug for Marker { .field("name", &self.name) .field("scope", &self.scope) .field("identity", &self.identity) - .field("within", &outers(&self.place)) + .field("within", &labels(&holders(&self.place))) .finish_non_exhaustive() } } @@ -218,21 +283,36 @@ impl Marker { self.place.narrow((self.contexts)()).narrowed(self.name) } - /// The marker of a field of the record this field holds, with this - /// field's context applied: `EncryptedAccount::ADDRESS.field(EncryptedAddress::STREET)`. - /// A query from it derives under the inner field's own context, then - /// this field's, then the outer record's caller scope, as the derive - /// sealed the inner field. It takes this marker's scope, and the inner - /// marker's query type. + /// Returns the marker of a field of the record that is this field's + /// type: `EncryptedAccount::ADDRESS.field(EncryptedAddress::STREET)`. /// - /// `inner` must be a marker of the record `T` this field holds, so a - /// marker of another record does not compile. That record must list no - /// scope of its own ([`NestedScope`]): a query of it would take two - /// scopes. - pub fn field( + /// This field's type is a record `R`, or `Option` ([`Holds`]). + /// `inner` must be a marker of `R`. A marker of another record does + /// not compile. + /// + /// A query from the returned marker uses the context that the derive + /// sealed the inner field under. The parts are, in order: + /// + /// 1. the inner field's label + /// 2. the inner record's scope, if the inner record lists one + /// 3. this field's context + /// 4. the outer record's scope, if the outer record lists one + /// + /// The query needs each scope that the two records list + /// ([`JoinScope`]). If both records list a scope, call + /// `.scope(Scope::new(outer).scope(inner))`. If one record lists a + /// scope, call `.scope(Scope::new(value))`. + /// + /// The returned marker has the query type of `inner`. + pub fn field( self, - inner: Marker, U, C2, Q>, - ) -> Marker, U, C, Q> { + inner: Marker, U, C2, Q>, + ) -> Marker, U, C::Joined, Q> + where + T: Holds, + C: JoinScope, + C2: ScopeInput, + { Marker { name: inner.name, scope: inner.scope, @@ -242,6 +322,7 @@ impl Marker { name: self.name, scope: self.scope, identity: self.identity, + scoped: C2::COUNT > 0, outer: self.place, }, target: PhantomData, @@ -254,12 +335,19 @@ impl Marker { In: Input, C: ScopeInput, { + let holders = holders(&self.place); + let nested = holders.iter().filter(|holder| holder.scoped).count(); MarkedQuery { label: self.label(), - outers: outers(&self.place), + // One record lists each scope the marker needs. + // `holders` covers the records that are fields. If the marker + // needs one more scope than those records list, the outermost + // record lists that scope. + root_scoped: C::COUNT > nested, + holders, value: Some(value), wipe, - scopes: DeclaredContext::default(), + scopes: Vec::new(), marker: PhantomData, } } @@ -273,29 +361,94 @@ fn label(scope: &'static str, identity: &'static str) -> Label { }) } -/// The labels of the record fields that hold a field at `place`, -/// outermost first: empty for a field of the record itself. -fn outers(place: &impl Place) -> Vec