Skip to content
Draft
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
45 changes: 1 addition & 44 deletions logstash-core/lib/logstash/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -438,51 +438,8 @@ def validate(value)
java_import org.logstash.settings.StringSetting

java_import org.logstash.settings.NullableStringSetting

java_import org.logstash.settings.PasswordSetting

class ValidatedPassword < Setting::PasswordSetting
def initialize(name, value, password_policies)
@password_policies = password_policies
super(name, value, true)
end

def coerce(password)
if password && !password.kind_of?(::LogStash::Util::Password)
raise(ArgumentError, "Setting `#{name}` could not coerce LogStash::Util::Password value to password")
end

policies = build_password_policies
validatedResult = LogStash::Util::PasswordValidator.new(policies).validate(password.value)
if validatedResult.length() > 0
if @password_policies.fetch(:mode).eql?("WARN")
logger.warn("Password #{validatedResult}.")
else
raise(ArgumentError, "Password #{validatedResult}.")
end
end
password
end

def build_password_policies
policies = {}
policies[Util::PasswordPolicyType::EMPTY_STRING] = Util::PasswordPolicyParam.new
policies[Util::PasswordPolicyType::LENGTH] = Util::PasswordPolicyParam.new("MINIMUM_LENGTH", @password_policies.dig(:length, :minimum).to_s)
if @password_policies.dig(:include, :upper).eql?("REQUIRED")
policies[Util::PasswordPolicyType::UPPER_CASE] = Util::PasswordPolicyParam.new
end
if @password_policies.dig(:include, :lower).eql?("REQUIRED")
policies[Util::PasswordPolicyType::LOWER_CASE] = Util::PasswordPolicyParam.new
end
if @password_policies.dig(:include, :digit).eql?("REQUIRED")
policies[Util::PasswordPolicyType::DIGIT] = Util::PasswordPolicyParam.new
end
if @password_policies.dig(:include, :symbol).eql?("REQUIRED")
policies[Util::PasswordPolicyType::SYMBOL] = Util::PasswordPolicyParam.new
end
policies
end
end
java_import org.logstash.settings.ValidatedPasswordSetting

# The CoercibleString allows user to enter any value which coerces to a String.
# For example for true/false booleans; if the possible_strings are ["foo", "true", "false"]
Expand Down
2 changes: 1 addition & 1 deletion logstash-core/lib/logstash/webserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def initialize(logger, agent, options = {})
username = options[:auth_basic].fetch(:username)
password = options[:auth_basic].fetch(:password)
password_policies = options[:auth_basic].fetch(:password_policies)
validated_password = Setting::ValidatedPassword.new("api.auth.basic.password", password, password_policies).freeze
validated_password = Setting::ValidatedPasswordSetting.new("api.auth.basic.password", password, password_policies).freeze
app = Rack::Auth::Basic.new(app, "logstash-api") { |u, p| u == username && p == validated_password.value.value }
end

Expand Down
29 changes: 5 additions & 24 deletions logstash-core/spec/logstash/settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -283,39 +283,20 @@
context "when running PasswordValidator coerce" do
it "raises an error when supplied value is not LogStash::Util::Password" do
expect {
LogStash::Setting::ValidatedPassword.new("test.validated.password", "testPassword", password_policies)
}.to raise_error(ArgumentError, a_string_including("Setting `test.validated.password` could not coerce LogStash::Util::Password value to password"))
LogStash::Setting::ValidatedPasswordSetting.new("test.validated.password", "testPassword", password_policies)
}.to raise_error(IllegalArgumentException, a_string_including("Setting `test.validated.password` could not coerce LogStash::Util::Password value to password"))
end

it "fails on validation" do
password = LogStash::Util::Password.new("Password!")
expect {
LogStash::Setting::ValidatedPassword.new("test.validated.password", password, password_policies)
}.to raise_error(ArgumentError, a_string_including("Password must contain at least one digit between 0 and 9."))
LogStash::Setting::ValidatedPasswordSetting.new("test.validated.password", password, password_policies)
}.to raise_error(IllegalArgumentException, a_string_including("Password must contain at least one digit between 0 and 9."))
end

it "validates the password successfully" do
password = LogStash::Util::Password.new("Password123!")
expect(LogStash::Setting::ValidatedPassword.new("test.validated.password", password, password_policies)).to_not be_nil
end
end

describe "mode WARN" do
let(:password_policies) { super().merge("mode": "WARN") }

context "when the password does not conform to the policy" do
let(:password) { LogStash::Util::Password.new("NoNumbers!") }
let(:mock_logger) { double("logger") }

before :each do
allow_any_instance_of(LogStash::Setting::ValidatedPassword).to receive(:logger).and_return(mock_logger)
end

it "logs a warning on validation failure" do
expect(mock_logger).to receive(:warn).with(a_string_including("Password must contain at least one digit between 0 and 9."))

LogStash::Setting::ValidatedPassword.new("test.validated.password", password, password_policies)
end
expect(LogStash::Setting::ValidatedPasswordSetting.new("test.validated.password", password, password_policies)).to_not be_nil
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 org.logstash.settings;

import co.elastic.logstash.api.Password;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.logstash.secret.password.PasswordPolicyParam;
import org.logstash.secret.password.PasswordPolicyType;
import org.logstash.secret.password.PasswordValidator;

import java.util.HashMap;
import java.util.Map;

public class ValidatedPasswordSetting extends PasswordSetting {

private static final Logger LOGGER = LogManager.getLogger(ValidatedPasswordSetting.class);

private final Map<String, Object> passwordPolicies;

@SuppressWarnings("this-escape")
public ValidatedPasswordSetting(String name, Object defaultValue, Map<Object, Object> passwordPolicies) {
super(name, defaultValue, false); // this super doesn't call validate and coerce

this.passwordPolicies = convertKeyRubyLabelsToStrings(passwordPolicies);
Password coercedDefault = coerce(defaultValue);
validate(coercedDefault);
this.defaultValue = coercedDefault;
}

@SuppressWarnings("unchecked")
private Map<String, Object> convertKeyRubyLabelsToStrings(Map<Object, Object> passwordPolicies) {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<Object, Object> entry : passwordPolicies.entrySet()) {
final String transformedKey = entry.getKey().toString();;
Object value = entry.getValue();
if (value instanceof Map) {
value = convertKeyRubyLabelsToStrings((Map<Object, Object>) value);
}
// TODO handle lists if needed ?
// TODO what if the value is a RubySymbol ?
result.put(transformedKey, value);
}
return result;
}

@Override
public Password coerce(Object password) {
if (password != null && !(password instanceof Password)) {
throw new IllegalArgumentException("Setting `" + getName() + "` could not coerce LogStash::Util::Password value to password");
}
LOGGER.info("Password policies: {}", passwordPolicies);
Map<PasswordPolicyType, PasswordPolicyParam> policies = buildPasswordPolicies();
String validatedResult = new PasswordValidator(policies).validate(((Password) password).getValue());

if (!validatedResult.isEmpty()) {
if ("WARN".equals(passwordPolicies.get("mode"))) {
LOGGER.warn("Password {}.", validatedResult);
} else {
throw new IllegalArgumentException("Password "+ validatedResult + ".");
}
}
return (Password) password;
}

private Map<PasswordPolicyType, PasswordPolicyParam> buildPasswordPolicies() {
Map<PasswordPolicyType, PasswordPolicyParam> policies = new HashMap<>();

policies.put(PasswordPolicyType.EMPTY_STRING, new PasswordPolicyParam());
Object minLengthPolicyValue = dig(passwordPolicies, "length", "minimum");

// in Ruby "nil.to_s" is "", so we do the same here
policies.put(PasswordPolicyType.LENGTH, new PasswordPolicyParam("MINIMUM_LENGTH",
minLengthPolicyValue != null? minLengthPolicyValue.toString() : ""));

if ("REQUIRED".equals(dig(passwordPolicies, "include", "upper"))) {
policies.put(PasswordPolicyType.UPPER_CASE, new PasswordPolicyParam());
}
if ("REQUIRED".equals(dig(passwordPolicies, "include", "lower"))) {
policies.put(PasswordPolicyType.LOWER_CASE, new PasswordPolicyParam());
}
if ("REQUIRED".equals(dig(passwordPolicies, "include", "digit"))) {
policies.put(PasswordPolicyType.DIGIT, new PasswordPolicyParam());
}
if ("REQUIRED".equals(dig(passwordPolicies, "include", "symbol"))) {
policies.put(PasswordPolicyType.SYMBOL, new PasswordPolicyParam());
}
return policies;
}

private static Object dig(Map<String, Object> map, String... path) {
Object current = map;
for (String key : path) {
if (!(current instanceof Map)) {
return null;
}
current = ((Map<?, ?>) current).get(key);
if (current == null) {
return null;
}
}
return current;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ public void setUp() {
}

@Test
public void givenUnsetPasswordSetting_thenIsConsideredAsValid() {
public void givenUnsetPasswordSetting_thenIsConsideredAsValid() {
assertNotThrown(() -> sut.validateValue());
assertThat(sut.value(), is(instanceOf(co.elastic.logstash.api.Password.class)));
assertNull(((co.elastic.logstash.api.Password) sut.value()).getValue());
}

@Test
public void givenUnsetPasswordSetting_whenIsSetIsInvoked_thenReturnFalse() {
public void givenUnsetPasswordSetting_wheIsSetIsInvoked_thenReturnFalse() {
assertFalse(sut.isSet());
}

Expand All @@ -59,14 +59,14 @@ public void givenSetPasswordSetting_thenIsValid() {
}

@Test
public void givenSetPasswordSetting_whenIsSetIsInvoked_thenReturnTrue() {
public void givenSetPasswordSetting_whenIsSetIsInvoked_thenReturnTrue() {
sut.set("s3cUr3p4$$w0rd");

assertTrue(sut.isSet());
}

@Test
public void givenSetPasswordSettingWithInvalidNonStringValue_thenRejectsTheInvalidValue() {
public void givenSetPasswordSettingWithInvalidNonStringValue_thenRejectsTheInvalidValue() {
Exception e = assertThrows(IllegalArgumentException.class, () -> sut.set(867_5309));
assertThat(e.getMessage(), is("Setting `" + SETTING_NAME + "` could not coerce non-string value to password"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 org.logstash.settings;

import co.elastic.logstash.api.Password;
import org.apache.logging.log4j.junit.LoggerContextRule;
import org.apache.logging.log4j.test.appender.ListAppender;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

/* Test copied from logstash-core/spec/logstash/settings_spec.rb */
public class ValidatedPasswordSettingTest {

private static final String CONFIG = "log4j2-test1.xml";

@ClassRule
public static LoggerContextRule CTX = new LoggerContextRule(CONFIG);

private Map<Object, Object> passwordPolicies;
private ListAppender appender;

@Before
public void setUp() {
passwordPolicies = createPasswordPolicies();

appender = CTX.getListAppender("EventLogger").clear();
}

private Map<Object, Object> createPasswordPolicies() {
return new HashMap<>(Map.<Object, Object>of(
"mode", "ERROR",
"length", Map.of("minimum", 8),
"include", Map.of(
"upper", "REQUIRED",
"lower", "REQUIRED",
"digit", "REQUIRED",
"symbol", "REQUIRED"
)
));
}

@Test
public void givenSomePasswordPolicies_whenCoercingSuppliedValueThatIsNotAPasswordInstance_thenThrowAnError() {
final var ex = assertThrows(IllegalArgumentException.class, () -> {
new ValidatedPasswordSetting("test.validated.password", "testPassword", passwordPolicies);
});
assertTrue(ex.getMessage().contains("Setting `test.validated.password` could not coerce LogStash::Util::Password value to password"));
}

@Test
public void givenSomePasswordPolicies_whenCoercingSuppliedAPasswordNotRespectingPolicies_thenThrowAnError() {
Password password = new Password("Password!");
final var ex = assertThrows(IllegalArgumentException.class, () -> {
new ValidatedPasswordSetting("test.validated.password", password, passwordPolicies);
});
assertTrue(ex.getMessage().contains("Password must contain at least one digit between 0 and 9."));
}

@Test
public void givenSomePasswordPolicies_whenCoercingSuppliedAPasswordRespectingPolicies_thenValidationPasses() {
Password password = new Password("Password123!");
ValidatedPasswordSetting setting = new ValidatedPasswordSetting("test.validated.password", password, passwordPolicies);
assertNotNull("new setting instance should be created", setting);
}

@Test
public void givenPasswordPoliciesWithWarnMode_whenCoercingPasswordNotConform_thenLogsWarningOnValidationFailure() {
passwordPolicies.put("mode", "WARN");
Password password = new Password("NoNumbers!");

new ValidatedPasswordSetting("test.validated.password", password, passwordPolicies);

boolean printStalling = appender.getMessages().stream().anyMatch((msg) -> msg.contains("Password must contain at least one digit between 0 and 9."));
assertTrue(printStalling);
}
}