Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Merge pull request #354 from flagon-io/344-identity-session-batch

A signed-in page waits about half as long on who the session is

syntaqxcommitted Parents18131aff428976Browse files
6 files+92−670/6 viewed
+0−5
1515
1616 # Generated by scripts/deploy.mjs for a runner deploy (the image by reference)
1717 wrangler.deploy.json
18−
19−# Personal notes for the next session
20−/SCRUTINY.md
21−/TODO.md
22−/.prompts/
+28−23
33 //!
44 //! The rules (which role may do what, and how a person's role is worked
55 //! out) live in `g1t_contracts::access`; this is where the roles are kept.
6−//! Every user identity resolves carries their grants ([`Identity::grants_of`],
6+//! Every user identity resolves carries their grants ([`Identity::grants_statement`],
77 //! under the workspace's policy) beside their memberships, so services
88 //! decide with `access::can` and never call here to authorize.
99 //!
2525 //! rename keeps them (deletion.rs, `transfer_repo_scopes`).
2626 //!
2727 //! **Teams** are another `principal_kind` in `repo_grants` (teams.rs):
28−//! [`Identity::grants_of`] resolves a team's grants into the same
28+//! [`Identity::grants_statement`] resolves a team's grants into the same
2929 //! `RepoGrant`s for each person in the team and in its child teams, and
3030 //! the access list shows where such a role comes from.
3131
111111 }
112112 }
113113
114+/// The grants, each with whether its workspace requires two-factor
115+/// authentication, from what `grants_statement` read.
116+pub(crate) fn grants_from(result: &worker::D1Result) -> Result<Vec<(RepoGrant, bool)>> {
117+ Ok(result
118+ .results::<GrantRow>()?
119+ .into_iter()
120+ .filter_map(|row| {
121+ Some((
122+ RepoGrant {
123+ repo_id: row.repo_id,
124+ workspace: row.workspace,
125+ role: RepoRole::parse(&row.role)?,
126+ team: row.team,
127+ },
128+ row.require_two_factor != 0,
129+ ))
130+ })
131+ .collect())
132+}
133+
114134 #[derive(Deserialize)]
115135 struct GrantRow {
116136 repo_id: String,
257277 ///
258278 /// Each comes with whether its workspace requires two-factor
259279 /// authentication (security.rs).
260− pub async fn grants_of(&self, user_id: &str) -> Result<Vec<(RepoGrant, bool)>> {
261− let rows = self
262− .db
280+ ///
281+ /// The statement, to batch with the other reads of an account (lib.rs
282+ /// `with_workspaces_via`); `grants_from` reads its result.
283+ pub(crate) fn grants_statement(&self, user_id: &str) -> Result<worker::D1PreparedStatement> {
284+ self.db
263285 .prepare(format!(
264286 "WITH RECURSIVE mine(id) AS (
265287 SELECT team_id FROM team_members WHERE user_id = ?1
276298 JOIN workspaces w ON w.id = g.workspace_id AND w.deleted_at IS NULL
277299 LIMIT {MAX_GRANTS}"
278300 ))
279− .bind(&[user_id.into()])?
280− .all()
281− .await?
282− .results::<GrantRow>()?;
283− Ok(rows
284− .into_iter()
285− .filter_map(|row| {
286− Some((
287− RepoGrant {
288− repo_id: row.repo_id,
289− workspace: row.workspace,
290− role: RepoRole::parse(&row.role)?,
291− team: row.team,
292− },
293− row.require_two_factor != 0,
294− ))
295− })
296− .collect())
301+ .bind(&[user_id.into()])
297302 }
298303
299304 /// The repository at `path` as `viewer` sees it: missing when they
+16−3
159159 let Some(mut user) = user else {
160160 return Ok(None);
161161 };
162+ // Read together, in one trip to the database: every signed-in page
163+ // waits on this before its loaders start.
164+ let read = self
165+ .db
166+ .batch(vec![
167+ self.user_suspension_statement(&user.id)?,
168+ self.memberships_statement(&user.id)?,
169+ self.grants_statement(&user.id)?,
170+ ])
171+ .await?;
172+ let [suspension, memberships, grants] = read.as_slice() else {
173+ return Err(worker::Error::RustError("the account's batch came back short".into()));
174+ };
162175 // A suspended account is nobody, however it is presented
163176 // (suspension.rs): its sessions, tokens, keys and password.
164− if self.user_suspension(&user.id).await?.is_some() {
177+ if suspension::user_suspension_from(suspension)?.is_some() {
165178 return Ok(None);
166179 }
167− let policed = self.memberships_and_policies(&user.id).await?;
180+ let policed = workspaces::policed_from(memberships)?;
168181 let sso_required: Vec<(String, g1t_contracts::Role)> =
169182 policed.iter().filter(|m| m.require_sso).map(|m| (m.membership.slug.clone(), m.membership.role)).collect();
170183 let memberships = policed.into_iter().map(|m| (m.membership, m.require_two_factor)).collect();
171184 // Roles on single repositories, under the same policy (access.rs).
172− let grants = self.grants_of(&user.id).await?;
185+ let grants = access::grants_from(grants)?;
173186 // Access to a workspace is used only within its policy; see security.rs.
174187 let within = self.within_policy(&user.id, memberships, grants).await?;
175188 user.workspaces = within.memberships;
+12−6
6969 }
7070 }
7171
72+/// The account's suspension, from what `user_suspension_statement` read.
73+pub(crate) fn user_suspension_from(result: &worker::D1Result) -> Result<Option<Suspension>> {
74+ Ok(result.results::<SuspensionRow>()?.into_iter().next().and_then(SuspensionRow::shown))
75+}
76+
7277 #[derive(Deserialize)]
7378 struct DomainRow {
7479 domain: String,
8085 impl Identity {
8186 /// The account's suspension, if it has one.
8287 pub(crate) async fn user_suspension(&self, user_id: &str) -> Result<Option<Suspension>> {
83− Ok(self
84− .db
88+ user_suspension_from(&self.user_suspension_statement(user_id)?.all().await?)
89+ }
90+
91+ /// What `user_suspension` reads, to batch with other reads.
92+ pub(crate) fn user_suspension_statement(&self, user_id: &str) -> Result<worker::D1PreparedStatement> {
93+ self.db
8594 .prepare("SELECT suspended_at, suspended_reason, suspended_by FROM users WHERE id = ?")
86− .bind(&[user_id.into()])?
87− .first::<SuspensionRow>(None)
88− .await?
89− .and_then(SuspensionRow::shown))
95+ .bind(&[user_id.into()])
9096 }
9197
9298 /// The workspace's suspension, if it has one, by slug.
+1−1
33 //! Kept beside the workspaces they belong to: `teams` and `team_members`,
44 //! and a team's roles on repositories as rows of `repo_grants` whose
55 //! principal is the team. Nothing here decides access on a request:
6−//! [`Identity::grants_of`] (access.rs) folds a person's teams' roles into
6+//! [`Identity::grants_statement`] (access.rs) folds a person's teams' roles into
77 //! the grants every resolved user carries.
88 //!
99 //! **Who may do what.**
+35−29
111111 pub require_sso: bool,
112112 }
113113
114+/// The memberships, from what `memberships_statement` read.
115+pub(crate) fn policed_from(result: &worker::D1Result) -> Result<Vec<Policed>> {
116+ Ok(result
117+ .results::<MembershipRow>()?
118+ .into_iter()
119+ .map(|row| {
120+ let mut org_roles = Vec::new();
121+ if row.billing_manager != 0 {
122+ org_roles.push(OrgRole::BillingManager);
123+ }
124+ if row.security_manager != 0 {
125+ org_roles.push(OrgRole::SecurityManager);
126+ }
127+ let membership = Membership {
128+ slug: row.slug,
129+ role: row.role,
130+ name: row.name,
131+ avatar: row.avatar,
132+ base_permission: row.base_permission.as_deref().and_then(BasePermission::parse),
133+ team_creation: row.team_creation.as_deref().and_then(TeamCreation::parse),
134+ org_roles,
135+ privileges: Some(MemberPrivileges::from_stored(row.member_privileges.as_deref())),
136+ };
137+ Policed { membership, require_two_factor: row.require_two_factor != 0, require_sso: row.require_sso != 0 }
138+ })
139+ .collect())
140+}
141+
114142 impl Identity {
115143 /// The workspaces a user belongs to, attached to every user resolved
116144 /// from credentials, with what the site needs to show each one, what
123151 /// The same, each with what its workspace requires: two-factor
124152 /// authentication (security.rs), single sign-on (sso.rs).
125153 pub async fn memberships_and_policies(&self, user_id: &str) -> Result<Vec<Policed>> {
126− let rows = self
127− .db
154+ policed_from(&self.memberships_statement(user_id)?.all().await?)
155+ }
156+
157+ /// What `memberships_and_policies` reads, to batch with other reads.
158+ pub(crate) fn memberships_statement(&self, user_id: &str) -> Result<worker::D1PreparedStatement> {
159+ self.db
128160 .prepare(
129161 "SELECT workspaces.slug, workspace_members.role, workspaces.name,
130162 workspaces.avatar, workspaces.base_permission, workspaces.team_creation,
135167 WHERE workspace_members.user_id = ? AND workspaces.deleted_at IS NULL
136168 ORDER BY workspaces.slug",
137169 )
138− .bind(&[user_id.into()])?
139− .all()
140− .await?
141− .results::<MembershipRow>()?;
142− Ok(rows
143− .into_iter()
144− .map(|row| {
145− let mut org_roles = Vec::new();
146− if row.billing_manager != 0 {
147− org_roles.push(OrgRole::BillingManager);
148− }
149− if row.security_manager != 0 {
150− org_roles.push(OrgRole::SecurityManager);
151− }
152− let membership = Membership {
153− slug: row.slug,
154− role: row.role,
155− name: row.name,
156− avatar: row.avatar,
157− base_permission: row.base_permission.as_deref().and_then(BasePermission::parse),
158− team_creation: row.team_creation.as_deref().and_then(TeamCreation::parse),
159− org_roles,
160− privileges: Some(MemberPrivileges::from_stored(row.member_privileges.as_deref())),
161− };
162− Policed { membership, require_two_factor: row.require_two_factor != 0, require_sso: row.require_sso != 0 }
163− })
164− .collect())
170+ .bind(&[user_id.into()])
165171 }
166172
167173 pub async fn create_workspace(&self, a: CreateWorkspaceArgs) -> Result<Outcome<Workspace>> {

Comments

Sign in to comment.