covert_userpass_auth/
lib.rs

1#![forbid(unsafe_code)]
2#![forbid(clippy::unwrap_used)]
3#![deny(clippy::pedantic)]
4#![deny(clippy::get_unwrap)]
5#![allow(clippy::module_name_repetitions)]
6
7mod error;
8mod store;
9
10use std::sync::Arc;
11
12use bcrypt::{hash, verify};
13use covert_framework::{
14    create, delete,
15    extract::{Extension, Json, Path},
16    update, update_with_config, Backend, RouteConfig, Router,
17};
18use covert_storage::{
19    migrator::{migration_scripts, MigrationError},
20    BackendStoragePool,
21};
22use covert_types::{
23    backend::{BackendCategory, BackendType},
24    methods::userpass::{
25        CreateUserParams, CreateUserResponse, ListUsersResponse, LoginParams, RemoveUserResponse,
26        UpdateUserPasswordParams, UpdateUserPasswordResponse, UserListItem,
27    },
28    response::Response,
29};
30use covert_types::{mount::MountConfig, response::AuthResponse};
31use error::{Error, ErrorType};
32use rust_embed::RustEmbed;
33use serde::{Deserialize, Serialize};
34use store::user::UsersRepo;
35
36// TODO: maybe increase this
37const DEFAULT_COST: u32 = 8;
38
39pub struct Context {
40    users_repo: UsersRepo,
41}
42
43#[derive(RustEmbed)]
44#[folder = "migrations/"]
45struct Migrations;
46
47#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, PartialEq, Eq, Clone)]
48pub struct User {
49    username: String,
50    password: String,
51}
52
53/// Returns a new userpass auth method.
54///
55/// # Errors
56///
57/// Returns an error if it fails to read the migration scripts.
58pub fn new_userpass_backend(pool: BackendStoragePool) -> Result<Backend, MigrationError> {
59    let ctx = Context {
60        users_repo: UsersRepo::new(pool),
61    };
62
63    let router = Router::new()
64        .route(
65            "/login",
66            update_with_config(login, RouteConfig::unauthenticated())
67                .create_with_config(login, RouteConfig::unauthenticated()),
68        )
69        .route("/users", create(create_user).read(list_users))
70        .route("/users/:username", delete(remove_user))
71        .route("/users/:username/password", update(update_user_password))
72        .layer(Extension(Arc::new(ctx)))
73        .build()
74        .into_service();
75
76    let migrations = migration_scripts::<Migrations>()?;
77
78    Ok(Backend {
79        handler: router,
80        category: BackendCategory::Credential,
81        variant: BackendType::Userpass,
82        migrations,
83    })
84}
85
86#[tracing::instrument(skip_all)]
87async fn user_by_username_and_password(
88    ctx: &Context,
89    username: &str,
90    password: &str,
91) -> Result<User, Error> {
92    let user = ctx
93        .users_repo
94        .get(username)
95        .await?
96        .ok_or_else(|| ErrorType::UserNotFound {
97            username: username.to_string(),
98        })?;
99
100    if !matches!(verify(password, &user.password), Ok(true)) {
101        return Err(ErrorType::IncorrectPassword.into());
102    }
103
104    Ok(user)
105}
106
107#[tracing::instrument(skip_all, fields(username = params.username))]
108async fn login(
109    Json(params): Json<LoginParams>,
110    Extension(config): Extension<MountConfig>,
111    Extension(ctx): Extension<Arc<Context>>,
112) -> Result<Response, Error> {
113    let _user = user_by_username_and_password(&ctx, &params.username, &params.password).await?;
114
115    let auth = AuthResponse {
116        alias: params.username,
117        ttl: Some(config.default_lease_ttl),
118    };
119    Ok(Response::Auth(auth))
120}
121
122#[tracing::instrument(skip_all, fields(username = params.username))]
123async fn create_user(
124    Json(params): Json<CreateUserParams>,
125    Extension(ctx): Extension<Arc<Context>>,
126) -> Result<Response, Error> {
127    let password =
128        hash(&params.password, DEFAULT_COST).map_err(|_| ErrorType::UnsupportedPassword)?;
129    let user = User {
130        username: params.username,
131        password,
132    };
133    ctx.users_repo.create(&user).await?;
134
135    let resp = CreateUserResponse {
136        username: user.username,
137    };
138    Response::raw(resp).map_err(Into::into)
139}
140
141#[tracing::instrument(skip_all)]
142async fn list_users(Extension(ctx): Extension<Arc<Context>>) -> Result<Response, Error> {
143    let users = ctx.users_repo.list().await?;
144
145    let resp = ListUsersResponse {
146        users: users
147            .into_iter()
148            .map(|user| UserListItem {
149                username: user.username,
150            })
151            .collect(),
152    };
153    Response::raw(resp).map_err(Into::into)
154}
155
156#[tracing::instrument(skip_all, fields(username = username))]
157async fn update_user_password(
158    Json(params): Json<UpdateUserPasswordParams>,
159    Path(username): Path<String>,
160    Extension(ctx): Extension<Arc<Context>>,
161) -> Result<Response, Error> {
162    let _user = user_by_username_and_password(&ctx, &username, &params.password).await?;
163    let new_password =
164        hash(&params.new_password, DEFAULT_COST).map_err(|_| ErrorType::UnsupportedPassword)?;
165    ctx.users_repo
166        .update_password(&username, &new_password)
167        .await?;
168
169    let resp = UpdateUserPasswordResponse { username };
170    Response::raw(resp).map_err(Into::into)
171}
172
173#[tracing::instrument(skip_all, fields(username = username))]
174async fn remove_user(
175    Path(username): Path<String>,
176    Extension(ctx): Extension<Arc<Context>>,
177) -> Result<Response, Error> {
178    ctx.users_repo.remove(&username).await?;
179
180    let resp = RemoveUserResponse { username };
181    Response::raw(resp).map_err(Into::into)
182}