Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
02572bdf45 Switch to action-rs/toolchain for Rust installation in CI
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-21 00:01:39 +00:00
copilot-swe-agent[bot]
30cb4ceae0 Use variable for Rust version in CI workflow
Replace hardcoded "1.85.0" references with RUST_VERSION environment variable for better maintainability. Now the Rust version only needs to be updated in one place.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 23:58:18 +00:00
copilot-swe-agent[bot]
2e2a67c13b Pin Rust version to 1.85.0 in CI workflows and Docker images
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 23:52:12 +00:00
copilot-swe-agent[bot]
4d8fccb502 Initial plan 2025-08-20 23:43:32 +00:00
28 changed files with 93 additions and 425 deletions

View File

@@ -1,4 +1,4 @@
FROM rust:1.85
FROM rust:1.85.0
ARG USERNAME=lldapdev
# We need to keep the user as 1001 to match the GitHub runner's UID.

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @nitnelave

View File

@@ -1,5 +1,5 @@
# Keep tracking base image
FROM rust:1.85-slim-bookworm
FROM rust:1.85.0-slim-bookworm
# Set needed env path
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"

View File

@@ -8,7 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: 1.85.0
RUST_VERSION: "1.85.0"
jobs:
pre_job:
@@ -35,18 +35,18 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v5.0.0
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
- name: Install Rust ${{ env.RUST_VERSION }}
uses: actions-rs/toolchain@v1
with:
toolchain: "${{ env.MSRV }}"
toolchain: ${{ env.RUST_VERSION }}
override: true
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose --workspace
- name: Run tests
run: cargo +${{steps.toolchain.outputs.name}} test --verbose --workspace
run: cargo test --verbose --workspace
- name: Generate GraphQL schema
run: cargo +${{steps.toolchain.outputs.name}} run -- export_graphql_schema -o generated_schema.graphql
run: cargo run -- export_graphql_schema -o generated_schema.graphql
- name: Check schema
run: diff schema.graphql generated_schema.graphql || (echo "The schema file is out of date. Please run `./export_schema.sh`" && false)
@@ -59,14 +59,21 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v5.0.0
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
- name: Install Rust ${{ env.RUST_VERSION }}
uses: actions-rs/toolchain@v1
with:
toolchain: "${{ env.MSRV }}"
components: clippy
toolchain: ${{ env.RUST_VERSION }}
override: true
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo +${{steps.toolchain.outputs.name}} clippy --tests --workspace -- -D warnings
- name: Run cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --tests --all -- -D warnings
format:
name: cargo fmt
@@ -76,14 +83,21 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v5.0.0
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
- name: Install Rust ${{ env.RUST_VERSION }}
uses: actions-rs/toolchain@v1
with:
toolchain: "${{ env.MSRV }}"
components: rustfmt
toolchain: ${{ env.RUST_VERSION }}
override: true
components: rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo +${{steps.toolchain.outputs.name}} fmt --check --all
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
coverage:
name: Code coverage
@@ -96,8 +110,17 @@ jobs:
- name: Checkout sources
uses: actions/checkout@v5.0.0
- name: Install Rust
run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
- name: Install Rust ${{ env.RUST_VERSION }} and nightly
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
override: true
components: llvm-tools-preview
- name: Install nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov

View File

@@ -16,6 +16,7 @@ edition = "2024"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
repository = "https://github.com/lldap/lldap"
rust-version = "1.85.0"
[profile.release]
lto = true

View File

@@ -1,5 +1,5 @@
# Build image
FROM rust:alpine3.21 AS chef
FROM rust:1.85.0-alpine3.21 AS chef
RUN set -x \
# Add user

View File

@@ -13,12 +13,7 @@ pub mod group {
"creation_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "creationdate",
aliases: vec![name, "createtimestamp"],
}),
"modified_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "modifydate",
aliases: vec![name, "modifytimestamp"],
aliases: vec![name, "createtimestamp", "modifytimestamp"],
}),
"display_name" => Some(AttributeDescription {
attribute_identifier: name,
@@ -65,17 +60,7 @@ pub mod user {
"creation_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "creationdate",
aliases: vec![name, "createtimestamp"],
}),
"modified_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "modifydate",
aliases: vec![name, "modifytimestamp"],
}),
"password_modified_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "passwordmodifydate",
aliases: vec![name, "pwdchangedtime"],
aliases: vec![name, "createtimestamp", "modifytimestamp"],
}),
"display_name" => Some(AttributeDescription {
attribute_identifier: name,

View File

@@ -14,7 +14,6 @@ pub struct Model {
pub lowercase_display_name: String,
pub creation_date: chrono::NaiveDateTime,
pub uuid: Uuid,
pub modified_date: chrono::NaiveDateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -40,7 +39,6 @@ impl From<Model> for lldap_domain::types::Group {
uuid: group.uuid,
users: vec![],
attributes: Vec::new(),
modified_date: group.modified_date,
}
}
}
@@ -53,7 +51,6 @@ impl From<Model> for lldap_domain::types::GroupDetails {
creation_date: group.creation_date,
uuid: group.uuid,
attributes: Vec::new(),
modified_date: group.modified_date,
}
}
}

