Skip to content
131 linesCodeBlameRaw
1//! Client for the g1t Worker's internal endpoints, which own all
2//! authentication and authorization decisions.
3//!
4//! Neither endpoint exists yet (docs/ARTIFACTS.md). What they are to do:
5//!
6//! - `POST /_internal/ssh/user` with `{ fingerprint, used }` resolves the key
7//! through identity's `principal_for_ssh_key` (`used` once the client has
8//! proved it holds the private key, so only then is its last use
9//! recorded) and answers [`User`], or 404 for an unknown key.
10//! - `POST /_internal/ssh/access` with `{ fingerprint, owner, repo, service }`
11//! resolves the key again, so a key deleted mid-session stops working,
12//! and asks repos' `git_access` with that principal: a deploy key reaches
13//! its one repository, and pushes only when it was given write access.
14
15use anyhow::{Context, Result};
16use serde::{Deserialize, Serialize};
17
18/// Who a key signs in as.
19#[derive(Clone, Debug, Deserialize)]
20pub struct User {
21 /// `usr_…`, or for a deploy key its repository's workspace (`wsp_…`).
22 pub id: String,
23 pub username: String,
24 /// Set for a deploy key: the one repository it reaches, `owner/name`.
25 #[serde(default)]
26 pub repo: Option<String>,
27 /// The key's fingerprint, as it was looked up with.
28 #[serde(skip)]
29 pub fingerprint: String,
30}
31
32impl User {
33 /// The name a greeting uses: the person, or a deploy key's repository.
34 pub fn greeting_name(&self) -> &str {
35 self.repo.as_deref().unwrap_or(&self.username)
36 }
37}
38
39/// An Artifacts remote and a short-lived token scoped to one repo.
40#[derive(Debug, Deserialize)]
41pub struct Access {
42 pub remote: String,
43 pub token: String,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
47pub enum Service {
48 #[serde(rename = "git-upload-pack")]
49 UploadPack,
50 #[serde(rename = "git-receive-pack")]
51 ReceivePack,
52}
53
54impl Service {
55 pub fn as_str(self) -> &'static str {
56 match self {
57 Service::UploadPack => "git-upload-pack",
58 Service::ReceivePack => "git-receive-pack",
59 }
60 }
61}
62
63#[derive(Deserialize)]
64struct ErrorBody {
65 error: String,
66}
67
68pub struct Api {
69 base: String,
70 secret: String,
71 pub http: reqwest::Client,
72}
73
74impl Api {
75 pub fn new(base: String, secret: String) -> Self {
76 Self {
77 base,
78 secret,
79 http: reqwest::Client::new(),
80 }
81 }
82
83 /// Who the key with this SHA-256 fingerprint signs in as: the person
84 /// who registered it, or a repository's deploy key. `used` once the
85 /// client has proved it holds the private key.
86 pub async fn user_for_key(&self, fingerprint: &str, used: bool) -> Result<Option<User>> {
87 let response = self
88 .http
89 .post(format!("{}/_internal/ssh/user", self.base))
90 .bearer_auth(&self.secret)
91 .json(&serde_json::json!({ "fingerprint": fingerprint, "used": used }))
92 .send()
93 .await
94 .context("key lookup failed")?;
95 if response.status() == reqwest::StatusCode::NOT_FOUND {
96 return Ok(None);
97 }
98 let mut user: User = response.error_for_status()?.json().await?;
99 user.fingerprint = fingerprint.to_owned();
100 Ok(Some(user))
101 }
102
103 /// `Ok(Err(message))` is a refusal to show the user.
104 pub async fn access(
105 &self,
106 user: &User,
107 owner: &str,
108 repo: &str,
109 service: Service,
110 ) -> Result<Result<Access, String>> {
111 let response = self
112 .http
113 .post(format!("{}/_internal/ssh/access", self.base))
114 .bearer_auth(&self.secret)
115 .json(&serde_json::json!({
116 "user_id": user.id,
117 "fingerprint": user.fingerprint,
118 "owner": owner,
119 "repo": repo,
120 "service": service,
121 }))
122 .send()
123 .await
124 .context("access check failed")?;
125 if response.status().is_client_error() {
126 let body: ErrorBody = response.json().await?;
127 return Ok(Err(body.error));
128 }
129 Ok(Ok(response.error_for_status()?.json().await?))
130 }
131}