github_scopes_rs/
oauth.rs

1//! Scopes for OAuth Apps, more details [here](https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps)
2//!
3//!
4use super::transform::{
5    GithubScopeAdminLevel, GithubScopeEnterprise, GithubScopeLevel, GithubScopeRepo,
6    GithubScopeUser, GithubTokenScope,
7};
8use anyhow::anyhow;
9use anyhow::Result as AnyResult;
10use log::debug;
11use reqwest::blocking::Client;
12use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
13use std::time::Duration;
14
15/// maximum wait time until kill the request.
16const MAX_REQUEST_TIME: Duration = Duration::from_secs(1);
17
18/// lists the scopes your token has authorized.
19const HEADER_SCOPE_KEY: &str = "x-oauth-scopes";
20
21/// base github api domain
22const GITHUB_API_DOMAIN: &str = "https://api.github.com";
23
24/// Scope context
25pub struct OAuthContext {
26    client: reqwest::blocking::Client,
27    token: String,
28    domain: String,
29    pub scope: Vec<String>,
30}
31
32impl OAuthContext {
33    /// Create a `OAuthContext`
34    pub fn new(token: &str) -> AnyResult<Self> {
35        OAuthContext::create(token, GITHUB_API_DOMAIN)
36    }
37
38    /// Create a `OAuthContext` with token and domain
39    pub fn with_domain(token: &str, domain: &str) -> AnyResult<Self> {
40        OAuthContext::create(token, domain)
41    }
42
43    fn create(token: &str, domain: &str) -> AnyResult<Self> {
44        let client = Client::builder()
45            .user_agent(env!("CARGO_PKG_NAME"))
46            .timeout(MAX_REQUEST_TIME)
47            .build()
48            .unwrap();
49
50        let mut scope = OAuthContext {
51            client,
52            token: token.to_string(),
53            domain: domain.to_string(),
54            scope: Vec::new(),
55        };
56        scope.refresh(None)?;
57        Ok(scope)
58    }
59
60    /// refresh OAuthContext with new token or when token permission changed in GitHub
61    pub fn refresh(&mut self, token: Option<String>) -> AnyResult<()> {
62        let mut headers = HeaderMap::new();
63
64        if let Some(t) = token {
65            self.token = t;
66            debug!("refresh token request from given token");
67        }
68
69        headers.insert(
70            AUTHORIZATION,
71            self.add_authorization_token(self.token.as_ref()),
72        );
73
74        let response = self
75            .client
76            .get(format!("{}/rate_limit", self.domain))
77            .headers(headers)
78            .send()?;
79
80        debug!(
81            "github rate limit response. statusCode:{}, headers:, {:?}",
82            response.status(),
83            response.headers(),
84        );
85
86        if !response.status().is_success() {
87            debug!("could not get token. StatusCode: {}", response.status());
88            return Err(anyhow!("request failed"));
89        }
90
91        let headers_response = response.headers();
92        self.scope = match headers_response.get(HEADER_SCOPE_KEY) {
93            Some(v) => v
94                .to_str()
95                .unwrap()
96                .split(',')
97                .map(|s| s.trim().to_string())
98                .collect::<Vec<String>>(),
99            None => {
100                return Err(anyhow!(format!(
101                    "could not get {} from headers response",
102                    HEADER_SCOPE_KEY
103                )))
104            }
105        };
106        Ok(())
107    }
108
109    /// return GitHub token scope as struct
110    pub fn get_scope_permissions(&self) -> GithubTokenScope {
111        GithubTokenScope {
112            repo: GithubScopeRepo {
113                all: self.scope.contains(&"repo".to_owned()),
114                status: self.scope.contains(&"repo".to_owned())
115                    || self.scope.contains(&"repo:status".to_owned()),
116                deployment: self.scope.contains(&"repo".to_owned())
117                    || self.scope.contains(&"repo_deployment".to_owned()),
118                public_repo: self.scope.contains(&"repo".to_owned())
119                    || self.scope.contains(&"public_repo".to_owned()),
120                invite: self.scope.contains(&"repo".to_owned())
121                    || self.scope.contains(&"repo:invite".to_owned()),
122                security_events: self.scope.contains(&"repo".to_owned())
123                    || self.scope.contains(&"security_events".to_owned()),
124            },
125            workflow: self.scope.contains(&"workflow".to_owned()),
126            packages: GithubScopeLevel {
127                write: self.scope.contains(&"write:packages".to_owned()),
128                read: self.scope.contains(&"write:packages".to_owned())
129                    || self.scope.contains(&"read:packages".to_owned()),
130            },
131            delete_packages: self.scope.contains(&"delete:packages".to_owned()),
132            org: GithubScopeAdminLevel {
133                admin: self.scope.contains(&"admin:org".to_owned()),
134                write: self.scope.contains(&"admin:org".to_owned())
135                    || self.scope.contains(&"write:org".to_owned()),
136                read: self.scope.contains(&"admin:org".to_owned())
137                    || self.scope.contains(&"write:org".to_owned())
138                    || self.scope.contains(&"read:org".to_owned()),
139            },
140            public_key: GithubScopeAdminLevel {
141                admin: self.scope.contains(&"admin:public_key".to_owned()),
142                write: self.scope.contains(&"admin:public_key".to_owned())
143                    || self.scope.contains(&"write:public_key".to_owned()),
144                read: self.scope.contains(&"admin:public_key".to_owned())
145                    || self.scope.contains(&"write:public_key".to_owned())
146                    || self.scope.contains(&"read:public_key".to_owned()),
147            },
148            repo_hook: GithubScopeAdminLevel {
149                admin: self.scope.contains(&"admin:repo_hook".to_owned()),
150                write: self.scope.contains(&"admin:repo_hook".to_owned())
151                    || self.scope.contains(&"write:repo_hook".to_owned()),
152                read: self.scope.contains(&"admin:repo_hook".to_owned())
153                    || self.scope.contains(&"write:repo_hook".to_owned())
154                    || self.scope.contains(&"read:repo_hook".to_owned()),
155            },
156            org_hook: self.scope.contains(&"admin:org_hook".to_owned()),
157            gist: self.scope.contains(&"gist".to_owned()),
158            notifications: self.scope.contains(&"notifications".to_owned()),
159            user: GithubScopeUser {
160                all: self.scope.contains(&"user".to_owned()),
161                read: self.scope.contains(&"user".to_owned())
162                    || self.scope.contains(&"read:user".to_owned()),
163                email: self.scope.contains(&"user".to_owned())
164                    || self.scope.contains(&"user:email".to_owned()),
165                follow: self.scope.contains(&"user".to_owned())
166                    || self.scope.contains(&"user:follow".to_owned()),
167            },
168            delete_repo: self.scope.contains(&"delete_repo".to_owned()),
169            discussion: GithubScopeLevel {
170                write: self.scope.contains(&"write:discussion".to_owned()),
171                read: self.scope.contains(&"write:discussion".to_owned())
172                    || self.scope.contains(&"read:discussion".to_owned()),
173            },
174            enterprise: GithubScopeEnterprise {
175                all: self.scope.contains(&"admin:enterprise".to_owned()),
176                manage_runners: self.scope.contains(&"admin:enterprise".to_owned())
177                    || self.scope.contains(&"manage_runners:enterprise".to_owned()),
178                manage_billing: self.scope.contains(&"admin:enterprise".to_owned())
179                    || self.scope.contains(&"manage_billing:enterprise".to_owned()),
180                read: self.scope.contains(&"admin:enterprise".to_owned())
181                    || self.scope.contains(&"manage_billing:enterprise".to_owned())
182                    || self.scope.contains(&"read:enterprise".to_owned()),
183            },
184            gpg_key: GithubScopeAdminLevel {
185                admin: self.scope.contains(&"admin:gpg_key".to_owned())
186                    || self.scope.contains(&"manage_runners:enterprise".to_owned()),
187                write: self.scope.contains(&"admin:gpg_key".to_owned())
188                    || self.scope.contains(&"write:gpg_key".to_owned()),
189                read: self.scope.contains(&"admin:gpg_key".to_owned())
190                    || self.scope.contains(&"write:gpg_key".to_owned())
191                    || self.scope.contains(&"read:gpg_key".to_owned()),
192            },
193            ssh_signing_key: GithubScopeAdminLevel {
194                admin: self.scope.contains(&"admin:ssh_signing_key".to_owned()),
195                write: self.scope.contains(&"admin:ssh_signing_key".to_owned())
196                    || self.scope.contains(&"write:ssh_signing_key".to_owned()),
197                read: self.scope.contains(&"admin:ssh_signing_key".to_owned())
198                    || self.scope.contains(&"write:ssh_signing_key".to_owned())
199                    || self.scope.contains(&"read:ssh_signing_key".to_owned()),
200            },
201        }
202    }
203
204    fn add_authorization_token(&self, token: &str) -> HeaderValue {
205        HeaderValue::from_str(format!("token {}", token).as_ref()).unwrap()
206    }
207}