Skip to content

Commit

Permalink
Add auto-unlocking of locked-out users
Browse files Browse the repository at this point in the history
When a focus object is locked, a trigger is created on it. Its handler
(UnlockTriggerHandler) then unlocks the object, if possible.

This should resolve MID-7762.

(cherry picked from commit faf154d)
  • Loading branch information
mederly committed Sep 2, 2022
1 parent 17029b2 commit 42b5f73
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ public class ModelPublicConstants {
public static final ActivityPath FOCUS_VALIDITY_SCAN_FULL_PATH = ActivityPath.fromId(FOCUS_VALIDITY_SCAN_FULL_ID);
public static final ActivityPath FOCUS_VALIDITY_SCAN_OBJECTS_PATH = ActivityPath.fromId(FOCUS_VALIDITY_SCAN_OBJECTS_ID);
public static final ActivityPath FOCUS_VALIDITY_SCAN_ASSIGNMENTS_PATH = ActivityPath.fromId(FOCUS_VALIDITY_SCAN_ASSIGNMENTS_ID);

// Trigger handlers
public static final String NS_MODEL_TRIGGER_PREFIX = SchemaConstants.NS_MODEL + "/trigger";
public static final String UNLOCK_TRIGGER_HANDLER_URI = NS_MODEL_TRIGGER_PREFIX + "/unlock/handler-3";
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;

import com.evolveum.midpoint.model.api.ModelPublicConstants;
import com.evolveum.midpoint.security.api.Authorization;
import com.evolveum.midpoint.security.api.ConnectionEnvironment;
import com.evolveum.midpoint.security.api.MidPointPrincipal;
Expand Down Expand Up @@ -509,9 +510,10 @@ public void recordAuthenticationBehavior(String username, MidPointPrincipal prin
}
}

