link_r/auth/
static_token.rs1use crate::auth::{AuthProvider, Credential};
10use crate::error::Result;
11use crate::resource::Resource;
12use compact_str::CompactString;
13use secrecy::{ExposeSecret, SecretString};
14use std::borrow::Cow;
15use std::future::Ready;
16
17#[derive(Clone, Copy, Debug)]
18enum TokenKind {
19 Bearer,
20 Header(&'static str),
21}
22
23#[derive(Debug)]
29pub struct StaticTokenAuth {
30 kind: TokenKind,
31 token: SecretString,
32 scope_host: Option<CompactString>,
34}
35
36impl StaticTokenAuth {
37 #[must_use]
42 pub fn bearer(token: impl Into<String>) -> Self {
43 Self {
44 kind: TokenKind::Bearer,
45 token: secret(token),
46 scope_host: None,
47 }
48 }
49
50 #[must_use]
52 pub fn bearer_scoped(token: impl Into<String>, host: impl Into<CompactString>) -> Self {
53 Self {
54 kind: TokenKind::Bearer,
55 token: secret(token),
56 scope_host: Some(host.into()),
57 }
58 }
59
60 #[must_use]
63 pub fn github_pat(token: impl Into<String>) -> Self {
64 Self::bearer(token)
65 }
66
67 #[must_use]
69 pub fn header(name: &'static str, token: impl Into<String>) -> Self {
70 Self {
71 kind: TokenKind::Header(name),
72 token: secret(token),
73 scope_host: None,
74 }
75 }
76}
77
78fn secret(s: impl Into<String>) -> SecretString {
79 SecretString::new(s.into().into_boxed_str())
80}
81
82fn host_in_scope(host: &str, base: &str) -> bool {
84 host == base
85 || (host.len() > base.len()
86 && host.ends_with(base)
87 && host.as_bytes()[host.len() - base.len() - 1] == b'.')
88}
89
90impl AuthProvider for StaticTokenAuth {
91 type RefreshFuture<'a> = Ready<Result<()>>;
92
93 fn credential(&self, resource: &Resource) -> Credential<'_> {
94 if let Some(scope) = &self.scope_host {
96 let in_scope = resource
97 .url
98 .host_str()
99 .is_some_and(|h| host_in_scope(h, scope));
100 if !in_scope {
101 return Credential::Anonymous;
102 }
103 }
104 let value = Cow::Borrowed(self.token.expose_secret());
105 match self.kind {
106 TokenKind::Bearer => Credential::Bearer(value),
107 TokenKind::Header(name) => Credential::Header { name, value },
108 }
109 }
110
111 fn refresh(&self) -> Self::RefreshFuture<'_> {
112 std::future::ready(Ok(()))
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use url::Url;
120
121 fn resource() -> Resource {
122 Resource::new(Url::parse("https://x.dev/a").unwrap())
123 }
124
125 #[test]
126 fn bearer_exposes_token_only_in_credential() {
127 let auth = StaticTokenAuth::bearer("s3cr3t");
128 match auth.credential(&resource()) {
129 Credential::Bearer(v) => assert_eq!(v, "s3cr3t"),
130 other => panic!("expected bearer, got {other:?}"),
131 }
132 assert!(!format!("{auth:?}").contains("s3cr3t"));
134 }
135
136 #[test]
137 fn custom_header_credential() {
138 let auth = StaticTokenAuth::header("x-api-key", "abc");
139 match auth.credential(&resource()) {
140 Credential::Header { name, value } => {
141 assert_eq!(name, "x-api-key");
142 assert_eq!(value, "abc");
143 }
144 other => panic!("expected header, got {other:?}"),
145 }
146 }
147
148 #[test]
149 fn scoped_token_confined_to_host_and_subdomains() {
150 let auth = StaticTokenAuth::bearer_scoped("s3cr3t", "github.com");
151 let on = Resource::new(Url::parse("https://github.com/o/r").unwrap());
152 let sub = Resource::new(Url::parse("https://api.github.com/x").unwrap());
153 let off = Resource::new(Url::parse("https://evil.dev/x").unwrap());
154 let lookalike = Resource::new(Url::parse("https://notgithub.com/x").unwrap());
155 assert!(matches!(auth.credential(&on), Credential::Bearer(_)));
156 assert!(matches!(auth.credential(&sub), Credential::Bearer(_)));
157 assert!(matches!(auth.credential(&off), Credential::Anonymous));
158 assert!(matches!(auth.credential(&lookalike), Credential::Anonymous));
159 }
160}