@@ -9,6 +9,11 @@ use hyper::{client::HttpConnector, Body, Client};
99use hyper_rustls:: { HttpsConnector , HttpsConnectorBuilder } ;
1010use jsonwebtoken:: jwk:: JwkSet ;
1111use oauth2:: CsrfToken ;
12+ use octorust:: {
13+ auth:: { Credentials , InstallationTokenGenerator , JWTCredentials } ,
14+ http_cache:: NoCache ,
15+ Client as GitHubClient ,
16+ } ;
1217use partial_struct:: partial;
1318use rfd_model:: {
1419 schema_ext:: { ContentFormat , LoginAttemptState , Visibility } ,
@@ -23,8 +28,12 @@ use rfd_model::{
2328 AccessGroup , AccessToken , ApiUser , ApiUserProvider , InvalidValueError , Job , LinkRequest ,
2429 LoginAttempt , Mapper , NewAccessGroup , NewAccessToken , NewApiKey , NewApiUser ,
2530 NewApiUserProvider , NewJob , NewLinkRequest , NewLoginAttempt , NewMapper , NewOAuthClient ,
26- NewOAuthClientRedirectUri , NewOAuthClientSecret , OAuthClient , OAuthClientRedirectUri ,
27- OAuthClientSecret , Rfd ,
31+ NewOAuthClientRedirectUri , NewOAuthClientSecret , NewRfdRevision , OAuthClient ,
32+ OAuthClientRedirectUri , OAuthClientSecret , Rfd , RfdRevision ,
33+ } ;
34+ use rsa:: {
35+ pkcs1:: { DecodeRsaPrivateKey , EncodeRsaPrivateKey } ,
36+ RsaPrivateKey ,
2837} ;
2938use schemars:: JsonSchema ;
3039use serde:: { Deserialize , Serialize } ;
@@ -45,7 +54,7 @@ use crate::{
4554 key:: { RawApiKey , SignedApiKey } ,
4655 AuthError , AuthToken , Signer ,
4756 } ,
48- config:: { AsymmetricKey , JwtConfig , SearchConfig } ,
57+ config:: { AsymmetricKey , GitHubAuthConfig , JwtConfig , SearchConfig , ServicesConfig } ,
4958 endpoints:: login:: {
5059 oauth:: {
5160 ClientType , OAuthProvider , OAuthProviderError , OAuthProviderFn , OAuthProviderName ,
@@ -118,6 +127,7 @@ pub struct ApiContext {
118127 pub secrets : SecretContext ,
119128 pub oauth_providers : HashMap < OAuthProviderName , Box < dyn OAuthProviderFn > > ,
120129 pub search : SearchContext ,
130+ pub github : GitHubClient ,
121131}
122132
123133pub struct JwtContext {
@@ -218,13 +228,28 @@ impl ApiContext {
218228 jwt : JwtConfig ,
219229 keys : Vec < AsymmetricKey > ,
220230 search : SearchConfig ,
231+ services : ServicesConfig ,
221232 ) -> Result < Self , AppError > {
222233 let mut jwt_signers = vec ! [ ] ;
223234
224235 for key in & keys {
225236 jwt_signers. push ( JwtSigner :: new ( & key) . await . unwrap ( ) )
226237 }
227238
239+ let http = reqwest:: Client :: builder ( )
240+ . build ( )
241+ . map_err ( AppError :: ClientConstruction ) ?;
242+ let retry_policy =
243+ reqwest_retry:: policies:: ExponentialBackoff :: builder ( ) . build_with_max_retries ( 3 ) ;
244+ let client = reqwest_middleware:: ClientBuilder :: new ( http)
245+ // Trace HTTP requests. See the tracing crate to make use of these traces.
246+ . with ( reqwest_tracing:: TracingMiddleware :: default ( ) )
247+ // Retry failed requests.
248+ . with ( reqwest_retry:: RetryTransientMiddleware :: new_with_policy (
249+ retry_policy,
250+ ) )
251+ . build ( ) ;
252+
228253 Ok ( Self {
229254 https_client : hyper:: Client :: builder ( ) . build (
230255 HttpsConnectorBuilder :: new ( )
@@ -269,6 +294,33 @@ impl ApiContext {
269294 search : SearchContext {
270295 client : SearchClient :: new ( search. host , search. index , search. key ) ,
271296 } ,
297+ github : match services. github {
298+ GitHubAuthConfig :: Installation {
299+ app_id,
300+ installation_id,
301+ private_key,
302+ } => GitHubClient :: custom (
303+ "rfd-api" ,
304+ Credentials :: InstallationToken ( InstallationTokenGenerator :: new (
305+ installation_id,
306+ JWTCredentials :: new (
307+ app_id,
308+ RsaPrivateKey :: from_pkcs1_pem ( & private_key) ?
309+ . to_pkcs1_der ( ) ?
310+ . to_bytes ( )
311+ . to_vec ( ) ,
312+ ) ?,
313+ ) ) ,
314+ client,
315+ Box :: new ( NoCache ) ,
316+ ) ,
317+ GitHubAuthConfig :: User { token } => GitHubClient :: custom (
318+ "rfd-api" ,
319+ Credentials :: Token ( token. to_string ( ) ) ,
320+ client,
321+ Box :: new ( NoCache ) ,
322+ ) ,
323+ } ,
272324 } )
273325 }
274326
@@ -668,6 +720,67 @@ impl ApiContext {
668720 }
669721 }
670722
723+ #[ instrument( skip( self , caller) ) ]
724+ pub async fn get_rfd_revision (
725+ & self ,
726+ caller : & ApiCaller ,
727+ rfd_number : i32 ,
728+ sha : Option < String > ,
729+ ) -> ResourceResult < RfdRevision , StoreError > {
730+ if caller. any ( & [
731+ & ApiPermission :: GetRfd ( rfd_number) ,
732+ & ApiPermission :: GetRfdsAll ,
733+ ] ) {
734+ let rfds = RfdStore :: list (
735+ & * self . storage ,
736+ RfdFilter :: default ( ) . rfd_number ( Some ( vec ! [ rfd_number] ) ) ,
737+ & ListPagination :: default ( ) . limit ( 1 ) ,
738+ )
739+ . await
740+ . to_resource_result ( ) ?;
741+ if let Some ( rfd) = rfds. into_iter ( ) . nth ( 0 ) {
742+ let latest_revision = RfdRevisionStore :: list (
743+ & * self . storage ,
744+ RfdRevisionFilter :: default ( )
745+ . rfd ( Some ( vec ! [ rfd. id] ) )
746+ . sha ( sha. map ( |sha| vec ! [ sha] ) ) ,
747+ & ListPagination :: default ( ) . limit ( 1 ) ,
748+ )
749+ . await
750+ . to_resource_result ( ) ?;
751+
752+ match latest_revision. into_iter ( ) . nth ( 0 ) {
753+ Some ( revision) => Ok ( revision) ,
754+ None => Err ( ResourceError :: DoesNotExist ) ,
755+ }
756+ } else {
757+ Err ( ResourceError :: DoesNotExist )
758+ }
759+ } else {
760+ resource_restricted ( )
761+ }
762+ }
763+
764+ #[ instrument( skip( self , caller) ) ]
765+ pub async fn update_rfd_content < ' a > (
766+ & self ,
767+ caller : & ApiCaller ,
768+ rfd_number : i32 ,
769+ revision : NewRfdRevision ,
770+ ) -> ResourceResult < RfdRevision , StoreError > {
771+ if caller. any ( & [
772+ & ApiPermission :: UpdateRfd ( rfd_number) ,
773+ & ApiPermission :: UpdateRfdsAll ,
774+ ] ) {
775+ let revision = RfdRevisionStore :: upsert ( & * self . storage , revision)
776+ . await
777+ . to_resource_result ( ) ?;
778+ Ok ( revision)
779+ } else {
780+ resource_restricted ( )
781+ }
782+ }
783+
671784 #[ instrument( skip( self , caller) ) ]
672785 pub async fn update_rfd_visibility (
673786 & self ,
@@ -1885,7 +1998,7 @@ pub(crate) mod test_mocks {
18851998 use w_api_permissions:: Caller ;
18861999
18872000 use crate :: {
1888- config:: { JwtConfig , SearchConfig } ,
2001+ config:: { GitHubAuthConfig , JwtConfig , SearchConfig , ServicesConfig } ,
18892002 endpoints:: login:: oauth:: { google:: GoogleOAuthProvider , OAuthProviderName } ,
18902003 permissions:: ApiPermission ,
18912004 util:: tests:: mock_key,
@@ -1904,6 +2017,11 @@ pub(crate) mod test_mocks {
19042017 mock_key( ) ,
19052018 ] ,
19062019 SearchConfig :: default ( ) ,
2020+ ServicesConfig {
2021+ github : GitHubAuthConfig :: User {
2022+ token : String :: default ( ) ,
2023+ } ,
2024+ } ,
19072025 )
19082026 . await
19092027 . unwrap ( ) ;
0 commit comments