View File

@@ -21,8 +21,6 @@ pub struct Model {
pub totp_secret: Option<String>,
pub mfa_type: Option<String>,
pub uuid: Uuid,
pub modified_date: chrono::NaiveDateTime,
pub password_modified_date: chrono::NaiveDateTime,
}
impl EntityName for Entity {
@@ -42,8 +40,6 @@ pub enum Column {
TotpSecret,
MfaType,
Uuid,
ModifiedDate,
PasswordModifiedDate,
}
impl ColumnTrait for Column {
@@ -60,8 +56,6 @@ impl ColumnTrait for Column {
Column::TotpSecret => ColumnType::String(StringLen::N(64)),
Column::MfaType => ColumnType::String(StringLen::N(64)),
Column::Uuid => ColumnType::String(StringLen::N(36)),
Column::ModifiedDate => ColumnType::DateTime,
Column::PasswordModifiedDate => ColumnType::DateTime,
}
.def()
}
@@ -127,8 +121,6 @@ impl From<Model> for lldap_domain::types::User {
creation_date: user.creation_date,
uuid: user.uuid,
attributes: Vec::new(),
modified_date: user.modified_date,
password_modified_date: user.password_modified_date,
}
}
}

View File

@@ -34,24 +34,6 @@ impl From<Schema> for PublicSchema {
is_hardcoded: true,
is_readonly: true,
},
AttributeSchema {
name: "modified_date".into(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
is_readonly: true,
},
AttributeSchema {
name: "password_modified_date".into(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
is_readonly: true,
},
AttributeSchema {
name: "mail".into(),
attribute_type: AttributeType::String,
@@ -103,15 +85,6 @@ impl From<Schema> for PublicSchema {
is_hardcoded: true,
is_readonly: true,
},
AttributeSchema {
name: "modified_date".into(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
is_readonly: true,
},
AttributeSchema {
name: "uuid".into(),
attribute_type: AttributeType::String,

View File

@@ -546,8 +546,6 @@ pub struct User {
pub creation_date: NaiveDateTime,
pub uuid: Uuid,
pub attributes: Vec<Attribute>,
pub modified_date: NaiveDateTime,
pub password_modified_date: NaiveDateTime,
}
#[cfg(feature = "test")]
@@ -561,8 +559,6 @@ impl Default for User {
creation_date: epoch,
uuid: Uuid::from_name_and_date("", &epoch),
attributes: Vec::new(),
modified_date: epoch,
password_modified_date: epoch,
}
}
}
@@ -658,7 +654,6 @@ pub struct Group {
pub uuid: Uuid,
pub users: Vec<UserId>,
pub attributes: Vec<Attribute>,
pub modified_date: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -668,7 +663,6 @@ pub struct GroupDetails {
pub creation_date: NaiveDateTime,
pub uuid: Uuid,
pub attributes: Vec<Attribute>,
pub modified_date: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -72,4 +72,4 @@ path = "../test-utils"
[dev-dependencies.tokio]
features = ["full"]
version = "1.25"
version = "1.25"

View File

@@ -716,8 +716,6 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
let value: Option<DomainAttributeValue> = match attribute_schema.name.as_str() {
"user_id" => Some(user.user_id.clone().into_string().into()),
"creation_date" => Some(user.creation_date.into()),
"modified_date" => Some(user.modified_date.into()),
"password_modified_date" => Some(user.password_modified_date.into()),
"mail" => Some(user.email.clone().into_string().into()),
"uuid" => Some(user.uuid.clone().into_string().into()),
"display_name" => user.display_name.as_ref().map(|d| d.clone().into()),
@@ -762,7 +760,6 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
match attribute_schema.name.as_str() {
"group_id" => (group.id.0 as i64).into(),
"creation_date" => group.creation_date.into(),
"modified_date" => group.modified_date.into(),
"uuid" => group.uuid.clone().into_string().into(),
"display_name" => group.display_name.clone().into_string().into(),
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
@@ -805,7 +802,6 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
match attribute_schema.name.as_str() {
"group_id" => (group.group_id.0 as i64).into(),
"creation_date" => group.creation_date.into(),
"modified_date" => group.modified_date.into(),
"uuid" => group.uuid.clone().into_string().into(),
"display_name" => group.display_name.clone().into_string().into(),
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
@@ -962,7 +958,6 @@ mod tests {
name: "club_name".into(),
value: "Gang of Four".to_string().into(),
}],
modified_date: chrono::Utc.timestamp_nanos(42).naive_utc(),
});
groups.insert(GroupDetails {
group_id: GroupId(7),
@@ -970,7 +965,6 @@ mod tests {
creation_date: chrono::Utc.timestamp_nanos(12).naive_utc(),
uuid: lldap_domain::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_nanos(12).naive_utc(),
});
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
@@ -999,14 +993,6 @@ mod tests {
"name": "mail",
"value": ["bob@bobbers.on"],
},
{
"name": "modified_date",
"value": ["1970-01-01T00:00:00+00:00"],
},
{
"name": "password_modified_date",
"value": ["1970-01-01T00:00:00+00:00"],
},
{
"name": "user_id",
"value": ["bob"],
@@ -1040,10 +1026,6 @@ mod tests {
"name": "group_id",
"value": ["3"],
},
{
"name": "modified_date",
"value": ["1970-01-01T00:00:00.000000042+00:00"],
},
{
"name": "uuid",
"value": ["a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"],
@@ -1071,10 +1053,6 @@ mod tests {
"name": "group_id",
"value": ["7"],
},
{
"name": "modified_date",
"value": ["1970-01-01T00:00:00.000000012+00:00"],
},
{
"name": "uuid",
"value": ["b1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"],
@@ -1268,22 +1246,6 @@ mod tests {
"isEditable": true,
"isHardcoded": true,
},
{
"name": "modified_date",
"attributeType": "DATE_TIME",
"isList": false,
"isVisible": true,
"isEditable": false,
"isHardcoded": true,
},
{
"name": "password_modified_date",
"attributeType": "DATE_TIME",
"isList": false,
"isVisible": true,
"isEditable": false,
"isHardcoded": true,
},
{
"name": "user_id",
"attributeType": "STRING",
@@ -1329,14 +1291,6 @@ mod tests {
"isEditable": false,
"isHardcoded": true,
},
{
"name": "modified_date",
"attributeType": "DATE_TIME",
"isList": false,
"isVisible": true,
"isEditable": false,
"isHardcoded": true,
},
{
"name": "uuid",
"attributeType": "STRING",
@@ -1411,8 +1365,6 @@ mod tests {
{"name": "creation_date"},
{"name": "display_name"},
{"name": "mail"},
{"name": "modified_date"},
{"name": "password_modified_date"},
{"name": "user_id"},
{"name": "uuid"},
],

View File

@@ -124,7 +124,6 @@ mod tests {
users: vec![UserId::new("bob")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
@@ -219,7 +218,6 @@ mod tests {
users: vec![UserId::new("bob")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;

View File

@@ -72,12 +72,6 @@ pub fn get_group_attribute(
.to_rfc3339()
.into_bytes(),
],
GroupFieldType::ModifiedDate => vec![
chrono::Utc
.from_utc_datetime(&group.modified_date)
.to_rfc3339()
.into_bytes(),
],
GroupFieldType::Member => group
.users
.iter()
@@ -266,10 +260,6 @@ fn convert_group_filter(
code: LdapResultCode::UnwillingToPerform,
message: "Creation date filter for groups not supported".to_owned(),
}),
GroupFieldType::ModifiedDate => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: "Modified date filter for groups not supported".to_owned(),
}),
}
}
LdapFilter::And(filters) => Ok(GroupRequestFilter::And(

View File

@@ -93,18 +93,6 @@ pub fn get_user_attribute(
.to_rfc3339()
.into_bytes(),
],
UserFieldType::PrimaryField(UserColumn::ModifiedDate) => vec![
chrono::Utc
.from_utc_datetime(&user.modified_date)
.to_rfc3339()
.into_bytes(),
],
UserFieldType::PrimaryField(UserColumn::PasswordModifiedDate) => vec![
chrono::Utc
.from_utc_datetime(&user.password_modified_date)
.to_rfc3339()
.into_bytes(),
],
UserFieldType::Attribute(attr, _, _) => get_custom_attribute(&user.attributes, &attr)?,
UserFieldType::NoMatch => match attribute.as_str() {
"1.1" => return None,

View File

@@ -239,15 +239,9 @@ pub fn map_user_field(field: &AttributeName, schema: &PublicSchema) -> UserField
AttributeType::JpegPhoto,
false,
),
"creationdate" | "createtimestamp" | "creation_date" => {
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
UserFieldType::PrimaryField(UserColumn::CreationDate)
}
"modifytimestamp" | "modifydate" | "modified_date" => {
UserFieldType::PrimaryField(UserColumn::ModifiedDate)
}
"pwdchangedtime" | "passwordmodifydate" | "password_modified_date" => {
UserFieldType::PrimaryField(UserColumn::PasswordModifiedDate)
}
"entryuuid" | "uuid" => UserFieldType::PrimaryField(UserColumn::Uuid),
_ => schema
.get_schema()
@@ -263,7 +257,6 @@ pub enum GroupFieldType {
GroupId,
DisplayName,
CreationDate,
ModifiedDate,
ObjectClass,
Dn,
// Like Dn, but returned as part of the attributes.
@@ -279,8 +272,9 @@ pub fn map_group_field(field: &AttributeName, schema: &PublicSchema) -> GroupFie
"entrydn" => GroupFieldType::EntryDn,
"objectclass" => GroupFieldType::ObjectClass,
"cn" | "displayname" | "uid" | "display_name" | "id" => GroupFieldType::DisplayName,
"creationdate" | "createtimestamp" | "creation_date" => GroupFieldType::CreationDate,
"modifytimestamp" | "modifydate" | "modified_date" => GroupFieldType::ModifiedDate,
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
GroupFieldType::CreationDate
}
"member" | "uniquemember" => GroupFieldType::Member,
"entryuuid" | "uuid" => GroupFieldType::Uuid,
"group_id" | "groupid" => GroupFieldType::GroupId,

View File

@@ -154,7 +154,6 @@ mod tests {
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
users: Vec::new(),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
mock.expect_delete_group()
@@ -285,7 +284,6 @@ mod tests {
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
users: Vec::new(),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
mock.expect_delete_group()

View File

@@ -398,7 +398,6 @@ pub mod tests {
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
});
Ok(set)
});

View File

@@ -158,7 +158,6 @@ mod tests {
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
});
}
Ok(g)

View File

@@ -263,7 +263,6 @@ pub mod tests {
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
});
Ok(set)
});
@@ -521,7 +520,6 @@ pub mod tests {
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
});
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))

View File

@@ -236,7 +236,6 @@ pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
vals: {
let hardcoded_attributes = [
b"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' 'user_id' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE NO-USER-MODIFICATION )".to_vec(),
b"( 1.2.840.113556.1.2.102 NAME 'memberOf' DESC 'Group that the entry belongs to' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'iPlanet Delegated Administrator' )".to_vec(),
b"( 1.3.6.1.1.16.4 NAME ( 'entryUUID' 'uuid' ) DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )".to_vec(),
b"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
@@ -256,10 +255,11 @@ pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
b"( 10.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
b"( 10.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
];
let num_hardcoded_attributes = hardcoded_attributes.len();
hardcoded_attributes.into_iter().chain(
ldap_schema_description
.formatted_attribute_list(
4, // The number of hardcoded attributes starting with "10." (LLDAP custom range)
num_hardcoded_attributes,
vec!["creation_date", "display_name", "last_name", "user_id", "uuid"]
)
).collect()
@@ -613,7 +613,6 @@ mod tests {
atype: "attributeTypes".to_owned(),
vals: vec![
b"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' 'user_id' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE NO-USER-MODIFICATION )".to_vec(),
b"( 1.2.840.113556.1.2.102 NAME 'memberOf' DESC 'Group that the entry belongs to' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'iPlanet Delegated Administrator' )".to_vec(),
b"( 1.3.6.1.1.16.4 NAME ( 'entryUUID' 'uuid' ) DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )".to_vec(),
b"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
@@ -632,15 +631,12 @@ mod tests {
b"( 10.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
b"( 10.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
b"( 10.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 10.4 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(),
b"( 10.5 NAME 'first_name' DESC 'LLDAP: builtin attribute' SUP String )"
b"( 10.19 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(),
b"( 10.20 NAME 'first_name' DESC 'LLDAP: builtin attribute' SUP String )"
.to_vec(),
b"( 10.6 NAME 'mail' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 10.7 NAME 'modified_date' DESC 'LLDAP: builtin attribute' SUP DateTime )".to_vec(),
b"( 10.8 NAME 'password_modified_date' DESC 'LLDAP: builtin attribute' SUP DateTime )".to_vec(),
b"( 10.9 NAME 'group_id' DESC 'LLDAP: builtin attribute' SUP Integer )"
b"( 10.21 NAME 'mail' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 10.22 NAME 'group_id' DESC 'LLDAP: builtin attribute' SUP Integer )"
.to_vec(),
b"( 10.10 NAME 'modified_date' DESC 'LLDAP: builtin attribute' SUP DateTime )".to_vec(),
]
}
);
@@ -649,8 +645,8 @@ mod tests {
LdapPartialAttribute {
atype: "objectClasses".to_owned(),
vals: vec![
b"( 3.0 NAME ( 'inetOrgPerson' 'posixAccount' 'mailAccount' 'person' 'customUserClass' ) DESC 'LLDAP builtin: a person' STRUCTURAL MUST ( mail $ user_id ) MAY ( avatar $ creation_date $ display_name $ first_name $ last_name $ modified_date $ password_modified_date $ uuid ) )".to_vec(),
b"( 3.1 NAME ( 'groupOfUniqueNames' 'groupOfNames' ) DESC 'LLDAP builtin: a group' STRUCTURAL MUST ( display_name ) MAY ( creation_date $ group_id $ modified_date $ uuid ) )".to_vec(),
b"( 3.0 NAME ( 'inetOrgPerson' 'posixAccount' 'mailAccount' 'person' 'customUserClass' ) DESC 'LLDAP builtin: a person' STRUCTURAL MUST ( mail $ user_id ) MAY ( avatar $ creation_date $ display_name $ first_name $ last_name $ uuid ) )".to_vec(),
b"( 3.1 NAME ( 'groupOfUniqueNames' 'groupOfNames' ) DESC 'LLDAP builtin: a group' STRUCTURAL MUST ( display_name ) MAY ( creation_date $ group_id $ uuid ) )".to_vec(),
]
}
);
@@ -738,7 +734,6 @@ mod tests {
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}]),
}])
});
@@ -844,14 +839,6 @@ mod tests {
.with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
.unwrap()
.naive_utc(),
modified_date: Utc
.with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
.unwrap()
.naive_utc(),
password_modified_date: Utc
.with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
.unwrap()
.naive_utc(),
},
groups: None,
},
@@ -986,7 +973,6 @@ mod tests {
users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
},
Group {
id: GroupId(3),
@@ -995,7 +981,6 @@ mod tests {
users: vec![UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
},
])
});
@@ -1086,7 +1071,6 @@ mod tests {
users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
@@ -1137,7 +1121,6 @@ mod tests {
users: vec![],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
@@ -1209,7 +1192,6 @@ mod tests {
users: vec![],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
@@ -1261,7 +1243,6 @@ mod tests {
name: "Attr".into(),
value: "TEST".to_string().into(),
}],
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
mock.expect_get_schema().returning(|| {
@@ -1719,7 +1700,6 @@ mod tests {
users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
@@ -1804,7 +1784,6 @@ mod tests {
users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
@@ -2065,7 +2044,6 @@ mod tests {
name: "club_name".into(),
value: "Breakfast Club".to_string().into(),
}],
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
}])
});
mock.expect_get_schema().returning(|| {

View File

@@ -206,7 +206,6 @@ impl GroupBackendHandler for SqlBackendHandler {
lowercase_display_name: Set(lower_display_name),
creation_date: Set(now),
uuid: Set(uuid),
modified_date: Set(now),
..Default::default()
};
Ok(self
@@ -269,12 +268,10 @@ impl SqlBackendHandler {
.display_name
.as_ref()
.map(|s| s.as_str().to_lowercase());
let now = chrono::Utc::now().naive_utc();
let update_group = model::groups::ActiveModel {
group_id: Set(request.group_id),
display_name: request.display_name.map(Set).unwrap_or_default(),
lowercase_display_name: lower_display_name.map(Set).unwrap_or_default(),
modified_date: Set(now),
..Default::default()
};
update_group.update(transaction).await?;

View File

@@ -27,8 +27,6 @@ pub enum Users {
TotpSecret,
MfaType,
Uuid,
ModifiedDate,
PasswordModifiedDate,
}
#[derive(DeriveIden, PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy)]
@@ -39,7 +37,6 @@ pub(crate) enum Groups {
LowercaseDisplayName,
CreationDate,
Uuid,
ModifiedDate,
}
#[derive(DeriveIden, Clone, Copy)]
@@ -1115,53 +1112,6 @@ async fn migrate_to_v10(transaction: DatabaseTransaction) -> Result<DatabaseTran
Ok(transaction)
}
async fn migrate_to_v11(transaction: DatabaseTransaction) -> Result<DatabaseTransaction, DbErr> {
let builder = transaction.get_database_backend();
// Add modified_date to users table
transaction
.execute(
builder.build(
Table::alter().table(Users::Table).add_column(
ColumnDef::new(Users::ModifiedDate)
.date_time()
.not_null()
.default(chrono::Utc::now().naive_utc()),
),
),
)
.await?;
// Add password_modified_date to users table
transaction
.execute(
builder.build(
Table::alter().table(Users::Table).add_column(
ColumnDef::new(Users::PasswordModifiedDate)
.date_time()
.not_null()
.default(chrono::Utc::now().naive_utc()),
),
),
)
.await?;
// Add modified_date to groups table
transaction
.execute(
builder.build(
Table::alter().table(Groups::Table).add_column(
ColumnDef::new(Groups::ModifiedDate)
.date_time()
.not_null()
.default(chrono::Utc::now().naive_utc()),
),
),
)
.await?;
Ok(transaction)
}
// This is needed to make an array of async functions.
macro_rules! to_sync {
($l:ident) => {
@@ -1192,7 +1142,6 @@ pub(crate) async fn migrate_from_version(
to_sync!(migrate_to_v8),
to_sync!(migrate_to_v9),
to_sync!(migrate_to_v10),
to_sync!(migrate_to_v11),
];
assert_eq!(migrations.len(), (LAST_SCHEMA_VERSION.0 - 1) as usize);
for migration in 2..=last_version.0 {

View File

@@ -3,7 +3,7 @@ use async_trait::async_trait;
use base64::Engine;
use lldap_auth::opaque;
use lldap_domain::types::UserId;
use lldap_domain_handlers::handler::{BindRequest, LoginHandler, UserRequestFilter, UserListerBackendHandler};
use lldap_domain_handlers::handler::{BindRequest, LoginHandler};
use lldap_domain_model::{
error::{DomainError, Result},
model::{self, UserColumn},
@@ -60,26 +60,6 @@ impl SqlBackendHandler {
.await?
.and_then(|u| u.0))
}
#[instrument(skip(self), level = "debug", err)]
async fn find_user_id_by_email(&self, email: &str) -> Result<Option<UserId>> {
// Find user ID by email address
let users = self
.list_users(
Some(UserRequestFilter::Equality(UserColumn::Email, email.to_owned())),
false,
)
.await?;
if users.len() > 1 {
warn!("Multiple users found with email '{}', login ambiguous", email);
return Ok(None);
}
Ok(users.first().map(|user_and_groups| user_and_groups.user.user_id.clone()))
}
}
#[async_trait]
@@ -121,33 +101,14 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self,
request: login::ClientLoginStartRequest,
) -> Result<login::ServerLoginStartResponse> {
// First try to authenticate with the provided name as a user ID
let mut actual_user_id = request.username.clone();
let mut maybe_password_file = self
.get_password_file_for_user(request.username.clone())
.await?;
// If no user found by user ID, try to find by email for web UI login
if maybe_password_file.is_none() {
debug!(r#"User "{}" not found by user ID, trying email lookup for web login"#, &request.username);
if let Some(user_id_by_email) = self
.find_user_id_by_email(request.username.as_str())
.await?
{
debug!(r#"Found user by email: "{}""#, &user_id_by_email);
actual_user_id = user_id_by_email;
maybe_password_file = self
.get_password_file_for_user(actual_user_id.clone())
.await?;
}
}
info!(r#"OPAQUE login attempt for "{}" (input: "{}")"#, &actual_user_id, &request.username);
let maybe_password_file = maybe_password_file
let user_id = request.username;
info!(r#"OPAQUE login attempt for "{}""#, &user_id);
let maybe_password_file = self
.get_password_file_for_user(user_id.clone())
.await?
.map(|bytes| {
opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| {
DomainError::InternalError(format!("Corrupted password file for {}", &actual_user_id))
DomainError::InternalError(format!("Corrupted password file for {}", &user_id))
})
})
.transpose()?;
@@ -159,11 +120,11 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self.opaque_setup,
maybe_password_file,
request.login_start_request,
&actual_user_id,
&user_id,
)?;
let secret_key = self.get_orion_secret_key()?;
let server_data = login::ServerData {
username: actual_user_id,
username: user_id,
server_login: start_response.state,
};
let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?;
@@ -236,12 +197,9 @@ impl OpaqueHandler for SqlOpaqueHandler {
let password_file =
opaque::server::registration::get_password_file(request.registration_upload);
// Set the user password to the new password.
let now = chrono::Utc::now().naive_utc();
let user_update = model::users::ActiveModel {
user_id: ActiveValue::Set(username.clone()),
password_hash: ActiveValue::Set(Some(password_file.serialize())),
password_modified_date: ActiveValue::Set(now),
modified_date: ActiveValue::Set(now),
..Default::default()
};
user_update.update(&self.sql_pool).await?;
@@ -345,7 +303,6 @@ mod tests {
let handler = SqlOpaqueHandler::new(generate_random_private_key(), sql_pool.clone());
insert_user(&handler, "bob", "bob00").await;
// Test login with username (should work)
handler
.bind(BindRequest {
name: UserId::new("bob"),
@@ -353,8 +310,6 @@ mod tests {
})
.await
.unwrap();
// Test login with non-existent user
handler
.bind(BindRequest {
name: UserId::new("andrew"),
@@ -362,8 +317,6 @@ mod tests {
})
.await
.unwrap_err();
// Test login with wrong password
handler
.bind(BindRequest {
name: UserId::new("bob"),
@@ -371,39 +324,6 @@ mod tests {
})
.await
.unwrap_err();
// Test that email login is NOT supported for LDAP bind
handler
.bind(BindRequest {
name: UserId::new("bob@bob.bob"),
password: "bob00".to_string(),
})
.await
.unwrap_err();
}
#[tokio::test]
async fn test_opaque_login_with_email() {
let sql_pool = get_initialized_db().await;
crate::logging::init_for_tests();
let backend_handler = SqlBackendHandler::new(generate_random_private_key(), sql_pool);
insert_user(&backend_handler, "bob", "bob00").await;
// Test OPAQUE login with username (should work as before)
attempt_login(&backend_handler, "bob", "bob00").await.unwrap();
// Test OPAQUE login with email (new functionality)
attempt_login(&backend_handler, "bob@bob.bob", "bob00").await.unwrap();
// Test OPAQUE login with non-existent email
attempt_login(&backend_handler, "nonexistent@bob.bob", "bob00")
.await
.unwrap_err();
// Test OPAQUE login with wrong password using email
attempt_login(&backend_handler, "bob@bob.bob", "wrong_password")
.await
.unwrap_err();
}
#[tokio::test]

View File

@@ -9,7 +9,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(11);
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(10);
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
pub struct PrivateKeyHash(pub [u8; 32]);

View File

@@ -190,13 +190,11 @@ impl SqlBackendHandler {
request: UpdateUserRequest,
) -> Result<()> {
let lower_email = request.email.as_ref().map(|s| s.as_str().to_lowercase());
let now = chrono::Utc::now().naive_utc();
let update_user = model::users::ActiveModel {
user_id: ActiveValue::Set(request.user_id.clone()),
email: request.email.map(ActiveValue::Set).unwrap_or_default(),
lowercase_email: lower_email.map(ActiveValue::Set).unwrap_or_default(),
display_name: to_value(&request.display_name),
modified_date: ActiveValue::Set(now),
..Default::default()
};
let mut update_user_attributes = Vec::new();
@@ -327,8 +325,6 @@ impl UserBackendHandler for SqlBackendHandler {
display_name: to_value(&request.display_name),
creation_date: ActiveValue::Set(now),
uuid: ActiveValue::Set(uuid),
modified_date: ActiveValue::Set(now),
password_modified_date: ActiveValue::Set(now),
..Default::default()
};
let mut new_user_attributes = Vec::new();
@@ -395,70 +391,24 @@ impl UserBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", err, fields(user_id = ?user_id.as_str(), group_id))]
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
let user_id = user_id.clone();
self.sql_pool
.transaction::<_, _, sea_orm::DbErr>(|transaction| {
Box::pin(async move {
let new_membership = model::memberships::ActiveModel {
user_id: ActiveValue::Set(user_id),
group_id: ActiveValue::Set(group_id),
};
new_membership.insert(transaction).await?;
// Update group modification time
let now = chrono::Utc::now().naive_utc();
let update_group = model::groups::ActiveModel {
group_id: Set(group_id),
modified_date: Set(now),
..Default::default()
};
update_group.update(transaction).await?;
Ok(())
})
})
.await?;
let new_membership = model::memberships::ActiveModel {
user_id: ActiveValue::Set(user_id.clone()),
group_id: ActiveValue::Set(group_id),
};
new_membership.insert(&self.sql_pool).await?;
Ok(())
}
#[instrument(skip_all, level = "debug", err, fields(user_id = ?user_id.as_str(), group_id))]
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
let user_id = user_id.clone();
self.sql_pool
.transaction::<_, _, sea_orm::DbErr>(|transaction| {
Box::pin(async move {
let res = model::Membership::delete_by_id((user_id.clone(), group_id))
.exec(transaction)
.await?;
if res.rows_affected == 0 {
return Err(sea_orm::DbErr::Custom(format!(
"No such membership: '{user_id}' -> {group_id:?}"
)));
}
// Update group modification time
let now = chrono::Utc::now().naive_utc();
let update_group = model::groups::ActiveModel {
group_id: Set(group_id),
modified_date: Set(now),
..Default::default()
};
update_group.update(transaction).await?;
Ok(())
})
})
.await
.map_err(|e| match e {
sea_orm::TransactionError::Connection(sea_orm::DbErr::Custom(msg)) => {
DomainError::EntityNotFound(msg)
}
sea_orm::TransactionError::Transaction(sea_orm::DbErr::Custom(msg)) => {
DomainError::EntityNotFound(msg)
}
sea_orm::TransactionError::Connection(e) => DomainError::DatabaseError(e),
sea_orm::TransactionError::Transaction(e) => DomainError::DatabaseError(e),
})?;
let res = model::Membership::delete_by_id((user_id.clone(), group_id))
.exec(&self.sql_pool)
.await?;
if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!(
"No such membership: '{user_id}' -> {group_id:?}"
)));
}
Ok(())
}
}

View File

@@ -125,7 +125,7 @@ async fn setup_sql_tables(database_url: &DatabaseUrl) -> Result<DatabaseConnecti
}
#[instrument(skip_all)]
async fn set_up_server(config: Configuration) -> Result<(ServerBuilder, DatabaseConnection)> {
async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
info!("Starting LLDAP version {}", env!("CARGO_PKG_VERSION"));
let sql_pool = setup_sql_tables(&config.database_url).await?;
@@ -214,9 +214,9 @@ async fn set_up_server(config: Configuration) -> Result<(ServerBuilder, Database
.await
.context("while binding the TCP server")?;
// Run every hour.
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool.clone());
let scheduler = Scheduler::new("0 0 * * * * *", sql_pool);
scheduler.start();
Ok((server_builder, sql_pool))
Ok(server_builder)
}
async fn run_server_command(opts: RunOpts) -> Result<()> {
@@ -225,14 +225,9 @@ async fn run_server_command(opts: RunOpts) -> Result<()> {
let config = configuration::init(opts)?;
logging::init(&config)?;
let (server, sql_pool) = set_up_server(config).await?;
let server = server.workers(1);
let server = set_up_server(config).await?.workers(1);
let result = server.run().await.context("while starting the server");
if let Err(e) = sql_pool.close().await {
error!("Error closing database connection pool: {}", e);
}
result
server.run().await.context("while starting the server")
}
async fn send_test_email_command(opts: TestEmailOpts) -> Result<()> {
@@ -280,11 +275,8 @@ async fn create_schema_command(opts: RunOpts) -> Result<()> {
debug!("CLI: {:#?}", &opts);
let config = configuration::init(opts)?;
logging::init(&config)?;
let sql_pool = setup_sql_tables(&config.database_url).await?;
setup_sql_tables(&config.database_url).await?;
info!("Schema created successfully.");
if let Err(e) = sql_pool.close().await {
error!("Error closing database connection pool: {}", e);
}
Ok(())
}