From 2c1b08705765163887c2f3fc9c8dd9aff9a62ebd Mon Sep 17 00:00:00 2001 From: Pavol Mederly Date: Tue, 21 May 2019 20:40:55 +0200 Subject: [PATCH] Store deltas and bulk actions encrypted (MID-5359) Also refactored CryptoUtil to be more generic and maintainable. Fixed executeChangesAsynchronously to return task with new OID. (cherry-picked from c42307ca4db8919dc5e236b81a4d897511a45a81 and adapted) --- .../midpoint/common/crypto/CryptoUtil.java | 417 ++++++------ .../crypto/task-modify-jack-password.xml | 64 ++ .../prism/xml/ns/_public/types_3/RawType.java | 66 ++ .../intest/scripting/TestScriptingBasic.java | 102 ++- .../scripting/modify-jack-password-task.xml | 64 ++ .../scripting/modify-jack-password.xml | 46 ++ .../scripting/system-configuration.xml | 592 ++++++++++++++++++ .../quartzimpl/TaskManagerQuartzImpl.java | 84 ++- 8 files changed, 1212 insertions(+), 223 deletions(-) create mode 100644 infra/common/src/test/resources/crypto/task-modify-jack-password.xml create mode 100644 model/model-intest/src/test/resources/scripting/modify-jack-password-task.xml create mode 100644 model/model-intest/src/test/resources/scripting/modify-jack-password.xml create mode 100644 model/model-intest/src/test/resources/scripting/system-configuration.xml diff --git a/infra/common/src/main/java/com/evolveum/midpoint/common/crypto/CryptoUtil.java b/infra/common/src/main/java/com/evolveum/midpoint/common/crypto/CryptoUtil.java index cd1bf0cd61e..7780245fba4 100644 --- a/infra/common/src/main/java/com/evolveum/midpoint/common/crypto/CryptoUtil.java +++ b/infra/common/src/main/java/com/evolveum/midpoint/common/crypto/CryptoUtil.java @@ -18,233 +18,119 @@ import java.io.ByteArrayOutputStream; import java.security.Provider; import java.security.Security; -import java.util.Collection; +import java.util.*; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; -import javax.xml.namespace.QName; - -import com.evolveum.midpoint.prism.ItemDefinition; -import com.evolveum.midpoint.prism.Itemable; -import com.evolveum.midpoint.prism.PrismObject; -import com.evolveum.midpoint.prism.PrismPropertyValue; -import com.evolveum.midpoint.prism.Visitable; -import com.evolveum.midpoint.prism.Visitor; +import javax.xml.bind.JAXBElement; + +import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.crypto.EncryptionException; import com.evolveum.midpoint.prism.crypto.Protector; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; +import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; +import com.evolveum.midpoint.util.Holder; +import com.evolveum.midpoint.util.QNameUtil; +import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.TunnelException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; -import com.evolveum.midpoint.xml.ns._public.common.common_3.MailServerConfigurationType; -import com.evolveum.midpoint.xml.ns._public.common.common_3.NotificationConfigurationType; -import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; -import com.evolveum.midpoint.xml.ns._public.common.common_3.SmsConfigurationType; -import com.evolveum.midpoint.xml.ns._public.common.common_3.SmsGatewayConfigurationType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.*; +import com.evolveum.midpoint.xml.ns._public.model.scripting_3.*; +import com.evolveum.prism.xml.ns._public.types_3.ItemDeltaType; +import com.evolveum.prism.xml.ns._public.types_3.ObjectDeltaType; import com.evolveum.prism.xml.ns._public.types_3.ProtectedStringType; +import com.evolveum.prism.xml.ns._public.types_3.RawType; +import org.jetbrains.annotations.NotNull; /** * @author semancik * */ public class CryptoUtil { - + private static final Trace LOGGER = TraceManager.getTrace(CryptoUtil.class); /** * Encrypts all encryptable values in the object. */ - public static void encryptValues(final Protector protector, final PrismObject object) throws EncryptionException{ - Visitor visitor = new Visitor() { - @Override - public void visit(Visitable visitable){ - if (!(visitable instanceof PrismPropertyValue)) { - return; - } - PrismPropertyValue pval = (PrismPropertyValue)visitable; - try { - encryptValue(protector, pval); - } catch (EncryptionException e) { - throw new TunnelException(e); - } - } - }; + public static void encryptValues(Protector protector, PrismObject object) throws EncryptionException { try { - object.accept(visitor); + object.accept(createEncryptingVisitor(protector)); } catch (TunnelException e) { - EncryptionException origEx = (EncryptionException)e.getCause(); - throw origEx; + throw (EncryptionException) e.getCause(); } } /** * Encrypts all encryptable values in delta. */ - public static void encryptValues(final Protector protector, final ObjectDelta delta) throws EncryptionException{ - Visitor visitor = new Visitor() { - @Override - public void visit(Visitable visitable){ - if (!(visitable instanceof PrismPropertyValue)) { - return; - } - PrismPropertyValue pval = (PrismPropertyValue)visitable; + public static void encryptValues(Protector protector, ObjectDelta delta) throws EncryptionException { + try { + delta.accept(createEncryptingVisitor(protector)); + } catch (TunnelException e) { + throw (EncryptionException) e.getCause(); + } + } + + @NotNull + private static Visitor createEncryptingVisitor(Protector protector) { + return createVisitor(createEncryptingProcessor(protector)); + } + + private static ProtectedStringProcessor createEncryptingProcessor(Protector protector) { + return ((protectedString, propertyName) -> { + if (protectedString != null && !protectedString.isHashed() && protectedString.getClearValue() != null) { try { - encryptValue(protector, pval); + protector.encrypt(protectedString); } catch (EncryptionException e) { - throw new TunnelException(e); + throw new EncryptionException("Failed to encrypt value for field " + propertyName + ": " + e.getMessage(), e); } } - }; - try { - delta.accept(visitor); - } catch (TunnelException e) { - EncryptionException origEx = (EncryptionException)e.getCause(); - throw origEx; - } + }); } - private static void encryptValue(Protector protector, PrismPropertyValue pval) throws EncryptionException{ - Itemable item = pval.getParent(); - if (item == null) { - return; - } - ItemDefinition itemDef = item.getDefinition(); - if (itemDef == null || itemDef.getTypeName() == null) { - return; - } - - if (itemDef.getTypeName().equals(ProtectedStringType.COMPLEX_TYPE)) { - QName propName = item.getElementName(); - PrismPropertyValue psPval = (PrismPropertyValue)pval; - ProtectedStringType ps = psPval.getValue(); - encryptProtectedStringType(protector, ps, propName.getLocalPart()); - if (pval.getParent() == null){ - pval.setParent(item); - } - } else if (itemDef.getTypeName().equals(NotificationConfigurationType.COMPLEX_TYPE)) { - // this is really ugly hack needed because currently it is not possible to break NotificationConfigurationType into prism item [pm] - NotificationConfigurationType ncfg = ((PrismPropertyValue) pval).getValue(); - if (ncfg.getMail() != null) { - for (MailServerConfigurationType mscfg : ncfg.getMail().getServer()) { - encryptProtectedStringType(protector, mscfg.getPassword(), "mail server password"); - } - } - if (ncfg.getSms() != null) { - for (SmsConfigurationType smscfg : ncfg.getSms()) { - for (SmsGatewayConfigurationType gwcfg : smscfg.getGateway()) { - encryptProtectedStringType(protector, gwcfg.getPassword(), "sms gateway password"); - } - } - } - } - } - - private static void encryptProtectedStringType(Protector protector, ProtectedStringType ps, String propName) throws EncryptionException { - if (ps != null && ps.getClearValue() != null) { - try { - protector.encrypt(ps); - } catch (EncryptionException e) { - throw new EncryptionException("Failed to encrypt value for field " + propName + ": " + e.getMessage(), e); - } - } - } - // Checks that everything is encrypted public static void checkEncrypted(final PrismObject object) { - Visitor visitor = new Visitor() { - @Override - public void visit(Visitable visitable){ - if (!(visitable instanceof PrismPropertyValue)) { - return; - } - PrismPropertyValue pval = (PrismPropertyValue)visitable; - checkEncrypted(pval); - } - }; try { - object.accept(visitor); + object.accept(createCheckingVisitor()); } catch (IllegalStateException e) { throw new IllegalStateException(e.getMessage() + " in " + object, e); } } - + // Checks that everything is encrypted public static void checkEncrypted(final ObjectDelta delta) { - Visitor visitor = new Visitor() { - @Override - public void visit(Visitable visitable){ - if (!(visitable instanceof PrismPropertyValue)) { - return; - } - PrismPropertyValue pval = (PrismPropertyValue)visitable; - checkEncrypted(pval); - } - }; try { - delta.accept(visitor); + delta.accept(createCheckingVisitor()); } catch (IllegalStateException e) { throw new IllegalStateException(e.getMessage() + " in delta " + delta, e); } } - - private static void checkEncrypted(PrismPropertyValue pval) { - Itemable item = pval.getParent(); - if (item == null) { - return; - } - ItemDefinition itemDef = item.getDefinition(); - if (itemDef == null || itemDef.getTypeName() == null) { - return; - } - if (itemDef.getTypeName().equals(ProtectedStringType.COMPLEX_TYPE)) { - QName propName = item.getElementName(); - PrismPropertyValue psPval = (PrismPropertyValue)pval; - ProtectedStringType ps = psPval.getValue(); - if (ps.getClearValue() != null) { - throw new IllegalStateException("Unencrypted value in field " + propName); - } - } else if (itemDef.getTypeName().equals(NotificationConfigurationType.COMPLEX_TYPE)) { - // this is really ugly hack needed because currently it is not possible to break NotificationConfigurationType into prism item [pm] - NotificationConfigurationType ncfg = ((PrismPropertyValue) pval).getValue(); - if (ncfg.getMail() != null) { - for (MailServerConfigurationType mscfg : ncfg.getMail().getServer()) { - if (mscfg.getPassword() != null && mscfg.getPassword().getClearValue() != null) { - throw new IllegalStateException("Unencrypted value in mail server config password entry"); - } - } - } - if (ncfg.getSms() != null) { - for (SmsConfigurationType smscfg : ncfg.getSms()) { - for (SmsGatewayConfigurationType gwcfg : smscfg.getGateway()) { - if (gwcfg.getPassword() != null && gwcfg.getPassword().getClearValue() != null) { - throw new IllegalStateException("Unencrypted value in SMS gateway config password entry"); - } - } - } - } - } - } - public static void checkEncrypted(Collection modifications) { - Visitor visitor = new Visitor() { - @Override - public void visit(Visitable visitable){ - if (!(visitable instanceof PrismPropertyValue)) { - return; - } - PrismPropertyValue pval = (PrismPropertyValue)visitable; - checkEncrypted(pval); + @NotNull + private static Visitor createCheckingVisitor() { + return createVisitor(createCheckingProcessor()); + } + + private static ProtectedStringProcessor createCheckingProcessor() { + return ((protectedString, propertyName) -> { + if (protectedString != null && protectedString.getClearValue() != null) { + throw new IllegalStateException("Unencrypted value in field " + propertyName); } - }; + }); + } + + public static void checkEncrypted(Collection modifications) { for (ItemDelta delta: modifications) { try { - delta.accept(visitor); + delta.accept(createCheckingVisitor()); } catch (IllegalStateException e) { throw new IllegalStateException(e.getMessage() + " in modification " + delta, e); } @@ -253,15 +139,15 @@ public void visit(Visitable visitable){ } private final static byte [] DEFAULT_IV_BYTES = { - (byte) 0x51,(byte) 0x65,(byte) 0x22,(byte) 0x23, - (byte) 0x64,(byte) 0x05,(byte) 0x6A,(byte) 0xBE, - (byte) 0x51,(byte) 0x65,(byte) 0x22,(byte) 0x23, - (byte) 0x64,(byte) 0x05,(byte) 0x6A,(byte) 0xBE, - }; - + (byte) 0x51,(byte) 0x65,(byte) 0x22,(byte) 0x23, + (byte) 0x64,(byte) 0x05,(byte) 0x6A,(byte) 0xBE, + (byte) 0x51,(byte) 0x65,(byte) 0x22,(byte) 0x23, + (byte) 0x64,(byte) 0x05,(byte) 0x6A,(byte) 0xBE, + }; + public static void securitySelfTest(OperationResult parentTestResult) { OperationResult result = parentTestResult.createSubresult(CryptoUtil.class.getName()+".securitySelfTest"); - + // Providers for (Provider provider: Security.getProviders()) { String providerName = provider.getName(); @@ -278,12 +164,12 @@ public static void securitySelfTest(OperationResult parentTestResult) { providerResult.recordFatalError(e); } } - + securitySelfTestAlgorithm("AES", "AES/CBC/PKCS5Padding", null, false, result); OperationResult cryptoResult = result.getLastSubresult(); if (cryptoResult.isError()) { // Do a test encryption. It happens sometimes that the key generator - // generates a key that is not supported by the cipher. + // generates a key that is not supported by the cipher. // Fall back to known key size supported by all JCE implementations securitySelfTestAlgorithm("AES", "AES/CBC/PKCS5Padding", 128, true, result); OperationResult cryptoResult2 = result.getLastSubresult(); @@ -291,11 +177,139 @@ public static void securitySelfTest(OperationResult parentTestResult) { cryptoResult.setStatus(OperationResultStatus.HANDLED_ERROR); } } - + result.computeStatus(); } - private static void securitySelfTestAlgorithm(String algorithmName, String transformationName, + @SuppressWarnings("unused") // used externally + public static boolean containsCleartext(PrismObject object) { + Holder result = new Holder<>(false); + object.accept(createVisitor((ps, propName) -> { + if (ps.getClearValue() != null) { + result.setValue(true); + } + })); + return result.getValue(); + } + + @SuppressWarnings("unused") // used externally + public static boolean containsHashedData(PrismObject object) { + Holder result = new Holder<>(false); + object.accept(createVisitor((ps, propName) -> { + if (ps.getHashedDataType() != null) { + result.setValue(true); + } + })); + return result.getValue(); + } + + @FunctionalInterface + interface ProtectedStringProcessor { + void apply(ProtectedStringType protectedString, String propertyName) throws EncryptionException; + } + + @NotNull + private static Visitor createVisitor(ProtectedStringProcessor processor) { + return visitable -> { + if (visitable instanceof PrismPropertyValue) { + try { + PrismPropertyValue pval = ((PrismPropertyValue) visitable); + applyToRealValue(pval.getRealValue(), determinePropName(pval), processor); + } catch (EncryptionException e) { + throw new TunnelException(e); + } + } + }; + } + + private static void applyToRealValues(Collection values, String propertyName, ProtectedStringProcessor processor) + throws EncryptionException { + if (values != null) { + for (Object value : values) { + applyToRealValue(value, propertyName, processor); + } + } + } + + private static void applyToRealValue(Object value, String propertyName, ProtectedStringProcessor processor) + throws EncryptionException { + if (value instanceof ProtectedStringType) { + processor.apply(((ProtectedStringType) value), propertyName); + } else if (value instanceof PrismPropertyValue) { + applyToRealValue(((PrismPropertyValue) value).getRealValue(), propertyName, processor); + } else if (value instanceof PrismReferenceValue) { + applyToRealValue(((PrismReferenceValue) value).getObject(), propertyName + "/object", processor); + } else if (value instanceof PrismContainerValue) { + ((PrismContainerValue) value).accept(createVisitor(processor)); + } else if (value instanceof Containerable) { + applyToRealValue(((Containerable) value).asPrismContainerValue(), propertyName, processor); + } else if (value instanceof RawType) { + RawType raw = (RawType) value; + if (raw.getAlreadyParsedValue() != null) { + applyToRealValue(raw.getAlreadyParsedValue(), propertyName, processor); + } else if (raw.getXnode() != null && raw.getXnode().getTypeQName() != null) { + try { + applyToRealValue(raw.getValue(true), propertyName, processor); + } catch (SchemaException e) { + throw new TunnelException(e); + } + } else { + // consider putting an approximate check here e.g. testing for "password", "clearValue" or such elements + } + } else if (value instanceof JAXBElement) { + applyToRealValue(((JAXBElement) value).getValue(), propertyName, processor); + } else if (value instanceof NotificationConfigurationType) { + NotificationConfigurationType cfg = (NotificationConfigurationType) value; + applyToRealValue(cfg.getMail(), propertyName+"/mail", processor); + applyToRealValue(cfg.getSms(), propertyName+"/sms", processor); + } else if (value instanceof MailConfigurationType) { + applyToRealValues(((MailConfigurationType) value).getServer(), propertyName+"/server", processor); + } else if (value instanceof MailServerConfigurationType) { + processor.apply(((MailServerConfigurationType) value).getPassword(), propertyName+"/password"); + } else if (value instanceof SmsConfigurationType) { + applyToRealValues(((SmsConfigurationType) value).getGateway(), propertyName + "/gateway", processor); + } else if (value instanceof SmsGatewayConfigurationType) { + processor.apply(((SmsGatewayConfigurationType) value).getPassword(), propertyName + "/password"); + } else if (value instanceof ExecuteScriptType) { + ExecuteScriptType es = (ExecuteScriptType) value; + applyToRealValue(es.getInput(), propertyName + "/input", processor); + applyToRealValue(es.getScriptingExpression(), propertyName + "/scriptingExpression", processor); + } else if (value instanceof ValueListType) { + applyToRealValues(((ValueListType) value).getValue(), propertyName + "/value", processor); + } else if (value instanceof ExpressionPipelineType) { + applyToRealValues(((ExpressionPipelineType) value).getScriptingExpression(), propertyName + "/scriptingExpression", processor); + } else if (value instanceof ExpressionSequenceType) { + applyToRealValues(((ExpressionSequenceType) value).getScriptingExpression(), propertyName + "/scriptingExpression", processor); + } else if (value instanceof ActionExpressionType) { + applyToRealValues(((ActionExpressionType) value).getParameter(), propertyName + "/parameter", processor); + } else if (value instanceof ActionParameterValueType) { + ActionParameterValueType p = (ActionParameterValueType) value; + applyToRealValue(p.getValue(), propertyName + "/value", processor); + applyToRealValue(p.getScriptingExpression(), propertyName + "/scriptingExpression", processor); + } else if (value instanceof ExpressionType) { + List> evaluators = ((ExpressionType) value).getExpressionEvaluator(); + for (JAXBElement evaluator : evaluators) { + if (QNameUtil.match(SchemaConstants.C_VALUE, evaluator.getName())) { + applyToRealValue(evaluator.getValue(), propertyName + "/value", processor); + } + } + } else if (value instanceof ObjectDeltaType) { + ObjectDeltaType delta = (ObjectDeltaType) value; + applyToRealValue(delta.getObjectToAdd(), propertyName + "/objectToAdd", processor); + applyToRealValues(delta.getItemDelta(), propertyName + "/itemDelta", processor); + } else if (value instanceof ItemDeltaType) { + ItemDeltaType delta = (ItemDeltaType) value; + applyToRealValues(delta.getValue(), propertyName + "/value", processor); + applyToRealValues(delta.getEstimatedOldValue(), propertyName + "/estimatedOldValue", processor); + } + } + + private static String determinePropName(PrismPropertyValue value) { + Itemable item = value.getParent(); + return item != null && item.getElementName() != null ? item.getElementName().getLocalPart() : ""; + } + + private static void securitySelfTestAlgorithm(String algorithmName, String transformationName, Integer keySize, boolean critical, OperationResult parentResult) { OperationResult subresult = parentResult.createSubresult(CryptoUtil.class.getName()+".securitySelfTest.algorithm."+algorithmName); try { @@ -305,51 +319,50 @@ private static void securitySelfTestAlgorithm(String algorithmName, String trans } subresult.addReturn("keyGeneratorProvider", keyGenerator.getProvider().getName()); subresult.addReturn("keyGeneratorAlgorithm", keyGenerator.getAlgorithm()); - subresult.addReturn("keyGeneratorKeySize", keySize); - + subresult.addReturn("keyGeneratorKeySize", keySize != null ? keySize : -1); + SecretKey key = keyGenerator.generateKey(); subresult.addReturn("keyAlgorithm", key.getAlgorithm()); subresult.addReturn("keyLength", key.getEncoded().length*8); subresult.addReturn("keyFormat", key.getFormat()); subresult.recordSuccess(); - + IvParameterSpec iv = new IvParameterSpec(DEFAULT_IV_BYTES); - + String plainString = "Scurvy seadog"; - + Cipher cipher = Cipher.getInstance(transformationName); subresult.addReturn("cipherAlgorithmName", algorithmName); subresult.addReturn("cipherTansfromationName", transformationName); subresult.addReturn("cipherAlgorithm", cipher.getAlgorithm()); subresult.addReturn("cipherBlockSize", cipher.getBlockSize()); subresult.addReturn("cipherProvider", cipher.getProvider().getName()); - subresult.addReturn("cipherMaxAllowedKeyLength", cipher.getMaxAllowedKeyLength(transformationName)); - cipher.init(Cipher.ENCRYPT_MODE, key, iv); - byte[] encryptedBytes = cipher.doFinal(plainString.getBytes()); - - cipher = Cipher.getInstance(transformationName); - cipher.init(Cipher.DECRYPT_MODE, key, iv); - byte[] decryptedBytes = cipher.doFinal(encryptedBytes); - String decryptedString = new String(decryptedBytes); - - if (!plainString.equals(decryptedString)) { - subresult.recordFatalError("Encryptor roundtrip failed; encrypted="+plainString+", decrypted="+decryptedString); + subresult.addReturn("cipherMaxAllowedKeyLength", Cipher.getMaxAllowedKeyLength(transformationName)); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] encryptedBytes = cipher.doFinal(plainString.getBytes()); + + cipher = Cipher.getInstance(transformationName); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + byte[] decryptedBytes = cipher.doFinal(encryptedBytes); + String decryptedString = new String(decryptedBytes); + + if (!plainString.equals(decryptedString)) { + subresult.recordFatalError("Encryptor roundtrip failed; encrypted="+plainString+", decrypted="+decryptedString); } else { subresult.recordSuccess(); } - LOGGER.debug("Security self test (algorithmName={}, transformationName={}, keySize={}) success", - new Object[] {algorithmName, transformationName, keySize}); + LOGGER.debug("Security self test (algorithmName={}, transformationName={}, keySize={}) success", + algorithmName, transformationName, keySize); } catch (Throwable e) { if (critical) { - LOGGER.error("Security self test (algorithmName={}, transformationName={}, keySize={}) failed: {}", - new Object[] {algorithmName, transformationName, keySize, e.getMessage() ,e}); + LOGGER.error("Security self test (algorithmName={}, transformationName={}, keySize={}) failed: {}", + algorithmName, transformationName, keySize, e.getMessage(),e); subresult.recordFatalError(e); } else { - LOGGER.warn("Security self test (algorithmName={}, transformationName={}, keySize={}) failed: {} (failure is expected in some cases)", - new Object[] {algorithmName, transformationName, keySize, e.getMessage() ,e}); + LOGGER.warn("Security self test (algorithmName={}, transformationName={}, keySize={}) failed: {} (failure is expected in some cases)", + algorithmName, transformationName, keySize, e.getMessage(),e); subresult.recordWarning(e); } } } - } diff --git a/infra/common/src/test/resources/crypto/task-modify-jack-password.xml b/infra/common/src/test/resources/crypto/task-modify-jack-password.xml new file mode 100644 index 00000000000..462ff2f9f28 --- /dev/null +++ b/infra/common/src/test/resources/crypto/task-modify-jack-password.xml @@ -0,0 +1,64 @@ + + + + Task 1555581798624-0-1 + + + + + c:UserType + + + c:name + jack + + + + + modify + + delta + + modify + UserType + + replace + credentials/password/value + + pass1234word + + + + + + + + + 1555581798624-0-1 + + + + runnable + BulkActions + http://midpoint.evolveum.com/xml/ns/public/model/scripting/handler-3 + single + tight + diff --git a/infra/prism/src/main/java/com/evolveum/prism/xml/ns/_public/types_3/RawType.java b/infra/prism/src/main/java/com/evolveum/prism/xml/ns/_public/types_3/RawType.java index fbe6b09003f..3f435faf85c 100644 --- a/infra/prism/src/main/java/com/evolveum/prism/xml/ns/_public/types_3/RawType.java +++ b/infra/prism/src/main/java/com/evolveum/prism/xml/ns/_public/types_3/RawType.java @@ -2,6 +2,8 @@ import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.path.ItemPath; +import com.evolveum.midpoint.prism.polystring.PolyString; +import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.prism.xnode.PrimitiveXNode; import com.evolveum.midpoint.prism.xnode.RootXNode; import com.evolveum.midpoint.prism.xnode.XNode; @@ -60,6 +62,70 @@ public RawType(PrismValue parsed, @NotNull PrismContext prismContext) { this.parsed = parsed; } + /** + * Extracts a "real value" from a potential RawType object without expecting any specific type beforehand. + * (Useful e.g. for determining value of xsd:anyType XML property.) + */ + public static Object getValue(Object value) throws SchemaException { + if (value instanceof RawType) { + return ((RawType) value).getValue(); + } else { + return value; + } + } + + public Object getValue() throws SchemaException { + return getValue(false); + } + + /** + * Extracts a "real value" from RawType object without expecting any specific type beforehand. + * If no explicit type is present, assumes xsd:string (and fails if the content is structured). + */ + public Object getValue(boolean store) throws SchemaException { + if (parsed != null) { + return parsed.getRealValue(); + } + if (xnode == null) { + return null; + } + if (xnode.getTypeQName() != null) { + TypeDefinition typeDefinition = prismContext.getSchemaRegistry().findTypeDefinitionByType(xnode.getTypeQName()); + if (typeDefinition != null && typeDefinition.getCompileTimeClass() != null) { + return storeIfRequested(getParsedRealValue(typeDefinition.getCompileTimeClass()), store); + } + Class javaClass = XsdTypeMapper.getXsdToJavaMapping(xnode.getTypeQName()); + if (javaClass != null) { + return storeIfRequested(getParsedRealValue(javaClass), store); + } + } + // unknown or null type -- try parsing as string + if (!(xnode instanceof PrimitiveXNode)) { + throw new SchemaException("Trying to parse non-primitive XNode as type '" + xnode.getTypeQName() + "'"); + } else { + return ((PrimitiveXNode) xnode).getStringValue(); + } + } + + private Object storeIfRequested(Object parsedValue, boolean store) { + if (parsed == null && store) { + if (parsedValue instanceof Containerable) { + parsed = ((Containerable) parsedValue).asPrismContainerValue(); + xnode = null; + } else if (parsedValue instanceof Referencable) { + parsed = ((Referencable) parsedValue).asReferenceValue(); + xnode = null; + } else if (parsedValue instanceof PolyStringType) { + parsed = new PrismPropertyValue<>(PolyString.toPolyString((PolyStringType) parsedValue)); // hack + xnode = null; + } else if (parsedValue != null) { + parsed = new PrismPropertyValue<>(parsedValue); + xnode = null; + } + } + return parsedValue; + } + @Override public void revive(PrismContext prismContext) throws SchemaException { Validate.notNull(prismContext); diff --git a/model/model-intest/src/test/java/com/evolveum/midpoint/model/intest/scripting/TestScriptingBasic.java b/model/model-intest/src/test/java/com/evolveum/midpoint/model/intest/scripting/TestScriptingBasic.java index 2f751efc5fb..b73af415016 100644 --- a/model/model-intest/src/test/java/com/evolveum/midpoint/model/intest/scripting/TestScriptingBasic.java +++ b/model/model-intest/src/test/java/com/evolveum/midpoint/model/intest/scripting/TestScriptingBasic.java @@ -37,6 +37,7 @@ import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.test.util.LogfileTestTailer; import com.evolveum.midpoint.test.util.TestUtil; +import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.exception.*; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.midpoint.xml.ns._public.model.scripting_3.*; @@ -51,6 +52,7 @@ import javax.xml.bind.JAXBException; import javax.xml.namespace.QName; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; @@ -66,8 +68,11 @@ @ContextConfiguration(locations = {"classpath:ctx-model-intest-test-main.xml"}) @DirtiesContext(classMode = ClassMode.AFTER_CLASS) public class TestScriptingBasic extends AbstractInitializedModelIntegrationTest { - + public static final File TEST_DIR = new File("src/test/resources/scripting"); + + public static final File SYSTEM_CONFIGURATION_FILE = new File(TEST_DIR, "system-configuration.xml"); + private static final String DOT_CLASS = TestScriptingBasic.class.getName() + "."; private static final File LOG_FILE = new File(TEST_DIR, "log.xml"); private static final File SEARCH_FOR_USERS_FILE = new File(TEST_DIR, "search-for-users.xml"); @@ -84,6 +89,9 @@ public class TestScriptingBasic extends AbstractInitializedModelIntegrationTest private static final File DELETE_AND_ADD_JACK_FILE = new File(TEST_DIR, "delete-and-add-jack.xml"); private static final File MODIFY_JACK_FILE = new File(TEST_DIR, "modify-jack.xml"); private static final File MODIFY_JACK_BACK_FILE = new File(TEST_DIR, "modify-jack-back.xml"); + private static final File MODIFY_JACK_PASSWORD_FILE = new File(TEST_DIR, "modify-jack-password.xml"); + private static final File MODIFY_JACK_PASSWORD_TASK_FILE = new File(TEST_DIR, "modify-jack-password-task.xml"); + private static final String MODIFY_JACK_PASSWORD_TASK_OID = "9de76345-0f02-48de-86bf-e7a887cb374a"; private static final File RECOMPUTE_JACK_FILE = new File(TEST_DIR, "recompute-jack.xml"); private static final File ASSIGN_TO_JACK_FILE = new File(TEST_DIR, "assign-to-jack.xml"); private static final File ASSIGN_TO_JACK_2_FILE = new File(TEST_DIR, "assign-to-jack-2.xml"); @@ -97,7 +105,11 @@ public class TestScriptingBasic extends AbstractInitializedModelIntegrationTest private static final File GENERATE_PASSWORDS_3_FILE = new File(TEST_DIR, "generate-passwords-3.xml"); private static final File ECHO_FILE = new File(TEST_DIR, "echo.xml"); - @Autowired + private static final String PASSWORD_PLAINTEXT_FRAGMENT = "pass1234wor"; + private static final String PASSWORD_PLAINTEXT_1 = "pass1234wor1"; + private static final String PASSWORD_PLAINTEXT_2 = "pass1234wor2"; + + @Autowired private ScriptingExpressionEvaluator scriptingExpressionEvaluator; @Override @@ -108,9 +120,16 @@ public void initSystem(Task initTask, OperationResult initResult) // InternalMonitor.setTraceShadowFetchOperation(true); // InternalMonitor.setTraceResourceSchemaOperations(true); + + DebugUtil.setPrettyPrintBeansAs(PrismContext.LANG_YAML); } - @Test + @Override + protected File getSystemConfigurationFile() { + return SYSTEM_CONFIGURATION_FILE; + } + + @Test public void test100EmptySequence() throws Exception { final String TEST_NAME = "test100EmptySequence"; TestUtil.displayTestTitle(this, TEST_NAME); @@ -872,7 +891,82 @@ public void test545SearchUserResolveRoleMembershipRef() throws Exception { } } - private void assertNoOutputData(ExecutionContext output) { + // MID-5359 + @Test + public void test600ModifyJackPasswordInBackground() throws Exception { + final String TEST_NAME = "test600ModifyJackPasswordInBackground"; + TestUtil.displayTestTitle(this, TEST_NAME); + + // GIVEN + OperationResult result = new OperationResult(DOT_CLASS + TEST_NAME); + PrismProperty expression = parseAnyData(MODIFY_JACK_PASSWORD_FILE); + + prepareNotifications(); + dummyAuditService.clear(); + + // WHEN + Task task = taskManager.createTaskInstance(); + task.setOwner(getUser(USER_ADMINISTRATOR_OID)); + scriptingExpressionEvaluator.evaluateExpressionInBackground(expression.getAnyValue().getValue(), task, result); + waitForTaskFinish(task.getOid(), false); + task.refresh(result); + + // THEN + display(task.getResult()); + TestUtil.assertSuccess(task.getResult()); + PrismObject jack = getUser(USER_JACK_OID); + display("jack after password change", jack); + assertEncryptedUserPassword(jack, PASSWORD_PLAINTEXT_1); + + String xml = prismContext.xmlSerializer().serialize(task.getTaskPrismObject()); + display("task", xml); + assertFalse("Plaintext password is present in the task", xml.contains(PASSWORD_PLAINTEXT_FRAGMENT)); + + display("Dummy transport", dummyTransport); + display("Audit", dummyAuditService); + } + + // MID-5359 + @Test + public void test610ModifyJackPasswordImportingTask() throws Exception { + final String TEST_NAME = "test610ModifyJackPasswordImportingTask"; + TestUtil.displayTestTitle(this, TEST_NAME); + + // GIVEN + Task opTask = taskManager.createTaskInstance(DOT_CLASS + TEST_NAME); + opTask.setOwner(getUser(USER_ADMINISTRATOR_OID)); + OperationResult result = opTask.getResult(); + + prepareNotifications(); + dummyAuditService.clear(); + + // WHEN + FileInputStream stream = new FileInputStream(MODIFY_JACK_PASSWORD_TASK_FILE); + modelService.importObjectsFromStream(stream, null, opTask, result); + stream.close(); + + result.computeStatus(); + assertSuccess(result); + + waitForTaskFinish(MODIFY_JACK_PASSWORD_TASK_OID, false); + Task task = taskManager.getTask(MODIFY_JACK_PASSWORD_TASK_OID, result); + + // THEN + display(task.getResult()); + TestUtil.assertSuccess(task.getResult()); + PrismObject jack = getUser(USER_JACK_OID); + display("jack after password change", jack); + assertEncryptedUserPassword(jack, PASSWORD_PLAINTEXT_2); + + String xml = prismContext.xmlSerializer().serialize(task.getTaskPrismObject()); + display("task", xml); + assertFalse("Plaintext password is present in the task", xml.contains(PASSWORD_PLAINTEXT_FRAGMENT)); + + display("Dummy transport", dummyTransport); + display("Audit", dummyAuditService); + } + + private void assertNoOutputData(ExecutionContext output) { assertTrue("Script returned unexpected data", output.getFinalOutput() == null || output.getFinalOutput().getData().isEmpty()); } diff --git a/model/model-intest/src/test/resources/scripting/modify-jack-password-task.xml b/model/model-intest/src/test/resources/scripting/modify-jack-password-task.xml new file mode 100644 index 00000000000..14d6f35d5f7 --- /dev/null +++ b/model/model-intest/src/test/resources/scripting/modify-jack-password-task.xml @@ -0,0 +1,64 @@ + + + + Task 1555581798624-0-1 + + + + + c:UserType + + + c:name + jack + + + + + modify + + delta + + modify + UserType + + replace + credentials/password/value + + pass1234wor2 + + + + + + + + + 1555581798624-0-1 + + + + runnable + BulkActions + http://midpoint.evolveum.com/xml/ns/public/model/scripting/handler-3 + single + tight + diff --git a/model/model-intest/src/test/resources/scripting/modify-jack-password.xml b/model/model-intest/src/test/resources/scripting/modify-jack-password.xml new file mode 100644 index 00000000000..1f46941413b --- /dev/null +++ b/model/model-intest/src/test/resources/scripting/modify-jack-password.xml @@ -0,0 +1,46 @@ + + + + + + c:UserType + + + c:name + jack + + + + + modify + + delta + + + replace + credentials/password/value + + pass1234wor1 + + + + + + diff --git a/model/model-intest/src/test/resources/scripting/system-configuration.xml b/model/model-intest/src/test/resources/scripting/system-configuration.xml new file mode 100644 index 00000000000..03d4b4e5bc5 --- /dev/null +++ b/model/model-intest/src/test/resources/scripting/system-configuration.xml @@ -0,0 +1,592 @@ + + + + + SystemConfiguration + + + File Appender + INFO + + TRACE + com.evolveum.midpoint.common.LoggingConfigurationManager + + + TRACE + com.evolveum.midpoint.notifications + + + %date [%thread] %-5level \(%logger{46}\): %message%n + target/test.log + true + + + + + + + recipient@evolveum.com + + true + dummy:taskNotifier + + + + + + recipient@evolveum.com + + dummy:policyRuleNotifier + + + + + + + + dummy:accountActivationNotifier + link + + + + + + recipient@evolveum.com + + dummy:accountPasswordNotifier + + + + + + recipient@evolveum.com + + dummy:userPasswordNotifier + + + + success + + + recipient@evolveum.com + + dummy:simpleAccountNotifier-SUCCESS + + + + failure + + + recipient@evolveum.com + + dummy:simpleAccountNotifier-FAILURE + + + + add + success + + + recipient@evolveum.com + + dummy:simpleAccountNotifier-ADD-SUCCESS + + + + delete + success + + + recipient@evolveum.com + + dummy:simpleAccountNotifier-DELETE-SUCCESS + + + + + + recipient@evolveum.com + + dummy:simpleUserNotifier + + + + + add + + + + + recipient@evolveum.com + + dummy:simpleUserNotifier-ADD + + + + + + delete + + + + + recipient@evolveum.com + + dummy:simpleUserNotifier-DELETE + + + + + failure + + + recipient@evolveum.com + + dummy:simpleUserNotifier-FAILURE + + + + customEvent + delete + failure + + + + + + recipient@evolveum.com + + + Failure notification of type 2 + + + + + dummy:CustomType2 + + + + target/mail-notifications.log + + + + + non-createable-role + + + add + + + + + + + RoleType + + + description + Thou shalt not create this role! + + + + + + non-createable-role-disabled + + + add + + + + + + + RoleType + + + description + Thou shalt not create this role! (disabled) + + + + + + + + + + + immutable-role + + + modify + + + + + + + RoleType + + + description + Thou shalt not modify this role! + + + + + + immutable-user-from-pluto-selector + + + modify + + + + + + + UserType + + + description + Came from Pluto + + + + + + immutable-user-from-pluto-condition + + + modify + + + + + + + UserType + + + + description + + + + + + + + immutable-role-description + + + modify + description + + + + + + + RoleType + + + description + Thou shalt not modify description of this role! + + + + + + + 30 + + + + + /foo + + + + + UserType + + basic + + + + RoleType + + fulltext + + + + ServiceType + + advanced + + + + + + RoleType + + com.piracy.form.PirateRolePanel + + + + Jamaica + + + + default + + name + take + + + givenName + take + ignore + + + familyName + + + fullName + take + + + employeeType + take + take + + + organization + take + take + + + organizationalUnit + take + + + activation + take + + + assignment + take + take + + + take + conflict + + + take + take + + + take + + + + + default-specific + + The default-specific config is almost the same as default, just the projections are selected + by specific resource. + + + name + take + + + givenName + take + ignore + + + familyName + + + fullName + take + + + employeeType + take + take + + + organization + take + take + + + organizationalUnit + take + + + activation + take + + + assignment + take + take + + + take + + + + + + take + take + + + take + + + + + empty + + + + allRight + + take + + + + + allLeft + + take + + + + + allLeftProjectionsBoth + + Takes all projections. There may be conflicting projections. + Attempt to merge should end with an error. + + + take + + + take + take + + + + + expression + + name + take + + + givenName + take + + + familyName + take + + + fullName + take + + + employeeType + take + expression + + + + + + organization + expression + expression + + + + + + organizationalUnit + take + + + activation + take + + + assignment + take + take + + + take + + + take + + + + diff --git a/repo/task-quartz-impl/src/main/java/com/evolveum/midpoint/task/quartzimpl/TaskManagerQuartzImpl.java b/repo/task-quartz-impl/src/main/java/com/evolveum/midpoint/task/quartzimpl/TaskManagerQuartzImpl.java index a168bff9ab6..261fa5e5347 100644 --- a/repo/task-quartz-impl/src/main/java/com/evolveum/midpoint/task/quartzimpl/TaskManagerQuartzImpl.java +++ b/repo/task-quartz-impl/src/main/java/com/evolveum/midpoint/task/quartzimpl/TaskManagerQuartzImpl.java @@ -15,6 +15,50 @@ */ package com.evolveum.midpoint.task.quartzimpl; +import static com.evolveum.midpoint.schema.result.OperationResultStatus.IN_PROGRESS; +import static com.evolveum.midpoint.schema.result.OperationResultStatus.SUCCESS; +import static com.evolveum.midpoint.schema.result.OperationResultStatus.UNKNOWN; +import static java.util.Collections.singleton; + +import java.text.ParseException; +import java.util.*; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.xml.datatype.Duration; +import javax.xml.datatype.XMLGregorianCalendar; + +import com.evolveum.midpoint.prism.ItemDefinition; +import com.evolveum.midpoint.prism.crypto.Protector; +import com.evolveum.midpoint.prism.delta.builder.DeltaBuilder; +import com.evolveum.midpoint.repo.api.PreconditionViolationException; +import com.evolveum.midpoint.repo.api.RepoAddOptions; +import com.evolveum.midpoint.task.api.*; +import com.evolveum.midpoint.xml.ns._public.common.common_3.*; +import com.evolveum.midpoint.common.crypto.CryptoUtil; +import com.evolveum.midpoint.prism.crypto.EncryptionException; +import com.evolveum.midpoint.prism.util.CloneUtil; +import com.evolveum.midpoint.schema.util.ObjectTypeUtil; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Service; + import com.evolveum.midpoint.common.configuration.api.MidpointConfiguration; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; @@ -153,11 +197,13 @@ public class TaskManagerQuartzImpl implements TaskManager, BeanFactoryAware { @Autowired private MidpointConfiguration midpointConfiguration; @Autowired private RepositoryService repositoryService; @Autowired private LightweightIdentifierGenerator lightweightIdentifierGenerator; + @Autowired private PrismContext prismContext; + @Autowired private Protector protector; + @Autowired @Qualifier("securityEnforcer") private SecurityEnforcer securityEnforcer; - @Autowired private PrismContext prismContext; - + private static final transient Trace LOGGER = TraceManager.getTrace(TaskManagerQuartzImpl.class); // how long to wait after TaskManager shutdown, if using JDBC Job Store (in order to give the jdbc thread pool a chance @@ -233,7 +279,7 @@ public void postInit(OperationResult parentResult) { throw new SystemException("Quartz task scheduler couldn't be started."); } } - + result.computeStatus(); } @@ -514,14 +560,14 @@ private void resumeOrUnpauseTask(Task task, OperationResult result) throws Objec public Task createTaskInstance() { return createTaskInstance(null); } - + @Override public Task createTaskInstance(String operationName) { LightweightIdentifier taskIdentifier = generateTaskIdentifier(); TaskQuartzImpl taskImpl = new TaskQuartzImpl(this, taskIdentifier, operationName); return taskImpl; } - + private LightweightIdentifier generateTaskIdentifier() { return lightweightIdentifierGenerator.generate(); } @@ -553,7 +599,7 @@ public Task getTask(String taskOid, OperationResult parentResult) throws ObjectN OperationResult result = parentResult.createMinorSubresult(DOT_INTERFACE + "getTask"); // todo ... or .createSubresult (without 'minor')? result.addParam(OperationResult.PARAM_OID, taskOid); result.addContext(OperationResult.CONTEXT_IMPLEMENTATION_CLASS, TaskManagerQuartzImpl.class); - + Task task; try { PrismObject taskPrism = repositoryService.getObject(TaskType.class, taskOid, null, result); @@ -565,7 +611,7 @@ public Task getTask(String taskOid, OperationResult parentResult) throws ObjectN result.recordFatalError("Task schema error: "+e.getMessage(), e); throw e; } - + result.recordSuccess(); return task; } @@ -617,7 +663,7 @@ private void persist(Task task, OperationResult parentResult) { if (taskImpl.getCategory() == null) { taskImpl.setCategoryTransient(taskImpl.getCategoryFromHandler()); } - + // taskImpl.setPersistenceStatusTransient(TaskPersistenceStatus.PERSISTENT); // Make sure that the task has repository service instance, so it can fully work as "persistent" @@ -627,6 +673,7 @@ private void persist(Task task, OperationResult parentResult) { } try { + CryptoUtil.encryptValues(protector, taskImpl.getTaskPrismObject()); addTaskToRepositoryAndQuartz(taskImpl, parentResult); } catch (ObjectAlreadyExistsException ex) { // This should not happen. If it does, it is a bug. It is OK to convert to a runtime exception @@ -634,9 +681,12 @@ private void persist(Task task, OperationResult parentResult) { } catch (SchemaException ex) { // This should not happen. If it does, it is a bug. It is OK to convert to a runtime exception throw new IllegalStateException("Got SchemaException while not expecting it (task:"+task+")",ex); + } catch (EncryptionException e) { + // TODO handle this better + throw new SystemException("Couldn't encrypt plain text values in " + task + ": " + e.getMessage(), e); } } - + @Override public String addTask(PrismObject taskPrism, OperationResult parentResult) throws ObjectAlreadyExistsException, SchemaException { OperationResult result = parentResult.createSubresult(DOT_INTERFACE + "addTask"); @@ -673,7 +723,7 @@ private String addTaskToRepositoryAndQuartz(Task task, OperationResult parentRes } ((TaskQuartzImpl) task).setOid(oid); - + synchronizeTaskWithQuartz((TaskQuartzImpl) task, result); result.computeStatus(); @@ -1068,7 +1118,7 @@ public SearchResultList> searchObjects(Cla throw new IllegalArgumentException("Unsupported object type: " + type); } } - + @Override public SearchResultMetadata searchObjectsIterative(Class type, ObjectQuery query, Collection> options, @@ -1087,11 +1137,11 @@ public SearchResultMetadata searchObjectsIterative(Class< } else { throw new IllegalArgumentException("Unsupported object type: " + type); } - + for (PrismObject object: objects) { handler.handle(object, result); } - + result.computeStatus(); return objects.getMetadata(); } @@ -1267,7 +1317,7 @@ public void registerHandler(String uri, TaskHandler handler) { LOGGER.trace("Registering task handler for URI " + uri); handlers.put(uri, handler); } - + public TaskHandler getHandler(String uri) { if (uri != null) return handlers.get(uri); @@ -1428,7 +1478,7 @@ public void notifyTaskThreadFinish(Task task) { /* * ********************* OTHER METHODS + GETTERS AND SETTERS ********************* */ - + PrismObjectDefinition getTaskObjectDefinition() { if (taskPrismDefinition == null) { taskPrismDefinition = prismContext.getSchemaRegistry().findObjectDefinitionByCompileTimeClass(TaskType.class); @@ -1476,7 +1526,7 @@ public ClusterManager getClusterManager() { public RepositoryService getRepositoryService() { return repositoryService; } - + public void setConfiguration(TaskManagerConfiguration configuration) { this.configuration = configuration; } @@ -1484,7 +1534,7 @@ public void setConfiguration(TaskManagerConfiguration configuration) { public ExecutionManager getExecutionManager() { return executionManager; } - + public SecurityEnforcer getSecurityEnforcer() { return securityEnforcer; }