Skip to main content

comindly_engine/
backends.rs

1//! What a server stores and how it signs people in, as traits; `server::Store` and [`Smtp`] are the
2//! defaults.
3
4use anyhow::{Context, Result};
5use futures::future::BoxFuture;
6
7use crate::server::{
8    AccessToken, AuditPage, AuditQuery, DevicePoll, DeviceRequest, Invite, Member, Role,
9    SecretInfo, Via, Who,
10};
11
12/// Members, invitations, sign-in links and codes, sessions and personal access tokens.
13pub trait AccountStore: Send + Sync {
14    /// Drop what has expired (links, sessions, tokens); called now and then by a running server.
15    fn tidy(&self) {}
16    /// The member `email`; deployment admins are always present and admin.
17    fn member(&self, email: &str) -> Result<Option<Who>>;
18    fn members(&self) -> Result<Vec<Member>>;
19    fn set_role(&self, by: &str, email: &str, role: Role) -> Result<()>;
20    /// Remove a member with their sessions, tokens and invitations.
21    fn remove(&self, by: &str, email: &str) -> Result<()>;
22    /// Invite `email`; returns the link token, or with `code` the invitation code.
23    fn invite(&self, by: &str, email: &str, role: Role, code: bool) -> Result<String>;
24    fn invites(&self) -> Result<Vec<Invite>>;
25    fn revoke_invite(&self, by: &str, email: &str) -> Result<()>;
26    /// A sign-in link token for a member; `None` for anyone else.
27    fn sign_in_link(&self, email: &str) -> Result<Option<String>>;
28    /// Exchange a link, an invitation or the admin token for a session.
29    fn redeem(&self, token: &str, client: &str) -> Result<(String, Who)>;
30    /// Sign in with an email and a code; an invitation code also returns the person's own code.
31    fn sign_in_code(
32        &self,
33        email: &str,
34        code: &str,
35        client: &str,
36    ) -> Result<(String, Who, Option<String>)>;
37    /// Sign in someone an identity provider vouched for: a member, someone invited, or with
38    /// `join` a newcomer as a viewer. `how` names the provider in the audit log.
39    fn sign_in_verified(&self, email: &str, how: &str, join: bool) -> Result<(String, Who)>;
40    /// The same check without a session: a member, or someone invited (or with `join` a
41    /// newcomer) who joins now. For callers an identity provider vouched for, such as the gateway.
42    fn admit(&self, email: &str, join: bool) -> Result<Who>;
43    /// A new sign-in code for `email`; their sessions end, except `keep`.
44    fn reset_code(&self, by: &str, email: &str, keep: Option<&str>) -> Result<String>;
45    fn sign_out(&self, session: &str, everywhere: bool) -> Result<()>;
46    /// A token that stops working after `expires_in_days`, or never with `None`.
47    fn create_token(&self, who: &Who, name: &str, expires_in_days: Option<u32>) -> Result<String>;
48    fn tokens(&self, who: &Who) -> Result<Vec<AccessToken>>;
49    fn revoke_token(&self, who: &Who, id: &str) -> Result<()>;
50    /// Revoke the personal access token whose value is `token`; `false` when none matched.
51    fn revoke_own_token(&self, token: &str) -> Result<bool>;
52}
53
54/// Who changed what in accounts, secrets and settings, for admins to read.
55pub trait AuditLog: Send + Sync {
56    /// Entries newest first, from before `query.before` and matching its filters.
57    fn audit_entries(&self, query: &AuditQuery) -> Result<AuditPage>;
58    /// Record a change made outside the account store, such as to the prompts.
59    fn record(&self, actor: &str, action: &str, detail: &str);
60}
61
62/// Command-line sign-in approved in the browser: the command starts it and polls, a signed-in
63/// member approves it, and the command receives a personal access token.
64pub trait DeviceSignIn: Send + Sync {
65    /// A device code for the command and a short code for the member to confirm.
66    fn device_start(&self, client: &str, from: &str) -> Result<(String, String)>;
67    fn device_info(&self, user_code: &str) -> Option<DeviceRequest>;
68    fn device_approve(&self, who: &Who, user_code: &str, approve: bool) -> Result<()>;
69    fn device_poll(&self, device_code: &str) -> Result<DevicePoll>;
70}
71
72/// Who a request comes from: a session cookie or a bearer token, or nobody.
73pub trait Authenticator: Send + Sync {
74    fn authenticate(
75        &self,
76        session: Option<&str>,
77        bearer: Option<&str>,
78    ) -> Result<Option<(Who, Via)>>;
79}
80
81/// Git and LLM tokens, by name.
82pub trait SecretStore: Send + Sync {
83    /// Store `value` under `name`; an empty value removes it.
84    fn set_secret(&self, by: &str, name: &str, value: &str) -> Result<()>;
85    fn secret(&self, name: &str) -> Option<String>;
86    fn secret_info(&self, name: &str) -> Option<SecretInfo>;
87    fn secret_names(&self) -> Vec<String>;
88}
89
90/// The server's own settings: the repositories it indexes.
91pub trait SettingsStore: Send + Sync {
92    fn repos(&self) -> Vec<crate::config::RepoEntry>;
93    fn set_repos(&self, by: &str, repos: &[crate::config::RepoEntry]) -> Result<()>;
94    /// The `config.toml` an older server kept, until it has been moved over.
95    fn legacy_config(&self) -> Option<String>;
96    fn drop_legacy_config(&self) -> Result<()>;
97    /// The kinds of log lines an admin switched off, comma-separated; `None` until one did.
98    fn log_skip(&self) -> Option<String>;
99    fn set_log_skip(&self, by: &str, skip: &str) -> Result<()>;
100}
101
102/// Sends sign-in links and invitations.
103pub trait Mailer: Send + Sync {
104    fn send<'a>(
105        &'a self,
106        to: &'a str,
107        subject: &'a str,
108        body: &'a str,
109    ) -> BoxFuture<'a, Result<()>>;
110}
111
112/// Mail through `COMINDLY_SMTP_URL` (e.g. `smtps://user:pass@host`).
113pub struct Smtp {
114    pub url: Option<String>,
115    pub from: String,
116}
117
118impl Mailer for Smtp {
119    fn send<'a>(
120        &'a self,
121        to: &'a str,
122        subject: &'a str,
123        body: &'a str,
124    ) -> BoxFuture<'a, Result<()>> {
125        Box::pin(async move {
126            use lettre::message::header::ContentType;
127            use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
128            let url = self.url.as_deref().context("no mail server configured")?;
129            let msg = Message::builder()
130                .from(
131                    self.from
132                        .parse()
133                        .context("COMINDLY_SMTP_FROM is not an address")?,
134                )
135                .to(to.parse().context("not an email address")?)
136                .subject(subject)
137                .header(ContentType::TEXT_PLAIN)
138                .body(body.to_string())?;
139            AsyncSmtpTransport::<Tokio1Executor>::from_url(url)
140                .context("COMINDLY_SMTP_URL is not a valid smtp:// or smtps:// URL")?
141                .build()
142                .send(msg)
143                .await
144                .map_err(|e| anyhow::anyhow!("sending mail failed: {e}"))?;
145            Ok(())
146        })
147    }
148}