Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/FreeDSx/Ldap/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\SafeModifyConstraint;
use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory;
use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine;
use FreeDSx\Ldap\Server\PasswordPolicy\Replica\InMemoryReplicaPasswordStateStore;
use FreeDSx\Ldap\Server\PasswordPolicy\Replica\ReplicaPasswordStateStoreInterface;
use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService;
use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher;
use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver;
Expand Down Expand Up @@ -226,6 +228,10 @@ className: ClockInterface::class,
className: PasswordPolicyEngine::class,
factory: $this->makePasswordPolicyEngine(...),
);
$this->registerFactory(
className: ReplicaPasswordStateStoreInterface::class,
factory: $this->makeReplicaPasswordStateStore(...),
);
$this->registerFactory(
className: ServerProtocolHandlerFactory::class,
factory: $this->makeServerProtocolHandlerFactory(...),
Expand Down Expand Up @@ -395,9 +401,20 @@ private function makeServerProtocolFactory(): ServerProtocolFactory
metricsRecorder: $this->get(MetricsRecorderInterface::class),
metricsSnapshots: $this->get(MetricsSnapshotProvider::class),
operationRollup: $this->makeOperationRollup(),
replicaPasswordStateStore: $this->get(ServerOptions::class)->isReadOnly()
? $this->get(ReplicaPasswordStateStoreInterface::class)
: null,
);
}

/**
* The replica-local password-policy state store, backing always-enforced lockout on a read-only replica.
*/
private function makeReplicaPasswordStateStore(): ReplicaPasswordStateStoreInterface
{
return new InMemoryReplicaPasswordStateStore();
}

