mirror of
https://github.com/lldap/lldap.git
synced 2026-07-25 17:58:44 +00:00
server: Add support for deleting users and groups via LDAP
This commit is contained in:
committed by
nitnelave
parent
c3ae149ae3
commit
7450ff1028
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
|
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
infra::{access_control::AdminBackendHandler, ldap::handler::make_add_error},
|
infra::{access_control::AdminBackendHandler, ldap::handler::make_add_response},
|
||||||
};
|
};
|
||||||
use ldap3_proto::proto::{
|
use ldap3_proto::proto::{
|
||||||
LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode,
|
LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode,
|
||||||
@@ -137,7 +137,10 @@ async fn create_user(
|
|||||||
code: LdapResultCode::OperationsError,
|
code: LdapResultCode::OperationsError,
|
||||||
message: format!("Could not create user: {:#?}", e),
|
message: format!("Could not create user: {:#?}", e),
|
||||||
})?;
|
})?;
|
||||||
Ok(vec![make_add_error(LdapResultCode::Success, String::new())])
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, level = "debug")]
|
#[instrument(skip_all, level = "debug")]
|
||||||
@@ -156,7 +159,10 @@ async fn create_group(
|
|||||||
code: LdapResultCode::OperationsError,
|
code: LdapResultCode::OperationsError,
|
||||||
message: format!("Could not create group: {:#?}", e),
|
message: format!("Could not create group: {:#?}", e),
|
||||||
})?;
|
})?;
|
||||||
Ok(vec![make_add_error(LdapResultCode::Success, String::new())])
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -192,7 +198,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ldap_handler.create_user_or_group(request).await,
|
ldap_handler.create_user_or_group(request).await,
|
||||||
Ok(vec![make_add_error(LdapResultCode::Success, String::new())])
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +225,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ldap_handler.create_user_or_group(request).await,
|
ldap_handler.create_user_or_group(request).await,
|
||||||
Ok(vec![make_add_error(LdapResultCode::Success, String::new())])
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +264,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ldap_handler.create_user_or_group(request).await,
|
ldap_handler.create_user_or_group(request).await,
|
||||||
Ok(vec![make_add_error(LdapResultCode::Success, String::new())])
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
use crate::{
|
||||||
|
domain::ldap::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
|
||||||
|
},
|
||||||
|
infra::access_control::AdminBackendHandler,
|
||||||
|
};
|
||||||
|
use ldap3_proto::proto::{LdapOp, LdapResult as LdapResultOp, LdapResultCode};
|
||||||
|
use lldap_domain::types::{GroupName, UserId};
|
||||||
|
use lldap_domain_handlers::handler::GroupRequestFilter;
|
||||||
|
use lldap_domain_model::error::DomainError;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
pub(crate) fn make_del_response(code: LdapResultCode, message: String) -> LdapOp {
|
||||||
|
LdapOp::DelResponse(LdapResultOp {
|
||||||
|
code,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message,
|
||||||
|
referral: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub(crate) async fn delete_user_or_group(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
ldap_info: &LdapInfo,
|
||||||
|
request: String,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let base_dn_str = &ldap_info.base_dn_str;
|
||||||
|
match get_user_or_group_id_from_distinguished_name(&request, &ldap_info.base_dn) {
|
||||||
|
UserOrGroupName::User(user_id) => delete_user(backend_handler, user_id).await,
|
||||||
|
UserOrGroupName::Group(group_name) => delete_group(backend_handler, group_name).await,
|
||||||
|
err => Err(err.into_ldap_error(
|
||||||
|
&request,
|
||||||
|
format!(
|
||||||
|
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
|
||||||
|
base_dn_str, base_dn_str
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn delete_user(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
backend_handler
|
||||||
|
.get_user_details(&user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
DomainError::EntityNotFound(_) => LdapError {
|
||||||
|
code: LdapResultCode::NoSuchObject,
|
||||||
|
message: "Could not find user".to_string(),
|
||||||
|
},
|
||||||
|
e => LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while finding user: {:?}", e),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
backend_handler
|
||||||
|
.delete_user(&user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while deleting user: {:?}", e),
|
||||||
|
})?;
|
||||||
|
Ok(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn delete_group(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
group_name: GroupName,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let groups = backend_handler
|
||||||
|
.list_groups(Some(GroupRequestFilter::DisplayName(group_name.clone())))
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while finding group: {:?}", e),
|
||||||
|
})?;
|
||||||
|
let group_id = groups
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.display_name == group_name)
|
||||||
|
.map(|g| g.id)
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::NoSuchObject,
|
||||||
|
message: "Could not find group".to_string(),
|
||||||
|
})?;
|
||||||
|
backend_handler
|
||||||
|
.delete_group(group_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while deleting group: {:?}", e),
|
||||||
|
})?;
|
||||||
|
Ok(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::infra::{
|
||||||
|
ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler,
|
||||||
|
};
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use lldap_domain::{
|
||||||
|
types::{Group, GroupId, User},
|
||||||
|
uuid,
|
||||||
|
};
|
||||||
|
use lldap_domain_model::error::DomainError;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tokio;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(User {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
mock.expect_delete_user()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(vec![Group {
|
||||||
|
id: GroupId(34),
|
||||||
|
display_name: GroupName::from("bob"),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
|
users: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
mock.expect_delete_group()
|
||||||
|
.with(eq(GroupId(34)))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_not_found() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| Err(DomainError::EntityNotFound("No such user".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::NoSuchObject,
|
||||||
|
"Could not find user".to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_lookup_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while finding user: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_deletion_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(User {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
mock.expect_delete_user()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while deleting user: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group_not_found() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| Ok(vec![]));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::NoSuchObject,
|
||||||
|
"Could not find group".to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group_lookup_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while finding group: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group_deletion_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(vec![Group {
|
||||||
|
id: GroupId(34),
|
||||||
|
display_name: GroupName::from("bob"),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
|
users: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
mock.expect_delete_group()
|
||||||
|
.with(eq(GroupId(34)))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while deleting group: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
infra::{
|
infra::{
|
||||||
access_control::AccessControlledBackendHandler,
|
access_control::AccessControlledBackendHandler,
|
||||||
ldap::{
|
ldap::{
|
||||||
compare, create, modify,
|
compare, create, delete, modify,
|
||||||
password::{self, do_password_modification},
|
password::{self, do_password_modification},
|
||||||
search::{
|
search::{
|
||||||
self, is_root_dse_request, make_search_error, make_search_request,
|
self, is_root_dse_request, make_search_error, make_search_request,
|
||||||
@@ -28,7 +28,9 @@ use lldap_domain::types::AttributeName;
|
|||||||
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
|
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
pub(crate) fn make_add_error(code: LdapResultCode, message: String) -> LdapOp {
|
use super::delete::make_del_response;
|
||||||
|
|
||||||
|
pub(crate) fn make_add_response(code: LdapResultCode, message: String) -> LdapOp {
|
||||||
LdapOp::AddResponse(LdapResultOp {
|
LdapOp::AddResponse(LdapResultOp {
|
||||||
code,
|
code,
|
||||||
matcheddn: "".to_string(),
|
matcheddn: "".to_string(),
|
||||||
@@ -267,6 +269,19 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
|||||||
create::create_user_or_group(backend_handler, &self.ldap_info, request).await
|
create::create_user_or_group(backend_handler, &self.ldap_info, request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub async fn delete_user_or_group(&self, request: String) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let backend_handler = self
|
||||||
|
.user_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|u| self.backend_handler.get_admin_handler(u))
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::InsufficentAccessRights,
|
||||||
|
message: "Unauthorized write".to_string(),
|
||||||
|
})?;
|
||||||
|
delete::delete_user_or_group(backend_handler, &self.ldap_info, request).await
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, level = "debug")]
|
#[instrument(skip_all, level = "debug")]
|
||||||
pub async fn do_compare(&mut self, request: LdapCompareRequest) -> LdapResult<Vec<LdapOp>> {
|
pub async fn do_compare(&mut self, request: LdapCompareRequest) -> LdapResult<Vec<LdapOp>> {
|
||||||
let req = make_search_request::<String>(
|
let req = make_search_request::<String>(
|
||||||
@@ -305,7 +320,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
|||||||
LdapOp::AddRequest(request) => self
|
LdapOp::AddRequest(request) => self
|
||||||
.create_user_or_group(request)
|
.create_user_or_group(request)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e: LdapError| vec![make_add_error(e.code, e.message)]),
|
.unwrap_or_else(|e: LdapError| vec![make_add_response(e.code, e.message)]),
|
||||||
|
LdapOp::DelRequest(request) => self
|
||||||
|
.delete_user_or_group(request)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_del_response(e.code, e.message)]),
|
||||||
LdapOp::CompareRequest(request) => self
|
LdapOp::CompareRequest(request) => self
|
||||||
.do_compare(request)
|
.do_compare(request)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod compare;
|
pub mod compare;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
|
pub mod delete;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod modify;
|
pub mod modify;
|
||||||
pub mod password;
|
pub mod password;
|
||||||
|
|||||||
Reference in New Issue
Block a user