tangled
alpha
login
or
join now
tranquil.farm
/
tranquil-pds
149
fork
atom
Our Personal Data Server from scratch!
tranquil.farm
oauth
atproto
pds
rust
postgresql
objectstorage
fun
149
fork
atom
overview
issues
19
pulls
2
pipelines
fix: match ref pds permission-levels for some endpoints
lewis.moe
1 month ago
2317df45
cc4769d4
+216
-110
14 changed files
expand all
collapse all
unified
split
crates
tranquil-pds
src
api
actor
preferences.rs
error.rs
identity
plc
request.rs
sign.rs
submit.rs
server
account_status.rs
app_password.rs
email.rs
invite.rs
session.rs
auth
extractor.rs
mod.rs
tests
actor.rs
auth_extractor.rs
+3
-3
crates/tranquil-pds/src/api/actor/preferences.rs
···
1
1
use crate::api::error::ApiError;
2
2
-
use crate::auth::{Active, Auth};
2
2
+
use crate::auth::{Auth, NotTakendown, Permissive};
3
3
use crate::state::AppState;
4
4
use axum::{
5
5
Json,
···
32
32
pub struct GetPreferencesOutput {
33
33
pub preferences: Vec<Value>,
34
34
}
35
35
-
pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Active>) -> Response {
35
35
+
pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Permissive>) -> Response {
36
36
let has_full_access = auth.permissions().has_full_access();
37
37
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
38
38
Ok(Some(id)) => id,
···
89
89
}
90
90
pub async fn put_preferences(
91
91
State(state): State<AppState>,
92
92
-
auth: Auth<Active>,
92
92
+
auth: Auth<NotTakendown>,
93
93
Json(input): Json<PutPreferencesInput>,
94
94
) -> Response {
95
95
let has_full_access = auth.permissions().has_full_access();
-1
crates/tranquil-pds/src/api/error.rs
···
546
546
crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed(
547
547
Some("Service authentication not allowed for this endpoint".to_string()),
548
548
),
549
549
-
crate::auth::extractor::AuthError::SigningKeyRequired => Self::InvalidSigningKey,
550
549
crate::auth::extractor::AuthError::InsufficientScope(msg) => {
551
550
Self::InsufficientScope(Some(msg))
552
551
}
+2
-2
crates/tranquil-pds/src/api/identity/plc/request.rs
···
1
1
use crate::api::EmptyResponse;
2
2
use crate::api::error::ApiError;
3
3
-
use crate::auth::{Auth, NotTakendown};
3
3
+
use crate::auth::{Auth, Permissive};
4
4
use crate::state::AppState;
5
5
use axum::{
6
6
extract::State,
···
15
15
16
16
pub async fn request_plc_operation_signature(
17
17
State(state): State<AppState>,
18
18
-
auth: Auth<NotTakendown>,
18
18
+
auth: Auth<Permissive>,
19
19
) -> Result<Response, ApiError> {
20
20
if let Err(e) = crate::auth::scope_check::check_identity_scope(
21
21
auth.is_oauth(),
+2
-2
crates/tranquil-pds/src/api/identity/plc/sign.rs
···
1
1
use crate::api::ApiError;
2
2
-
use crate::auth::{Auth, NotTakendown};
2
2
+
use crate::auth::{Auth, Permissive};
3
3
use crate::circuit_breaker::with_circuit_breaker;
4
4
use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation};
5
5
use crate::state::AppState;
···
40
40
41
41
pub async fn sign_plc_operation(
42
42
State(state): State<AppState>,
43
43
-
auth: Auth<NotTakendown>,
43
43
+
auth: Auth<Permissive>,
44
44
Json(input): Json<SignPlcOperationInput>,
45
45
) -> Result<Response, ApiError> {
46
46
if let Err(e) = crate::auth::scope_check::check_identity_scope(
+2
-2
crates/tranquil-pds/src/api/identity/plc/submit.rs
···
1
1
use crate::api::{ApiError, EmptyResponse};
2
2
-
use crate::auth::{Auth, NotTakendown};
2
2
+
use crate::auth::{Auth, Permissive};
3
3
use crate::circuit_breaker::with_circuit_breaker;
4
4
use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation};
5
5
use crate::state::AppState;
···
20
20
21
21
pub async fn submit_plc_operation(
22
22
State(state): State<AppState>,
23
23
-
auth: Auth<NotTakendown>,
23
23
+
auth: Auth<Permissive>,
24
24
Json(input): Json<SubmitPlcOperationInput>,
25
25
) -> Result<Response, ApiError> {
26
26
if let Err(e) = crate::auth::scope_check::check_identity_scope(
+4
-4
crates/tranquil-pds/src/api/server/account_status.rs
···
1
1
use crate::api::EmptyResponse;
2
2
use crate::api::error::ApiError;
3
3
-
use crate::auth::{Active, Auth, NotTakendown};
3
3
+
use crate::auth::{Auth, NotTakendown, Permissive};
4
4
use crate::cache::Cache;
5
5
use crate::plc::PlcClient;
6
6
use crate::state::AppState;
···
41
41
42
42
pub async fn check_account_status(
43
43
State(state): State<AppState>,
44
44
-
auth: Auth<NotTakendown>,
44
44
+
auth: Auth<Permissive>,
45
45
) -> Result<Response, ApiError> {
46
46
let did = &auth.did;
47
47
let user_id = state
···
306
306
307
307
pub async fn activate_account(
308
308
State(state): State<AppState>,
309
309
-
auth: Auth<NotTakendown>,
309
309
+
auth: Auth<Permissive>,
310
310
) -> Result<Response, ApiError> {
311
311
info!("[MIGRATION] activateAccount called");
312
312
info!(
···
470
470
471
471
pub async fn deactivate_account(
472
472
State(state): State<AppState>,
473
473
-
auth: Auth<Active>,
473
473
+
auth: Auth<Permissive>,
474
474
Json(input): Json<DeactivateAccountInput>,
475
475
) -> Result<Response, ApiError> {
476
476
if let Err(e) = crate::auth::scope_check::check_account_scope(
+4
-4
crates/tranquil-pds/src/api/server/app_password.rs
···
1
1
use crate::api::EmptyResponse;
2
2
use crate::api::error::ApiError;
3
3
-
use crate::auth::{Active, Auth, generate_app_password};
3
3
+
use crate::auth::{Auth, NotTakendown, Permissive, generate_app_password};
4
4
use crate::delegation::{DelegationActionType, intersect_scopes};
5
5
use crate::state::{AppState, RateLimitKind};
6
6
use axum::{
···
33
33
34
34
pub async fn list_app_passwords(
35
35
State(state): State<AppState>,
36
36
-
auth: Auth<Active>,
36
36
+
auth: Auth<Permissive>,
37
37
) -> Result<Response, ApiError> {
38
38
let user = state
39
39
.user_repo
···
90
90
pub async fn create_app_password(
91
91
State(state): State<AppState>,
92
92
headers: HeaderMap,
93
93
-
auth: Auth<Active>,
93
93
+
auth: Auth<NotTakendown>,
94
94
Json(input): Json<CreateAppPasswordInput>,
95
95
) -> Result<Response, ApiError> {
96
96
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
···
227
227
228
228
pub async fn revoke_app_password(
229
229
State(state): State<AppState>,
230
230
-
auth: Auth<Active>,
230
230
+
auth: Auth<Permissive>,
231
231
Json(input): Json<RevokeAppPasswordInput>,
232
232
) -> Result<Response, ApiError> {
233
233
let user = state
+5
-5
crates/tranquil-pds/src/api/server/email.rs
···
1
1
use crate::api::error::ApiError;
2
2
use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
3
3
-
use crate::auth::{Active, Auth};
3
3
+
use crate::auth::{Auth, NotTakendown};
4
4
use crate::state::{AppState, RateLimitKind};
5
5
use axum::{
6
6
Json,
···
45
45
pub async fn request_email_update(
46
46
State(state): State<AppState>,
47
47
headers: axum::http::HeaderMap,
48
48
-
auth: Auth<Active>,
48
48
+
auth: Auth<NotTakendown>,
49
49
input: Option<Json<RequestEmailUpdateInput>>,
50
50
) -> Result<Response, ApiError> {
51
51
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
···
140
140
pub async fn confirm_email(
141
141
State(state): State<AppState>,
142
142
headers: axum::http::HeaderMap,
143
143
-
auth: Auth<Active>,
143
143
+
auth: Auth<NotTakendown>,
144
144
Json(input): Json<ConfirmEmailInput>,
145
145
) -> Result<Response, ApiError> {
146
146
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
···
233
233
234
234
pub async fn update_email(
235
235
State(state): State<AppState>,
236
236
-
auth: Auth<Active>,
236
236
+
auth: Auth<NotTakendown>,
237
237
Json(input): Json<UpdateEmailInput>,
238
238
) -> Result<Response, ApiError> {
239
239
if let Err(e) = crate::auth::scope_check::check_account_scope(
···
500
500
pub async fn check_email_update_status(
501
501
State(state): State<AppState>,
502
502
headers: axum::http::HeaderMap,
503
503
-
auth: Auth<Active>,
503
503
+
auth: Auth<NotTakendown>,
504
504
) -> Result<Response, ApiError> {
505
505
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
506
506
if !state
+2
-2
crates/tranquil-pds/src/api/server/invite.rs
···
1
1
use crate::api::ApiError;
2
2
-
use crate::auth::{Active, Admin, Auth};
2
2
+
use crate::auth::{Admin, Auth, NotTakendown};
3
3
use crate::state::AppState;
4
4
use crate::types::Did;
5
5
use axum::{
···
193
193
194
194
pub async fn get_account_invite_codes(
195
195
State(state): State<AppState>,
196
196
-
auth: Auth<Active>,
196
196
+
auth: Auth<NotTakendown>,
197
197
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
198
198
) -> Result<Response, ApiError> {
199
199
let include_used = params.include_used.unwrap_or(true);
+2
-2
crates/tranquil-pds/src/api/server/session.rs
···
1
1
use crate::api::error::ApiError;
2
2
use crate::api::{EmptyResponse, SuccessResponse};
3
3
-
use crate::auth::{Active, Auth, NotTakendown};
3
3
+
use crate::auth::{Active, Auth, Permissive};
4
4
use crate::state::{AppState, RateLimitKind};
5
5
use crate::types::{AccountState, Did, Handle, PlainPassword};
6
6
use axum::{
···
279
279
280
280
pub async fn get_session(
281
281
State(state): State<AppState>,
282
282
-
auth: Auth<NotTakendown>,
282
282
+
auth: Auth<Permissive>,
283
283
) -> Result<Response, ApiError> {
284
284
let permissions = auth.permissions();
285
285
let can_read_email = permissions.allows_email_read();
+22
-81
crates/tranquil-pds/src/auth/extractor.rs
···
27
27
AccountTakedown,
28
28
AdminRequired,
29
29
ServiceAuthNotAllowed,
30
30
-
SigningKeyRequired,
31
30
InsufficientScope(String),
32
31
OAuthExpiredToken(String),
33
32
UseDpopNonce(String),
···
430
429
}
431
430
}
432
431
432
432
+
impl OptionalFromRequestParts<AppState> for ServiceAuth {
433
433
+
type Rejection = AuthError;
434
434
+
435
435
+
async fn from_request_parts(
436
436
+
parts: &mut Parts,
437
437
+
state: &AppState,
438
438
+
) -> Result<Option<Self>, Self::Rejection> {
439
439
+
match extract_auth_internal(parts, state).await {
440
440
+
Ok(ExtractedAuth::Service(claims)) => {
441
441
+
let did: Did = claims
442
442
+
.iss
443
443
+
.parse()
444
444
+
.map_err(|_| AuthError::AuthenticationFailed)?;
445
445
+
Ok(Some(ServiceAuth { did, claims }))
446
446
+
}
447
447
+
Ok(ExtractedAuth::User(_)) => Err(AuthError::AuthenticationFailed),
448
448
+
Err(AuthError::MissingToken) => Ok(None),
449
449
+
Err(e) => Err(e),
450
450
+
}
451
451
+
}
452
452
+
}
453
453
+
433
454
pub enum AuthAny<P: AuthPolicy = Active> {
434
455
User(Auth<P>),
435
456
Service(ServiceAuth),
···
514
535
Err(AuthError::MissingToken) => Ok(None),
515
536
Err(e) => Err(e),
516
537
}
517
517
-
}
518
518
-
}
519
519
-
520
520
-
pub struct SigningAuth<P: AuthPolicy = Active> {
521
521
-
pub did: Did,
522
522
-
pub key_bytes: Vec<u8>,
523
523
-
pub is_admin: bool,
524
524
-
pub status: AccountStatus,
525
525
-
pub scope: Option<String>,
526
526
-
pub controller_did: Option<Did>,
527
527
-
is_oauth: bool,
528
528
-
_policy: PhantomData<P>,
529
529
-
}
530
530
-
531
531
-
impl<P: AuthPolicy> SigningAuth<P> {
532
532
-
pub fn needs_scope_check(&self) -> bool {
533
533
-
self.is_oauth
534
534
-
}
535
535
-
536
536
-
pub fn permissions(&self) -> ScopePermissions {
537
537
-
if let Some(ref scope) = self.scope
538
538
-
&& scope != super::SCOPE_ACCESS
539
539
-
{
540
540
-
return ScopePermissions::from_scope_string(Some(scope));
541
541
-
}
542
542
-
if !self.is_oauth {
543
543
-
return ScopePermissions::from_scope_string(Some("atproto"));
544
544
-
}
545
545
-
ScopePermissions::from_scope_string(self.scope.as_deref())
546
546
-
}
547
547
-
548
548
-
#[allow(clippy::result_large_err)]
549
549
-
pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> {
550
550
-
if !self.needs_scope_check() {
551
551
-
return Ok(());
552
552
-
}
553
553
-
self.permissions()
554
554
-
.assert_repo(action, collection)
555
555
-
.map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response())
556
556
-
}
557
557
-
}
558
558
-
559
559
-
impl<P: AuthPolicy> FromRequestParts<AppState> for SigningAuth<P> {
560
560
-
type Rejection = AuthError;
561
561
-
562
562
-
async fn from_request_parts(
563
563
-
parts: &mut Parts,
564
564
-
state: &AppState,
565
565
-
) -> Result<Self, Self::Rejection> {
566
566
-
let user = extract_user_auth_internal(parts, state).await?;
567
567
-
P::validate(&user)?;
568
568
-
569
569
-
let key_bytes = match user.key_bytes {
570
570
-
Some(kb) => kb,
571
571
-
None => {
572
572
-
let user_with_key = state
573
573
-
.user_repo
574
574
-
.get_with_key_by_did(&user.did)
575
575
-
.await
576
576
-
.ok()
577
577
-
.flatten()
578
578
-
.ok_or(AuthError::SigningKeyRequired)?;
579
579
-
crate::config::decrypt_key(
580
580
-
&user_with_key.key_bytes,
581
581
-
user_with_key.encryption_version,
582
582
-
)
583
583
-
.map_err(|_| AuthError::SigningKeyRequired)?
584
584
-
}
585
585
-
};
586
586
-
587
587
-
Ok(SigningAuth {
588
588
-
did: user.did,
589
589
-
key_bytes,
590
590
-
is_admin: user.is_admin,
591
591
-
status: user.status,
592
592
-
scope: user.scope,
593
593
-
controller_did: user.controller_did,
594
594
-
is_oauth: user.auth_source.is_oauth(),
595
595
-
_policy: PhantomData,
596
596
-
})
597
538
}
598
539
}
599
540
+1
-2
crates/tranquil-pds/src/auth/mod.rs
···
18
18
19
19
pub use extractor::{
20
20
Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown,
21
21
-
Permissive, ServiceAuth, SigningAuth, extract_auth_token_from_header,
22
22
-
extract_bearer_token_from_header,
21
21
+
Permissive, ServiceAuth, extract_auth_token_from_header, extract_bearer_token_from_header,
23
22
};
24
23
pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token};
25
24
+102
crates/tranquil-pds/tests/actor.rs
···
436
436
assert_eq!(declared_age["isOverAge16"], false);
437
437
assert_eq!(declared_age["isOverAge18"], false);
438
438
}
439
439
+
440
440
+
#[tokio::test]
441
441
+
async fn test_deactivated_account_can_get_preferences() {
442
442
+
let client = client();
443
443
+
let base = base_url().await;
444
444
+
let (token, _did) = create_account_and_login(&client).await;
445
445
+
446
446
+
let prefs = json!({
447
447
+
"preferences": [
448
448
+
{
449
449
+
"$type": "app.bsky.actor.defs#adultContentPref",
450
450
+
"enabled": true
451
451
+
}
452
452
+
]
453
453
+
});
454
454
+
let put_resp = client
455
455
+
.post(format!("{}/xrpc/app.bsky.actor.putPreferences", base))
456
456
+
.header("Authorization", format!("Bearer {}", token))
457
457
+
.json(&prefs)
458
458
+
.send()
459
459
+
.await
460
460
+
.unwrap();
461
461
+
assert_eq!(put_resp.status(), 200);
462
462
+
463
463
+
let deactivate = client
464
464
+
.post(format!(
465
465
+
"{}/xrpc/com.atproto.server.deactivateAccount",
466
466
+
base
467
467
+
))
468
468
+
.header("Authorization", format!("Bearer {}", token))
469
469
+
.json(&json!({}))
470
470
+
.send()
471
471
+
.await
472
472
+
.unwrap();
473
473
+
assert_eq!(deactivate.status(), 200);
474
474
+
475
475
+
let get_resp = client
476
476
+
.get(format!("{}/xrpc/app.bsky.actor.getPreferences", base))
477
477
+
.header("Authorization", format!("Bearer {}", token))
478
478
+
.send()
479
479
+
.await
480
480
+
.unwrap();
481
481
+
assert_eq!(
482
482
+
get_resp.status(),
483
483
+
200,
484
484
+
"Deactivated account should still be able to get preferences"
485
485
+
);
486
486
+
let body: Value = get_resp.json().await.unwrap();
487
487
+
let prefs_arr = body["preferences"].as_array().unwrap();
488
488
+
assert_eq!(prefs_arr.len(), 1);
489
489
+
}
490
490
+
491
491
+
#[tokio::test]
492
492
+
async fn test_deactivated_account_can_put_preferences() {
493
493
+
let client = client();
494
494
+
let base = base_url().await;
495
495
+
let (token, _did) = create_account_and_login(&client).await;
496
496
+
497
497
+
let deactivate = client
498
498
+
.post(format!(
499
499
+
"{}/xrpc/com.atproto.server.deactivateAccount",
500
500
+
base
501
501
+
))
502
502
+
.header("Authorization", format!("Bearer {}", token))
503
503
+
.json(&json!({}))
504
504
+
.send()
505
505
+
.await
506
506
+
.unwrap();
507
507
+
assert_eq!(deactivate.status(), 200);
508
508
+
509
509
+
let prefs = json!({
510
510
+
"preferences": [
511
511
+
{
512
512
+
"$type": "app.bsky.actor.defs#adultContentPref",
513
513
+
"enabled": true
514
514
+
}
515
515
+
]
516
516
+
});
517
517
+
let put_resp = client
518
518
+
.post(format!("{}/xrpc/app.bsky.actor.putPreferences", base))
519
519
+
.header("Authorization", format!("Bearer {}", token))
520
520
+
.json(&prefs)
521
521
+
.send()
522
522
+
.await
523
523
+
.unwrap();
524
524
+
assert_eq!(
525
525
+
put_resp.status(),
526
526
+
200,
527
527
+
"Deactivated account should still be able to put preferences"
528
528
+
);
529
529
+
530
530
+
let get_resp = client
531
531
+
.get(format!("{}/xrpc/app.bsky.actor.getPreferences", base))
532
532
+
.header("Authorization", format!("Bearer {}", token))
533
533
+
.send()
534
534
+
.await
535
535
+
.unwrap();
536
536
+
assert_eq!(get_resp.status(), 200);
537
537
+
let body: Value = get_resp.json().await.unwrap();
538
538
+
let prefs_arr = body["preferences"].as_array().unwrap();
539
539
+
assert_eq!(prefs_arr.len(), 1);
540
540
+
}
+65
crates/tranquil-pds/tests/auth_extractor.rs
···
581
581
let proof = format!("{}.{}", signing_input, sig_b64);
582
582
(jwk, proof)
583
583
}
584
584
+
585
585
+
#[tokio::test]
586
586
+
async fn test_optional_service_auth_extractor_behavior() {
587
587
+
let url = base_url().await;
588
588
+
let http_client = client();
589
589
+
let (access_jwt, did) = create_account_and_login(&http_client).await;
590
590
+
591
591
+
let service_auth_res = http_client
592
592
+
.get(format!("{}/xrpc/com.atproto.server.getServiceAuth", url))
593
593
+
.bearer_auth(&access_jwt)
594
594
+
.query(&[("aud", "did:web:test.example")])
595
595
+
.send()
596
596
+
.await
597
597
+
.unwrap();
598
598
+
assert_eq!(service_auth_res.status(), StatusCode::OK);
599
599
+
let service_body: Value = service_auth_res.json().await.unwrap();
600
600
+
let service_token = service_body["token"].as_str().unwrap();
601
601
+
602
602
+
let no_auth_res = http_client
603
603
+
.get(format!(
604
604
+
"{}/xrpc/com.atproto.sync.getBlob?did={}&cid=bafyreifakecidfornowfakecidfornow1234567",
605
605
+
url, did
606
606
+
))
607
607
+
.send()
608
608
+
.await
609
609
+
.unwrap();
610
610
+
assert!(
611
611
+
no_auth_res.status() == StatusCode::NOT_FOUND
612
612
+
|| no_auth_res.status() == StatusCode::BAD_REQUEST,
613
613
+
"getBlob with no auth should reach handler (AuthAny optional path) - got {}",
614
614
+
no_auth_res.status()
615
615
+
);
616
616
+
617
617
+
let service_auth_blob_res = http_client
618
618
+
.get(format!(
619
619
+
"{}/xrpc/com.atproto.sync.getBlob?did={}&cid=bafyreifakecidfornowfakecidfornow1234567",
620
620
+
url, did
621
621
+
))
622
622
+
.bearer_auth(service_token)
623
623
+
.send()
624
624
+
.await
625
625
+
.unwrap();
626
626
+
assert!(
627
627
+
service_auth_blob_res.status() == StatusCode::NOT_FOUND
628
628
+
|| service_auth_blob_res.status() == StatusCode::BAD_REQUEST,
629
629
+
"getBlob with service auth should reach handler (AuthAny service path) - got {}",
630
630
+
service_auth_blob_res.status()
631
631
+
);
632
632
+
633
633
+
let user_auth_blob_res = http_client
634
634
+
.get(format!(
635
635
+
"{}/xrpc/com.atproto.sync.getBlob?did={}&cid=bafyreifakecidfornowfakecidfornow1234567",
636
636
+
url, did
637
637
+
))
638
638
+
.bearer_auth(&access_jwt)
639
639
+
.send()
640
640
+
.await
641
641
+
.unwrap();
642
642
+
assert!(
643
643
+
user_auth_blob_res.status() == StatusCode::NOT_FOUND
644
644
+
|| user_auth_blob_res.status() == StatusCode::BAD_REQUEST,
645
645
+
"getBlob with user auth should reach handler (AuthAny user path) - got {}",
646
646
+
user_auth_blob_res.status()
647
647
+
);
648
648
+
}