diff --git a/crates/ogar-ad/src/lib.rs b/crates/ogar-ad/src/lib.rs index 0322b783..29f1caa2 100644 --- a/crates/ogar-ad/src/lib.rs +++ b/crates/ogar-ad/src/lib.rs @@ -17,20 +17,22 @@ pub mod ldif; use ogar_dir_core::dn::Dn; +use ogar_dir_core::label::{self, CloudLabel}; use ogar_dir_core::record::{FLAG_DN_UNENCODED, FLAG_NON_OU_CONTAINER}; use ogar_dir_core::{ AttrDef, AttrKind, DirRecord, Guid128, OuDictionary, SchemaFamily, SchemaId, ValuePool, }; /// Encoder schema version understood by this crate. -pub const SCHEMA_VERSION: u16 = 1; +pub const SCHEMA_VERSION: u16 = 3; /// This crate's schema id. pub const SCHEMA: SchemaId = SchemaId { family: SchemaFamily::AdDs, version: SCHEMA_VERSION, }; -/// The embedded AD attribute table, v1. +/// The embedded AD attribute table. Append-only: each entry carries the +/// schema version that introduced it (`since`); the name is kept from v1. /// /// Selection: the attributes needed to (a) name an account in every form AD /// and Exchange use (`sAMAccountName`, `userPrincipalName`, `mail`, @@ -38,8 +40,22 @@ pub const SCHEMA: SchemaId = SchemaId { /// (`objectClass`, `objectSid`, `userAccountControl`), (c) say where it is /// (`distinguishedName`) and (d) date it (`whenCreated`, `whenChanged`), plus /// the display triplet. Group membership (`memberOf`/`member`) is a relation -/// and becomes edges later, never an inline list. Further Exchange attributes -/// (`msExch*`) are deferred until a consumer needs them. +/// and becomes edges later, never an inline list. +/// +/// v2 adds the Exchange recipient triplet, raw, for the hybrid recipient +/// model (`ogar-dir-sim::exchange`): `msExchRemoteRecipientType` (flags, +/// numeric), `msExchRecipientDisplayType` (signed, numeric, bit-cast like +/// every signed LDAP integer here) and `msExchRecipientTypeDetails` (64-bit, +/// so kept as its LDAP decimal text in a pooled slot). Other `msExch*` +/// attributes stay out until a consumer needs them. +/// +/// v3 adds the hybrid identity anchors as 128-bit ids in inline guid slots, +/// never as text: `mS-DS-ConsistencyGuid` (16 mixed-endian bytes, the source +/// anchor whose base64 is the cloud `ImmutableId`) and +/// `msDS-ExternalDirectoryObjectId` (written back by Entra Connect as +/// `User_`). The `User_` / `Group_` label is stripped on the +/// way in — it must match the object's kind — and added back on the way out +/// from the kind ([`external_directory_object_id`]); it is never stored. pub const SCHEMA_V1: &[AttrDef] = &[ AttrDef { name: "distinguishedName", @@ -131,6 +147,36 @@ pub const SCHEMA_V1: &[AttrDef] = &[ kind: AttrKind::U32, since: 1, }, + AttrDef { + name: "msExchRemoteRecipientType", + slot: 1, + kind: AttrKind::U32, + since: 2, + }, + AttrDef { + name: "msExchRecipientDisplayType", + slot: 2, + kind: AttrKind::U32, + since: 2, + }, + AttrDef { + name: "msExchRecipientTypeDetails", + slot: 14, + kind: AttrKind::Str, + since: 2, + }, + AttrDef { + name: "mS-DS-ConsistencyGuid", + slot: 0, + kind: AttrKind::Guid, + since: 3, + }, + AttrDef { + name: "msDS-ExternalDirectoryObjectId", + slot: 1, + kind: AttrKind::Guid, + since: 3, + }, ]; /// AD object kinds (family-scoped codes in `object_kind`). @@ -152,6 +198,20 @@ pub enum AdKind { } impl AdKind { + /// From the record's `object_kind` code. + pub fn from_code(code: u16) -> Option { + [ + Self::Other, + Self::User, + Self::Group, + Self::Computer, + Self::Contact, + Self::OrganizationalUnit, + ] + .into_iter() + .find(|k| *k as u16 == code) + } + /// From `objectClass` values; most specific class wins. pub fn from_object_class>(classes: &[S]) -> Self { let has = |c: &str| classes.iter().any(|x| x.as_ref().eq_ignore_ascii_case(c)); @@ -202,6 +262,9 @@ pub enum AdError { MultipleValues(&'static str), /// A numeric attribute did not parse. BadNumber(&'static str), + /// A 128-bit id attribute was not a non-nil id in its source spelling + /// (16 raw bytes; `