private function makeServerProtocolFactoryInterface(): ServerProtocolFactoryInterface
{
if ($this->has(ProxyOptions::class)) {
Expand Down
2 changes: 1 addition & 1 deletion src/FreeDSx/Ldap/Exception/SchemaRuleException.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace FreeDSx\Ldap\Exception;

use FreeDSx\Ldap\Server\Backend\Write\SchemaViolations;
use FreeDSx\Ldap\Server\Backend\Write\Schema\SchemaViolations;

/**
* A rejected schema violation that carries the violations collected during the write so they can be audited.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
use FreeDSx\Ldap\Operation\OperationType;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\Write\Command\DeleteCommand;
use FreeDSx\Ldap\Server\Backend\Write\SchemaViolations;
use FreeDSx\Ldap\Server\Backend\Write\Schema\SchemaViolations;
use FreeDSx\Ldap\Server\Backend\Write\WritableLdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\Write\WriteCommandFactory;
use FreeDSx\Ldap\Server\Backend\Write\WriteContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
use FreeDSx\Ldap\Schema\SchemaValidationMode;
use FreeDSx\Ldap\Schema\Schema;
use FreeDSx\Ldap\Schema\Validation\SchemaValidator;
use FreeDSx\Ldap\Server\Backend\Write\SchemaViolationDisposition;
use FreeDSx\Ldap\Server\Backend\Write\Schema\SchemaViolationDisposition;
use FreeDSx\Ldap\Server\Backend\Write\WritableLdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\Write\WriteContext;
use FreeDSx\Ldap\Server\Backend\Write\WriteRequestInterface;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\Write\Command\UpdateCommand;
use FreeDSx\Ldap\Server\Backend\Write\SystemChange\SystemChangeWriterInterface;
use FreeDSx\Ldap\Server\PasswordPolicy\Attempt\PasswordModifyAttempt;
use FreeDSx\Ldap\Server\PasswordPolicy\Guard\PasswordPolicyChangeGuard;
use FreeDSx\Ldap\Server\Token\AuthenticatedTokenInterface;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write;
namespace FreeDSx\Ldap\Server\Backend\Write\Schema;

use FreeDSx\Ldap\Exception\OperationException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write;
namespace FreeDSx\Ldap\Server\Backend\Write\Schema;

/**
* How a schema violation was handled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write;
namespace FreeDSx\Ldap\Server\Backend\Write\Schema;

use FreeDSx\Ldap\Exception\OperationException;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write\SystemChange;

use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Server\PasswordPolicy\Decision\OperationalChanges;
use FreeDSx\Ldap\Server\PasswordPolicy\Replica\ReplicaPasswordStateStoreInterface;

/**
* Records password-policy bind state to a replica-local store instead of the replicated entry.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class LocalStateSystemChangeWriter implements SystemChangeWriterInterface
{
public function __construct(private ReplicaPasswordStateStoreInterface $store) {}

public function write(
Dn $dn,
OperationalChanges $changes,
): void {
$this->store->apply(
$dn,
$changes,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write;
namespace FreeDSx\Ldap\Server\Backend\Write\SystemChange;

use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Server\PasswordPolicy\Decision\OperationalChanges;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write;
namespace FreeDSx\Ldap\Server\Backend\Write\SystemChange;

use FreeDSx\Ldap\Control\ControlBag;
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Server\Backend\Write\Command\UpdateCommand;
use FreeDSx\Ldap\Server\Backend\Write\WriteContext;
use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher;
use FreeDSx\Ldap\Server\PasswordPolicy\Decision\OperationalChanges;
use FreeDSx\Ldap\Server\Token\SystemToken;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Backend\Write;
namespace FreeDSx\Ldap\Server\Backend\Write\SystemChange;

use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Exception\OperationException;
Expand Down
1 change: 1 addition & 0 deletions src/FreeDSx/Ldap/Server/Backend/Write/WriteContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use FreeDSx\Ldap\Control\ControlBag;
use FreeDSx\Ldap\Protocol\Authorization\AuthzId;
use FreeDSx\Ldap\Server\Backend\Write\Schema\SchemaViolations;
use FreeDSx\Ldap\Server\Token\TokenInterface;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/FreeDSx/Ldap/Server/Logging/OperationAuditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
use FreeDSx\Ldap\Server\AccessControl\OperationTargetDn;
use FreeDSx\Ldap\Operation\OperationType;
use FreeDSx\Ldap\Server\Backend\Write\SchemaViolations;
use FreeDSx\Ldap\Server\Backend\Write\Schema\SchemaViolations;
use FreeDSx\Ldap\Server\Token\TokenInterface;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/FreeDSx/Ldap/Server/Operation/WriteOperationResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
use FreeDSx\Ldap\Server\Backend\Write\SchemaViolations;
use FreeDSx\Ldap\Server\Backend\Write\Schema\SchemaViolations;
use FreeDSx\Ldap\Server\Logging\OperationAuditor;
use FreeDSx\Ldap\Server\Token\TokenInterface;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\PasswordPolicy\Guard\BindStrategy;

use FreeDSx\Ldap\Server\PasswordPolicy\Attempt\PasswordBindAttempt;
use FreeDSx\Ldap\Server\PasswordPolicy\Decision\PasswordPolicyOutcome;
use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine;
use FreeDSx\Ldap\Server\PasswordPolicy\UserPasswordState;

/**
* Evaluates every bind operation against the authoritative entry state (the primary / writable server).
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class EntryBindStrategy implements PasswordPolicyBindStrategyInterface
{
public function __construct(private PasswordPolicyEngine $engine) {}

public function preBindOutcome(PasswordBindAttempt $attempt): PasswordPolicyOutcome
{
return $this->engine->evaluatePreBind(
$attempt->state,
$attempt->policy,
);
}

public function failureState(PasswordBindAttempt $attempt): UserPasswordState
{
return $attempt->state;
}

public function successState(PasswordBindAttempt $attempt): UserPasswordState
{
return $attempt->state;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\PasswordPolicy\Guard\BindStrategy;

use FreeDSx\Ldap\Server\PasswordPolicy\Attempt\PasswordBindAttempt;
use FreeDSx\Ldap\Server\PasswordPolicy\Decision\PasswordPolicyOutcome;
use FreeDSx\Ldap\Server\PasswordPolicy\UserPasswordState;

/**
* Supplies the password-policy state each bind operation is evaluated and recorded against.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
interface PasswordPolicyBindStrategyInterface
{
/**
* The pre-bind lockout decision (worst-outcome across every state that governs the bind).
*/
public function preBindOutcome(PasswordBindAttempt $attempt): PasswordPolicyOutcome;

/**
* The state a failed bind is counted and recorded against.
*/
public function failureState(PasswordBindAttempt $attempt): UserPasswordState;

/**
* The state a successful bind is evaluated and cleared against.
*/
public function successState(PasswordBindAttempt $attempt): UserPasswordState;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\PasswordPolicy\Guard\BindStrategy;

use FreeDSx\Ldap\Server\PasswordPolicy\Attempt\PasswordBindAttempt;
use FreeDSx\Ldap\Server\PasswordPolicy\Decision\PasswordPolicyOutcome;
use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine;
use FreeDSx\Ldap\Server\PasswordPolicy\Replica\ReplicaPasswordStateStoreInterface;
use FreeDSx\Ldap\Server\PasswordPolicy\UserPasswordState;

/**
* Evaluates the worst of the replicated entry state and the replica-local bind state on a read-only replica.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class ReplicaBindStrategy implements PasswordPolicyBindStrategyInterface
{
public function __construct(
private PasswordPolicyEngine $engine,
private ReplicaPasswordStateStoreInterface $store,
) {}

/**
* Deny on the primary's entry decision (validity / idle / lock), otherwise on the replica-local failure lock.
*/
public function preBindOutcome(PasswordBindAttempt $attempt): PasswordPolicyOutcome
{
$entryOutcome = $this->engine->evaluatePreBind(
$attempt->state,
$attempt->policy,
);

if ($entryOutcome->denied) {
return $entryOutcome;
}

return $this->engine->evaluateLocalLockout(
$this->localState($attempt),
$attempt->policy,
);
}

public function failureState(PasswordBindAttempt $attempt): UserPasswordState
{
return $this->localState($attempt);
}

/**
* Combine primary expiry/validity with replica-local volatile state so success clears local failures and grace.
*/
public function successState(PasswordBindAttempt $attempt): UserPasswordState
{
$entry = $attempt->state;
$local = $this->localState($attempt);

return new UserPasswordState(
changedAt: $entry->changedAt,
accountLockedAt: $local->accountLockedAt,
permanentlyLocked: $local->permanentlyLocked,
failureTimes: $local->failureTimes,
graceUseTimes: $local->graceUseTimes,
mustChange: $entry->mustChange,
policySubentry: $entry->policySubentry,
startTime: $entry->startTime,
endTime: $entry->endTime,
lastSuccess: $local->lastSuccess,
);
}

private function localState(PasswordBindAttempt $attempt): UserPasswordState
{
return $this->store
->load($attempt->dn)
->toUserPasswordState($attempt->dn);
}
}
Loading
Loading