pr_01m47d15m3e54sn21z27rpy5n9/crates/sshd/src/api.rs
| 1 | //! Client for the g1t Worker's internal endpoints, which own all |
| 2 | //! authentication and authorization decisions. |
| 3 | |
| 4 | use anyhow::{Context, Result}; |
| 5 | use serde::{Deserialize, Serialize}; |
| 6 | |
| 7 | #[derive(Clone, Debug, Deserialize)] |
| 8 | pub struct User { |
| 9 | pub id: u64, |
| 10 | pub username: String, |
| 11 | } |
| 12 | |
| 13 | /// An Artifacts remote and a short-lived token scoped to one repo. |
| 14 | #[derive(Debug, Deserialize)] |
| 15 | pub struct Access { |
| 16 | pub remote: String, |
| 17 | pub token: String, |
| 18 | } |
| 19 | |
| 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] |
| 21 | pub enum Service { |
| 22 | #[serde(rename = "git-upload-pack")] |
| 23 | UploadPack, |
| 24 | #[serde(rename = "git-receive-pack")] |
| 25 | ReceivePack, |
| 26 | } |
| 27 | |
| 28 | impl Service { |
| 29 | pub fn as_str(self) -> &'static str { |
| 30 | match self { |
| 31 | Service::UploadPack => "git-upload-pack", |
| 32 | Service::ReceivePack => "git-receive-pack", |
| 33 | } |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | #[derive(Deserialize)] |
| 38 | struct ErrorBody { |
| 39 | error: String, |
| 40 | } |
| 41 | |
| 42 | pub struct Api { |
| 43 | base: String, |
| 44 | secret: String, |
| 45 | pub http: reqwest::Client, |
| 46 | } |
| 47 | |
| 48 | impl Api { |
| 49 | pub fn new(base: String, secret: String) -> Self { |
| 50 | Self { |
| 51 | base, |
| 52 | secret, |
| 53 | http: reqwest::Client::new(), |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | /// The user who registered the key with this SHA-256 fingerprint. |
| 58 | pub async fn user_for_key(&self, fingerprint: &str) -> Result<Option<User>> { |
| 59 | let response = self |
| 60 | .http |
| 61 | .post(format!("{}/_internal/ssh/user", self.base)) |
| 62 | .bearer_auth(&self.secret) |
| 63 | .json(&serde_json::json!({ "fingerprint": fingerprint })) |
| 64 | .send() |
| 65 | .await |
| 66 | .context("key lookup failed")?; |
| 67 | if response.status() == reqwest::StatusCode::NOT_FOUND { |
| 68 | return Ok(None); |
| 69 | } |
| 70 | Ok(Some(response.error_for_status()?.json().await?)) |
| 71 | } |
| 72 | |
| 73 | /// `Ok(Err(message))` is a refusal to show the user. |
| 74 | pub async fn access( |
| 75 | &self, |
| 76 | user: &User, |
| 77 | owner: &str, |
| 78 | repo: &str, |
| 79 | service: Service, |
| 80 | ) -> Result<Result<Access, String>> { |
| 81 | let response = self |
| 82 | .http |
| 83 | .post(format!("{}/_internal/ssh/access", self.base)) |
| 84 | .bearer_auth(&self.secret) |
| 85 | .json(&serde_json::json!({ |
| 86 | "user_id": user.id, |
| 87 | "owner": owner, |
| 88 | "repo": repo, |
| 89 | "service": service, |
| 90 | })) |
| 91 | .send() |
| 92 | .await |
| 93 | .context("access check failed")?; |
| 94 | if response.status().is_client_error() { |
| 95 | let body: ErrorBody = response.json().await?; |
| 96 | return Ok(Err(body.error)); |
| 97 | } |
| 98 | Ok(Ok(response.error_for_status()?.json().await?)) |
| 99 | } |
| 100 | } |