gRPC Accounts: Provision from pre-derived SCRAM verifiers
Adds Accounts.Provision(name, scram256, scram512, email) so an external authority that stores verifiers rather than passwords (a website) can bulk- backfill accounts it never had the plaintext for. It creates the account with the given SCRAM verifiers — SCRAM and keycard login work immediately; typed-password login stays disabled (empty password_hash) until a real password lands via Register/SetPassword. Refuses an existing account, so a re-run can't clobber a fully-credentialed one. scram512 may be empty (SCRAM-SHA-512 then falls back to 256).
This commit is contained in:
parent
fba91b4d62
commit
3f8649414f
4 changed files with 105 additions and 4 deletions
48
src/grpc.rs
48
src/grpc.rs
|
|
@ -28,7 +28,7 @@ use pb::{
|
|||
AccountDropped, AccountEmailSet, AccountRecord, AccountRegistered, AccountReply, AccountVerified,
|
||||
AuthenticateReply, AuthenticateRequest, ChannelDescSet, ChannelDropped, ChannelFounderSet, ChannelRecord,
|
||||
ChannelRegistered, ConfirmRequest, DropRequest, ForceLogoutReply, ForceLogoutRequest, GroupNickRequest,
|
||||
NickGrouped, NickUngrouped, RegisterRequest, ReplicationEvent, SetEmailRequest, SetPasswordRequest,
|
||||
NickGrouped, NickUngrouped, ProvisionRequest, RegisterRequest, ReplicationEvent, SetEmailRequest, SetPasswordRequest,
|
||||
SnapshotRequest, SnapshotResponse, Status as PbStatus, StatsRequest, StatsResponse, SubscribeRequest,
|
||||
UngroupNickRequest,
|
||||
};
|
||||
|
|
@ -251,6 +251,17 @@ impl Accounts for AccountsService {
|
|||
Ok(Response::new(reply(status, describe(status))))
|
||||
}
|
||||
|
||||
async fn provision(&self, req: Request<ProvisionRequest>) -> Result<Response<AccountReply>, Status> {
|
||||
authorize(&req, &self.token)?;
|
||||
let msg = req.into_inner();
|
||||
if msg.name.is_empty() || msg.scram256.is_empty() {
|
||||
return Ok(Response::new(reply(AuthorityStatus::Invalid, "name and scram256 are required")));
|
||||
}
|
||||
let email = if msg.email.is_empty() { None } else { Some(msg.email) };
|
||||
let status = self.engine.lock().await.authority_provision(&msg.name, &msg.scram256, &msg.scram512, email);
|
||||
Ok(Response::new(reply(status, describe(status))))
|
||||
}
|
||||
|
||||
async fn authenticate(&self, req: Request<AuthenticateRequest>) -> Result<Response<AuthenticateReply>, Status> {
|
||||
authorize(&req, &self.token)?;
|
||||
let msg = req.into_inner();
|
||||
|
|
@ -561,6 +572,41 @@ mod tests {
|
|||
assert_eq!(bad.status, PbStatus::Invalid as i32);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provision_creates_account_from_verifiers() {
|
||||
let (engine, _tx) = engine_with("acct-provision");
|
||||
let svc = accounts_svc(engine);
|
||||
// The authority (e.g. the website) already holds SCRAM verifiers.
|
||||
let creds = Db::derive_credentials("password1", 4096).unwrap();
|
||||
|
||||
let prov = svc
|
||||
.provision(authed(ProvisionRequest { name: "heidi".into(), scram256: creds.scram256.clone(), scram512: creds.scram512.clone(), email: String::new() }, "t"))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(prov.status, PbStatus::Ok as i32);
|
||||
|
||||
// Backfill is safe to re-run: an existing account isn't clobbered.
|
||||
let dup = svc
|
||||
.provision(authed(ProvisionRequest { name: "heidi".into(), scram256: creds.scram256.clone(), scram512: String::new(), email: String::new() }, "t"))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(dup.status, PbStatus::AlreadyExists as i32);
|
||||
|
||||
// A missing scram256 is rejected, and a bad bearer never gets in.
|
||||
let invalid = svc
|
||||
.provision(authed(ProvisionRequest { name: "ivan".into(), scram256: String::new(), scram512: String::new(), email: String::new() }, "t"))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(invalid.status, PbStatus::Invalid as i32);
|
||||
assert!(svc
|
||||
.provision(authed(ProvisionRequest { name: "ivan".into(), scram256: "v".into(), scram512: String::new(), email: String::new() }, "wrong"))
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_rejects_a_bad_bearer_token() {
|
||||
let (engine, _tx) = engine_with("acct-auth");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue