/*
* Copyright 2017 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.config;
import com.thoughtworks.go.config.remote.ConfigOrigin;
import com.thoughtworks.go.config.remote.ConfigOriginTraceable;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.domain.PersistentObject;
import com.thoughtworks.go.security.GoCipher;
import com.thoughtworks.go.util.StringUtil;
import com.thoughtworks.go.util.command.EnvironmentVariableContext;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.bouncycastle.crypto.InvalidCipherTextException;
import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @understands an environment variable value that will be passed to a job when it is run
*/
@ConfigTag("variable")
public class EnvironmentVariableConfig extends PersistentObject implements Serializable, Validatable, ParamsAttributeAware, PasswordEncrypter, ConfigOriginTraceable {
@ConfigAttribute(value = "name", optional = false)
private String name;
@ConfigAttribute(value = "secure", optional = true)
private boolean isSecure = false;
@ConfigSubtag
private VariableValueConfig value;
@ConfigSubtag
private EncryptedVariableValueConfig encryptedValue;
private long entityId;
private String entityType;
private final ConfigErrors configErrors = new ConfigErrors();
public static final String NAME = "name";
public static final String VALUE = "valueForDisplay";
public static final String ENCRYPTEDVALUE = "encryptedValue";
public static final String SECURE = "secure";
private GoCipher goCipher = null;
public static final String ISCHANGED = "isChanged";
private ConfigOrigin origin;
public EnvironmentVariableConfig() {
this.goCipher = new GoCipher();
}
public EnvironmentVariableConfig(String name, String value) {
this(new GoCipher(), name, value, false);
}
public EnvironmentVariableConfig(GoCipher goCipher, String name, String value, boolean isSecure) {
this(goCipher);
this.name = name;
this.isSecure = isSecure;
setValue(value);
}
public EnvironmentVariableConfig(GoCipher goCipher, String name, String encryptedValue) {
this(goCipher);
this.name = name;
this.isSecure = true;
this.setEncryptedValue(new EncryptedVariableValueConfig(encryptedValue));
}
public EnvironmentVariableConfig(EnvironmentVariableConfig variable) {
this(variable.goCipher, variable.name, variable.getValue(), variable.isSecure);
}
private String encrypt(String value) {
try {
return goCipher.encrypt(value);
} catch (InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
public EnvironmentVariableConfig(GoCipher goCipher) {
this.goCipher = goCipher;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EnvironmentVariableConfig that = (EnvironmentVariableConfig) o;
if (isSecure != that.isSecure) {
return false;
}
if (encryptedValue != null ? !encryptedValue.equals(that.encryptedValue) : that.encryptedValue != null) {
return false;
}
if (name != null ? !name.equals(that.name) : that.name != null) {
return false;
}
if (value != null ? !value.equals(that.value) : that.value != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (isSecure ? 1 : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
result = 31 * result + (encryptedValue != null ? encryptedValue.hashCode() : 0);
result = 31 * result + (configErrors != null ? configErrors.hashCode() : 0);
return result;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
void addTo(EnvironmentVariableContext context) {
context.setProperty(name, getValue(), isSecure());
}
public void addToIfExists(EnvironmentVariableContext context) {
if (context.hasProperty(name)) {
addTo(context);
}
}
public void validateName(Map<String, EnvironmentVariableConfig> variableConfigMap, ValidationContext validationContext) {
String currentVariableName = name.toLowerCase();
String parentDisplayName = validationContext.getParentDisplayName();
CaseInsensitiveString parentName = getParentNameFrom(validationContext);
if(!currentVariableName.trim().equals(currentVariableName)){
configErrors.add(NAME, String.format("Environment Variable cannot start or end with spaces for %s '%s'.", parentDisplayName, parentName));
return;
}
if (StringUtil.isBlank(currentVariableName)) {
configErrors.add(NAME, String.format("Environment Variable cannot have an empty name for %s '%s'.", parentDisplayName, parentName));
return;
}
EnvironmentVariableConfig configWithSameName = variableConfigMap.get(currentVariableName);
if (configWithSameName == null) {
variableConfigMap.put(currentVariableName, this);
} else {
configWithSameName.addNameConflictError(parentDisplayName, parentName);
this.addNameConflictError(parentDisplayName, parentName);
}
}
private void addNameConflictError(String parentDisplayName, Object parentName) {
configErrors.add(NAME, String.format("Environment Variable name '%s' is not unique for %s '%s'.", this.name, parentDisplayName, parentName));
}
private CaseInsensitiveString getParentNameFrom(ValidationContext validationContext) {
EnvironmentVariableScope parent = (EnvironmentVariableScope) validationContext.getParent();
return parent.name();
}
/**
* We do this to avoid breaking encapsulation.
* We should remove this method when we move to Hibernate.
*/
public Map<String, Object> getSqlCriteria() {
HashMap<String, Object> map = new HashMap<>();
map.put("variableName", name);
map.put("variableValue", getValue());
map.put("isSecure", isSecure);
return map;
}
public boolean hasName(String variableName) {
return name.equals(variableName);
}
public boolean validateTree(ValidationContext validationContext) {
validate(validationContext);
return errors().isEmpty();
}
public void validate(ValidationContext validationContext) {
try {
getValue();
} catch (Exception e) {
errors().add(VALUE, String.format("Encrypted value for variable named '%s' is invalid. This usually happens when the cipher text is modified to have an invalid value.", getName()));
}
}
public ConfigErrors errors() {
return configErrors;
}
public void addError(String fieldName, String message) {
configErrors.add(fieldName, message);
}
public boolean isSecure() {
return isSecure;
}
public void setIsSecure(boolean isSecure) {
this.isSecure = isSecure;
}
public boolean isPlain() {
return !isSecure();
}
@Deprecated
// prefer using deserialize instead
public void setValue(String value) {
if (isSecure) {
encryptedValue = new EncryptedVariableValueConfig(encrypt(value));
} else {
this.value = new VariableValueConfig(value);
}
}
public void setEncryptedValue(String encrypted) {
this.encryptedValue = new EncryptedVariableValueConfig(encrypted);
}
public void setValue(VariableValueConfig value) {
this.value = value;
}
public void setEncryptedValue(EncryptedVariableValueConfig encryptedValue) {
this.encryptedValue = encryptedValue;
}
public String getValue() {
if (isSecure) {
try {
return goCipher.decrypt(encryptedValue.getValue());
} catch (InvalidCipherTextException e) {
throw new RuntimeException(String.format("Could not decrypt secure environment variable value for name %s", getName()), e);
}
} else {
return value.getValue();
}
}
public String getDisplayValue() {
if (isSecure()) return "****";
return getValue();
}
public String getEncryptedValue() {
return encryptedValue.getValue();
}
public void setConfigAttributes(Object attributes) {
Map attributeMap = (Map) attributes;
this.name = (String) attributeMap.get(EnvironmentVariableConfig.NAME);
String value = (String) attributeMap.get(EnvironmentVariableConfig.VALUE);
if (StringUtil.isBlank(name) && StringUtil.isBlank(value)) {
throw new IllegalArgumentException(String.format("Need not null/empty name & value %s:%s", this.name, value));
}
this.isSecure = BooleanUtils.toBoolean((String) attributeMap.get(EnvironmentVariableConfig.SECURE));
Boolean isChanged = BooleanUtils.toBoolean((String) attributeMap.get(EnvironmentVariableConfig.ISCHANGED));
if (isSecure) {
this.encryptedValue = isChanged ? new EncryptedVariableValueConfig(encrypt(value)) : new EncryptedVariableValueConfig(value);
} else {
this.value = new VariableValueConfig(value);
}
}
@PostConstruct
public void ensureEncrypted() {
if (isSecure && value != null) {
encryptedValue = new EncryptedVariableValueConfig(encrypt(value.getValue()));
value = null;
}
}
public void deserialize(String name, String value, boolean isSecure, String encryptedValue) throws InvalidCipherTextException {
setName(name);
setIsSecure(isSecure);
if (!isSecure && encryptedValue != null) {
errors().add(ENCRYPTEDVALUE, "You may specify encrypted value only when option 'secure' is true.");
}
if (value != null && encryptedValue != null) {
addError("value", "You may only specify `value` or `encrypted_value`, not both!");
addError(ENCRYPTEDVALUE, "You may only specify `value` or `encrypted_value`, not both!");
}
if (encryptedValue != null) {
setEncryptedValue(new EncryptedVariableValueConfig(encryptedValue));
}
if (isSecure) {
if (value != null) {
setEncryptedValue(new EncryptedVariableValueConfig(new GoCipher().encrypt(value)));
}
} else {
setValue(new VariableValueConfig(value));
}
}
public String getValueForDisplay() {
if (isSecure) {
return getEncryptedValue();
}
return value.getValue();
}
public void setEntityId(Long entityId) {
this.entityId = entityId;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
@Override
public ConfigOrigin getOrigin() {
return origin;
}
public boolean isRemote()
{
return origin != null && !origin.isLocal();
}
public void setOrigins(ConfigOrigin origins) {
this.origin = origins;
}
}