protected void recordPasswordAuthenticationFailure(@NotNull MidPointPrincipal principal, @NotNull ConnectionEnvironment connEnv,
private void recordPasswordAuthenticationFailure(@NotNull MidPointPrincipal principal, @NotNull ConnectionEnvironment connEnv,
@NotNull AuthenticationBehavioralDataType passwordType, CredentialPolicyType credentialsPolicy, String reason, boolean audit) {
FocusType focusBefore = principal.getFocus().clone();
FocusType focusAfter = principal.getFocus();
FocusType focusBefore = focusAfter.clone();
Integer failedLogins = passwordType.getFailedLogins();
LoginEventType lastFailedLogin = passwordType.getLastFailedLogin();
XMLGregorianCalendar lastFailedLoginTs = null;
Expand Down Expand Up @@ -544,23 +546,26 @@ protected void recordPasswordAuthenticationFailure(@NotNull MidPointPrincipal pr

passwordType.setLastFailedLogin(event);

ActivationType activationType = principal.getFocus().getActivation();

if (isOverFailedLockoutAttempts(failedLogins, credentialsPolicy)) {
if (activationType == null) {
activationType = new ActivationType();
principal.getFocus().setActivation(activationType);
ActivationType activation = focusAfter.getActivation();
if (activation == null) {
activation = new ActivationType();
focusAfter.setActivation(activation);
}
activationType.setLockoutStatus(LockoutStatusType.LOCKED);
activation.setLockoutStatus(LockoutStatusType.LOCKED);
XMLGregorianCalendar lockoutExpirationTs = null;
Duration lockoutDuration = credentialsPolicy.getLockoutDuration();
if (lockoutDuration != null) {
lockoutExpirationTs = XmlTypeConverter.addDuration(event.getTimestamp(), lockoutDuration);
}
activationType.setLockoutExpirationTimestamp(lockoutExpirationTs);
activation.setLockoutExpirationTimestamp(lockoutExpirationTs);
focusAfter.getTrigger().add(
new TriggerType()
.handlerUri(ModelPublicConstants.UNLOCK_TRIGGER_HANDLER_URI)
.timestamp(lockoutExpirationTs));
}

focusProfileService.updateFocus(principal, computeModifications(focusBefore, principal.getFocus()));
focusProfileService.updateFocus(principal, computeModifications(focusBefore, focusAfter));
if (audit) {
recordAuthenticationFailure(principal, connEnv, reason);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

import javax.annotation.PostConstruct;

import com.evolveum.midpoint.model.api.ModelPublicConstants;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.evolveum.midpoint.model.api.ModelExecuteOptions;
import com.evolveum.midpoint.model.impl.ModelConstants;
import com.evolveum.midpoint.model.impl.lens.Clockwork;
import com.evolveum.midpoint.model.impl.lens.ContextFactory;
import com.evolveum.midpoint.model.impl.lens.LensContext;
Expand All @@ -35,7 +35,7 @@
@Component
public class RecomputeTriggerHandler implements SingleTriggerHandler {

public static final String HANDLER_URI = ModelConstants.NS_MODEL_TRIGGER_PREFIX + "/recompute/handler-3";
public static final String HANDLER_URI = ModelPublicConstants.NS_MODEL_TRIGGER_PREFIX + "/recompute/handler-3";

private static final Trace LOGGER = TraceManager.getTrace(RecomputeTriggerHandler.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;

import com.evolveum.midpoint.model.api.ModelPublicConstants;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
Expand All @@ -31,7 +32,6 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.evolveum.midpoint.model.impl.ModelConstants;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.RunningTask;
Expand All @@ -53,7 +53,7 @@
@Component
public class ShadowReconcileTriggerHandler implements SingleTriggerHandler {

public static final String HANDLER_URI = ModelConstants.NS_MODEL_TRIGGER_PREFIX + "/shadow-reconcile/handler-3";
public static final String HANDLER_URI = ModelPublicConstants.NS_MODEL_TRIGGER_PREFIX + "/shadow-reconcile/handler-3";

private static final String OP_SYNCHRONIZE_SHADOW = ShadowReconcileTriggerHandler.class.getName() + ".synchronizeShadow";
private static final String OP_RESCHEDULE = ShadowReconcileTriggerHandler.class.getName() + ".reschedule";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) 2013-2017 Evolveum and contributors
*
* This work is dual-licensed under the Apache License 2.0
* and European Union Public License. See LICENSE file for details.
*/
package com.evolveum.midpoint.model.impl.trigger;

import static javax.xml.datatype.DatatypeConstants.LESSER;

import java.util.List;
import javax.annotation.PostConstruct;
import javax.xml.datatype.XMLGregorianCalendar;

import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.evolveum.midpoint.common.Clock;
import com.evolveum.midpoint.model.api.ModelPublicConstants;
import com.evolveum.midpoint.model.api.ModelService;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.RunningTask;
import com.evolveum.midpoint.util.exception.CommonException;
import com.evolveum.midpoint.util.logging.LoggingUtils;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;

/**
* Unlocks the focus object (if the time has come).
*/
@Component
public class UnlockTriggerHandler implements SingleTriggerHandler {

public static final String HANDLER_URI = ModelPublicConstants.UNLOCK_TRIGGER_HANDLER_URI;

private static final Trace LOGGER = TraceManager.getTrace(UnlockTriggerHandler.class);

@Autowired private TriggerHandlerRegistry triggerHandlerRegistry;
@Autowired private ModelService modelService;
@Autowired private PrismContext prismContext;
@Autowired private Clock clock;

@PostConstruct
private void initialize() {
triggerHandlerRegistry.register(HANDLER_URI, this);
}

@Override
public <O extends ObjectType> void handle(@NotNull PrismObject<O> object, @NotNull TriggerType trigger,
@NotNull RunningTask task, @NotNull OperationResult result) {
O objectable = object.asObjectable();
LOGGER.trace("Considering unlocking {}", objectable);
if (!(objectable instanceof FocusType)) {
LOGGER.debug("Not a focus object: {}", objectable);
result.recordNotApplicable("Not a focus object");
return;
}
FocusType focus = (FocusType) objectable;
try {
ActivationType activation = focus.getActivation();
if (activation == null) {
LOGGER.debug("No activation in {}", objectable);
result.recordNotApplicable("No activation");
return;
}

XMLGregorianCalendar lockoutExpirationTimestamp = activation.getLockoutExpirationTimestamp();
if (lockoutExpirationTimestamp != null
&& clock.currentTimeXMLGregorianCalendar().compare(lockoutExpirationTimestamp) == LESSER) {
LOGGER.debug("The lockout for {} has not expired yet: {}", focus, lockoutExpirationTimestamp);
result.recordNotApplicable("The lockout has not expired yet");
return;
}

LOGGER.debug("Unlocking {}", focus);
// We do this intentionally via model API, as there's some non-trivial processing inside
// (e.g., clearing the number of failed logins).
// This also causes the change to be audited.
modelService.executeChanges(
List.of(
prismContext.deltaFor(FocusType.class)
.item(FocusType.F_ACTIVATION, ActivationType.F_LOCKOUT_STATUS)
.replace(LockoutStatusType.NORMAL)
.asObjectDelta(focus.getOid())),
null,
task,
result);
LOGGER.debug("Unlocked {}", focus);
} catch (CommonException | RuntimeException | Error e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't unlock object {}", e, object);
// Intentionally not retrying.
}
}

@Override
public boolean isIdempotent() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import com.evolveum.midpoint.test.TestTask;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.*;
Expand Down Expand Up @@ -60,6 +62,9 @@ public abstract class TestAbstractAuthenticationEvaluator<V, AC extends Abstract

protected static final String USER_GUYBRUSH_PASSWORD = "XmarksTHEspot";

private static final TestTask TASK_TRIGGER_SCANNER_ON_DEMAND =
new TestTask(COMMON_DIR, "task-trigger-scanner-on-demand.xml", "2ee5c2a9-0f46-438a-8748-7ac71f46a343");

@Autowired private LocalizationMessageSource messageSource;
@Autowired private GuiProfiledPrincipalManager focusProfileService;
@Autowired private Clock clock;
Expand Down Expand Up @@ -89,6 +94,8 @@ public abstract class TestAbstractAuthenticationEvaluator<V, AC extends Abstract
public void initSystem(Task initTask, OperationResult initResult) throws Exception {
super.initSystem(initTask, initResult);

TASK_TRIGGER_SCANNER_ON_DEMAND.initialize(this, initTask, initResult);

messages = new MessageSourceAccessor(messageSource);

((AuthenticationEvaluatorImpl) getAuthenticationEvaluator()).focusProfileService = new GuiProfiledPrincipalManager() {
Expand Down Expand Up @@ -579,34 +586,35 @@ public void test137PasswordLoginLockedoutGoodPasswordAgain() throws Exception {

@Test
public void test138UnlockUserGoodPassword() throws Exception {
// GIVEN
Task task = getTestTask();
OperationResult result = task.getResult();

ConnectionEnvironment connEnv = createConnectionEnvironment();

// WHEN
when();
modifyUserReplace(USER_JACK_OID, SchemaConstants.PATH_ACTIVATION_LOCKOUT_STATUS, task, result, LockoutStatusType.NORMAL);
when("trigger scanner runs (after 30 minutes) - should clear the lockout flag");
clock.overrideDuration("PT30M");
TASK_TRIGGER_SCANNER_ON_DEMAND.rerun(result);
clock.resetOverride();

// THEN
then();
then("user is unlocked");

TASK_TRIGGER_SCANNER_ON_DEMAND.assertAfter(); // just show the task

PrismObject<UserType> userBetween = getUser(USER_JACK_OID);
display("user after", userBetween);
assertFailedLoginsForCredentials(userBetween, 0);
assertFailedLoginsForBehavior(userBetween, 0);
assertUserLockout(userBetween, LockoutStatusType.NORMAL);

// GIVEN
given("preparing for new login");
XMLGregorianCalendar startTs = clock.currentTimeXMLGregorianCalendar();

// WHEN
when();
Authentication authentication = getAuthenticationEvaluator().authenticate(connEnv, getAuthenticationContext(USER_JACK_USERNAME, getGoodPasswordJack()));
when("a good password is provided");
Authentication authentication =
getAuthenticationEvaluator()
.authenticate(connEnv, getAuthenticationContext(USER_JACK_USERNAME, getGoodPasswordJack()));

// THEN
then();
then("everything is OK");
XMLGregorianCalendar endTs = clock.currentTimeXMLGregorianCalendar();
assertGoodPasswordAuthentication(authentication, USER_JACK_USERNAME);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2010-2017 Evolveum and contributors
~
~ This work is dual-licensed under the Apache License 2.0
~ and European Union Public License. See LICENSE file for details.
-->

<task oid="2ee5c2a9-0f46-438a-8748-7ac71f46a343"
xmlns="http://midpoint.evolveum.com/xml/ns/public/common/common-3">

<name>Trigger Scanner (on demand)</name>

<ownerRef oid="00000000-0000-0000-0000-000000000002"/>
<executionState>closed</executionState> <!-- run on demand only -->

<handlerUri>http://midpoint.evolveum.com/xml/ns/public/model/trigger/scanner/handler-3</handlerUri>
</task>

0 comments on commit 42b5f73

Please sign in to comment.