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
36const 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
53pub 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, ¶ms.username, ¶ms.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(¶ms.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, ¶ms.password).await?;
163 let new_password =
164 hash(¶ms.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}