mirror of https://github.com/lldap/lldap.git
Merge branch 'main' into user-attribute-form
This commit is contained in:
commit
66097f1880
|
@ -16,21 +16,26 @@ fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
|
|||
Ok(token.claims().clone())
|
||||
}
|
||||
|
||||
const NO_BODY: Option<()> = None;
|
||||
enum RequestType<Body: Serialize> {
|
||||
Get,
|
||||
Post(Body),
|
||||
}
|
||||
|
||||
const GET_REQUEST: RequestType<()> = RequestType::Get;
|
||||
|
||||
fn base_url() -> String {
|
||||
yew_router::utils::base_url().unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn call_server(
|
||||
async fn call_server<Body: Serialize>(
|
||||
url: &str,
|
||||
body: Option<impl Serialize>,
|
||||
body: RequestType<Body>,
|
||||
error_message: &'static str,
|
||||
) -> Result<String> {
|
||||
let mut request = Request::new(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.credentials(RequestCredentials::SameOrigin);
|
||||
if let Some(b) = body {
|
||||
if let RequestType::Post(b) = body {
|
||||
request = request
|
||||
.body(serde_json::to_string(&b)?)
|
||||
.method(Method::POST);
|
||||
|
@ -51,7 +56,7 @@ async fn call_server(
|
|||
|
||||
async fn call_server_json_with_error_message<CallbackResult, Body: Serialize>(
|
||||
url: &str,
|
||||
request: Option<Body>,
|
||||
request: RequestType<Body>,
|
||||
error_message: &'static str,
|
||||
) -> Result<CallbackResult>
|
||||
where
|
||||
|
@ -63,7 +68,7 @@ where
|
|||
|
||||
async fn call_server_empty_response_with_error_message<Body: Serialize>(
|
||||
url: &str,
|
||||
request: Option<Body>,
|
||||
request: RequestType<Body>,
|
||||
error_message: &'static str,
|
||||
) -> Result<()> {
|
||||
call_server(url, request, error_message).await.map(|_| ())
|
||||
|
@ -102,7 +107,7 @@ impl HostService {
|
|||
let request_body = QueryType::build_query(variables);
|
||||
call_server_json_with_error_message::<graphql_client::Response<_>, _>(
|
||||
&(base_url() + "/api/graphql"),
|
||||
Some(request_body),
|
||||
RequestType::Post(request_body),
|
||||
error_message,
|
||||
)
|
||||
.await
|
||||
|
@ -114,7 +119,7 @@ impl HostService {
|
|||
) -> Result<Box<login::ServerLoginStartResponse>> {
|
||||
call_server_json_with_error_message(
|
||||
&(base_url() + "/auth/opaque/login/start"),
|
||||
Some(request),
|
||||
RequestType::Post(request),
|
||||
"Could not start authentication: ",
|
||||
)
|
||||
.await
|
||||
|
@ -123,7 +128,7 @@ impl HostService {
|
|||
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
|
||||
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
|
||||
&(base_url() + "/auth/opaque/login/finish"),
|
||||
Some(request),
|
||||
RequestType::Post(request),
|
||||
"Could not finish authentication",
|
||||
)
|
||||
.await
|
||||
|
@ -135,7 +140,7 @@ impl HostService {
|
|||
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
|
||||
call_server_json_with_error_message(
|
||||
&(base_url() + "/auth/opaque/register/start"),
|
||||
Some(request),
|
||||
RequestType::Post(request),
|
||||
"Could not start registration: ",
|
||||
)
|
||||
.await
|
||||
|
@ -146,7 +151,7 @@ impl HostService {
|
|||
) -> Result<()> {
|
||||
call_server_empty_response_with_error_message(
|
||||
&(base_url() + "/auth/opaque/register/finish"),
|
||||
Some(request),
|
||||
RequestType::Post(request),
|
||||
"Could not finish registration",
|
||||
)
|
||||
.await
|
||||
|
@ -155,7 +160,7 @@ impl HostService {
|
|||
pub async fn refresh() -> Result<(String, bool)> {
|
||||
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
|
||||
&(base_url() + "/auth/refresh"),
|
||||
NO_BODY,
|
||||
GET_REQUEST,
|
||||
"Could not start authentication: ",
|
||||
)
|
||||
.await
|
||||
|
@ -166,7 +171,7 @@ impl HostService {
|
|||
pub async fn logout() -> Result<()> {
|
||||
call_server_empty_response_with_error_message(
|
||||
&(base_url() + "/auth/logout"),
|
||||
NO_BODY,
|
||||
GET_REQUEST,
|
||||
"Could not logout",
|
||||
)
|
||||
.await
|
||||
|
@ -179,7 +184,7 @@ impl HostService {
|
|||
base_url(),
|
||||
url_escape::encode_query(&username)
|
||||
),
|
||||
NO_BODY,
|
||||
RequestType::Post(""),
|
||||
"Could not initiate password reset",
|
||||
)
|
||||
.await
|
||||
|
@ -190,7 +195,7 @@ impl HostService {
|
|||
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
|
||||
call_server_json_with_error_message(
|
||||
&format!("{}/auth/reset/step2/{}", base_url(), token),
|
||||
NO_BODY,
|
||||
GET_REQUEST,
|
||||
"Could not validate token",
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::infra::api::HostService;
|
|||
use anyhow::Result;
|
||||
use graphql_client::GraphQLQuery;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use yew::{use_effect, use_state, UseStateHandle};
|
||||
use yew::{use_effect, use_state_eq, UseStateHandle};
|
||||
|
||||
// Enum to represent a result that is fetched asynchronously.
|
||||
#[derive(Debug)]
|
||||
|
@ -13,14 +13,28 @@ pub enum LoadableResult<T> {
|
|||
Loaded(Result<T>),
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for LoadableResult<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(LoadableResult::Loading, LoadableResult::Loading) => true,
|
||||
(LoadableResult::Loaded(Ok(d1)), LoadableResult::Loaded(Ok(d2))) => d1.eq(d2),
|
||||
(LoadableResult::Loaded(Err(e1)), LoadableResult::Loaded(Err(e2))) => {
|
||||
e1.to_string().eq(&e2.to_string())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_graphql_call<QueryType>(
|
||||
variables: QueryType::Variables,
|
||||
) -> UseStateHandle<LoadableResult<QueryType::ResponseData>>
|
||||
where
|
||||
QueryType: GraphQLQuery + 'static,
|
||||
<QueryType as graphql_client::GraphQLQuery>::ResponseData: std::cmp::PartialEq,
|
||||
{
|
||||
let loadable_result: UseStateHandle<LoadableResult<QueryType::ResponseData>> =
|
||||
use_state(|| LoadableResult::Loading);
|
||||
use_state_eq(|| LoadableResult::Loading);
|
||||
{
|
||||
let loadable_result = loadable_result.clone();
|
||||
use_effect(move || {
|
||||
|
|
|
@ -18,6 +18,10 @@ type Mutation {
|
|||
addGroupAttribute(name: String!, attributeType: AttributeType!, isList: Boolean!, isVisible: Boolean!, isEditable: Boolean!): Success!
|
||||
deleteUserAttribute(name: String!): Success!
|
||||
deleteGroupAttribute(name: String!): Success!
|
||||
addUserObjectClass(name: String!): Success!
|
||||
addGroupObjectClass(name: String!): Success!
|
||||
deleteUserObjectClass(name: String!): Success!
|
||||
deleteGroupObjectClass(name: String!): Success!
|
||||
}
|
||||
|
||||
type Group {
|
||||
|
@ -162,6 +166,7 @@ enum AttributeType {
|
|||
|
||||
type AttributeList {
|
||||
attributes: [AttributeSchema!]!
|
||||
extraLdapObjectClasses: [String!]!
|
||||
}
|
||||
|
||||
type Success {
|
||||
|
|
|
@ -2,7 +2,8 @@ use crate::domain::{
|
|||
error::Result,
|
||||
types::{
|
||||
AttributeName, AttributeType, AttributeValue, Email, Group, GroupDetails, GroupId,
|
||||
GroupName, JpegPhoto, Serialized, User, UserAndGroups, UserColumn, UserId, Uuid,
|
||||
GroupName, JpegPhoto, LdapObjectClass, Serialized, User, UserAndGroups, UserColumn, UserId,
|
||||
Uuid,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
@ -175,6 +176,8 @@ impl AttributeList {
|
|||
pub struct Schema {
|
||||
pub user_attributes: AttributeList,
|
||||
pub group_attributes: AttributeList,
|
||||
pub extra_user_object_classes: Vec<LdapObjectClass>,
|
||||
pub extra_group_object_classes: Vec<LdapObjectClass>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -227,6 +230,11 @@ pub trait SchemaBackendHandler: ReadSchemaBackendHandler {
|
|||
// Note: It's up to the caller to make sure that the attribute is not hardcoded.
|
||||
async fn delete_user_attribute(&self, name: &AttributeName) -> Result<()>;
|
||||
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()>;
|
||||
|
||||
async fn add_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn add_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::domain::{
|
|||
handler::{GroupListerBackendHandler, GroupRequestFilter},
|
||||
ldap::error::LdapError,
|
||||
schema::{PublicSchema, SchemaGroupAttributeExtractor},
|
||||
types::{AttributeName, AttributeType, Group, UserId, Uuid},
|
||||
types::{AttributeName, AttributeType, Group, LdapObjectClass, UserId, Uuid},
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -30,7 +30,17 @@ pub fn get_group_attribute(
|
|||
) -> Option<Vec<Vec<u8>>> {
|
||||
let attribute = AttributeName::from(attribute);
|
||||
let attribute_values = match map_group_field(&attribute, schema) {
|
||||
GroupFieldType::ObjectClass => vec![b"groupOfUniqueNames".to_vec()],
|
||||
GroupFieldType::ObjectClass => {
|
||||
let mut classes = vec![b"groupOfUniqueNames".to_vec()];
|
||||
classes.extend(
|
||||
schema
|
||||
.get_schema()
|
||||
.extra_group_object_classes
|
||||
.iter()
|
||||
.map(|c| c.as_str().as_bytes().to_vec()),
|
||||
);
|
||||
classes
|
||||
}
|
||||
// Always returned as part of the base response.
|
||||
GroupFieldType::Dn => return None,
|
||||
GroupFieldType::EntryDn => {
|
||||
|
@ -167,10 +177,13 @@ fn convert_group_filter(
|
|||
)?;
|
||||
Ok(GroupRequestFilter::Member(user_name))
|
||||
}
|
||||
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(matches!(
|
||||
value.as_str(),
|
||||
"groupofuniquenames" | "groupofnames"
|
||||
))),
|
||||
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(
|
||||
matches!(value.as_str(), "groupofuniquenames" | "groupofnames")
|
||||
|| schema
|
||||
.get_schema()
|
||||
.extra_group_object_classes
|
||||
.contains(&LdapObjectClass::from(value)),
|
||||
)),
|
||||
GroupFieldType::Dn | GroupFieldType::EntryDn => {
|
||||
Ok(get_group_id_from_distinguished_name(
|
||||
value.as_str(),
|
||||
|
|
|
@ -15,7 +15,10 @@ use crate::domain::{
|
|||
},
|
||||
},
|
||||
schema::{PublicSchema, SchemaUserAttributeExtractor},
|
||||
types::{AttributeName, AttributeType, GroupDetails, User, UserAndGroups, UserColumn, UserId},
|
||||
types::{
|
||||
AttributeName, AttributeType, GroupDetails, LdapObjectClass, User, UserAndGroups,
|
||||
UserColumn, UserId,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn get_user_attribute(
|
||||
|
@ -28,12 +31,22 @@ pub fn get_user_attribute(
|
|||
) -> Option<Vec<Vec<u8>>> {
|
||||
let attribute = AttributeName::from(attribute);
|
||||
let attribute_values = match map_user_field(&attribute, schema) {
|
||||
UserFieldType::ObjectClass => vec![
|
||||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec(),
|
||||
],
|
||||
UserFieldType::ObjectClass => {
|
||||
let mut classes = vec![
|
||||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec(),
|
||||
];
|
||||
classes.extend(
|
||||
schema
|
||||
.get_schema()
|
||||
.extra_user_object_classes
|
||||
.iter()
|
||||
.map(|c| c.as_str().as_bytes().to_vec()),
|
||||
);
|
||||
classes
|
||||
}
|
||||
// dn is always returned as part of the base response.
|
||||
UserFieldType::Dn => return None,
|
||||
UserFieldType::EntryDn => {
|
||||
|
@ -196,10 +209,15 @@ fn convert_user_filter(
|
|||
}
|
||||
Ok(UserRequestFilter::from(false))
|
||||
}
|
||||
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(matches!(
|
||||
value.as_str(),
|
||||
"person" | "inetorgperson" | "posixaccount" | "mailaccount"
|
||||
))),
|
||||
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(
|
||||
matches!(
|
||||
value.as_str(),
|
||||
"person" | "inetorgperson" | "posixaccount" | "mailaccount"
|
||||
) || schema
|
||||
.get_schema()
|
||||
.extra_user_object_classes
|
||||
.contains(&LdapObjectClass::from(value)),
|
||||
)),
|
||||
UserFieldType::MemberOf => Ok(UserRequestFilter::MemberOf(
|
||||
get_group_id_from_distinguished_name(
|
||||
&value,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::domain::types::LdapObjectClass;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "group_object_classes")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub lower_object_class: String,
|
||||
pub object_class: LdapObjectClass,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl From<Model> for LdapObjectClass {
|
||||
fn from(value: Model) -> Self {
|
||||
value.object_class
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.3
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod groups;
|
||||
|
@ -11,8 +9,10 @@ pub mod users;
|
|||
|
||||
pub mod user_attribute_schema;
|
||||
pub mod user_attributes;
|
||||
pub mod user_object_classes;
|
||||
|
||||
pub mod group_attribute_schema;
|
||||
pub mod group_attributes;
|
||||
pub mod group_object_classes;
|
||||
|
||||
pub use prelude::*;
|
||||
|
|
|
@ -4,6 +4,8 @@ pub use super::group_attribute_schema::Column as GroupAttributeSchemaColumn;
|
|||
pub use super::group_attribute_schema::Entity as GroupAttributeSchema;
|
||||
pub use super::group_attributes::Column as GroupAttributesColumn;
|
||||
pub use super::group_attributes::Entity as GroupAttributes;
|
||||
pub use super::group_object_classes::Column as GroupObjectClassesColumn;
|
||||
pub use super::group_object_classes::Entity as GroupObjectClasses;
|
||||
pub use super::groups::Column as GroupColumn;
|
||||
pub use super::groups::Entity as Group;
|
||||
pub use super::jwt_refresh_storage::Column as JwtRefreshStorageColumn;
|
||||
|
@ -18,5 +20,7 @@ pub use super::user_attribute_schema::Column as UserAttributeSchemaColumn;
|
|||
pub use super::user_attribute_schema::Entity as UserAttributeSchema;
|
||||
pub use super::user_attributes::Column as UserAttributesColumn;
|
||||
pub use super::user_attributes::Entity as UserAttributes;
|
||||
pub use super::user_object_classes::Column as UserObjectClassesColumn;
|
||||
pub use super::user_object_classes::Entity as UserObjectClasses;
|
||||
pub use super::users::Column as UserColumn;
|
||||
pub use super::users::Entity as User;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::domain::types::LdapObjectClass;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_object_classes")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub lower_object_class: String,
|
||||
pub object_class: LdapObjectClass,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl From<Model> for LdapObjectClass {
|
||||
fn from(value: Model) -> Self {
|
||||
value.object_class
|
||||
}
|
||||
}
|
|
@ -88,6 +88,20 @@ pub enum GroupAttributes {
|
|||
GroupAttributeValue,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden, PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub enum UserObjectClasses {
|
||||
Table,
|
||||
LowerObjectClass,
|
||||
ObjectClass,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden, PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub enum GroupObjectClasses {
|
||||
Table,
|
||||
LowerObjectClass,
|
||||
ObjectClass,
|
||||
}
|
||||
|
||||
// Metadata about the SQL DB.
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Metadata {
|
||||
|
@ -1031,6 +1045,51 @@ async fn migrate_to_v8(transaction: DatabaseTransaction) -> Result<DatabaseTrans
|
|||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn migrate_to_v9(transaction: DatabaseTransaction) -> Result<DatabaseTransaction, DbErr> {
|
||||
let builder = transaction.get_database_backend();
|
||||
transaction
|
||||
.execute(
|
||||
builder.build(
|
||||
Table::create()
|
||||
.table(UserObjectClasses::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(UserObjectClasses::LowerObjectClass)
|
||||
.string_len(255)
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserObjectClasses::ObjectClass)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
transaction
|
||||
.execute(
|
||||
builder.build(
|
||||
Table::create()
|
||||
.table(GroupObjectClasses::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(GroupObjectClasses::LowerObjectClass)
|
||||
.string_len(255)
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(GroupObjectClasses::ObjectClass)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
// This is needed to make an array of async functions.
|
||||
macro_rules! to_sync {
|
||||
($l:ident) => {
|
||||
|
@ -1059,6 +1118,7 @@ pub async fn migrate_from_version(
|
|||
to_sync!(migrate_to_v6),
|
||||
to_sync!(migrate_to_v7),
|
||||
to_sync!(migrate_to_v8),
|
||||
to_sync!(migrate_to_v9),
|
||||
];
|
||||
assert_eq!(migrations.len(), (LAST_SCHEMA_VERSION.0 - 1) as usize);
|
||||
for migration in 2..=last_version.0 {
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::domain::{
|
|||
},
|
||||
model,
|
||||
sql_backend_handler::SqlBackendHandler,
|
||||
types::AttributeName,
|
||||
types::{AttributeName, LdapObjectClass},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
|
@ -66,6 +66,44 @@ impl SchemaBackendHandler for SqlBackendHandler {
|
|||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_user_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
let mut name_key = name.to_string();
|
||||
name_key.make_ascii_lowercase();
|
||||
model::user_object_classes::ActiveModel {
|
||||
lower_object_class: Set(name_key),
|
||||
object_class: Set(name.clone()),
|
||||
}
|
||||
.insert(&self.sql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_group_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
let mut name_key = name.to_string();
|
||||
name_key.make_ascii_lowercase();
|
||||
model::group_object_classes::ActiveModel {
|
||||
lower_object_class: Set(name_key),
|
||||
object_class: Set(name.clone()),
|
||||
}
|
||||
.insert(&self.sql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
model::UserObjectClasses::delete_by_id(name.as_str().to_ascii_lowercase())
|
||||
.exec(&self.sql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
model::GroupObjectClasses::delete_by_id(name.as_str().to_ascii_lowercase())
|
||||
.exec(&self.sql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SqlBackendHandler {
|
||||
|
@ -79,6 +117,8 @@ impl SqlBackendHandler {
|
|||
group_attributes: AttributeList {
|
||||
attributes: Self::get_group_attributes(transaction).await?,
|
||||
},
|
||||
extra_user_object_classes: Self::get_user_object_classes(transaction).await?,
|
||||
extra_group_object_classes: Self::get_group_object_classes(transaction).await?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -105,6 +145,30 @@ impl SqlBackendHandler {
|
|||
.map(|m| m.into())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_user_object_classes(
|
||||
transaction: &DatabaseTransaction,
|
||||
) -> Result<Vec<LdapObjectClass>> {
|
||||
Ok(model::UserObjectClasses::find()
|
||||
.order_by_asc(model::UserObjectClassesColumn::ObjectClass)
|
||||
.all(transaction)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_group_object_classes(
|
||||
transaction: &DatabaseTransaction,
|
||||
) -> Result<Vec<LdapObjectClass>> {
|
||||
Ok(model::GroupObjectClasses::find()
|
||||
.order_by_asc(model::GroupObjectClassesColumn::ObjectClass)
|
||||
.all(transaction)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -151,7 +215,9 @@ mod tests {
|
|||
},
|
||||
group_attributes: AttributeList {
|
||||
attributes: Vec::new()
|
||||
}
|
||||
},
|
||||
extra_user_object_classes: Vec::new(),
|
||||
extra_group_object_classes: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -247,4 +313,50 @@ mod tests {
|
|||
.attributes
|
||||
.contains(&expected_value));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_object_class_add_and_delete() {
|
||||
let fixture = TestFixture::new().await;
|
||||
let new_object_class = LdapObjectClass::new("newObjectClass");
|
||||
fixture
|
||||
.handler
|
||||
.add_user_object_class(&new_object_class)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fixture
|
||||
.handler
|
||||
.get_schema()
|
||||
.await
|
||||
.unwrap()
|
||||
.extra_user_object_classes,
|
||||
vec![new_object_class.clone()]
|
||||
);
|
||||
fixture
|
||||
.handler
|
||||
.add_user_object_class(&LdapObjectClass::new("newobjEctclass"))
|
||||
.await
|
||||
.expect_err("Should not be able to add the same object class twice");
|
||||
assert_eq!(
|
||||
fixture
|
||||
.handler
|
||||
.get_schema()
|
||||
.await
|
||||
.unwrap()
|
||||
.extra_user_object_classes,
|
||||
vec![new_object_class.clone()]
|
||||
);
|
||||
fixture
|
||||
.handler
|
||||
.delete_user_object_class(&new_object_class)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(fixture
|
||||
.handler
|
||||
.get_schema()
|
||||
.await
|
||||
.unwrap()
|
||||
.extra_user_object_classes
|
||||
.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ pub type DbConnection = sea_orm::DatabaseConnection;
|
|||
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord, DeriveValueType)]
|
||||
pub struct SchemaVersion(pub i16);
|
||||
|
||||
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(8);
|
||||
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(9);
|
||||
|
||||
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
|
||||
pub struct PrivateKeyHash(pub [u8; 32]);
|
||||
|
|
|
@ -271,6 +271,8 @@ impl TryFromU64 for AttributeName {
|
|||
))
|
||||
}
|
||||
}
|
||||
|
||||
make_case_insensitive_comparable_string!(LdapObjectClass);
|
||||
make_case_insensitive_comparable_string!(Email);
|
||||
make_case_insensitive_comparable_string!(GroupName);
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@ use crate::domain::{
|
|||
UpdateUserRequest, UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
|
||||
},
|
||||
schema::PublicSchema,
|
||||
types::{AttributeName, Group, GroupDetails, GroupId, GroupName, User, UserAndGroups, UserId},
|
||||
types::{
|
||||
AttributeName, Group, GroupDetails, GroupId, GroupName, LdapObjectClass, User,
|
||||
UserAndGroups, UserId,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
|
@ -112,6 +115,10 @@ pub trait AdminBackendHandler:
|
|||
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()>;
|
||||
async fn delete_user_attribute(&self, name: &AttributeName) -> Result<()>;
|
||||
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()>;
|
||||
async fn add_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn add_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -187,6 +194,18 @@ impl<Handler: BackendHandler> AdminBackendHandler for Handler {
|
|||
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()> {
|
||||
<Handler as SchemaBackendHandler>::delete_group_attribute(self, name).await
|
||||
}
|
||||
async fn add_user_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
<Handler as SchemaBackendHandler>::add_user_object_class(self, name).await
|
||||
}
|
||||
async fn add_group_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
<Handler as SchemaBackendHandler>::add_group_object_class(self, name).await
|
||||
}
|
||||
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
<Handler as SchemaBackendHandler>::delete_user_object_class(self, name).await
|
||||
}
|
||||
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()> {
|
||||
<Handler as SchemaBackendHandler>::delete_group_object_class(self, name).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccessControlledBackendHandler<Handler> {
|
||||
|
|
|
@ -677,7 +677,7 @@ where
|
|||
if enable_password_reset {
|
||||
cfg.service(
|
||||
web::resource("/reset/step1/{user_id}")
|
||||
.route(web::get().to(get_password_reset_step1_handler::<Backend>)),
|
||||
.route(web::post().to(get_password_reset_step1_handler::<Backend>)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/reset/step2/{token}")
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
},
|
||||
types::{
|
||||
AttributeName, AttributeType, AttributeValue as DomainAttributeValue, GroupId,
|
||||
JpegPhoto, UserId,
|
||||
JpegPhoto, LdapObjectClass, UserId,
|
||||
},
|
||||
},
|
||||
infra::{
|
||||
|
@ -490,6 +490,90 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
.await?;
|
||||
Ok(Success::new())
|
||||
}
|
||||
|
||||
async fn add_user_object_class(
|
||||
context: &Context<Handler>,
|
||||
name: String,
|
||||
) -> FieldResult<Success> {
|
||||
let span = debug_span!("[GraphQL mutation] add_user_object_class");
|
||||
span.in_scope(|| {
|
||||
debug!(?name);
|
||||
});
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized object class addition",
|
||||
))?;
|
||||
handler
|
||||
.add_user_object_class(&LdapObjectClass::from(name))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
Ok(Success::new())
|
||||
}
|
||||
|
||||
async fn add_group_object_class(
|
||||
context: &Context<Handler>,
|
||||
name: String,
|
||||
) -> FieldResult<Success> {
|
||||
let span = debug_span!("[GraphQL mutation] add_group_object_class");
|
||||
span.in_scope(|| {
|
||||
debug!(?name);
|
||||
});
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized object class addition",
|
||||
))?;
|
||||
handler
|
||||
.add_group_object_class(&LdapObjectClass::from(name))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
Ok(Success::new())
|
||||
}
|
||||
|
||||
async fn delete_user_object_class(
|
||||
context: &Context<Handler>,
|
||||
name: String,
|
||||
) -> FieldResult<Success> {
|
||||
let span = debug_span!("[GraphQL mutation] delete_user_object_class");
|
||||
span.in_scope(|| {
|
||||
debug!(?name);
|
||||
});
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized object class deletion",
|
||||
))?;
|
||||
handler
|
||||
.delete_user_object_class(&LdapObjectClass::from(name))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
Ok(Success::new())
|
||||
}
|
||||
|
||||
async fn delete_group_object_class(
|
||||
context: &Context<Handler>,
|
||||
name: String,
|
||||
) -> FieldResult<Success> {
|
||||
let span = debug_span!("[GraphQL mutation] delete_group_object_class");
|
||||
span.in_scope(|| {
|
||||
debug!(?name);
|
||||
});
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized object class deletion",
|
||||
))?;
|
||||
handler
|
||||
.delete_group_object_class(&LdapObjectClass::from(name))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
Ok(Success::new())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_group_with_details<Handler: BackendHandler>(
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
ldap::utils::{map_user_field, UserFieldType},
|
||||
model::UserColumn,
|
||||
schema::PublicSchema,
|
||||
types::{AttributeType, GroupDetails, GroupId, JpegPhoto, UserId},
|
||||
types::{AttributeType, GroupDetails, GroupId, JpegPhoto, LdapObjectClass, UserId},
|
||||
},
|
||||
infra::{
|
||||
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
|
||||
|
@ -523,26 +523,32 @@ impl<Handler: BackendHandler> From<DomainAttributeSchema> for AttributeSchema<Ha
|
|||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct AttributeList<Handler: BackendHandler> {
|
||||
schema: DomainAttributeList,
|
||||
attributes: DomainAttributeList,
|
||||
extra_classes: Vec<LdapObjectClass>,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||
fn attributes(&self) -> Vec<AttributeSchema<Handler>> {
|
||||
self.schema
|
||||
self.attributes
|
||||
.attributes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extra_ldap_object_classes(&self) -> Vec<String> {
|
||||
self.extra_classes.iter().map(|c| c.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> From<DomainAttributeList> for AttributeList<Handler> {
|
||||
fn from(value: DomainAttributeList) -> Self {
|
||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||
fn new(attributes: DomainAttributeList, extra_classes: Vec<LdapObjectClass>) -> Self {
|
||||
Self {
|
||||
schema: value,
|
||||
attributes,
|
||||
extra_classes,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -557,10 +563,16 @@ pub struct Schema<Handler: BackendHandler> {
|
|||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> Schema<Handler> {
|
||||
fn user_schema(&self) -> AttributeList<Handler> {
|
||||
self.schema.get_schema().user_attributes.clone().into()
|
||||
AttributeList::<Handler>::new(
|
||||
self.schema.get_schema().user_attributes.clone(),
|
||||
self.schema.get_schema().extra_user_object_classes.clone(),
|
||||
)
|
||||
}
|
||||
fn group_schema(&self) -> AttributeList<Handler> {
|
||||
self.schema.get_schema().group_attributes.clone().into()
|
||||
AttributeList::<Handler>::new(
|
||||
self.schema.get_schema().group_attributes.clone(),
|
||||
self.schema.get_schema().extra_group_object_classes.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -670,7 +682,7 @@ mod tests {
|
|||
use crate::{
|
||||
domain::{
|
||||
handler::AttributeList,
|
||||
types::{AttributeName, AttributeType, Serialized},
|
||||
types::{AttributeName, AttributeType, LdapObjectClass, Serialized},
|
||||
},
|
||||
infra::{
|
||||
access_control::{Permission, ValidationResults},
|
||||
|
@ -755,6 +767,11 @@ mod tests {
|
|||
is_hardcoded: false,
|
||||
}],
|
||||
},
|
||||
extra_user_object_classes: vec![
|
||||
LdapObjectClass::from("customUserClass"),
|
||||
LdapObjectClass::from("myUserClass"),
|
||||
],
|
||||
extra_group_object_classes: vec![LdapObjectClass::from("customGroupClass")],
|
||||
})
|
||||
});
|
||||
mock.expect_get_user_details()
|
||||
|
@ -946,6 +963,7 @@ mod tests {
|
|||
isEditable
|
||||
isHardcoded
|
||||
}
|
||||
extraLdapObjectClasses
|
||||
}
|
||||
groupSchema {
|
||||
attributes {
|
||||
|
@ -956,6 +974,7 @@ mod tests {
|
|||
isEditable
|
||||
isHardcoded
|
||||
}
|
||||
extraLdapObjectClasses
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
@ -1040,7 +1059,8 @@ mod tests {
|
|||
"isEditable": false,
|
||||
"isHardcoded": true,
|
||||
},
|
||||
]
|
||||
],
|
||||
"extraLdapObjectClasses": ["customUserClass"],
|
||||
},
|
||||
"groupSchema": {
|
||||
"attributes": [
|
||||
|
@ -1076,7 +1096,8 @@ mod tests {
|
|||
"isEditable": false,
|
||||
"isHardcoded": true,
|
||||
},
|
||||
]
|
||||
],
|
||||
"extraLdapObjectClasses": [],
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
@ -1093,6 +1114,7 @@ mod tests {
|
|||
attributes {
|
||||
name
|
||||
}
|
||||
extraLdapObjectClasses
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
@ -1114,6 +1136,8 @@ mod tests {
|
|||
group_attributes: AttributeList {
|
||||
attributes: Vec::new(),
|
||||
},
|
||||
extra_user_object_classes: vec![LdapObjectClass::from("customUserClass")],
|
||||
extra_group_object_classes: Vec::new(),
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -1139,7 +1163,8 @@ mod tests {
|
|||
{"name": "mail"},
|
||||
{"name": "user_id"},
|
||||
{"name": "uuid"},
|
||||
]
|
||||
],
|
||||
"extraLdapObjectClasses": ["customUserClass"],
|
||||
}
|
||||
}
|
||||
} ),
|
||||
|
|
|
@ -1290,7 +1290,8 @@ mod tests {
|
|||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec()
|
||||
b"person".to_vec(),
|
||||
b"customUserClass".to_vec(),
|
||||
]
|
||||
},
|
||||
LdapPartialAttribute {
|
||||
|
@ -1332,7 +1333,8 @@ mod tests {
|
|||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec()
|
||||
b"person".to_vec(),
|
||||
b"customUserClass".to_vec(),
|
||||
]
|
||||
},
|
||||
LdapPartialAttribute {
|
||||
|
@ -1919,7 +1921,49 @@ mod tests {
|
|||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec()
|
||||
b"person".to_vec(),
|
||||
b"customUserClass".to_vec(),
|
||||
]
|
||||
},]
|
||||
}),
|
||||
make_search_success()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_filters_custom_object_class() {
|
||||
let mut mock = MockTestBackendHandler::new();
|
||||
mock.expect_list_users()
|
||||
.with(eq(Some(UserRequestFilter::from(true))), eq(false))
|
||||
.times(1)
|
||||
.return_once(|_, _| {
|
||||
Ok(vec![UserAndGroups {
|
||||
user: User {
|
||||
user_id: UserId::new("bob_1"),
|
||||
..Default::default()
|
||||
},
|
||||
groups: None,
|
||||
}])
|
||||
});
|
||||
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||
let request = make_user_search_request(
|
||||
LdapFilter::Equality("objectClass".to_owned(), "CUSTOMuserCLASS".to_owned()),
|
||||
vec!["objectclass"],
|
||||
);
|
||||
assert_eq!(
|
||||
ldap_handler.do_search_or_dse(&request).await,
|
||||
Ok(vec![
|
||||
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
||||
dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
|
||||
attributes: vec![LdapPartialAttribute {
|
||||
atype: "objectclass".to_string(),
|
||||
vals: vec![
|
||||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec(),
|
||||
b"customUserClass".to_vec(),
|
||||
]
|
||||
},]
|
||||
}),
|
||||
|
@ -1983,7 +2027,8 @@ mod tests {
|
|||
b"inetOrgPerson".to_vec(),
|
||||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec()
|
||||
b"person".to_vec(),
|
||||
b"customUserClass".to_vec(),
|
||||
]
|
||||
},
|
||||
LdapPartialAttribute {
|
||||
|
@ -2068,6 +2113,7 @@ mod tests {
|
|||
b"posixAccount".to_vec(),
|
||||
b"mailAccount".to_vec(),
|
||||
b"person".to_vec(),
|
||||
b"customUserClass".to_vec(),
|
||||
],
|
||||
},
|
||||
LdapPartialAttribute {
|
||||
|
@ -2849,6 +2895,11 @@ mod tests {
|
|||
is_hardcoded: false,
|
||||
}],
|
||||
},
|
||||
extra_user_object_classes: vec![
|
||||
LdapObjectClass::from("customUserClass"),
|
||||
LdapObjectClass::from("myUserClass"),
|
||||
],
|
||||
extra_group_object_classes: vec![LdapObjectClass::from("customGroupClass")],
|
||||
})
|
||||
});
|
||||
let mut ldap_handler = setup_bound_readonly_handler(mock).await;
|
||||
|
|
|
@ -47,6 +47,10 @@ mockall::mock! {
|
|||
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()>;
|
||||
async fn delete_user_attribute(&self, name: &AttributeName) -> Result<()>;
|
||||
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()>;
|
||||
async fn add_user_object_class(&self, request: &LdapObjectClass) -> Result<()>;
|
||||
async fn add_group_object_class(&self, request: &LdapObjectClass) -> Result<()>;
|
||||
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
|
||||
}
|
||||
#[async_trait]
|
||||
impl BackendHandler for TestBackendHandler {}
|
||||
|
@ -102,6 +106,8 @@ pub fn setup_default_schema(mock: &mut MockTestBackendHandler) {
|
|||
group_attributes: AttributeList {
|
||||
attributes: Vec::new(),
|
||||
},
|
||||
extra_user_object_classes: vec![LdapObjectClass::from("customUserClass")],
|
||||
extra_group_object_classes: Vec::new(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue