1use crate::auth::{JWT_IDENTIFIER, verify_token};
5use crate::key_management::KeyStore;
6use crate::rpc::{CANCEL_METHOD_NAME, Permission, RpcMethod as _, chain};
7use ahash::{HashMap, HashMapExt as _};
8use futures::future::Either;
9use http::{
10 HeaderMap,
11 header::{AUTHORIZATION, HeaderValue},
12};
13use itertools::Itertools as _;
14use jsonrpsee::MethodResponse;
15use jsonrpsee::core::middleware::{Batch, BatchEntry, BatchEntryErr, Notification};
16use jsonrpsee::server::middleware::rpc::RpcServiceT;
17use jsonrpsee::types::Id;
18use jsonrpsee::types::{ErrorObject, error::ErrorCode};
19use parking_lot::RwLock;
20use std::sync::{Arc, LazyLock};
21use tower::Layer;
22use tracing::debug;
23
24static METHOD_NAME2REQUIRED_PERMISSION: LazyLock<HashMap<&str, Permission>> = LazyLock::new(|| {
25 let mut access = HashMap::new();
26
27 macro_rules! insert {
28 ($ty:ty) => {
29 access.insert(<$ty>::NAME, <$ty>::PERMISSION);
30
31 if let Some(alias) = <$ty>::NAME_ALIAS {
32 access.insert(alias, <$ty>::PERMISSION);
33 }
34 };
35 }
36 super::for_each_rpc_method!(insert);
37
38 access.insert(chain::CHAIN_NOTIFY, Permission::Read);
39 access.insert(CANCEL_METHOD_NAME, Permission::Read);
40
41 access
42});
43
44fn is_allowed(required_by_method: Permission, claimed_by_user: &[String]) -> bool {
45 let needle = match required_by_method {
46 Permission::Admin => "admin",
47 Permission::Sign => "sign",
48 Permission::Write => "write",
49 Permission::Read => "read",
50 };
51 claimed_by_user.iter().any(|haystack| haystack == needle)
52}
53
54#[derive(Clone)]
55pub struct AuthLayer {
56 pub headers: HeaderMap,
57 pub keystore: Arc<RwLock<KeyStore>>,
58}
59
60impl<S> Layer<S> for AuthLayer {
61 type Service = Auth<S>;
62
63 fn layer(&self, service: S) -> Self::Service {
64 Auth {
65 headers: self.headers.clone(),
66 keystore: self.keystore.clone(),
67 service,
68 }
69 }
70}
71
72#[derive(Clone)]
73pub struct Auth<S> {
74 headers: HeaderMap,
75 keystore: Arc<RwLock<KeyStore>>,
76 service: S,
77}
78
79impl<S> Auth<S> {
80 fn authorize<'a>(&self, method_name: &str) -> Result<(), ErrorObject<'a>> {
81 match check_permissions(&self.keystore, self.headers.get(AUTHORIZATION), method_name) {
82 Ok(true) => Ok(()),
83 Ok(false) => Err(ErrorObject::borrowed(
84 http::StatusCode::UNAUTHORIZED.as_u16() as _,
85 "Unauthorized",
86 None,
87 )),
88 Err(code) => Err(ErrorObject::from(code)),
89 }
90 }
91}
92
93impl<S> RpcServiceT for Auth<S>
94where
95 S: RpcServiceT<
96 MethodResponse = MethodResponse,
97 NotificationResponse = MethodResponse,
98 BatchResponse = MethodResponse,
99 > + Send
100 + Sync
101 + Clone
102 + 'static,
103{
104 type MethodResponse = S::MethodResponse;
105 type NotificationResponse = S::NotificationResponse;
106 type BatchResponse = S::BatchResponse;
107
108 fn call<'a>(
109 &self,
110 req: jsonrpsee::types::Request<'a>,
111 ) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
112 match self.authorize(req.method_name()) {
113 Ok(()) => Either::Left(self.service.call(req)),
114 Err(e) => Either::Right(async move { MethodResponse::error(req.id(), e) }),
115 }
116 }
117
118 fn notification<'a>(
119 &self,
120 n: Notification<'a>,
121 ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
122 match self.authorize(n.method_name()) {
123 Ok(()) => Either::Left(self.service.notification(n)),
124 Err(e) => Either::Right(async move { MethodResponse::error(Id::Null, e) }),
125 }
126 }
127
128 fn batch<'a>(&self, batch: Batch<'a>) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
129 let entries = batch
130 .into_iter()
131 .filter_map(|entry| match entry {
132 Ok(BatchEntry::Call(req)) => Some(match self.authorize(req.method_name()) {
133 Ok(()) => Ok(BatchEntry::Call(req)),
134 Err(e) => Err(BatchEntryErr::new(req.id(), e)),
135 }),
136 Ok(BatchEntry::Notification(n)) => match self.authorize(n.method_name()) {
137 Ok(_) => Some(Ok(BatchEntry::Notification(n))),
138 Err(_) => None,
139 },
140 Err(err) => Some(Err(err)),
141 })
142 .collect_vec();
143 self.service.batch(Batch::from(entries))
144 }
145}
146
147fn auth_verify(token: &str, keystore: &RwLock<KeyStore>) -> anyhow::Result<Vec<String>> {
149 let key_info = keystore.read().get(JWT_IDENTIFIER)?;
150 Ok(verify_token(token, key_info.private_key())?)
151}
152
153fn check_permissions(
154 keystore: &RwLock<KeyStore>,
155 auth_header: Option<&HeaderValue>,
156 method: &str,
157) -> Result<bool, ErrorCode> {
158 let claims = match auth_header {
159 Some(token) => {
160 let token = token
161 .to_str()
162 .map_err(|_| ErrorCode::ParseError)?
163 .trim_start_matches("Bearer ");
164
165 debug!("JWT from HTTP Header: {}", token);
166
167 auth_verify(token, keystore).map_err(|_| ErrorCode::InvalidRequest)?
168 }
169 None => vec!["read".to_owned()],
171 };
172 debug!("Decoded JWT Claims: {}", claims.join(","));
173
174 match METHOD_NAME2REQUIRED_PERMISSION.get(&method) {
175 Some(required_by_method) => Ok(is_allowed(*required_by_method, &claims)),
176 None => Err(ErrorCode::MethodNotFound),
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use self::chain::ChainHead;
183 use super::*;
184 use crate::rpc::wallet;
185 use chrono::Duration;
186
187 #[test]
188 fn check_permissions_no_header() {
189 let keystore = Arc::new(RwLock::new(
190 KeyStore::new(crate::KeyStoreConfig::Memory).unwrap(),
191 ));
192
193 let res = check_permissions(&keystore, None, ChainHead::NAME);
194 assert_eq!(res, Ok(true));
195
196 let res = check_permissions(&keystore, None, "Cthulhu.InvokeElderGods");
197 assert_eq!(res.unwrap_err(), ErrorCode::MethodNotFound);
198
199 let res = check_permissions(&keystore, None, wallet::WalletNew::NAME);
200 assert_eq!(res, Ok(false));
201 }
202
203 #[test]
204 fn check_permissions_invalid_header() {
205 let keystore = Arc::new(RwLock::new(
206 KeyStore::new(crate::KeyStoreConfig::Memory).unwrap(),
207 ));
208
209 let auth_header = HeaderValue::from_static("Bearer Azathoth");
210 let res = check_permissions(&keystore, Some(&auth_header), ChainHead::NAME);
211 assert_eq!(res.unwrap_err(), ErrorCode::InvalidRequest);
212
213 let auth_header = HeaderValue::from_static("Cthulhu");
214 let res = check_permissions(&keystore, Some(&auth_header), ChainHead::NAME);
215 assert_eq!(res.unwrap_err(), ErrorCode::InvalidRequest);
216 }
217
218 #[test]
219 fn check_permissions_valid_header() {
220 use crate::auth::*;
221 let keystore = Arc::new(RwLock::new(
222 KeyStore::new(crate::KeyStoreConfig::Memory).unwrap(),
223 ));
224
225 let key_info = generate_priv_key();
227 keystore
228 .write()
229 .put(JWT_IDENTIFIER, key_info.clone())
230 .unwrap();
231 let token_exp = Duration::hours(1);
232 let token = create_token(
233 ADMIN.iter().map(ToString::to_string).collect(),
234 key_info.private_key(),
235 token_exp,
236 )
237 .unwrap();
238
239 let auth_header = HeaderValue::from_str(&format!("Bearer {token}")).unwrap();
241 let res = check_permissions(&keystore, Some(&auth_header), ChainHead::NAME);
242 assert_eq!(res, Ok(true));
243
244 let res = check_permissions(&keystore, Some(&auth_header), wallet::WalletNew::NAME);
245 assert_eq!(res, Ok(true));
246
247 let auth_header = HeaderValue::from_str(&token).unwrap();
249 let res = check_permissions(&keystore, Some(&auth_header), wallet::WalletNew::NAME);
250 assert_eq!(res, Ok(true));
251 }
252}