From 2eb7647590cc45155bd937718ea571db7340dd91 Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Tue, 21 Jul 2026 02:31:23 -0400 Subject: [PATCH 1/2] Separate adapted libraries. --- .github/agents/CSharpExpert.agent.md | 204 +++++++ .../agents/NETSerilogLoggerExpert.agent.md | 137 +++++ .github/agents/READMESpecialist.agent.md | 76 +++ Bitai.Ldap.Helper.sln | 126 ---- Bitai.Ldap.Helper.slnx | 24 + Clean Bin & Obj folders.ps1 | 51 -- README.md | 554 +++++++++++++----- ...PHelper.LdapAdapters.LdapHelperMock.csproj | 49 ++ .../LICENSE.md | 0 .../LdapData/MockLdapDataSeeder.cs | 4 +- .../LdapData/MockLdapDataStore.cs | 4 +- .../MockLdapAttributeAdapter.cs | 4 +- .../MockLdapAttributeSetAdapter.cs | 2 +- .../MockLdapConnectionAdapter.cs | 2 +- .../MockLdapConnectionFactoryAdapter.cs | 2 +- .../MockLdapEntryAdapter.cs | 2 +- .../MockLdapMessageAdapter.cs | 2 +- .../MockLdapModificationAdapter.cs | 2 +- .../MockLdapPersistentConnectionAdapter.cs | 4 +- ...kLdapPersistentConnectionFactoryAdapter.cs | 2 +- .../MockLdapSearchQueueAdapter.cs | 2 +- .../README.md | 457 +++++++++++++++ ...itai.LDAPHelper.LdapAdapters.Novell.csproj | 46 ++ .../LICENSE.md | 21 + .../NovellLdapAttributeAdapter.cs | 0 .../NovellLdapAttributeSetAdapter.cs | 0 .../NovellLdapConnectionAdapter.cs | 0 .../NovellLdapConnectionFactoryAdapter.cs | 0 .../NovellLdapEntryAdapter.cs | 0 .../NovellLdapMessageAdapter.cs | 0 .../NovellLdapModificationAdapter.cs | 0 .../NovellLdapSearchQueueAdapter.cs | 0 .../README.md | 382 ++++++++++++ .../Bitai.LDAPHelper.Demo.csproj | 4 +- demo/Bitai.LDAPHelper.Demo/Program.cs | 4 +- src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj | 7 +- src/Bitai.LDAPHelper/LdapAdapters/Enums.cs | 43 ++ .../LdapAdapters/IConnectionInfo.cs | 27 + .../LdapAdapters/ILdapAttributeAdapter.cs | 27 + .../LdapAdapters/ILdapAttributeSetAdapter.cs | 42 ++ .../LdapAdapters/ILdapConnectionAdapter.cs | 101 ++++ .../ILdapConnectionFactoryAdapter.cs | 25 + .../LdapAdapters/ILdapEntryAdapter.cs | 18 + .../LdapAdapters/ILdapMessageAdapter.cs | 22 + .../LdapAdapters/ILdapModificationAdapter.cs | 17 + .../ILdapSearchConstraintsAdapter.cs | 17 + .../LdapAdapters/ILdapSearchQueueAdapter.cs | 13 + .../LdapAdapters/ISearchLimits.cs | 27 + src/Bitai.LDAPHelper/LdapAdapters/README.md | 110 ++++ .../Bitai.LDAPHelper.Tests.Mocks.csproj | 40 -- tests/Bitai.LDAPHelper.Tests.Mocks/README.md | 146 ----- .../AccountManagerAdapterTests.cs | 2 +- .../AuthenticatorAdapterTests.cs | 2 +- tests/Bitai.LDAPHelper.Tests/BaseTests.cs | 2 +- .../Bitai.LDAPHelper.Tests.csproj | 6 +- .../GroupMembershipValidatorTests.cs | 2 +- .../SearcherAdapterTests.cs | 2 +- 57 files changed, 2322 insertions(+), 543 deletions(-) create mode 100644 .github/agents/CSharpExpert.agent.md create mode 100644 .github/agents/NETSerilogLoggerExpert.agent.md create mode 100644 .github/agents/READMESpecialist.agent.md delete mode 100644 Bitai.Ldap.Helper.sln create mode 100644 Bitai.Ldap.Helper.slnx delete mode 100644 Clean Bin & Obj folders.ps1 create mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj rename {tests/Bitai.LDAPHelper.Tests.Mocks => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/LICENSE.md (100%) rename {tests/Bitai.LDAPHelper.Tests.Mocks => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/LdapData/MockLdapDataSeeder.cs (99%) rename {tests/Bitai.LDAPHelper.Tests.Mocks => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/LdapData/MockLdapDataStore.cs (95%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapAttributeAdapter.cs (99%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapAttributeSetAdapter.cs (98%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapConnectionAdapter.cs (98%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapConnectionFactoryAdapter.cs (95%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapEntryAdapter.cs (96%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapMessageAdapter.cs (89%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapModificationAdapter.cs (94%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapPersistentConnectionAdapter.cs (98%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapPersistentConnectionFactoryAdapter.cs (93%) rename {tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters => adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock}/MockLdapSearchQueueAdapter.cs (89%) create mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md create mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj create mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.Novell/LICENSE.md rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapAttributeAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapAttributeSetAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapConnectionAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapConnectionFactoryAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapEntryAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapMessageAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapModificationAdapter.cs (100%) rename {src/Bitai.LDAPHelper/LdapAdapters/Novell => adapters/Bitai.LDAPHelper.LdapAdapters.Novell}/NovellLdapSearchQueueAdapter.cs (100%) create mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.Novell/README.md create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/Enums.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/IConnectionInfo.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeSetAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionFactoryAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapEntryAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapMessageAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapModificationAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchConstraintsAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchQueueAdapter.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/ISearchLimits.cs create mode 100644 src/Bitai.LDAPHelper/LdapAdapters/README.md delete mode 100644 tests/Bitai.LDAPHelper.Tests.Mocks/Bitai.LDAPHelper.Tests.Mocks.csproj delete mode 100644 tests/Bitai.LDAPHelper.Tests.Mocks/README.md diff --git a/.github/agents/CSharpExpert.agent.md b/.github/agents/CSharpExpert.agent.md new file mode 100644 index 0000000..f20b420 --- /dev/null +++ b/.github/agents/CSharpExpert.agent.md @@ -0,0 +1,204 @@ +--- +name: ".NET C# Expert" +description: An agent designed to assist with software development tasks for .NET projects. +# version: 2026-01-20a +--- + +You are an expert C#/.NET developer. You help with .NET tasks by giving clean, well-designed, error-free, fast, secure, readable, and maintainable code that follows .NET conventions. You also give insights, best practices, general software design tips, and testing best practices. + +You are familiar with the currently released .NET and C# versions (for example, up to .NET 10 and C# 14 at the time of writing). (Refer to https://learn.microsoft.com/en-us/dotnet/core/whats-new +and https://learn.microsoft.com/en-us/dotnet/csharp/whats-new for details.) + +When invoked: + +- Understand the user's .NET task and context +- Propose clean, organized solutions that follow .NET conventions +- Cover security (authentication, authorization, data protection) +- Use and explain patterns: Async/Await, Dependency Injection, Unit of Work, CQRS, Gang of Four +- Apply SOLID principles +- Plan and write tests (TDD/BDD) with xUnit, NUnit, or MSTest +- Improve performance (memory, async code, data access) + +# General C# Development + +- Follow the project's own conventions first, then common C# conventions. +- Keep naming, formatting, and project structure consistent. + +## Code Design Rules + +- DON'T add interfaces/abstractions unless used for external dependencies or testing. +- Don't wrap existing abstractions. +- Don't default to `public`. Least-exposure rule: `private` > `internal` > `protected` > `public` +- Keep names consistent; pick one style (e.g., `WithHostPort` or `WithBrowserPort`) and stick to it. +- Don't edit auto-generated code (`/api/*.cs`, `*.g.cs`, `// `). +- Comments explain **why**, not what. +- Don't add unused methods/params. +- When fixing one method, check siblings for the same issue. +- Reuse existing methods as much as possible +- Add comments when adding public methods +- Move user-facing strings (e.g., AnalyzeAndConfirmNuGetConfigChanges) into resource files. Keep error/help text localizable. + +## Error Handling & Edge Cases + +- **Null checks**: use `ArgumentNullException.ThrowIfNull(x)`; for strings use `string.IsNullOrWhiteSpace(x)`; guard early. Avoid blanket `!`. +- **Exceptions**: choose precise types (e.g., `ArgumentException`, `InvalidOperationException`); don't throw or catch base Exception. +- **No silent catches**: don't swallow errors; log and rethrow or let them bubble. + +## Goals for .NET Applications + +### Productivity + +- Prefer modern C# (file-scoped ns, raw """ strings, switch expr, ranges/indices, async streams) when TFM allows. +- Keep diffs small; reuse code; avoid new layers unless needed. +- Be IDE-friendly (go-to-def, rename, quick fixes work). + +### Production-ready + +- Secure by default (no secrets; input validate; least privilege). +- Resilient I/O (timeouts; retry with backoff when it fits). +- Structured logging with scopes; useful context; no log spam. +- Use precise exceptions; don’t swallow; keep cause/context. + +### Performance + +- Simple first; optimize hot paths when measured. +- Stream large payloads; avoid extra allocs. +- Use Span/Memory/pooling when it matters. +- Async end-to-end; no sync-over-async. + +### Cloud-native / cloud-ready + +- Cross-platform; guard OS-specific APIs. +- Diagnostics: health/ready when it fits; metrics + traces. +- Observability: ILogger + OpenTelemetry hooks. +- 12-factor: config from env; avoid stateful singletons. + +# .NET quick checklist + +## Do first + +- Read TFM + C# version. +- Check `global.json` SDK. + +## Initial check + +- App type: web / desktop / console / lib. +- Packages (and multi-targeting). +- Nullable on? (`enable` / `#nullable enable`) +- Repo config: `Directory.Build.*`, `Directory.Packages.props`. + +## C# version + +- **Don't** set C# newer than TFM default. +- C# 14 (NET 10+): extension members; `field` accessor; implicit `Span` conv; `?.=`; `nameof` with unbound generic; lambda param mods w/o types; partial ctors/events; user-defined compound assign. + +## Build + +- .NET 5+: `dotnet build`, `dotnet publish`. +- .NET Framework: May use `MSBuild` directly or require Visual Studio +- Look for custom targets/scripts: `Directory.Build.targets`, `build.cmd/.sh`, `Build.ps1`. + +## Good practice + +- Always compile or check docs first if there is unfamiliar syntax. Don't try to correct the syntax if code can compile. +- Don't change TFM, SDK, or `` unless asked. + +# Async Programming Best Practices + +- **Naming:** all async methods end with `Async` (incl. CLI handlers). +- **Always await:** no fire-and-forget; if timing out, **cancel the work**. +- **Cancellation end-to-end:** accept a `CancellationToken`, pass it through, call `ThrowIfCancellationRequested()` in loops, make delays cancelable (`Task.Delay(ms, ct)`). +- **Timeouts:** use linked `CancellationTokenSource` + `CancelAfter` (or `WhenAny` **and** cancel the pending task). +- **Context:** use `ConfigureAwait(false)` in helper/library code; omit in app entry/UI. +- **Stream JSON:** `GetAsync(..., ResponseHeadersRead)` → `ReadAsStreamAsync` → `JsonDocument.ParseAsync`; avoid `ReadAsStringAsync` when large. +- **Exit code on cancel:** return non-zero (e.g., `130`). +- **`ValueTask`:** use only when measured to help; default to `Task`. +- **Async dispose:** prefer `await using` for async resources; keep streams/readers properly owned. +- **No pointless wrappers:** don’t add `async/await` if you just return the task. + +## Immutability + +- Prefer records to classes for DTOs + +# Testing best practices + +## Test structure + +- Separate test project: **`[ProjectName].Tests`**. +- Mirror classes: `CatDoor` -> `CatDoorTests`. +- Name tests by behavior: `WhenCatMeowsThenCatDoorOpens`. +- Follow existing naming conventions. +- Use **public instance** classes; avoid **static** fields. +- No branching/conditionals inside tests. + +## Unit Tests + +- One behavior per test; +- Avoid Unicode symbols. +- Follow the Arrange-Act-Assert (AAA) pattern +- Use clear assertions that verify the outcome expressed by the test name +- Avoid using multiple assertions in one test method. In this case, prefer multiple tests. +- When testing multiple preconditions, write a test for each +- When testing multiple outcomes for one precondition, use parameterized tests +- Tests should be able to run in any order or in parallel +- Avoid disk I/O; if needed, randomize paths, don't clean up, log file locations. +- Test through **public APIs**; don't change visibility; avoid `InternalsVisibleTo`. +- Require tests for new/changed **public APIs**. +- Assert specific values and edge cases, not vague outcomes. + +## Test workflow + +### Run Test Command + +- Look for custom targets/scripts: `Directory.Build.targets`, `test.ps1/.cmd/.sh` +- .NET Framework: May use `vstest.console.exe` directly or require Visual Studio Test Explorer +- Work on only one test until it passes. Then run other tests to ensure nothing has been broken. + +### Code coverage (dotnet-coverage) + +- **Tool (one-time):** + bash + `dotnet tool install -g dotnet-coverage` +- **Run locally (every time add/modify tests):** + bash + `dotnet-coverage collect -f cobertura -o coverage.cobertura.xml dotnet test` + +## Test framework-specific guidance + +- **Use the framework already in the solution** (xUnit/NUnit/MSTest) for new tests. + +### xUnit + +- Packages: `Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio` +- No class attribute; use `[Fact]` +- Parameterized tests: `[Theory]` with `[InlineData]` +- Setup/teardown: constructor and `IDisposable` + +### xUnit v3 + +- Packages: `xunit.v3`, `xunit.runner.visualstudio` 3.x, `Microsoft.NET.Test.Sdk` +- `ITestOutputHelper` and `[Theory]` are in `Xunit` + +### NUnit + +- Packages: `Microsoft.NET.Test.Sdk`, `NUnit`, `NUnit3TestAdapter` +- Class `[TestFixture]`, test `[Test]` +- Parameterized tests: **use `[TestCase]`** + +### MSTest + +- Class `[TestClass]`, test `[TestMethod]` +- Setup/teardown: `[TestInitialize]`, `[TestCleanup]` +- Parameterized tests: **use `[TestMethod]` + `[DataRow]`** + +### Assertions + +- If **FluentAssertions/AwesomeAssertions** are already used, prefer them. +- Otherwise, use the framework’s asserts. +- Use `Throws/ThrowsAsync` (or MSTest `Assert.ThrowsException`) for exceptions. + +## Mocking + +- Avoid mocks/Fakes if possible +- External dependencies can be mocked. Never mock code whose implementation is part of the solution under test. +- Try to verify that the outputs (e.g. return values, exceptions) of the mock match the outputs of the dependency. You can write a test for this but leave it marked as skipped/explicit so that developers can verify it later. diff --git a/.github/agents/NETSerilogLoggerExpert.agent.md b/.github/agents/NETSerilogLoggerExpert.agent.md new file mode 100644 index 0000000..c6284b3 --- /dev/null +++ b/.github/agents/NETSerilogLoggerExpert.agent.md @@ -0,0 +1,137 @@ +--- +name: .NET Serilog Production Logger +description: Design, review, and implement production-grade logging for .NET applications using Serilog. +version: 2026-01-25 +--- + +## .NET Serilog Production Logger Architect Mission + +You are a specialized logging architect for .NET applications. +Your job is to ensure that all logging is: + +- **Primary Stack:** .NET (Core/ASP.NET Core), Serilog, common Serilog sinks (Console, File, Seq, Loki, Elasticsearch, Application Insights, etc.). +- **Production-ready:** resilient, structured, secure, and observable. +- **Maintainable:** easy to extend, consistent, and aligned with best practices. +- **Actionable:** logs must support troubleshooting, monitoring, and auditing. + +You focus on Serilog configuration, usage patterns, and integration with the wider observability stack. + +--- + +## Core Responsibilities + +- **Serilog Setup & Configuration** + - Configure `LoggerConfiguration` for ASP.NET Core and worker services. + - Use structured logging with properties and enrichers. + - Configure environment-specific logging (Development vs Production). + - Recommend appropriate sinks and minimum levels per environment. + +- **Logging Design & Patterns** + - Define logging strategy: levels, categories, correlation IDs, and scopes. + - Promote structured events over plain text. + - Ensure logs are consistent across layers (API, domain, infrastructure). + - Avoid noisy or redundant logs; focus on signal over noise. + +- **Reliability & Performance** + - Use asynchronous logging where appropriate. + - Avoid blocking calls or heavy computation in logging. + - Consider batching, buffering, and backpressure for sinks. + - Minimize overhead in hot paths while preserving useful detail. + +- **Security & Compliance** + - Prevent logging of secrets, credentials, tokens, PII, and sensitive data. + - Recommend redaction or hashing strategies when needed. + - Ensure logs support audit trails without exposing confidential information. + +- **Observability Integration** + - Integrate logging with tracing and metrics (e.g., OpenTelemetry). + - Use correlation IDs and request IDs across services. + - Design log formats that work well with log aggregation tools (Seq, ELK, etc.). + +--- + +## Behavioral Guidelines + +- **Be opinionated but practical:** Prefer proven patterns and production-ready defaults over experimental approaches. +- **Explain trade-offs:** When suggesting a configuration, briefly note pros/cons (e.g., performance vs detail). +- **Favor clarity:** Use clear naming, consistent templates, and well-structured examples. +- **Guardrails:** Explicitly call out anti-patterns (e.g., logging exceptions without stack traces, logging in tight loops). + +--- + +## Implementation Preferences + +- **Frameworks & Versions** + - Target modern .NET (e.g., .NET 6+), but keep guidance broadly applicable. + - Use current Serilog packages and idiomatic configuration patterns. + +- **Configuration Style** + - Prefer configuration via `appsettings.json` for production scenarios. + - Support both code-based and configuration-based setups, but keep examples cohesive. + - Show how to bind configuration sections to Serilog using `UseSerilog()` in ASP.NET Core. + +- **Code Style** + - Use C# with clear, concise examples. + - Demonstrate best practices with `ILogger` usage and Serilog static logger when appropriate. + - Include example log messages that show structured properties and correlation IDs. + +--- + +## Example Tasks You Handle Well + +- **Set up Serilog in an ASP.NET Core app** with environment-specific sinks and minimum levels. +- **Design a logging strategy** for a microservices architecture, including correlation IDs and shared enrichers. +- **Refactor existing logging** from `Console.WriteLine` or `ILogger` misuse into structured Serilog events. +- **Recommend sinks and configuration** for: + - High-throughput APIs + - Background workers + - Cloud-native deployments (Docker, Kubernetes, Azure, AWS) +- **Review logging code** and suggest improvements for: + - Log levels + - Message templates + - Exception handling and logging + - Sensitive data handling + +--- + +## Example Guidance Snippets + +### ASP.NET Core Serilog Bootstrap + +```csharp +public class Program +{ + public static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables() + .Build()) + .CreateLogger(); + + try + { + Log.Information("Starting up"); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Application terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSerilog() // Use Serilog for logging + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} diff --git a/.github/agents/READMESpecialist.agent.md b/.github/agents/READMESpecialist.agent.md new file mode 100644 index 0000000..2f47850 --- /dev/null +++ b/.github/agents/READMESpecialist.agent.md @@ -0,0 +1,76 @@ +--- +name: README Specialist +description: An agent focused on producing high-quality `README.md` files for software repositories. +#version: 2025-10-21 +--- + +## Mission + +You are a documentation specialist focused primarily on README files, but you can also help with other project documentation when requested. +Your scope is limited to documentation files only - do not modify or analyze code files. + +## Core Behavior + +1. Inspect the existing root `README.md` first. +2. Preserve all critical project-specific content already present. +3. Improve structure, readability, and technical accuracy. +4. Prefer concise, task-oriented writing over marketing language. +5. Ensure setup and usage steps are directly executable. + +## Required Minimum Sections + +At minimum, generated README content must include: + +1. **Project Title + Badges** +2. **Quick Start** +3. **Table of Contents** +4. **Overview** +5. **Solution Layout** +6. **Key Features** +7. **API Endpoints** +8. **Configuration** +9. **Client Library** +10. **Build & Test** +11. **Docker Support** +12. **Observability** +13. **Project Structure** +14. **License** + +## Quality Upgrades (Expected) + +When possible, improve beyond the baseline by adding: + +- **Prerequisites** with exact version requirements (aligned to current target framework, e.g., .NET 10). +- **Security Guidance** for secrets, tokens, and environment variable usage. +- **Operational Notes** for production deployment concerns. +- **Troubleshooting** section with common failures and fixes. +- **Examples** that are complete and runnable. +- **Consistency checks** for route names, configuration keys, and sample values. + +## Writing Standards + +- Use clear Markdown headings and predictable section ordering. +- Use tables for configuration keys and endpoint catalogs. +- Use fenced code blocks with language tags (`bash`, `json`, `csharp`). +- Keep terminology consistent across API, client, and configuration sections. +- Avoid placeholders unless clearly marked and explained. + +## Accuracy Rules + +Before finalizing README output: + +1. Verify commands match actual solution/project paths. +2. Verify endpoints match implemented controller routes. +3. Verify configuration examples match real keys in `appsettings*.json`. +4. Verify referenced projects/files exist in repository structure. +5. Ensure no contradiction between Quick Start and detailed sections. + +## Output Checklist + +A README result is acceptable only if: + +- It contains all required minimum sections. +- It is technically accurate for the current repository. +- It improves clarity and usability versus the previous README. +- It includes at least one practical example for API or client usage. +- It is ready for onboarding a new developer without additional verbal guidance. diff --git a/Bitai.Ldap.Helper.sln b/Bitai.Ldap.Helper.sln deleted file mode 100644 index 01bbdf8..0000000 --- a/Bitai.Ldap.Helper.sln +++ /dev/null @@ -1,126 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.6.11822.322 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitai.LDAPHelper.DTO", "src\Bitai.LDAPHelper.DTO\Bitai.LDAPHelper.DTO.csproj", "{2386A126-F237-43FD-9A7C-FDF6718ACAD7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitai.LDAPHelper", "src\Bitai.LDAPHelper\Bitai.LDAPHelper.csproj", "{87B1AB51-6C42-4EC0-9ECB-189370A724C2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitai.LDAPHelper.Demo", "demo\Bitai.LDAPHelper.Demo\Bitai.LDAPHelper.Demo.csproj", "{EF0E7C00-6116-46A7-BC21-6B30DE8361DD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{D026C7BE-09AB-4BFD-B78E-1DF35734EBFD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{53DFC702-B482-433C-8946-A26ED8711DFB}" - ProjectSection(SolutionItems) = preProject - README.md = README.md - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitai.LDAPHelper.Tests", "tests\Bitai.LDAPHelper.Tests\Bitai.LDAPHelper.Tests.csproj", "{2D343BE2-3906-4541-B9A5-CBB9A866BE54}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C4F50587-D862-43DF-AFE6-D2EEC0B7454D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitai.LDAPHelper.Tests.Mocks", "tests\Bitai.LDAPHelper.Tests.Mocks\Bitai.LDAPHelper.Tests.Mocks.csproj", "{39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitai.LDAPHelper.LdapAdapters", "src\Bitai.LDAPHelper.LdapAdapters\Bitai.LDAPHelper.LdapAdapters.csproj", "{A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Debug|x64.ActiveCfg = Debug|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Debug|x64.Build.0 = Debug|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Debug|x86.ActiveCfg = Debug|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Debug|x86.Build.0 = Debug|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Release|Any CPU.Build.0 = Release|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Release|x64.ActiveCfg = Release|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Release|x64.Build.0 = Release|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Release|x86.ActiveCfg = Release|Any CPU - {2386A126-F237-43FD-9A7C-FDF6718ACAD7}.Release|x86.Build.0 = Release|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Debug|x64.ActiveCfg = Debug|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Debug|x64.Build.0 = Debug|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Debug|x86.ActiveCfg = Debug|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Debug|x86.Build.0 = Debug|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Release|Any CPU.Build.0 = Release|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Release|x64.ActiveCfg = Release|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Release|x64.Build.0 = Release|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Release|x86.ActiveCfg = Release|Any CPU - {87B1AB51-6C42-4EC0-9ECB-189370A724C2}.Release|x86.Build.0 = Release|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Debug|x64.ActiveCfg = Debug|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Debug|x64.Build.0 = Debug|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Debug|x86.ActiveCfg = Debug|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Debug|x86.Build.0 = Debug|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Release|Any CPU.Build.0 = Release|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Release|x64.ActiveCfg = Release|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Release|x64.Build.0 = Release|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Release|x86.ActiveCfg = Release|Any CPU - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD}.Release|x86.Build.0 = Release|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Debug|x64.ActiveCfg = Debug|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Debug|x64.Build.0 = Debug|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Debug|x86.ActiveCfg = Debug|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Debug|x86.Build.0 = Debug|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Release|Any CPU.Build.0 = Release|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Release|x64.ActiveCfg = Release|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Release|x64.Build.0 = Release|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Release|x86.ActiveCfg = Release|Any CPU - {2D343BE2-3906-4541-B9A5-CBB9A866BE54}.Release|x86.Build.0 = Release|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Debug|x64.ActiveCfg = Debug|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Debug|x64.Build.0 = Debug|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Debug|x86.ActiveCfg = Debug|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Debug|x86.Build.0 = Debug|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Release|Any CPU.Build.0 = Release|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Release|x64.ActiveCfg = Release|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Release|x64.Build.0 = Release|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Release|x86.ActiveCfg = Release|Any CPU - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6}.Release|x86.Build.0 = Release|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Debug|x64.ActiveCfg = Debug|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Debug|x64.Build.0 = Debug|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Debug|x86.ActiveCfg = Debug|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Debug|x86.Build.0 = Debug|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Release|Any CPU.Build.0 = Release|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Release|x64.ActiveCfg = Release|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Release|x64.Build.0 = Release|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Release|x86.ActiveCfg = Release|Any CPU - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {2386A126-F237-43FD-9A7C-FDF6718ACAD7} = {C4F50587-D862-43DF-AFE6-D2EEC0B7454D} - {87B1AB51-6C42-4EC0-9ECB-189370A724C2} = {C4F50587-D862-43DF-AFE6-D2EEC0B7454D} - {EF0E7C00-6116-46A7-BC21-6B30DE8361DD} = {D026C7BE-09AB-4BFD-B78E-1DF35734EBFD} - {2D343BE2-3906-4541-B9A5-CBB9A866BE54} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {39BD48F5-8B64-469F-B6D7-9BB4582AE5F6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {A5F5FA73-059E-411F-BBF1-A8AC4C8B65BC} = {C4F50587-D862-43DF-AFE6-D2EEC0B7454D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {DB0C2252-0846-4C49-970A-C7C68C82A4F3} - EndGlobalSection -EndGlobal diff --git a/Bitai.Ldap.Helper.slnx b/Bitai.Ldap.Helper.slnx new file mode 100644 index 0000000..a2b3afc --- /dev/null +++ b/Bitai.Ldap.Helper.slnx @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Clean Bin & Obj folders.ps1 b/Clean Bin & Obj folders.ps1 deleted file mode 100644 index 2f779f4..0000000 --- a/Clean Bin & Obj folders.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -# PowerShell script that recursively deletes all 'bin' and 'obj' (or any other specified) folders inside current folder - -$CurrentPath = (Get-Location -PSProvider FileSystem).ProviderPath - -# recursively get all folders matching given includes, except ignored folders -$FoldersToRemove = Get-ChildItem .\ -include bin,obj -Recurse | where {$_ -notmatch '_tools' -and $_ -notmatch '_build'} | foreach {$_.fullname} - -# recursively get all folders matching given includes -$AllFolders = Get-ChildItem .\ -include bin,obj -Recurse | foreach {$_.fullname} - -# subtract arrays to calculate ignored ones -$IgnoredFolders = $AllFolders | where {$FoldersToRemove -notcontains $_} - -# remove folders and print to output -if($FoldersToRemove -ne $null) -{ - Write-Host - foreach ($item in $FoldersToRemove) - { - remove-item $item -Force -Recurse; - Write-Host "Removed: ." -nonewline; - Write-Host $item.replace($CurrentPath, ""); - } -} - -# print ignored folders to output -if($IgnoredFolders -ne $null) -{ - Write-Host - foreach ($item in $IgnoredFolders) - { - Write-Host "Ignored: ." -nonewline; - Write-Host $item.replace($CurrentPath, ""); - } - - Write-Host - Write-Host $IgnoredFolders.count "folders ignored" -foregroundcolor yellow -} - -# print summary of the operation -Write-Host -if($FoldersToRemove -ne $null) -{ - Write-Host $FoldersToRemove.count "folders removed" -foregroundcolor green -} -else { Write-Host "No folders to remove" -foregroundcolor green } - -Write-Host - -# prevent closing the window immediately -$dummy = Read-Host "Completed, press enter to continue." diff --git a/README.md b/README.md index fa967d7..cd2b9ad 100644 --- a/README.md +++ b/README.md @@ -1,234 +1,494 @@ # Bitai.LDAPHelper ![Logo](resources/hierarchy_32.png) -A high-performance library wrapping **Novell.Directory.Ldap.NETStandard** functionality to interact with LDAP-compliant Directory Services (such as Microsoft Active Directory). It simplifies operations like searching, authenticating users, and creating, modifying, disabling, or deleting accounts. +![.NET 10.0](https://img.shields.io/badge/.NET-10.0-512BD4) +![License: MIT](https://img.shields.io/badge/license-MIT-green) +![Package](https://img.shields.io/badge/package-Bitai.LDAPHelper-blue) +![Version](https://img.shields.io/badge/version-10.2.0-orange) -This library is a key component of the [Bitai.LDAPWebApi](https://github.com/bitai-cs/LDAPWebApi) solution ecosystem. +A high-performance, adapter-driven .NET 10 library that simplifies interacting with Microsoft Active Directory and RFC-compliant LDAP Directory Services. + +It abstracts complex low-level LDAP network protocol operations—such as binds, asynchronous searches, attribute modifications, Unicode password encoding, and Active Directory `userAccountControl` bitwise flags—into clean, async C# services. --- -## 🚀 Key Features +## Table of Contents + +- [Overview](#overview) +- [Solution Architecture](#solution-architecture) +- [Core Functional Modules](#core-functional-modules) + - [1. Authentication (`Authenticator`)](#1-authentication-authenticator) + - [2. Account Lifecycle Management (`AccountManager`)](#2-account-lifecycle-management-accountmanager) + - [3. Directory Search Engine (`Searcher`)](#3-directory-search-engine-searcher) + - [4. Group Membership Validation (`GroupMembershipValidator`)](#4-group-membership-validation-groupmembershipvalidator) + - [5. Combinable Query Filters Engine (`Bitai.LDAPHelper.QueryFilters`)](#5-combinable-query-filters-engine-bitaildaphelperqueryfilters) +- [Adapter Infrastructure](#adapter-infrastructure) +- [Class Architecture Taxonomy](#class-architecture-taxonomy) +- [Quick Start](#quick-start) +- [Detailed Usage Scenarios](#detailed-usage-scenarios) + - [Scenario 1: User Credentials Authentication](#scenario-1-user-credentials-authentication) + - [Scenario 2: Active Directory Account Provisioning](#scenario-2-active-directory-account-provisioning) + - [Scenario 3: Filtered Directory Searching](#scenario-3-filtered-directory-searching) + - [Scenario 4: Nested Group Membership Resolution](#scenario-4-nested-group-membership-resolution) + - [Scenario 5: Offline Unit Testing with Mock Adapter](#scenario-5-offline-unit-testing-with-mock-adapter) +- [Demo Console Application](#demo-console-application) +- [Build, Test & Package](#build-test--package) +- [Troubleshooting Matrix](#troubleshooting-matrix) +- [Security & Operational Guidance](#security--operational-guidance) +- [License & Community Ecosystem](#license--community-ecosystem) -The library is organized into specialized helper classes targeting specific directory operations: +--- -### 🛡️ 1. Authentication (`Authenticator`) -Enables quick and secure validation of user credentials on the LDAP/AD server. -- Supports both **User Distinguished Name (DN)** (`LDAPDistinguishedNameCredential`) and **Domain Username** (`LDAPDomainAccountCredential`) credential structures. -- Provides a simple authentication bind check (`AuthenticateAsync(credential)`). -- Provides a comprehensive, pre-validated authentication workflow that first searches the user entry in Active Directory to ensure uniqueness and validity before attempting the bind (`AuthenticateAsync(credential, searchLimits, searchCredential)`). +## Overview -### 👤 2. Account Management (`AccountManager`) -Simplifies user provisioning and lifecycle management within Active Directory. -- **Create MS AD User Accounts** (`CreateUserAccountForMsAD`): Provision new Active Directory entries (`LDAPMsADUserAccount`) with extensive attribute mappings (such as UPN, sAMAccountName, unicodePwd, department, memberOf, object classes, and user control flags). -- **Set/Change Passwords** (`SetUserAccountPasswordForMsAD`): Safe password replacement utilizing secure Unicode encoding. It can optionally test immediate post-update authentication. -- **Disable MS AD Accounts** (`DisableUserAccountForMsAD`): Securely disables accounts by updating the `userAccountControl` attribute with the `ACCOUNTDISABLE` flag, dynamically preserving all other existing account flags to prevent unintended configuration loss. -- **Remove MS AD Accounts** (`RemoveUserAccountForMsAD`): Permanently deletes user entries from the directory service. +`Bitai.LDAPHelper` provides enterprise-grade infrastructure for enterprise applications requiring directory authentication, Active Directory user management, and organizational membership resolution. -### 🔍 3. Directory Searching (`Searcher`) -Facilitates fast, flexible searching using highly customizable LDAP search configurations. -- **Filtered Searching** (`SearchEntriesAsync`): Execute directory searches using strongly-typed, combinable query filters (`AttributeFilter`, `AttributeFilterCombiner`). -- **Parent Hierarchy Resolution** (`SearchParentEntriesAsync`): Traverses directory hierarchies and recursively resolves full parent group/container entries (from the `memberOf` attribute) rather than just returning raw string paths. -- **Performance Optimized**: Fine-tune query execution by specifying search size/timeout limits and choosing which attributes to load via the `RequiredEntryAttributes` enum (e.g. `OnlyCN`, `OnlyObjectSid`, `Few`, `All`). +### Primary Design Goals -### 👥 4. Group Membership Validation (`GroupMembershipValidator`) -Specialized operations to determine security group memberships and roles. -- **Membership Verification** (`CheckGroupMembershipAsync`): Resolves whether a specific user (`sAMAccountName`) is a member (either direct or via nested/inherited groups) of a given target group (`CN`). -- **Complete Group Resolution** (`GetAllGroupMembershipsAsync`): Recursively retrieves all unique group common names (`CN`s) a user belongs to. +- **Driver Decoupling via Adapters:** Decouples higher-level services from specific LDAP protocol drivers (`Novell.Directory.Ldap.NETStandard`) via pluggable adapter interfaces (`ILdapConnectionFactoryAdapter`, `ILdapConnectionAdapter`). +- **Production & Testing Parity:** Enables seamless switching between production directory servers (`Bitai.LDAPHelper.LdapAdapters.Novell`) and high-speed in-memory mock datasets (`Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`). +- **Active Directory Specialization:** Built-in support for MS Active Directory specific attributes (`sAMAccountName`, `userPrincipalName`, `unicodePwd`, `userAccountControl`, `objectSid`). --- -## 🏛️ Project Architecture +## Solution Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Consumer Application │ +│ (ASP.NET Core Web API, IdentityServer4, Background Worker) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Bitai.LDAPHelper │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────┐ ┌──────────────┐ │ +│ │ Authenticator │ │ AccountManager │ │ Searcher │ │ GroupVal... │ │ +│ └─────────────────┘ └─────────────────┘ └──────────┘ └──────────────┘ │ +│ │ │ │ │ │ +│ └─────────────────────┼─────────────────┴─────────────┘ │ +│ ▼ │ +│ QueryFilters Engine & BaseHelper │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ILdapConnectionFactoryAdapter + ILdapConnectionAdapter + │ + ┌────────────────────────┴────────────────────────┐ + ▼ ▼ +┌───────────────────────────────────────┐ ┌───────────────────────────────┐ +│ Bitai.LDAPHelper.LdapAdapters.Novell │ │ ...LdapAdapters.LdapHelperMock│ +│ (Production / Novell LDAP Driver) │ │ (In-Memory Test / CI Mode) │ +└───────────────────────────────────────┘ └───────────────────────────────┘ + │ │ + ▼ ▼ +┌───────────────────────────────────────┐ ┌───────────────────────────────┐ +│ Live LDAP / Active Directory │ │ MockLdapDataStore │ +└───────────────────────────────────────┘ └───────────────────────────────┘ +``` -The solution `LDAP Helper Libraries.sln` consists of the following projects: +### Solution Project Breakdown -1. **`Bitai.LDAPHelper`** (`src/Bitai.LDAPHelper`): The core library containing helper classes (`Authenticator`, `AccountManager`, `Searcher`, `GroupMembershipValidator`) and query construction engines. -2. **`Bitai.LDAPHelper.DTO`** (`src/Bitai.LDAPHelper.DTO`): Light-weight, structured Data Transfer Objects, interfaces, and enums (e.g., `LDAPEntry`, `LDAPMsADUserAccount`, results structures, and secure cloning models). -3. **`Bitai.LDAPHelper.Demo`** (`demo/Bitai.LDAPHelper.Demo`): An interactive console application demonstrating all features using both real LDAP connections and simulated in-memory mock data. -4. **`Bitai.LDAPHelper.Tests`** (`tests/Bitai.LDAPHelper.Tests`): xUnit testing suite verifying LDAP wrappers, filters, and helper methods. +| Project | Path | Purpose | +|---|---|---| +| **`Bitai.LDAPHelper`** | [`src/Bitai.LDAPHelper`](file:///c:/_bitai/Bitai.LDAPHelper/src/Bitai.LDAPHelper) | Core service framework, query engine, and adapter interfaces. | +| **`Bitai.LDAPHelper.DTO`** | [`src/Bitai.LDAPHelper.DTO`](file:///c:/_bitai/Bitai.LDAPHelper/src/Bitai.LDAPHelper.DTO) | Data Transfer Objects, credentials, and operation result models. | +| **`Bitai.LDAPHelper.LdapAdapters.Novell`** | [`adapters/Bitai.LDAPHelper.LdapAdapters.Novell`](file:///c:/_bitai/Bitai.LDAPHelper/adapters/Bitai.LDAPHelper.LdapAdapters.Novell) | Novell-backed production LDAP adapter for live network servers. | +| **`Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`** | [`adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`](file:///c:/_bitai/Bitai.LDAPHelper/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock) | In-memory mock adapter and directory seeder for unit/integration tests. | +| **`Bitai.LDAPHelper.Demo`** | [`demo/Bitai.LDAPHelper.Demo`](file:///c:/_bitai/Bitai.LDAPHelper/demo/Bitai.LDAPHelper.Demo) | Interactive console test bench supporting both Mock and Novell modes. | +| **`Bitai.LDAPHelper.Tests`** | [`tests/Bitai.LDAPHelper.Tests`](file:///c:/_bitai/Bitai.LDAPHelper/tests/Bitai.LDAPHelper.Tests) | xUnit testing suite covering services, filters, and adapters. | --- -## ⚙️ Requirements & Dependencies +## Core Functional Modules + +### 1. Authentication (`Authenticator`) + +`Authenticator` validates user credentials against LDAP servers. + +- **Direct Credentials Bind:** Supports both domain user account credentials (`LDAPDomainAccountCredential`) and explicit distinguished name credentials (`LDAPDistinguishedNameCredential`). +- **Pre-Validated Search & Bind:** Performs an initial directory search to verify user account existence, uniqueness, and active state before attempting the bind operation, guarding against ambiguous domain lookups. + +### 2. Account Lifecycle Management (`AccountManager`) + +`AccountManager` handles directory object provisioning and state updates for Active Directory: + +- **User Provisioning (`CreateUserAccountForMsADAsync`)**: Creates Active Directory user objects (`LDAPMsADUserAccount`) with automatic mapping of attributes (`cn`, `sAMAccountName`, `userPrincipalName`, `givenName`, `sn`, `mail`, `department`, `unicodePwd`, `objectClass`, `userAccountControl`). +- **Password Updates (`SetUserAccountPasswordForMsADAsync`)**: Encodes passwords in double-quoted UTF-16LE binary format required by Active Directory `unicodePwd`. Supports optional immediate authentication verification. +- **Account Disabling (`DisableUserAccountForMsADAsync`)**: Performs bitwise OR operations on `userAccountControl` to add `ACCOUNTDISABLE` (0x0002) while preserving all existing account control flags (`NORMAL_ACCOUNT`, `DONT_EXPIRE_PASSWORD`, etc.). +- **Account Deletion (`RemoveUserAccountForMsADAsync`)**: Permanently removes specified user entries from Active Directory. +- **Generic Directory Operations (`AddEntryAsync`, `ModifyEntryAsync`, `DeleteEntryAsync`)**: Low-level CRUD operations on raw LDAP entries and attribute sets. + +### 3. Directory Search Engine (`Searcher`) + +`Searcher` executes optimized directory queries: + +- **Targeted Lookups (`GetLdapEntryBySamAccountNameAsync`)**: Fast search by `sAMAccountName`. +- **Advanced Query Filtering (`SearchEntriesAsync`)**: Executes directory searches using combinable query filters. +- **Parent Hierarchy Resolution (`SearchParentEntriesAsync`)**: Traverses `memberOf` attributes to recursively fetch full `LDAPEntry` parent container objects rather than raw string DNs. +- **Attribute Projection Optimization (`RequiredEntryAttributes`)**: Controls memory and network overhead by selecting attribute projections (`OnlyCN`, `OnlyObjectSid`, `Few`, `All`). + +### 4. Group Membership Validation (`GroupMembershipValidator`) -- **.NET 10 SDK** or higher. -- **Novell.Directory.Ldap.NETStandard** (v4.0.0+) package dependency. +`GroupMembershipValidator` inspects security group assignments: + +- **Recursive Membership Resolution (`CheckGroupMembershipAsync`)**: Determines if a user (`sAMAccountName`) belongs to a group (`CN`), automatically evaluating direct memberships and nested/inherited child group trees. +- **Full Group Membership Extraction (`GetAllGroupMembershipsAsync`)**: Returns a list of all unique group common names (`CN`s) assigned to a given account. + +### 5. Combinable Query Filters Engine (`Bitai.LDAPHelper.QueryFilters`) + +The query engine constructs clean, injection-safe LDAP filter strings: + +```csharp +// Example: Active users in Engineering department matching 'smith*' +var activeFilter = new OnlyActiveAccountsFilterCombiner(); +var userFilter = new OnlyUsersFilterCombiner(); +var nameFilter = new AttributeFilter(EntryAttribute.sAMAccountName, new FilterValue("*smith*")); + +var combined = new AttributeFilterCombiner( + negate: false, + conjunctive: true, + new ICombinableFilter[] { activeFilter, userFilter, nameFilter } +); + +string ldapQuery = combined.GetFilter(); +// Output: (&(&(objectCategory=person)(objectClass=user))(!(userAccountControl:1.2.840.113556.1.4.803:=2))(sAMAccountName=*smith*)) +``` --- -## 💻 Quick Start & Usage Examples +## Adapter Infrastructure + +To prevent direct coupling to specific LDAP drivers, `Bitai.LDAPHelper` relies on core adapter abstractions defined in `Bitai.LDAPHelper.LdapAdapters`: + +```csharp +namespace Bitai.LDAPHelper.LdapAdapters +{ + public interface ILdapConnectionFactoryAdapter + { + Task CreateConnectionAsync( + IConnectionInfo connectionInfo, + string userAccount, + string password, + bool bindRequired = true); + } + + public interface ILdapConnectionAdapter : IDisposable + { + int ConnectionTimeout { get; set; } + bool SecureSocketLayer { get; set; } + bool IsBound { get; } + + Task ConnectAsync(string host, int port); + Task BindAsync(string userDN, string password); + Task SearchAsync(ISearchLimits searchLimits, string searchFilter, string[] attributeNames, bool typesOnly); + Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAdapter attributes); + Task ModifyEntryAsync(string distinguishedName, IEnumerable modifications); + Task DeleteEntryAsync(string distinguishedName); + } +} +``` + +--- + +## Class Architecture Taxonomy + +| Class / Interface | Namespace | Role | +|---|---|---| +| `Authenticator` | `Bitai.LDAPHelper` | Handles domain account & DN credential authentication workflows. | +| `AccountManager` | `Bitai.LDAPHelper` | Provisions, updates passwords, disables, and deletes MS AD accounts. | +| `Searcher` | `Bitai.LDAPHelper` | Executes searches, projections, and parent group resolutions. | +| `GroupMembershipValidator` | `Bitai.LDAPHelper` | Validates direct and nested security group memberships. | +| `BaseHelper` | `Bitai.LDAPHelper` | Base class managing connection factory lifetimes, logging, and error handling. | +| `AttributeFilter` | `Bitai.LDAPHelper.QueryFilters` | Represents single attribute filter conditions (`attr=value`). | +| `AttributeFilterCombiner` | `Bitai.LDAPHelper.QueryFilters` | Combines multiple filters into AND (`&`) or OR (`\|`) expressions. | +| `LDAPEntry` | `Bitai.LDAPHelper.DTO` | DTO representing an LDAP directory record with parsed properties. | +| `LDAPMsADUserAccount` | `Bitai.LDAPHelper.DTO` | Detailed model representing Active Directory user account attributes. | +| `ConnectionInfo` | `Bitai.LDAPHelper` | Defines LDAP server host, port, SSL settings, and timeouts. | +| `SearchLimits` | `Bitai.LDAPHelper` | Defines search Base DN, scope, max results, and execution timeouts. | + +--- + +## Quick Start + +### Installation + +Add `Bitai.LDAPHelper` and a connection adapter package (such as `Bitai.LDAPHelper.LdapAdapters.Novell`) to your .NET project: + +```bash +dotnet add package Bitai.LDAPHelper +dotnet add package Bitai.LDAPHelper.LdapAdapters.Novell +``` + +--- + +## Detailed Usage Scenarios + +### Scenario 1: User Credentials Authentication -### 1. Authenticate a Domain User ```csharp using Bitai.LDAPHelper; using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Adapters.Novell; +using Bitai.LDAPHelper.LdapAdapters.Novell; + +var connectionInfo = new ConnectionInfo("dc01.contoso.com", 389, useSSL: false, connectionTimeout: 15); +var searchLimits = new SearchLimits("DC=contoso,DC=com"); +var serviceCreds = new LDAPDomainAccountCredential("CONTOSO", "svc_auth", "ServicePass123!"); -var connectionInfo = new ConnectionInfo("dc.company.com", 389, useSsl: false, timeoutSeconds: 15); var factory = new NovellLdapConnectionFactoryAdapter(); -var authenticator = new Authenticator(connectionInfo, factory); +var authenticator = new Authenticator(connectionInfo, searchLimits, serviceCreds, factory); -var credential = new LDAPDomainAccountCredential("COMPANY", "john.doe", "MySecurePassword123!"); -var authResult = await authenticator.AuthenticateAsync(credential); +// Authenticate user +var userCreds = new LDAPDomainAccountCredential("CONTOSO", "jdoe", "UserPassword!"); +var result = await authenticator.AuthenticateDomainAccountAsync(userCreds); -if (authResult.IsSuccessfulOperation && authResult.IsAuthenticated) +if (result.IsAuthenticated) { - Console.WriteLine("User successfully authenticated!"); + Console.WriteLine($"Successfully authenticated user: {result.UserEntry?.CN}"); } else { - Console.WriteLine($"Authentication failed: {authResult.OperationMessage}"); + Console.WriteLine($"Authentication failed: {result.ResultDetail}"); +} +``` + +--- + +### Scenario 2: Active Directory Account Provisioning + +```csharp +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.Novell; + +var connectionInfo = new ConnectionInfo("dc01.contoso.com", 389, useSSL: false, connectionTimeout: 15); +var searchLimits = new SearchLimits("DC=contoso,DC=com"); +var adminCreds = new LDAPDomainAccountCredential("CONTOSO", "DomainAdmin", "AdminSecret!"); +var factory = new NovellLdapConnectionFactoryAdapter(); + +var accountManager = new AccountManager(connectionInfo, searchLimits, adminCreds, factory); + +// Build new MS AD User Account model +var newUser = new LDAPMsADUserAccount +{ + SamAccountName = "alice.smith", + UserPrincipalName = "alice.smith@contoso.com", + Name = "Alice", + Surname = "Smith", + Email = "alice.smith@contoso.com", + ContainerDN = "OU=Employees,DC=contoso,DC=com", + Password = "P@ssword2026!Secure", + UserAccountControlFlags = UserAccountControlFlags.NORMAL_ACCOUNT | UserAccountControlFlags.DONT_EXPIRE_PASSWORD +}; + +var createResult = await accountManager.CreateUserAccountForMsADAsync(newUser); + +if (createResult.IsSuccess) +{ + Console.WriteLine($"User account created successfully: {createResult.DistinguishedName}"); } ``` -### 2. Search for Users +--- + +### Scenario 3: Filtered Directory Searching + ```csharp using Bitai.LDAPHelper; using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.QueryFilters; -using Bitai.LDAPHelper.Adapters.Novell; +using Bitai.LDAPHelper.LdapAdapters.Novell; -var connectionInfo = new ConnectionInfo("dc.company.com", 389, useSsl: false, timeoutSeconds: 15); -var searchLimits = new SearchLimits("DC=company,DC=com", maxSearchResults: 100, maxSearchTimeoutSeconds: 30); -var adminCredential = new LDAPDomainAccountCredential("COMPANY", "admin.user", "AdminPassword!"); +var connectionInfo = new ConnectionInfo("dc01.contoso.com", 389, useSSL: false, connectionTimeout: 15); +var searchLimits = new SearchLimits("DC=contoso,DC=com", maxSearchResults: 200, maxSearchTimeout: 30); +var adminCreds = new LDAPDomainAccountCredential("CONTOSO", "svc_reader", "Pass123!"); var factory = new NovellLdapConnectionFactoryAdapter(); -var searcher = new Searcher(connectionInfo, searchLimits, adminCredential, factory); +var searcher = new Searcher(connectionInfo, searchLimits, adminCreds, factory); -// Build search filter: user must be active and have sAMAccountName match '*smith*' -var onlyUsersFilter = AttributeFilterCombiner.CreateOnlyUsersFilterCombiner(); -var nameFilter = new AttributeFilter(EntryAttribute.sAMAccountName, new FilterValue("*smith*")); -var combinedFilter = new AttributeFilterCombiner(negate: false, conjunctive: true, new ICombinableFilter[] { onlyUsersFilter, nameFilter }); +// Query active accounts matching 'j*' in sAMAccountName +var userFilter = AttributeFilterCombiner.CreateOnlyUsersFilterCombiner(); +var activeFilter = AttributeFilterCombiner.CreateOnlyActiveAccountsFilterCombiner(); +var samFilter = new AttributeFilter(EntryAttribute.sAMAccountName, new FilterValue("j*")); -var searchResult = await searcher.SearchEntriesAsync(combinedFilter, RequiredEntryAttributes.Few, requestLabel: "UserSearch"); +var combinedFilter = new AttributeFilterCombiner( + negate: false, + conjunctive: true, + new ICombinableFilter[] { userFilter, activeFilter, samFilter } +); -if (searchResult.IsSuccessfulOperation) +var searchResult = await searcher.SearchEntriesAsync(combinedFilter, RequiredEntryAttributes.Few, requestLabel: "ActiveUserSearch"); + +foreach (var entry in searchResult.Entries) { - foreach (var entry in searchResult.Entries) - { - Console.WriteLine($"User: {entry.cn} (sAMAccountName: {entry.samAccountName}), UPN: {entry.userPrincipalName}"); - } + Console.WriteLine($"Found Account: {entry.SamAccountName} | Mail: {entry.Mail} | UPN: {entry.UserPrincipalName}"); } ``` -### 3. Verify Nested Group Membership +--- + +### Scenario 4: Nested Group Membership Resolution + ```csharp using Bitai.LDAPHelper; using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Adapters.Novell; +using Bitai.LDAPHelper.LdapAdapters.Novell; -var connectionInfo = new ConnectionInfo("dc.company.com", 389, useSsl: false, timeoutSeconds: 15); -var searchLimits = new SearchLimits("DC=company,DC=com", maxSearchResults: 100, maxSearchTimeoutSeconds: 30); -var adminCredential = new LDAPDomainAccountCredential("COMPANY", "admin.user", "AdminPassword!"); +var connectionInfo = new ConnectionInfo("dc01.contoso.com", 389, useSSL: false, connectionTimeout: 15); +var searchLimits = new SearchLimits("DC=contoso,DC=com"); +var adminCreds = new LDAPDomainAccountCredential("CONTOSO", "svc_reader", "Pass123!"); var factory = new NovellLdapConnectionFactoryAdapter(); -var validator = new GroupMembershipValidator(connectionInfo, searchLimits, adminCredential, factory); +var validator = new GroupMembershipValidator(connectionInfo, searchLimits, adminCreds, factory); + +// Check if 'jdoe' belongs to 'Finance_Admins' (either directly or via nested group inheritance) +bool isFinanceAdmin = await validator.CheckGroupMembershipAsync("jdoe", "Finance_Admins"); -bool isMember = await validator.CheckGroupMembershipAsync(sAMAccountName: "john.doe", parentGroupCN: "HR_Department_Managers"); +Console.WriteLine($"User jdoe belongs to Finance_Admins: {isFinanceAdmin}"); -Console.WriteLine($"Is member (direct or nested): {isMember}"); +// Fetch all unique group membership CNs for user +var groups = await validator.GetAllGroupMembershipsAsync("jdoe"); +Console.WriteLine($"Assigned Groups: {string.Join(", ", groups)}"); ``` --- -## 🛠️ Configuring and Running the Demo Application +### Scenario 5: Offline Unit Testing with Mock Adapter + +```csharp +using Xunit; +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; -The **`Bitai.LDAPHelper.Demo`** console application is a rich test bench designed to execute all primary library methods. +public class SearcherTests +{ + [Fact] + public async Task GetUser_UsingMockAdapter_ShouldReturnStubbedUser() + { + // 1. Instantiate in-memory mock connection + var mockConnection = new MockLdapConnectionAdapter(); + + // 2. Add stubbed search response + var stubEntry = new MockLdapEntryAdapter("CN=Test User,OU=Users,DC=example,DC=com"); + stubEntry.AddAttribute("sAMAccountName", "testuser"); + stubEntry.AddAttribute("mail", "testuser@example.com"); + + mockConnection.AddSearchResult("(sAMAccountName=testuser)", new List { stubEntry }); + + // 3. Pass mock factory to Searcher + var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + var searcher = new Searcher( + new ConnectionInfo("localhost", 389, false, 5), + new SearchLimits("DC=example,DC=com"), + new LDAPDomainAccountCredential("EXAMPLE", "admin", "pass"), + factory + ); + + // 4. Test service without network access + var user = await searcher.GetLdapEntryBySamAccountNameAsync("testuser"); + + Assert.NotNull(user); + Assert.Equal("testuser", user.SamAccountName); + Assert.Equal("testuser@example.com", user.Mail); + } +} +``` -### 💡 Two Execution Modes -The demo application provides two implementations to let developers test functionality seamlessly: -1. **Mock Mode (Simulated LDAP)**: **Does not require a live LDAP server.** It runs entirely in-memory using simulated directory entries seeded upon startup (`MockDataStore` & `MockDataSeeder`). This mode is ideal for local development, rapid feedback, and offline debugging. -2. **Novell Mode (Real LDAP)**: Connects directly to an active directory service. It prompts you interactively for server connection details and password bindings. +--- -### 📋 Setup & Configuration Steps +## Demo Console Application -#### Step 1: Deploy Configuration File -The demo application is programmed to load its configuration file named **`ldaphelper_demosetup.json`** directly from the logged-in user's **Desktop** folder. +The repository includes an interactive console test bench at [`demo/Bitai.LDAPHelper.Demo`](file:///c:/_bitai/Bitai.LDAPHelper/demo/Bitai.LDAPHelper.Demo). -1. Locate the sample setup file at: `demo/Bitai.LDAPHelper.Demo/ldaphelper_demosetup.json` -2. **Copy this file directly to your Desktop directory.** +### Dual Execution Modes -#### Step 2: Configure Environment Parameters -Edit the copied **`ldaphelper_demosetup.json`** on your Desktop. Adjust parameters as needed for either Mock or Novell operations: +1. **Mock Mode (Simulated / Offline)**: Runs in-memory using `MockLdapDataSeeder` data. Requires no live directory server or credentials. +2. **Novell Mode (Live LDAP / Active Directory)**: Connects to a physical Active Directory server using settings configured in `ldaphelper_demosetup.json`. + +### Configuration Setup + +Copy `demo/Bitai.LDAPHelper.Demo/ldaphelper_demosetup.json` to your user's **Desktop** directory and configure: ```json { - "LdapServers": [ - { "Address": "dcsrvr1.company.com" }, - { "Address": "10.100.54.4" } - ], - "BaseDNs": [ - { "DN": "DC=company,DC=com" } - ], + "LdapServers": [{ "Address": "dc01.contoso.com" }], + "BaseDNs": [{ "DN": "DC=contoso,DC=com" }], "ConnectionTimeout": 15, - "DomainUserAccountForRunTests": "COMPANY\\service.account", - "AccountPassword": "SecretPassword123!", - - // Set true to execute individual test cases when the app runs: - "Demo_AccountManager_CreateUserAccount_RunTest": true, - "Demo_AccountManager_CreateUserAccount_UserAccountName": "victor.bastidas", - "Demo_AccountManager_CreateUserAccount_Password": "NewPassword2026!", - "Demo_AccountManager_CreateUserAccount_ContainerDN": "OU=Users,DC=company,DC=com", - "Demo_AccountManager_CreateUserAccount_Name": "Victor", - "Demo_AccountManager_CreateUserAccount_Surname": "Bastidas", - "Demo_AccountManager_CreateUserAccount_DNSDomainName": "company.com", - "Demo_AccountManager_CreateUserAccount_MemberOf": null, - "Demo_AccountManager_CreateUserAccount_ObjectClasses": "user,top,person,organizationalPerson", - "Demo_AccountManager_CreateUserControlFlags": "NORMAL_ACCOUNT,DONT_EXPIRE_PASSWORD", - - "Demo_AccountManager_SetAccountPassword_RunTest": false, - "Demo_AccountManager_SetAccountPassword_DistinguishedName": "CN=Victor Bastidas,OU=Users,DC=company,DC=com", - - "Demo_Authenticator_Authenticate_RunTest": false, - "Demo_Authenticator_Authenticate_RunTest_Simple": false, - "Demo_Authenticator_Authenticate_DomainAccountName": "COMPANY\\victor.bastidas", - - "Demo_AccountManager_DisableUserAccount_RunTest": false, - "Demo_AccountManager_DisableUserAccount_UserAccountDistinguishedName": "CN=Victor Bastidas,OU=Users,DC=company,DC=com", - - "Demo_AccountManager_RemoveUserAccount_RunTest": false, - "Demo_AccountManager_RemoveUserAccount_UserAccountDistinguishedName": "CN=Victor Bastidas,OU=Users,DC=company,DC=com", - + "DomainUserAccountForRunTests": "CONTOSO\\svc_demo", + "AccountPassword": "DemoPassword123!", "Demo_Searcher_SearchUsers_RunTest": true, - "Demo_Searcher_SearchUsers_Filter_sAMAccountName": "victor.bastidas", - "Demo_Searcher_SearchUsers_Filter_cn": "*Bastidas*", - - "Demo_Searcher_SearchEntries_RunTest": false, - "Demo_Searcher_SearchEntries_Filter_cn": "DEVSERVER01", - - "Demo_Searcher_SearchParentEntries_RunTest": false, - "Demo_Searcher_SearchParentEntries_Filter_sAMAccountName": "victor.bastidas", - - "Demo_GroupMembershipValidator_RunTest": true, - "Demo_GroupMembershipValidator_CheckGroupmembership_sAMAccountName": "victor.bastidas", - "Demo_GroupMembershipValidator_CheckGroupmembership_Check_GroupName": "Administrators" + "Demo_GroupMembershipValidator_RunTest": true } ``` -### 🏃 Running the Demo - -Open a terminal at the root of the LDAPHelper solution folder and execute: +### Running the Demo -```powershell +```bash +# Interactive mode (prompts for mode selection): dotnet run --project demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj + +# Direct Mock mode execution: +dotnet run --project demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj -- --implementation mock + +# Direct Novell mode execution: +dotnet run --project demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj -- --implementation novell +``` + +--- + +## Build, Test & Package + +### Prerequisites + +- **.NET SDK 10.0** or newer. + +### Build Solution + +```bash +dotnet build Bitai.Ldap.Helper.sln -c Release ``` -#### Selection and Arguments: -- **Interactive Mode**: By default, starting the console will prompt you to select: - - `1` for Novell (Real LDAP Connection) - - `2` for Mock (Simulated LDAP, Offline) -- **Automatic / Scripted Mode**: You can bypass the interactive prompts by passing the `--implementation` or `-i` command line arguments: - ```powershell - # Directly start Mock implementation: - dotnet run --project demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj -- --implementation mock - - # Directly start Novell implementation: - dotnet run --project demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj -- --implementation novell - ``` +### Execute Test Suite -Once started, the console app will automatically execute the active test routines toggled as `true` in your Desktop's `ldaphelper_demosetup.json`, print step-by-step logs, record performance, and output a detailed execution summary table. +```bash +dotnet test tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj -c Release +``` + +### Generate NuGet Packages + +```bash +dotnet pack src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj -c Release -o ./artifacts +dotnet pack adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj -c Release -o ./artifacts +dotnet pack adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj -c Release -o ./artifacts +``` + +--- + +## Troubleshooting Matrix + +| Issue / Error | Cause | Fix | +|---|---|---| +| `LdapOperationException: Invalid credentials` | Server refused bind credentials or user DN format is incorrect. | Verify UPN (`user@domain.com`) or short domain account format (`DOMAIN\user`). | +| `EntryNotFoundException` | Specified DN or account filter yielded no matching records. | Verify `SearchLimits.BaseDN` and ensure target account exists in directory. | +| Password Change Fails (`CONSTRAINT_VIOLATION` / 0x13) | New password violates Active Directory password policy (length, complexity, history). | Ensure password meets Active Directory domain password policy requirements. | +| Password Change Fails (`UNWILLING_TO_PERFORM` / 0x35) | Active Directory requires LDAPS (SSL port 636) to update `unicodePwd`. | Enable `UseSSL = true` and connect over port 636. Active Directory rejects password changes over unencrypted LDAP. | +| Group Membership Check Returns False | Parent group name `CN` is incorrect or user belongs to nested group without subtree traversal. | Specify common name `CN` (not full DN) for target group. `GroupMembershipValidator` automatically evaluates nested inheritance. | --- -## 🤝 Community & Ecosystem +## Security & Operational Guidance + +- **LDAPS Requirement for Passwords:** Active Directory strictly rejects `unicodePwd` updates over unencrypted LDAP (port 389). Always configure `UseSSL = true` on port 636 when updating credentials via `AccountManager`. +- **Credential Storage:** Store service account credentials in secure configuration providers (e.g. Azure Key Vault, AWS Secrets Manager, or User Secrets) rather than hardcoded source strings. +- **Bitwise Account Control:** When disabling accounts using `AccountManager.DisableUserAccountForMsADAsync`, existing control flags are preserved dynamically to avoid unintentionally stripping settings like `DONT_EXPIRE_PASSWORD`. + +--- + +## License & Community Ecosystem + +This project is licensed under the **MIT License**. See [LICENSE.md](LICENSE.md) for details. + +- Part of the **Bitai LDAP Ecosystem**. +- Related Projects: [Bitai.LDAPWebApi](https://github.com/bitai-cs/LDAPWebApi), [IdentityServer4.Admin](https://github.com/bitai-cs/IdentityServer4.Admin). -- Part of the wider [Bitai.LDAPWebApi](https://github.com/bitai-cs/LDAPWebApi) project. -- Feel free to check out [Bitai.IdentityServer4.Admin](https://github.com/bitai-cs/IdentityServer4.Admin) (forked from `Skoruba.IdentityServer4.Admin`) which provides an identity server administration UI backed by LDAP authentication pipelines. +© 2026 **BITAI**. All rights reserved. diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj new file mode 100644 index 0000000..f8c69ff --- /dev/null +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj @@ -0,0 +1,49 @@ + + + net10.0 + Viko Bastidas (BITAI) + BITAI + LDAP Services Wrappers + © 2026 BITAI. All rights reserved. + 10.0.0 + 10.0.0 + 10.0.0 + hierarchy_32.png + true + Bitai.LDAPHelper.LdapAdapters.LdapHelperMock + enable + enable + © 2026 BITAI. MIT License. + https://github.com/bitai-cs/LDAPHelper.git + GitHub + ldap;authentication;authorization;directory;helper;oauth;openid;ad;security;identity + Mocks for testing BITAI.LDAPHelper library. + https://github.com/bitai-cs/LDAPHelper + True + LICENSE.md + README.md + + + + + + True + + + + + + + + + + True + \ + + + True + \ + + + + diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LICENSE.md b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LICENSE.md similarity index 100% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LICENSE.md rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LICENSE.md diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapData/MockLdapDataSeeder.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs similarity index 99% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapData/MockLdapDataSeeder.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs index f19c763..a4961ea 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapData/MockLdapDataSeeder.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs @@ -1,7 +1,7 @@ -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; using Microsoft.Extensions.Logging; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapData; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; /// /// Seeds deterministic mock LDAP data for integration-style tests and demos. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapData/MockLdapDataStore.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataStore.cs similarity index 95% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapData/MockLdapDataStore.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataStore.cs index 300abf0..c1c5ffe 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapData/MockLdapDataStore.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataStore.cs @@ -1,6 +1,6 @@ -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapData; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; /// /// Thread-safe in-memory LDAP entry store used by persistent mock adapters. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapAttributeAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs similarity index 99% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapAttributeAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs index 12a07d7..61af718 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapAttributeAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs @@ -1,7 +1,7 @@ -using System.Text; +using System.Text; using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// Mock implementation of ILdapAttributeAdapter that mimics Novell.Directory.Ldap.LdapAttribute behavior diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapAttributeSetAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeSetAdapter.cs similarity index 98% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapAttributeSetAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeSetAdapter.cs index 10112e4..10e8840 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapAttributeSetAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeSetAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// In-memory mock implementation of for tests. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs similarity index 98% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapConnectionAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs index 3d7d1ff..34297d6 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapConnectionAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs @@ -1,7 +1,7 @@ using Bitai.LDAPHelper.LdapAdapters; using Bitai.LDAPHelper.DTO; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// In-memory mock LDAP connection used by tests to simulate bind/search/add/modify/delete operations. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs similarity index 95% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapConnectionFactoryAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs index bde94af..f3b4167 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// Mock implementation of that returns a provided mock connection. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapEntryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapEntryAdapter.cs similarity index 96% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapEntryAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapEntryAdapter.cs index efd0529..b504f8c 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapEntryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapEntryAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// In-memory mock LDAP entry used by test scenarios. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapMessageAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapMessageAdapter.cs similarity index 89% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapMessageAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapMessageAdapter.cs index 477ccd1..83e8afb 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapMessageAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapMessageAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// In-memory mock LDAP message wrapper used in search queue responses. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapModificationAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapModificationAdapter.cs similarity index 94% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapModificationAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapModificationAdapter.cs index 83e683a..b0c9acd 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapModificationAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapModificationAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// In-memory mock implementation of . diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapPersistentConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs similarity index 98% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapPersistentConnectionAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs index 629ebe7..e6a6b08 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapPersistentConnectionAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs @@ -1,8 +1,8 @@ using System.Text; using Bitai.LDAPHelper.LdapAdapters; -using Bitai.LDAPHelper.Tests.Mocks.LdapData; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// Persistent mock connection backed by a shared in-memory LDAP data store. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapPersistentConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs similarity index 93% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapPersistentConnectionFactoryAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs index fc5a363..d0d93ed 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapPersistentConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// Factory for creating persistent mock LDAP connections backed by shared in-memory data. diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapSearchQueueAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapSearchQueueAdapter.cs similarity index 89% rename from tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapSearchQueueAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapSearchQueueAdapter.cs index 1c80be4..c211e1e 100644 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/LdapAdapters/MockLdapSearchQueueAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapSearchQueueAdapter.cs @@ -1,6 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; -namespace Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// In-memory mock implementation of . diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md new file mode 100644 index 0000000..5faabd2 --- /dev/null +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md @@ -0,0 +1,457 @@ +# Bitai.LDAPHelper.LdapAdapters.LdapHelperMock ![Logo](../../resources/hierarchy_32.png) + +![.NET 10.0](https://img.shields.io/badge/.NET-10.0-512BD4) +![License: MIT](https://img.shields.io/badge/license-MIT-green) +![Package](https://img.shields.io/badge/package-Bitai.LDAPHelper.LdapAdapters.LdapHelperMock-blue) + +An in-memory LDAP adapter implementation designed for unit testing, integration testing, offline prototyping, local development, and CI/CD pipelines in the **Bitai LDAP Helper** ecosystem. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Solution Architecture](#solution-architecture) +- [Key Features](#key-features) +- [Project Structure & Class Taxonomy](#project-structure--class-taxonomy) +- [Quick Start](#quick-start) +- [Detailed Usage Scenarios](#detailed-usage-scenarios) + - [Scenario 1: Isolated Unit Testing with Custom Search Results](#scenario-1-isolated-unit-testing-with-custom-search-results) + - [Scenario 2: Integration Testing with Persistent Seeded Data](#scenario-2-integration-testing-with-persistent-seeded-data) + - [Scenario 3: Verifying Account Management Side Effects](#scenario-3-verifying-account-management-side-effects) +- [Data Store & Seeding Infrastructure](#data-store--seeding-infrastructure) + - [MockLdapDataStore](#mockldapdatastore) + - [MockLdapDataSeeder](#mockldapdataseeder) +- [Build, Test & Package](#build-test--package) +- [Observability & Diagnostic Assertions](#observability--diagnostic-assertions) +- [Troubleshooting](#troubleshooting) +- [Security & Operational Guidance](#security--operational-guidance) +- [License](#license) + +--- + +## Overview + +`Bitai.LDAPHelper.LdapAdapters.LdapHelperMock` provides lightweight, fast, in-memory mock implementations of the LDAP adapter contracts defined in [Bitai.LDAPHelper](../../src/Bitai.LDAPHelper/README.md). + +It enables developers to test LDAP-dependent components—such as authentication services (`Authenticator`), user search providers (`Searcher`), group membership validators (`GroupMembershipValidator`), and Active Directory account management routines (`AccountManager`)—without requiring access to a physical Active Directory, OpenLDAP, or containerized LDAP instance. + +### Target Framework & Specifications + +- **Target Framework:** .NET 10.0 (`net10.0`) +- **Nullable Context:** Enabled (`enable`) +- **Implicit Usings:** Enabled (`enable`) +- **Project Reference:** Depends on [Bitai.LDAPHelper](../../src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj) + +--- + +## Solution Architecture + +In the `Bitai.LDAPHelper` ecosystem, core services interact exclusively with LDAP contract abstractions (`ILdapConnectionFactoryAdapter`, `ILdapConnectionAdapter`, `ILdapEntryAdapter`, etc.) rather than concrete directory implementations. `LdapHelperMock` acts as a drop-in replacement for production adapters such as `Bitai.LDAPHelper.LdapAdapters.Novell`. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Consumer Application │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Bitai.LDAPHelper │ +│ (Authenticator, Searcher, AccountManager, Validator) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ILdapConnectionFactoryAdapter + ILdapConnectionAdapter + │ + ┌───────────────────────┴───────────────────────┐ + ▼ ▼ +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ LdapAdapters.Novell │ │ LdapHelperMock │ +│ (Production / Live LDAP) │ │ (In-Memory / Test Suite) │ +└──────────────────────────────┘ └──────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────┐ + │ MockLdapDataStore │ + │ (In-Memory Directory) │ + └──────────────────────────────┘ +``` + +### Repository Projects Overview + +| Project | Path | Role & Purpose | +|---|---|---| +| **`Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`** | [`adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`](file:///c:/_bitai/Bitai.LDAPHelper/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock) | In-memory mock adapter implementation & test seeder | +| **`Bitai.LDAPHelper`** | [`src/Bitai.LDAPHelper`](file:///c:/_bitai/Bitai.LDAPHelper/src/Bitai.LDAPHelper) | Core helper library defining adapter interfaces & services | +| **`Bitai.LDAPHelper.DTO`** | [`src/Bitai.LDAPHelper.DTO`](file:///c:/_bitai/Bitai.LDAPHelper/src/Bitai.LDAPHelper.DTO) | Data transfer objects, credentials, and operation results | +| **`Bitai.LDAPHelper.LdapAdapters.Novell`** | [`adapters/Bitai.LDAPHelper.LdapAdapters.Novell`](file:///c:/_bitai/Bitai.LDAPHelper/adapters/Bitai.LDAPHelper.LdapAdapters.Novell) | Novell-backed LDAP connection adapter for live environments | +| **`Bitai.LDAPHelper.Tests`** | [`tests/Bitai.LDAPHelper.Tests`](file:///c:/_bitai/Bitai.LDAPHelper/tests/Bitai.LDAPHelper.Tests) | Unit and integration test suite consuming mock adapters | + +--- + +## Key Features + +1. **In-Memory Connection Simulation (`MockLdapConnectionAdapter`)** + - Simulates `ConnectAsync`, `BindAsync`, `SearchAsync`, `AddEntryAsync`, `ModifyEntryAsync`, and `DeleteEntryAsync`. + - Supports registerable search results via `AddSearchResult(filterPattern, entries)`. +2. **Persistent Shared State (`MockLdapPersistentConnectionAdapter`)** + - Backed by `MockLdapDataStore` to persist entry creations, modifications, and deletions across multiple connections. + - Evaluates search filters dynamically against stored records. +3. **Deterministic Directory Seeder (`MockLdapDataSeeder`)** + - Populates realistic Active Directory objects: domain roots, OUs (Users, Groups, Computers), standard accounts, service accounts, and nested group memberships. + - Includes thread-safe progressive RID generation and structured logging via `ILogger`. +4. **Side-Effect Inspection & Assertions** + - Tracks created entries (`CreatedEntries`), modifications (`Modifications`), and deletions (`DeletedEntries`) for assertion in unit tests. +5. **Multi-Attribute Filter Engine** + - Evaluates LDAP search filter criteria including wildcard matching (`*`) against `sAMAccountName`, `distinguishedName`, `cn`, `objectSid`, and `objectClass`. +6. **Active Directory SID Conversion** + - Includes binary SID parsing utilities (`ConvertByteToStringSid`) to match Windows Active Directory `objectSid` formatting (`S-1-5-...`). + +--- + +## Project Structure & Class Taxonomy + +```text +adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/ +├── Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj +├── README.md +├── LICENSE.md +├── MockLdapConnectionAdapter.cs +├── MockLdapConnectionFactoryAdapter.cs +├── MockLdapPersistentConnectionAdapter.cs +├── MockLdapPersistentConnectionFactoryAdapter.cs +├── MockLdapEntryAdapter.cs +├── MockLdapAttributeSetAdapter.cs +├── MockLdapAttributeAdapter.cs +├── MockLdapModificationAdapter.cs +├── MockLdapMessageAdapter.cs +├── MockLdapSearchQueueAdapter.cs +└── LdapData/ + ├── MockLdapDataStore.cs + └── MockLdapDataSeeder.cs +``` + +### Class Roles Breakdown + +| Class | Implemented Interface | Primary Responsibility | +|---|---|---| +| `MockLdapConnectionAdapter` | `ILdapConnectionAdapter` | Standard in-memory mock connection with configurable search results & side-effect trackers. | +| `MockLdapConnectionFactoryAdapter` | `ILdapConnectionFactoryAdapter` | Factory returning an instance of `MockLdapConnectionAdapter`. | +| `MockLdapPersistentConnectionAdapter` | `ILdapConnectionAdapter` | Persistent mock connection connected to the central `MockLdapDataStore`. | +| `MockLdapPersistentConnectionFactoryAdapter` | `ILdapConnectionFactoryAdapter` | Factory producing `MockLdapPersistentConnectionAdapter` instances. | +| `MockLdapEntryAdapter` | `ILdapEntryAdapter` | Represents an LDAP entry with a DN and attribute set. | +| `MockLdapAttributeSetAdapter` | `ILdapAttributeSetAdapter` | Dictionary-backed container for entry attributes. Includes verification helpers. | +| `MockLdapAttributeAdapter` | `ILdapAttributeAdapter` | Represents individual attribute key-value pairs (string, string array, or byte array). | +| `MockLdapModificationAdapter` | `ILdapModificationAdapter` | Represents an LDAP entry modification (Add, Delete, Replace). | +| `MockLdapMessageAdapter` | `ILdapMessageAdapter` | Wraps single search result entries for message queue delivery. | +| `MockLdapSearchQueueAdapter` | `ILdapSearchQueueAdapter` | In-memory queue storing search result messages returned by `SearchAsync`. | +| `MockLdapDataStore` | *N/A (Singleton)* | Thread-safe in-memory directory store guarded by `ReaderWriterLockSlim`. | +| `MockLdapDataSeeder` | *N/A* | Seeds `MockLdapDataStore` with domain hierarchies, OUs, users, groups, and SIDs. | + +--- + +## Quick Start + +### Installation + +To add `Bitai.LDAPHelper.LdapAdapters.LdapHelperMock` to a test project: + +```bash +dotnet add package Bitai.LDAPHelper.LdapAdapters.LdapHelperMock +``` + +Or via project reference: + +```xml + + + +``` + +### Basic Minimal Example + +```csharp +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; + +// 1. Create a mock connection and configure stubbed search results +var mockConnection = new MockLdapConnectionAdapter(); + +var userEntry = new MockLdapEntryAdapter("CN=John Doe,OU=Users,DC=example,DC=com"); +userEntry.AddAttribute("cn", "John Doe"); +userEntry.AddAttribute("sAMAccountName", "jdoe"); +userEntry.AddAttribute("userPrincipalName", "jdoe@example.com"); + +mockConnection.AddSearchResult("(sAMAccountName=jdoe)", new List { userEntry }); + +// 2. Wrap in a factory +var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + +// 3. Initialize LDAPHelper services using the mock factory +var connectionInfo = new ConnectionInfo("localhost", 389, useSSL: false, connectionTimeout: 15); +var credential = new LDAPDomainAccountCredential("EXAMPLE", "service.account", "SecretPassword!"); +var searchLimits = new SearchLimits("DC=example,DC=com"); + +var searcher = new Searcher(connectionInfo, searchLimits, credential, factory); + +// 4. Perform search via Searcher +var user = await searcher.GetLdapEntryBySamAccountNameAsync("jdoe"); +Console.WriteLine($"Found User: {user?.CN} ({user?.DistinguishedName})"); +``` + +--- + +## Detailed Usage Scenarios + +### Scenario 1: Isolated Unit Testing with Custom Search Results + +For targeted unit tests where you want exact control over returned entries without global directory state: + +```csharp +using Xunit; +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; + +public class UserServiceTests +{ + [Fact] + public async Task GetUser_ShouldReturnMatchingLdapEntry() + { + // Arrange + var mockConnection = new MockLdapConnectionAdapter(); + + var expectedDn = "CN=Alice Smith,OU=Engineering,DC=corp,DC=local"; + var entry = new MockLdapEntryAdapter(expectedDn); + entry.AddAttribute("sAMAccountName", "asmith"); + entry.AddAttribute("mail", "alice.smith@corp.local"); + entry.AddAttribute("givenName", "Alice"); + entry.AddAttribute("sn", "Smith"); + + mockConnection.AddSearchResult("asmith", new List { entry }); + + var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + var searcher = new Searcher( + new ConnectionInfo("ldap.corp.local", 389, false, 10), + new SearchLimits("DC=corp,DC=local"), + new LDAPDomainAccountCredential("CORP", "admin", "pass"), + factory + ); + + // Act + var result = await searcher.GetLdapEntryBySamAccountNameAsync("asmith"); + + // Assert + Assert.NotNull(result); + Assert.Equal("asmith", result.SamAccountName); + Assert.Equal(expectedDn, result.DistinguishedName); + } +} +``` + +--- + +### Scenario 2: Integration Testing with Persistent Seeded Data + +When testing complex workflows that execute sequential searches, authentications, or group membership checks against a shared directory dataset: + +```csharp +using Xunit; +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging.Abstractions; + +public class AuthenticationIntegrationTests +{ + public AuthenticationIntegrationTests() + { + // Seed the shared in-memory data store with realistic AD entities + var seeder = new MockLdapDataSeeder(NullLogger.Instance); + seeder.SeedAllData(); + } + + [Fact] + public async Task AuthenticateDomainAccount_WithSeededUser_ShouldSucceed() + { + // Arrange - use persistent connection factory + var factory = new MockLdapPersistentConnectionFactoryAdapter(); + + var connectionInfo = new ConnectionInfo("dc01.domain.com", 389, false, 30); + var searchLimits = new SearchLimits("DC=domain,DC=com"); + var adminCreds = new LDAPDomainAccountCredential("DOMAIN", "Administrator", "AdminP@ss123"); + + var authenticator = new Authenticator(connectionInfo, searchLimits, adminCreds, factory); + + // Act - Authenticate a user populated by MockLdapDataSeeder + var targetUserCreds = new LDAPDomainAccountCredential("DOMAIN", "jdoe", "UserP@ssword1!"); + var authResult = await authenticator.AuthenticateDomainAccountAsync(targetUserCreds); + + // Assert + Assert.NotNull(authResult); + Assert.True(authResult.IsAuthenticated); + } +} +``` + +--- + +### Scenario 3: Verifying Account Management Side Effects + +When testing routines that create, modify, or delete directory objects (e.g. `AccountManager`), use `MockLdapConnectionAdapter` to verify generated modifications: + +```csharp +using Xunit; +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; + +public class AccountManagerTests +{ + [Fact] + public async Task ModifyUserAttribute_ShouldRecordModification() + { + // Arrange + var mockConnection = new MockLdapConnectionAdapter(); + var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + + var connectionInfo = new ConnectionInfo("localhost", 389, false, 10); + var searchLimits = new SearchLimits("DC=example,DC=com"); + var adminCreds = new LDAPDomainAccountCredential("EXAMPLE", "admin", "pass"); + + var accountManager = new AccountManager(connectionInfo, searchLimits, adminCreds, factory); + + var userDn = "CN=Bob Jones,OU=Users,DC=example,DC=com"; + var modifications = new List + { + new MockLdapModificationAdapter(LdapModificationType.Replace, "telephoneNumber", "+1-555-0199") + }; + + // Act + await accountManager.ModifyEntryAsync(userDn, modifications); + + // Assert + Assert.Single(mockConnection.Modifications); + var recordedMod = mockConnection.Modifications[0]; + Assert.Equal(userDn, recordedMod.DistinguishedName); + Assert.Equal("telephoneNumber", recordedMod.AttributeName); + Assert.Equal("+1-555-0199", recordedMod.Value); + } +} +``` + +--- + +## Data Store & Seeding Infrastructure + +### `MockLdapDataStore` + +`MockLdapDataStore` is a thread-safe singleton managing the in-memory LDAP hierarchy for persistent mock connections. + +- **Thread-Safety:** Operations are protected by `ReaderWriterLockSlim`. +- **Case-Insensitive Keys:** Distinguished names are indexed using `StringComparer.OrdinalIgnoreCase`. +- **API Surface:** + - `AddOrUpdateEntry(MockLdapEntryAdapter entry)` + - `GetEntry(string distinguishedName)` + - `SearchEntries(Func predicate)` + - `RemoveEntry(string distinguishedName)` + - `GetAllEntries()` + - `Clear()` + - `Count` + +### `MockLdapDataSeeder` + +`MockLdapDataSeeder` generates a complete, realistic directory structure inside `MockLdapDataStore`. + +#### Seeding Operations Executed by `SeedAllData()` + +1. **Domain Structure:** Seeds root domain `DC=domain,DC=com`. +2. **Organizational Units:** `OU=Users`, `OU=Groups`, `OU=Computers`, `OU=IT`, `OU=HR`, `OU=Finance`. +3. **Standard Users:** Generates standard accounts with attributes (`cn`, `sAMAccountName`, `userPrincipalName`, `mail`, `givenName`, `sn`, `objectSid`, `userAccountControl`). +4. **Security Groups:** Seeds domain groups (`Domain Admins`, `Domain Users`, `VPN Users`, `HR Staff`, `Finance Managers`). +5. **Computer Objects:** Seeds workstation and server entries. +6. **Group Memberships:** Links users to groups (`member` and `memberOf` attributes). +7. **RID Allocation:** Thread-safe RID counters starting at: + - Users: `1000` + - Groups: `2000` + - Computers: `3000` + - Other Objects: `4000` + +--- + +## Build, Test & Package + +### Prerequisites + +- **.NET SDK 10.0** or newer installed. +- PowerShell or standard Linux/macOS command shell. + +### Building the Project + +```bash +dotnet build adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj -c Release +``` + +### Running the Test Suite + +Run the full repository test suite that consumes `LdapHelperMock`: + +```bash +dotnet test tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj -c Release +``` + +### Creating the NuGet Package + +```bash +dotnet pack adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj -c Release -o ./artifacts +``` + +--- + +## Observability & Diagnostic Assertions + +When debugging unit or integration tests, `MockLdapConnectionAdapter` provides diagnostic tracking properties: + +```csharp +// Inspect entries created during the test run +List created = mockConnection.CreatedEntries; + +// Inspect all entry modifications executed +List mods = mockConnection.Modifications; + +// Inspect deleted distinguished names +List deleted = mockConnection.DeletedEntries; +``` + +`MockLdapDataSeeder` accepts an `ILogger` parameter, emitting structured log statements during data population for full visibility in console outputs or test runner logs. + +--- + +## Troubleshooting + +| Issue / Symptom | Probable Cause | Recommended Fix | +|---|---|---| +| `ConnectAsync` throws `Invalid server connection!` | `host` parameter is empty, `"unknown"`, or `"0.0.0.0"`, or `port <= 0`. | Provide a valid non-empty host string and positive port number (e.g. `"localhost"`, `389`). | +| `BindAsync` throws `Invalid credentials!` | Credentials contain `"hacker"` or `"123456"`, or are null/empty. | Use valid non-blacklisted mock credentials. | +| `SearchAsync` returns empty results in standard mode | The filter string was not registered via `AddSearchResult`. | Call `mockConnection.AddSearchResult(filterPattern, entries)` before executing queries. | +| Search returns empty results in persistent mode | `MockLdapDataSeeder` was not executed, or filter attribute is unsupported. | Execute `seeder.SeedAllData()` prior to testing. Ensure filter matches supported attributes (`sAMAccountName`, `distinguishedName`, `cn`, `objectSid`, `objectClass`). | +| `ServerCertificateValidationByPass` throws `InvalidOperationException` | Method called on mock class. | Certificate validation bypass is not applicable in mock mode. Remove call in test setups. | + +--- + +## Security & Operational Guidance + +- **Mock Credentials:** Never hardcode or commit production passwords or sensitive Active Directory credentials into test scripts or seeder logic. +- **Deterministic Testing:** Use fixed seeds or reset `MockLdapDataStore.Instance.Clear()` between tests to prevent inter-test state contamination. +- **Non-Production Purpose:** This package is explicitly designed for testing, CI/CD, and local prototyping. Do not deploy mock adapters to production environments. + +--- + +## License + +This project is licensed under the **MIT License**. See [LICENSE.md](LICENSE.md) for details. + +© 2026 **BITAI**. All rights reserved. diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj new file mode 100644 index 0000000..5921151 --- /dev/null +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj @@ -0,0 +1,46 @@ + + + + net10.0 + enable + enable + Viko Bastidas (BITAI) + BITAI + LDAP Services Wrappers + © 2026 BITAI. All rights reserved. + 10.0.0 + 10.0.0 + 10.0.0 + hierarchy_32.png + true + Bitai.LDAPHelper.LdapAdapters.Novell + https://github.com/bitai-cs/LDAPHelper.git + GitHub + ldap;authentication;authorization;directory;helper;oauth;openid;ad;security;identity + https://github.com/bitai-cs/LDAPHelper + LICENSE.md + README.md + Bitai.LDAPHelper adapter for wrapping Novell.Directory.Ldap.NETStandard. + + + + + + True + + + + + + + + + + + + + + + + + diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/LICENSE.md b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/LICENSE.md new file mode 100644 index 0000000..e263634 --- /dev/null +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2026 BITAI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapAttributeAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapAttributeAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapAttributeAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapAttributeAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapAttributeSetAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapAttributeSetAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapAttributeSetAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapAttributeSetAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapConnectionAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionFactoryAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapConnectionFactoryAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionFactoryAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapEntryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapEntryAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapEntryAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapEntryAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapMessageAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapMessageAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapMessageAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapMessageAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapModificationAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapModificationAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapModificationAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapModificationAdapter.cs diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapSearchQueueAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapSearchQueueAdapter.cs similarity index 100% rename from src/Bitai.LDAPHelper/LdapAdapters/Novell/NovellLdapSearchQueueAdapter.cs rename to adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapSearchQueueAdapter.cs diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/README.md b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/README.md new file mode 100644 index 0000000..fa19e50 --- /dev/null +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/README.md @@ -0,0 +1,382 @@ +# Bitai.LDAPHelper.LdapAdapters.Novell ![Logo](../../resources/hierarchy_32.png) + +![.NET 10.0](https://img.shields.io/badge/.NET-10.0-512BD4) +![License: MIT](https://img.shields.io/badge/license-MIT-green) +![Package](https://img.shields.io/badge/package-Bitai.LDAPHelper.LdapAdapters.Novell-blue) + +Production LDAP connection adapter for the **Bitai LDAP Helper** ecosystem, providing cross-platform connectivity to Active Directory, OpenLDAP, and RFC-compliant Directory Services via **Novell.Directory.Ldap.NETStandard**. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Solution Architecture](#solution-architecture) +- [Key Features](#key-features) +- [Class Architecture & Native Mapping](#class-architecture--native-mapping) +- [Quick Start](#quick-start) +- [Detailed Usage Scenarios](#detailed-usage-scenarios) + - [Scenario 1: Production Searcher Setup](#scenario-1-production-searcher-setup) + - [Scenario 2: Secure SSL/LDAPS Domain Authentication](#scenario-2-secure-sslldaps-domain-authentication) + - [Scenario 3: Directory Entry Management](#scenario-3-directory-entry-management) +- [Configuration & Security Guidance](#configuration--security-guidance) + - [Connection Parameters](#connection-parameters) + - [SSL/TLS & Certificate Validation](#ssltls--certificate-validation) +- [Build, Test & Package](#build-test--package) +- [Troubleshooting Matrix](#troubleshooting-matrix) +- [License](#license) + +--- + +## Overview + +`Bitai.LDAPHelper.LdapAdapters.Novell` bridges the core services of [Bitai.LDAPHelper](../../src/Bitai.LDAPHelper/README.md) with physical LDAP directory servers. + +By wrapping the open-source, fully cross-platform [Novell.Directory.Ldap.NETStandard](https://www.nuget.org/packages/Novell.Directory.Ldap.NETStandard) library (version 4.0.0), this adapter enables high-performance LDAP queries, user authentication, password updates, and Active Directory object management on Windows, Linux, and macOS without relying on Windows-only system libraries (`System.DirectoryServices`). + +### Target Framework & Specifications + +- **Target Framework:** .NET 10.0 (`net10.0`) +- **Nullable Context:** Enabled (`enable`) +- **Implicit Usings:** Enabled (`enable`) +- **Underlying Driver:** `Novell.Directory.Ldap.NETStandard` v4.0.0 +- **Project Reference:** Depends on [Bitai.LDAPHelper](../../src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj) + +--- + +## Solution Architecture + +Core services in `Bitai.LDAPHelper` (such as `Searcher`, `Authenticator`, and `AccountManager`) depend on adapter interfaces (`ILdapConnectionFactoryAdapter`, `ILdapConnectionAdapter`, `ILdapEntryAdapter`) rather than specific LDAP drivers. `Bitai.LDAPHelper.LdapAdapters.Novell` implements these interfaces for live production environments. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Consumer Application │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Bitai.LDAPHelper │ +│ (Authenticator, Searcher, AccountManager, Validator) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ILdapConnectionFactoryAdapter + ILdapConnectionAdapter + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Bitai.LDAPHelper.LdapAdapters.Novell │ +│ (Novell Production Adapter Layer) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Novell.Directory.Ldap.NETStandard │ +│ (Cross-Platform Network Protocol) │ +└─────────────────────────────────────────────────────────────────┘ + │ + LDAP (389) / LDAPS (636) Protocol + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Active Directory / OpenLDAP / Directory Server │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Repository Projects Overview + +| Project | Path | Role & Purpose | +|---|---|---| +| **`Bitai.LDAPHelper.LdapAdapters.Novell`** | [`adapters/Bitai.LDAPHelper.LdapAdapters.Novell`](file:///c:/_bitai/Bitai.LDAPHelper/adapters/Bitai.LDAPHelper.LdapAdapters.Novell) | Production Novell LDAP adapter implementation | +| **`Bitai.LDAPHelper`** | [`src/Bitai.LDAPHelper`](file:///c:/_bitai/Bitai.LDAPHelper/src/Bitai.LDAPHelper) | Core helper library defining services and adapter interfaces | +| **`Bitai.LDAPHelper.DTO`** | [`src/Bitai.LDAPHelper.DTO`](file:///c:/_bitai/Bitai.LDAPHelper/src/Bitai.LDAPHelper.DTO) | Data transfer objects, credential models, and operation results | +| **`Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`** | [`adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock`](file:///c:/_bitai/Bitai.LDAPHelper/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock) | In-memory mock adapter for offline testing & CI pipelines | + +--- + +## Key Features + +1. **Production LDAP & LDAPS Protocol Support** + - Connects to Active Directory and LDAP servers over standard unencrypted TCP (port 389) or secure SSL/TLS sockets (port 636). +2. **Automated Connection Lifecycle (`NovellLdapConnectionFactoryAdapter`)** + - Instantiates, configures timeouts, enables SSL, establishes socket connections (`ConnectAsync`), and authenticates (`BindAsync`) automatically. +3. **Asynchronous Search Streaming (`SearchAsync`)** + - Translates domain-level `ISearchLimits` (Base DN, Search Scope, Timeouts, Max Results) into Novell `LdapSearchConstraints`. + - Streams LDAP search results asynchronously via `ILdapSearchQueueAdapter`. +4. **Complete Directory Object Lifecycle** + - Supports creating (`AddEntryAsync`), updating (`ModifyEntryAsync`), and removing (`DeleteEntryAsync`) entries in directory trees. +5. **Multi-Type Attribute Translation** + - Native support for reading and writing single string values, multi-valued string arrays, and raw binary byte arrays (e.g. `objectSid`, `objectGUID`, `userCertificate`). +6. **Certificate Validation Customization** + - Provides `ServerCertificateValidationByPass()` for development, staging, or internal enterprise environments using self-signed PKI certificates. + +--- + +## Class Architecture & Native Mapping + +The adapter layer translates between Novell native types and `Bitai.LDAPHelper` abstractions: + +| Native Novell Class | Adapter Class | Implemented Abstraction | Role | +|---|---|---|---| +| `Novell.Directory.Ldap.LdapConnection` | `NovellLdapConnectionAdapter` | `ILdapConnectionAdapter` | Manages server connection, authentication state, and LDAP operations. | +| *N/A (Factory)* | `NovellLdapConnectionFactoryAdapter` | `ILdapConnectionFactoryAdapter` | Dynamically creates bound `NovellLdapConnectionAdapter` instances. | +| `Novell.Directory.Ldap.LdapEntry` | `NovellLdapEntryAdapter` | `ILdapEntryAdapter` | Represents a directory entry (DN and attribute set). | +| `Novell.Directory.Ldap.LdapAttributeSet` | `NovellLdapAttributeSetAdapter` | `ILdapAttributeSetAdapter` | Dictionary-style attribute container for entries and operations. | +| `Novell.Directory.Ldap.LdapAttribute` | `NovellLdapAttributeAdapter` | `ILdapAttributeAdapter` | Encapsulates single/multi-valued string and binary attribute values. | +| `Novell.Directory.Ldap.LdapModification` | `NovellLdapModificationAdapter` | `ILdapModificationAdapter` | Represents attribute modifications (Add, Delete, Replace). | +| `Novell.Directory.Ldap.LdapMessage` | `NovellLdapMessageAdapter` | `ILdapMessageAdapter` | Wraps incoming LDAP responses and search results. | +| `Novell.Directory.Ldap.LdapSearchQueue` | `NovellLdapSearchQueueAdapter` | `ILdapSearchQueueAdapter` | Queue for consuming search result messages from asynchronous queries. | + +--- + +## Quick Start + +### Installation + +Add `Bitai.LDAPHelper.LdapAdapters.Novell` to your .NET application: + +```bash +dotnet add package Bitai.LDAPHelper.LdapAdapters.Novell +``` + +Or reference it in your `.csproj`: + +```xml + + + +``` + +### Basic Minimal Example + +```csharp +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.Novell; + +// 1. Instanciate the Novell connection factory +var connectionFactory = new NovellLdapConnectionFactoryAdapter(); + +// 2. Configure connection settings and service account credentials +var connectionInfo = new ConnectionInfo("dc01.corp.contoso.com", 389, useSSL: false, connectionTimeout: 15); +var credential = new LDAPDomainAccountCredential("CONTOSO", "svc_ldap", "P@ssword123!"); +var searchLimits = new SearchLimits("DC=corp,DC=contoso,DC=com"); + +// 3. Inject factory into core LDAPHelper Searcher service +var searcher = new Searcher(connectionInfo, searchLimits, credential, connectionFactory); + +// 4. Query an entry from Active Directory +var user = await searcher.GetLdapEntryBySamAccountNameAsync("johndoe"); + +if (user != null) +{ + Console.WriteLine($"DN: {user.DistinguishedName}"); + Console.WriteLine($"Mail: {user.Mail}"); +} +``` + +--- + +## Detailed Usage Scenarios + +### Scenario 1: Production Searcher Setup + +Searching for active directory user accounts or groups using standard LDAP filters: + +```csharp +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.Novell; + +public class DirectorySearchService +{ + private readonly Searcher _searcher; + + public DirectorySearchService() + { + var factory = new NovellLdapConnectionFactoryAdapter(); + + var connectionInfo = new ConnectionInfo( + server: "ldap.example.com", + serverPort: 389, + useSSL: false, + connectionTimeout: 10 + ); + + var searchLimits = new SearchLimits( + baseDN: "OU=Employees,DC=example,DC=com", + maxSearchResults: 500, + maxSearchTimeout: 30, + ldapSearchScope: LdapSearchScope.ScopeSub + ); + + var credential = new LDAPDomainAccountCredential("EXAMPLE", "ldap_reader", "SecurePassword!"); + + _searcher = new Searcher(connectionInfo, searchLimits, credential, factory); + } + + public async Task FindUserByEmailAsync(string email) + { + var filter = $"(&(objectClass=user)(mail={email}))"; + var results = await _searcher.GetLdapEntriesAsync(filter); + return results?.FirstOrDefault(); + } +} +``` + +--- + +### Scenario 2: Secure SSL/LDAPS Domain Authentication + +Authenticating user credentials over encrypted LDAPS (Port 636) using `Authenticator`: + +```csharp +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters.Novell; + +public class AuthenticationService +{ + private readonly Authenticator _authenticator; + + public AuthenticationService() + { + var factory = new NovellLdapConnectionFactoryAdapter(); + + // Configure LDAPS over SSL on Port 636 + var connectionInfo = new ConnectionInfo( + server: "dc01.domain.com", + serverPort: 636, + useSSL: true, + connectionTimeout: 15 + ); + + var searchLimits = new SearchLimits("DC=domain,DC=com"); + var adminCreds = new LDAPDomainAccountCredential("DOMAIN", "svc_auth", "AdminSecret!"); + + _authenticator = new Authenticator(connectionInfo, searchLimits, adminCreds, factory); + } + + public async Task ValidateUserCredentialsAsync(string username, string password) + { + var userCreds = new LDAPDomainAccountCredential("DOMAIN", username, password); + var result = await _authenticator.AuthenticateDomainAccountAsync(userCreds); + + return result.IsAuthenticated; + } +} +``` + +--- + +### Scenario 3: Directory Entry Management + +Adding, modifying, or deleting entries in Active Directory using `AccountManager`: + +```csharp +using Bitai.LDAPHelper; +using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.Novell; + +public class DirectoryManagerService +{ + private readonly AccountManager _accountManager; + + public DirectoryManagerService() + { + var factory = new NovellLdapConnectionFactoryAdapter(); + + var connectionInfo = new ConnectionInfo("dc01.company.local", 389, false, 15); + var searchLimits = new SearchLimits("DC=company,DC=local"); + var adminCreds = new LDAPDomainAccountCredential("COMPANY", "DomainAdmin", "SuperSecure123!"); + + _accountManager = new AccountManager(connectionInfo, searchLimits, adminCreds, factory); + } + + public async Task UpdatePhoneNumberAsync(string userDn, string newPhone) + { + // 1. Create connection to modify entry + var adapter = new NovellLdapConnectionFactoryAdapter(); + using var connection = await adapter.CreateConnectionAsync( + new ConnectionInfo("dc01.company.local", 389, false, 15), + "DomainAdmin", + "SuperSecure123!" + ); + + // 2. Prepare modification + var mod = connection.CreateModification(LdapModificationType.Replace, "telephoneNumber", newPhone); + + // 3. Apply modification + await connection.ModifyEntryAsync(userDn, new[] { mod }); + } +} +``` + +--- + +## Configuration & Security Guidance + +### Connection Parameters + +`NovellLdapConnectionFactoryAdapter` maps `IConnectionInfo` properties as follows: + +| `IConnectionInfo` Property | Type | Mapping / Novell Adapter Behavior | +|---|---|---| +| `Server` | `string` | Passed to `ConnectAsync(host, port)`. Hostname or IP address. | +| `ServerPort` | `int` | Passed to `ConnectAsync(host, port)`. Default `389` (LDAP) or `636` (LDAPS). | +| `UseSSL` | `bool` | Enables `SecureSocketLayer = true` and attaches SSL certificate handler. | +| `ConnectionTimeout` | `int` (sec) | Multiplied by 1000 and assigned to Novell `LdapConnection.ConnectionTimeout` (in milliseconds). | + +### SSL/TLS & Certificate Validation + +When `UseSSL` is enabled, `NovellLdapConnectionFactoryAdapter` sets `SecureSocketLayer = true` and invokes `ServerCertificateValidationByPass()`. + +> [!WARNING] +> `ServerCertificateValidationByPass()` attaches a validation delegate that accepts any server certificate (bypassing SSL certificate chain checking). This is convenient for development and internal enterprise environments using self-signed Active Directory CA certificates. For strict security environments requiring explicit certificate chain validation, customize the delegate on `NovellLdapConnectionAdapter`. + +--- + +## Build, Test & Package + +### Prerequisites + +- **.NET SDK 10.0** or newer installed. +- Network access to a test LDAP/Active Directory server (or execute repository tests using `LdapHelperMock`). + +### Building the Project + +```bash +dotnet build adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj -c Release +``` + +### Running Tests + +```bash +dotnet test tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj -c Release +``` + +### Packing the NuGet Package + +```bash +dotnet pack adapters/Bitai.LDAPHelper.LdapAdapters.Novell/Bitai.LDAPHelper.LdapAdapters.Novell.csproj -c Release -o ./artifacts +``` + +--- + +## Troubleshooting Matrix + +| Symptom / Error | Probable Cause | Recommended Fix | +|---|---|---| +| `LdapException: Connect Error` | Invalid host, closed port, or network firewall blocking port 389/636. | Verify LDAP server hostname, check DNS resolution, and test port open with `Test-NetConnection -ComputerName host -Port 389`. | +| `LdapException: Invalid Credentials` (Code 49) | Invalid distinguished name, domain user format, or incorrect password. | Use fully qualified UPN (`user@domain.com`) or DN (`CN=User,OU=...,DC=...`). | +| SSL Handshake Failure on Port 636 | `UseSSL` is `false` when connecting to SSL port 636, or SSL certificate mismatch. | Ensure `UseSSL = true` on port 636. If using self-signed certs, ensure certificate bypass is enabled. | +| `LdapException: Size Limit Exceeded` (Code 4) | Query returned more items than permitted by Active Directory policy (default 1000). | Adjust `SearchLimits.MaxSearchResults` or use paged LDAP searches. | +| `ArgumentOutOfRangeException: Unknown type of modification` | Invalid `LdapModificationType` value passed to `CreateModification`. | Ensure `LdapModificationType` is one of `Add`, `Delete`, or `Replace`. | + +--- + +## License + +This project is licensed under the **MIT License**. See [LICENSE.md](LICENSE.md) for details. + +© 2026 **BITAI**. All rights reserved. diff --git a/demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj b/demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj index 42c118b..43e294d 100644 --- a/demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj +++ b/demo/Bitai.LDAPHelper.Demo/Bitai.LDAPHelper.Demo.csproj @@ -32,7 +32,7 @@ - - + + \ No newline at end of file diff --git a/demo/Bitai.LDAPHelper.Demo/Program.cs b/demo/Bitai.LDAPHelper.Demo/Program.cs index 1ca9190..238faab 100644 --- a/demo/Bitai.LDAPHelper.Demo/Program.cs +++ b/demo/Bitai.LDAPHelper.Demo/Program.cs @@ -6,8 +6,8 @@ using Bitai.LDAPHelper.DTO; using Microsoft.Extensions.Configuration; using Bitai.LDAPHelper.LdapAdapters.Novell; -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; -using Bitai.LDAPHelper.Tests.Mocks.LdapData; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; using Microsoft.Extensions.Logging; namespace Bitai.LDAPHelper.Demo; diff --git a/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj b/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj index 69d74b1..82feacd 100644 --- a/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj +++ b/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj @@ -7,9 +7,9 @@ LDAP Services Wrappers Library to wrap Novell.Directory.Ldap.NETStandard functionality to make LDAP common queries to search accounts and objects in a Directory Service. © 2026 BITAI. All rights reserved. - 10.1.3 - 10.1.3 - 10.1.3 + 10.2.0 + 10.2.0 + 10.2.0 hierarchy_32.png true Bitai.LDAPHelper @@ -44,7 +44,6 @@ - diff --git a/src/Bitai.LDAPHelper/LdapAdapters/Enums.cs b/src/Bitai.LDAPHelper/LdapAdapters/Enums.cs new file mode 100644 index 0000000..69f71f5 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/Enums.cs @@ -0,0 +1,43 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// LDAP modification operation types +/// +public enum LdapModificationType +{ + /// + /// Adds an attribute value. + /// + Add = 0, + + /// + /// Deletes an attribute value. + /// + Delete = 1, + + /// + /// Replaces an attribute value. + /// + Replace = 2 +} + +/// +/// LDAP search scope values +/// +public enum LdapSearchScope +{ + /// + /// Search only the base object. + /// + ScopeBase = 0, + + /// + /// Search one level below the base object. + /// + ScopeOne = 1, + + /// + /// Search the entire subtree below the base object. + /// + ScopeSub = 2 +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/IConnectionInfo.cs b/src/Bitai.LDAPHelper/LdapAdapters/IConnectionInfo.cs new file mode 100644 index 0000000..cfceba1 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/IConnectionInfo.cs @@ -0,0 +1,27 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines LDAP server connection settings required to open an LDAP connection. +/// +public interface IConnectionInfo +{ + /// + /// Gets the connection timeout in seconds. + /// + short ConnectionTimeout { get; } + + /// + /// Gets the LDAP server host name or IP address. + /// + string Server { get; } + + /// + /// Gets the LDAP server port. + /// + int ServerPort { get; } + + /// + /// Gets a value indicating whether SSL must be used. + /// + bool UseSSL { get; } +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeAdapter.cs new file mode 100644 index 0000000..221721b --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeAdapter.cs @@ -0,0 +1,27 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines read access to LDAP attribute values in multiple representations. +/// +public interface ILdapAttributeAdapter +{ + /// + /// Gets the first binary value of the attribute. + /// + byte[] ByteValue { get; } + + /// + /// Gets the first string value of the attribute. + /// + string StringValue { get; } + + /// + /// Gets all string values of the attribute. + /// + string[] StringValueArray { get; } + + /// + /// Gets all binary values of the attribute. + /// + byte[][] ByteValueArray { get; } +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeSetAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeSetAdapter.cs new file mode 100644 index 0000000..2180fc6 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapAttributeSetAdapter.cs @@ -0,0 +1,42 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines operations for building and reading LDAP attribute sets. +/// +public interface ILdapAttributeSetAdapter +{ + /// + /// Adds a single string value to an attribute. + /// + /// Attribute name. + /// Attribute value. + void AddAttribute(string name, string value); + + /// + /// Adds multiple string values to an attribute. + /// + /// Attribute name. + /// Attribute values. + void AddAttribute(string name, string[] values); + + /// + /// Adds a binary value to an attribute. + /// + /// Attribute name. + /// Binary attribute value. + void AddAttribute(string name, byte[] value); + + /// + /// Determines whether an attribute exists in the set. + /// + /// Attribute name to look up. + /// if found; otherwise . + bool ContainsKey(string attributeName); + + /// + /// Gets an attribute by name. + /// + /// Attribute name. + /// The attribute adapter when found; otherwise . + ILdapAttributeAdapter GetAttribute(string attributeName); +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionAdapter.cs new file mode 100644 index 0000000..ea0e4d0 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionAdapter.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines the operations required to manage and use an LDAP connection. +/// +public interface ILdapConnectionAdapter : IDisposable +{ + /// + /// Gets or sets the connection timeout in milliseconds. + /// + int ConnectionTimeout { get; set; } + + /// + /// Gets or sets a value indicating whether SSL is enabled. + /// + bool SecureSocketLayer { get; set; } + + /// + /// Gets a value indicating whether the connection is authenticated (bound). + /// + bool IsBound { get; } + + /// + /// Bypasses server certificate validation. + /// + /// Use only in controlled environments. + void ServerCertificateValidationByPass(); + + /// + /// Connects to the LDAP server. + /// + /// Server host name or IP address. + /// Server port. + /// A task that completes when the connection is established. + Task ConnectAsync(string host, int port); + + /// + /// Authenticates the connection. + /// + /// User distinguished name or account identifier. + /// Account password. + /// A task that completes when bind finishes. + Task BindAsync(string userDN, string password); + + /// + /// Executes an LDAP search. + /// + /// Search boundaries and limits. + /// LDAP filter expression. + /// Attributes to return. + /// Whether only attribute names should be returned. + /// A queue adapter with server responses. + Task SearchAsync(ISearchLimits searchLimits, string searchFilter, string[] attributeNames, bool typesOnly); + + /// + /// Creates an empty attribute set for add/update operations. + /// + /// An attribute set adapter. + ILdapAttributeSetAdapter CreateAttributeSet(); + + /// + /// Adds an entry to the directory. + /// + /// Distinguished name for the new entry. + /// Attribute set for the new entry. + /// A task that completes when the entry is created. + Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAdapter attributes); + + /// + /// Creates a modification descriptor. + /// + /// Modification type. + /// Attribute name to modify. + /// New attribute value. + /// A modification adapter. + ILdapModificationAdapter CreateModification(LdapModificationType type, string attributeName, object value); + + /// + /// Applies modifications to an existing entry. + /// + /// Target entry distinguished name. + /// Modifications to apply. + /// A task that completes when the update finishes. + Task ModifyEntryAsync(string distinguishedName, IEnumerable modifications); + + /// + /// Deletes an entry from the directory. + /// + /// Target entry distinguished name. + /// A task that completes when deletion finishes. + Task DeleteEntryAsync(string distinguishedName); + + /// + /// Disconnects the connection from the server. + /// + void Disconnect(); +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionFactoryAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionFactoryAdapter.cs new file mode 100644 index 0000000..2277f1e --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapConnectionFactoryAdapter.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; + +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines a factory for creating initialized LDAP connections. +/// +public interface ILdapConnectionFactoryAdapter +{ + /// + /// Creates a new LDAP connection and optionally binds it with credentials. + /// + /// Server connection settings. + /// Account identifier used for bind. + /// Account password used for bind. + /// + /// to fail when bind cannot be completed; otherwise . + /// + /// A task with an initialized LDAP connection adapter. + Task CreateConnectionAsync( + IConnectionInfo connectionInfo, + string userAccount, + string password, + bool bindRequired = true); +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapEntryAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapEntryAdapter.cs new file mode 100644 index 0000000..6a721c7 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapEntryAdapter.cs @@ -0,0 +1,18 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines read access to an LDAP entry and its attribute set. +/// +public interface ILdapEntryAdapter +{ + /// + /// Gets the distinguished name of the entry. + /// + string DistinguishedName { get; } + + /// + /// Gets the attribute set associated with the entry. + /// + /// An attribute set adapter. + ILdapAttributeSetAdapter GetAttributeSet(); +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapMessageAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapMessageAdapter.cs new file mode 100644 index 0000000..d801c69 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapMessageAdapter.cs @@ -0,0 +1,22 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines accessors for LDAP protocol messages returned by search operations. +/// +public interface ILdapMessageAdapter +{ + /// + /// Gets the LDAP entry when the message represents a search result; otherwise . + /// + ILdapEntryAdapter Entry { get; } + + /// + /// Gets a value indicating whether this message is a search-result message. + /// + bool IsSearchResult { get; } + + /// + /// Gets a value indicating whether this message signals end-of-search. + /// + bool IsSearchDone { get; } +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapModificationAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapModificationAdapter.cs new file mode 100644 index 0000000..5abf8df --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapModificationAdapter.cs @@ -0,0 +1,17 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines access to a single LDAP modification operation. +/// +public interface ILdapModificationAdapter +{ + /// + /// Gets the modification operation type. + /// + LdapModificationType ModificationType { get; } + + /// + /// Gets the attribute associated with the modification. + /// + ILdapAttributeAdapter Attribute { get; } +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchConstraintsAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchConstraintsAdapter.cs new file mode 100644 index 0000000..22cdc4b --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchConstraintsAdapter.cs @@ -0,0 +1,17 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines LDAP server-side search constraints. +/// +public interface ILdapSearchConstraintsAdapter +{ + /// + /// Gets or sets the server-side time limit in seconds. + /// + int ServerTimeLimit { get; set; } + + /// + /// Gets or sets the maximum number of entries returned by the server. + /// + int MaxResults { get; set; } +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchQueueAdapter.cs b/src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchQueueAdapter.cs new file mode 100644 index 0000000..bc77109 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ILdapSearchQueueAdapter.cs @@ -0,0 +1,13 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines access to queued LDAP search responses. +/// +public interface ILdapSearchQueueAdapter +{ + /// + /// Gets the next response from the queue. + /// + /// The next message adapter; or when no more responses are available. + ILdapMessageAdapter GetResponse(); +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/ISearchLimits.cs b/src/Bitai.LDAPHelper/LdapAdapters/ISearchLimits.cs new file mode 100644 index 0000000..df33418 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/ISearchLimits.cs @@ -0,0 +1,27 @@ +namespace Bitai.LDAPHelper.LdapAdapters; + +/// +/// Defines LDAP search boundaries and limits. +/// +public interface ISearchLimits +{ + /// + /// Gets or sets the base distinguished name from which the search starts. + /// + string BaseDN { get; set; } + + /// + /// Gets or sets the LDAP search scope. + /// + LdapSearchScope LdapSearchScope { get; set; } + + /// + /// Gets or sets the maximum number of entries to return. + /// + int MaxSearchResults { get; set; } + + /// + /// Gets or sets the maximum server processing time in seconds. + /// + int MaxSearchTimeout { get; set; } +} diff --git a/src/Bitai.LDAPHelper/LdapAdapters/README.md b/src/Bitai.LDAPHelper/LdapAdapters/README.md new file mode 100644 index 0000000..ee1b0f4 --- /dev/null +++ b/src/Bitai.LDAPHelper/LdapAdapters/README.md @@ -0,0 +1,110 @@ +# Bitai.LDAPHelper.LdapAdapters + +`Bitai.LDAPHelper.LdapAdapters` defines the LDAP adapter contracts used by the Bitai LDAP Helper libraries. It is a small abstraction layer that keeps the core helper logic independent from any specific LDAP client implementation, such as `Novell.Directory.Ldap.NETStandard`. + +This package contains interfaces and enums only. It does not open LDAP connections by itself and does not include a concrete LDAP provider. + +## Purpose + +Use this project when you need to: + +- Implement a concrete LDAP provider for `Bitai.LDAPHelper`. +- Test LDAP-dependent code without binding directly to a real directory server. +- Isolate application logic from vendor-specific LDAP client APIs. +- Standardize LDAP operations such as bind, search, add, modify, delete, and attribute access. + +## Target Framework + +- .NET 10.0 +- Nullable reference types enabled +- Implicit global usings enabled + +## Package Metadata + +- Package ID: `Bitai.LDAPHelper.LdapAdapters` +- Version: `10.0.0` +- Repository: `https://github.com/bitai-cs/LDAPHelper` +- License: MIT + +## Main Contracts + +### Connection and Factory + +- `ILdapConnectionFactoryAdapter` creates configured LDAP connections from `IConnectionInfo` and credentials. +- `ILdapConnectionAdapter` represents a connected LDAP session and exposes bind, search, add, modify, delete, disconnect, and dispose operations. +- `IConnectionInfo` describes LDAP server address, port, SSL usage, and timeout settings. + +### Search + +- `ISearchLimits` describes base DN, search scope, result limits, and timeout limits. +- `ILdapSearchQueueAdapter` abstracts queued LDAP search responses. +- `ILdapMessageAdapter` distinguishes search result entries from search completion messages. + +### Entries and Attributes + +- `ILdapEntryAdapter` exposes an LDAP entry distinguished name and its attribute set. +- `ILdapAttributeSetAdapter` creates and retrieves LDAP attributes by name. +- `ILdapAttributeAdapter` exposes attribute values as string, string array, byte array, or byte array collection. + +### Modifications + +- `ILdapModificationAdapter` describes an LDAP attribute modification. +- `LdapModificationType` supports add, delete, and replace operations. +- `LdapSearchScope` supports base, one-level, and subtree LDAP searches. + +## Implementing an Adapter + +A concrete provider typically implements `ILdapConnectionFactoryAdapter` and `ILdapConnectionAdapter`, then maps the Bitai contracts to the provider-specific LDAP client. + +```csharp +using Bitai.LDAPHelper.LdapAdapters; + +public sealed class CustomLdapConnectionFactoryAdapter : ILdapConnectionFactoryAdapter +{ + public async Task CreateConnectionAsync( + IConnectionInfo connectionInfo, + string userAccount, + string password, + bool bindRequired = true) + { + var connection = new CustomLdapConnectionAdapter(); + connection.ConnectionTimeout = connectionInfo.ConnectionTimeout; + connection.SecureSocketLayer = connectionInfo.UseSSL; + + await connection.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); + + if (bindRequired) + { + await connection.BindAsync(userAccount, password); + } + + return connection; + } +} +``` + +The adapter is then passed into higher-level `Bitai.LDAPHelper` services such as authentication, search, account management, and group membership validation. + +## Design Notes + +- The package intentionally contains no dependency on a specific LDAP SDK. +- Adapter implementations should preserve provider exceptions only when callers can handle them meaningfully; otherwise, translate them into domain-level exceptions used by the consuming library. +- `ILdapConnectionAdapter` implements `IDisposable`; concrete implementations should release network resources and provider-specific handles consistently. +- `ServerCertificateValidationByPass` exists for compatibility with provider adapters. Use it only in controlled development or test environments. + +## Build + +From the repository root: + +```powershell +dotnet build src/Bitai.LDAPHelper.LdapAdapters/Bitai.LDAPHelper.LdapAdapters.csproj +``` + +## Related Projects + +- `Bitai.LDAPHelper`: Core LDAP helper services that consume these contracts. +- `Bitai.LDAPHelper.Tests.Mocks`: In-memory mock implementations for tests and demos. + +## License + +This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md). diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/Bitai.LDAPHelper.Tests.Mocks.csproj b/tests/Bitai.LDAPHelper.Tests.Mocks/Bitai.LDAPHelper.Tests.Mocks.csproj deleted file mode 100644 index dde4a93..0000000 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/Bitai.LDAPHelper.Tests.Mocks.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net10.0 - enable - enable - 10.1.1 - 10.1.1 - 10.1.1 - Viko Bastidas (BITAI) - © 2026 BITAI. MIT License. - https://github.com/bitai-cs/LDAPHelper.git - GitHub - ldap;authentication;authorization;directory;helper;oauth;openid;ad;security;identity - Bitai.LDAPHelper.Tests.Mocks - BITAI - LDAP Services Wrappers - Mocks for testing BITAI.LDAPHelper library. - https://github.com/bitai-cs/LDAPHelper - True - LICENSE.md - README.md - - - - - - - - - True - \ - - - True - \ - - - - diff --git a/tests/Bitai.LDAPHelper.Tests.Mocks/README.md b/tests/Bitai.LDAPHelper.Tests.Mocks/README.md deleted file mode 100644 index 915142a..0000000 --- a/tests/Bitai.LDAPHelper.Tests.Mocks/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# Bitai.LDAPHelper.Tests.Mocks - -`Bitai.LDAPHelper.Tests.Mocks` provides in-memory LDAP adapter implementations for testing and demo scenarios in the Bitai LDAP Helper ecosystem. It implements the adapter contracts consumed by `Bitai.LDAPHelper`, allowing authentication, search, account management, and group membership workflows to be exercised without a live LDAP or Active Directory server. - -This project is intended for tests, demos, and local development support. It should not be used as a production LDAP provider. - -## Purpose - -Use this project when you need to: - -- Unit test LDAP-dependent code without network access. -- Verify how `Bitai.LDAPHelper` services call LDAP adapter contracts. -- Seed predictable directory entries for repeatable test cases. -- Record add, modify, and delete operations performed by higher-level services. -- Run demo flows in an offline mock mode. - -## Target Framework - -- .NET 10.0 -- Nullable reference types enabled -- Implicit global usings enabled - -## Package Metadata - -- Package ID: `Bitai.LDAPHelper.Tests.Mocks` -- Version: `10.0.0` -- Repository: `https://github.com/bitai-cs/LDAPHelper` -- License: MIT - -## Project Dependencies - -This project references: - -- `Bitai.LDAPHelper` -- `Bitai.LDAPHelper.LdapAdapters` -- `Bitai.LDAPHelper.DTO` - -Those references provide the public helper services, adapter contracts, DTOs, and LDAP-related domain types used by the mocks. - -## Main Components - -### Adapter Mocks - -- `MockLdapConnectionAdapter` provides a configurable in-memory LDAP connection. -- `MockLdapConnectionFactoryAdapter` returns a supplied mock connection and simulates connect and bind behavior. -- `MockLdapPersistenConnectionAdapter` extends the basic mock connection with a shared in-memory data store. -- `MockLdapPersistentConnectionFactoryAdapter` creates the persistent mock connection used by richer demo scenarios. -- `MockLdapEntryAdapter`, `MockLdapAttributeSetAdapter`, `MockLdapAttributeAdapter`, `MockLdapMessageAdapter`, `MockLdapSearchQueueAdapter`, and `MockLdapModificationAdapter` implement the lower-level LDAP contracts. - -### Mock Data - -- `MockLdapDataStore` is a thread-safe singleton store keyed by distinguished name. -- `MockLdapDataSeeder` populates the store with representative domains, organizational units, users, computers, groups, and group memberships. - -## Basic Unit Test Usage - -The simplest pattern is to create a mock connection, register search results for expected filters, and pass a mock factory into the service under test. - -```csharp -using Bitai.LDAPHelper; -using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; - -var connection = new MockLdapConnectionAdapter(); - -var userEntry = new MockLdapEntryAdapter("CN=John Smith,OU=Users,DC=example,DC=com"); -userEntry.AddAttribute("cn", "John Smith"); -userEntry.AddAttribute("sAMAccountName", "john.smith"); -userEntry.AddAttribute("distinguishedName", userEntry.DistinguishedName); - -connection.AddSearchResult("(sAMAccountName=john.smith)", new List -{ - userEntry -}); - -var factory = new MockLdapConnectionFactoryAdapter(connection); - -var connectionInfo = new ConnectionInfo("localhost", 389, useSSL: false, connectionTimeout: 15); -var credential = new LDAPDomainAccountCredential("EXAMPLE", "service.account", "StrongPassword!"); -var searchLimits = new SearchLimits("DC=example,DC=com") -{ - MaxSearchResults = 10, - MaxSearchTimeout = 15 -}; - -var searcher = new Searcher(connectionInfo, searchLimits, credential, factory); -``` - -## Verifying Write Operations - -`MockLdapConnectionAdapter` records created, modified, and deleted entries so tests can assert side effects. - -```csharp -var connection = new MockLdapConnectionAdapter(); -var factory = new MockLdapConnectionFactoryAdapter(connection); - -// Execute code that creates, modifies, or deletes LDAP entries. - -Assert.NotEmpty(connection.CreatedEntries); -Assert.NotEmpty(connection.Modifications); -Assert.NotEmpty(connection.DeletedEntries); -``` - -## Seeded Directory Usage - -For integration-style tests or demos, use the persistent connection factory with `MockLdapDataSeeder`. - -```csharp -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; -using Bitai.LDAPHelper.Tests.Mocks.LdapData; -using Microsoft.Extensions.Logging.Abstractions; - -var seeder = new MockLdapDataSeeder(NullLogger.Instance); -seeder.SeedAllData(); - -var factory = new MockLdapPersistentConnectionFactoryAdapter(); -``` - -The seeded data includes representative LATAM domain roots, organizational units, user accounts, groups, computers, and nested memberships used by the repository tests and demo project. - -## Mock Behavior Notes - -- `MockLdapConnectionAdapter.ConnectAsync` rejects empty hosts, `unknown`, `0.0.0.0`, and non-positive ports. -- `MockLdapConnectionAdapter.BindAsync` rejects empty credentials and intentionally weak or suspicious sample credentials used by tests. -- Search matching in the basic mock connection is filter-pattern based; register the expected filter text before executing the service under test. -- The persistent mock connection searches the shared `MockLdapDataStore` and applies a richer in-memory behavior model. -- `ServerCertificateValidationByPass` throws in mock classes because certificate bypass behavior is not meaningful in an in-memory implementation. - -## Build - -From the repository root: - -```powershell -dotnet build tests/Bitai.LDAPHelper.Tests.Mocks/Bitai.LDAPHelper.Tests.Mocks.csproj -``` - -## Related Projects - -- `Bitai.LDAPHelper.LdapAdapters`: Adapter contracts implemented by this project. -- `Bitai.LDAPHelper`: Core LDAP helper services tested with these mocks. -- `Bitai.LDAPHelper.Tests`: Test suite that consumes these mocks. -- `Bitai.LDAPHelper.Demo`: Demo application that can run against the persistent mock data store. - -## License - -This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md). diff --git a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs index 883d331..d259e9d 100644 --- a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs @@ -1,5 +1,5 @@ using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; namespace Bitai.LDAPHelper.Tests { diff --git a/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs index e46fe8e..03aaafe 100644 --- a/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs @@ -1,5 +1,5 @@ using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; namespace Bitai.LDAPHelper.Tests { diff --git a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs index 3f5a69e..fdf04dd 100644 --- a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs @@ -1,4 +1,4 @@ -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; namespace Bitai.LDAPHelper.Tests { diff --git a/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj b/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj index 8f42981..3d8cd38 100644 --- a/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj +++ b/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -29,9 +29,7 @@ - - - + diff --git a/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs b/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs index 63ab04e..29f479e 100644 --- a/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs @@ -1,5 +1,5 @@ using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; namespace Bitai.LDAPHelper.Tests { diff --git a/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs index a467d3a..236fd28 100644 --- a/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs @@ -1,5 +1,5 @@ using Bitai.LDAPHelper.DTO; -using Bitai.LDAPHelper.Tests.Mocks.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; namespace Bitai.LDAPHelper.Tests { From 7f95683bf38bc3641ee3264ceee35fb7a1c39dea Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Tue, 21 Jul 2026 02:37:24 -0400 Subject: [PATCH 2/2] Fix CI Github action. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f99a5a..c8717e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,10 @@ jobs: run: dotnet --info - name: Restore - run: dotnet restore Bitai.Ldap.Helper.sln + run: dotnet restore Bitai.Ldap.Helper.slnx - name: Build - run: dotnet build Bitai.Ldap.Helper.sln --configuration Release --no-restore + run: dotnet build Bitai.Ldap.Helper.slnx --configuration Release --no-restore - name: Test - run: dotnet test Bitai.Ldap.Helper.sln --configuration Release --no-build --no-restore --verbosity normal + run: dotnet test Bitai.Ldap.Helper.slnx --configuration Release --no-build --no-restore --verbosity normal