From e7060f56d5ec7adb90e729a7d0018957db6a572b Mon Sep 17 00:00:00 2001 From: Pascal Pellmont Date: Sun, 4 Feb 2018 12:08:48 +0100 Subject: [PATCH] [ZEPPELIN-1070] Inject Credentials in any Interpreter-Code --- .../zeppelin/notebook/CredentialInjector.java | 98 +++++++++++++++++++ .../apache/zeppelin/notebook/Paragraph.java | 7 +- .../notebook/CredentialInjectorTest.java | 88 +++++++++++++++++ .../zeppelin/notebook/ParagraphTest.java | 56 +++++++++-- 4 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/CredentialInjector.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/CredentialInjectorTest.java diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/CredentialInjector.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/CredentialInjector.java new file mode 100644 index 00000000000..a81e7eb57f1 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/CredentialInjector.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.zeppelin.notebook; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.user.UserCredentials; +import org.apache.zeppelin.user.UsernamePassword; + +/** + * Class for replacing $[user.>credentialkey<] and + * $[password.>credentialkey<] tags with the matching credentials from + * zeppelin + */ +class CredentialInjector { + + private Set passwords = new HashSet<>(); + private final UserCredentials creds; + + public CredentialInjector(UserCredentials creds) { + this.creds = creds; + } + + public String replaceCredentials(String code) { + if (code == null) { + return null; + } + String replaced = code; + Pattern userpattern = Pattern.compile("\\$\\[user\\.([^\\]]+)\\]"); + Pattern passwordpattern = Pattern.compile("\\$\\[password\\.([^\\]]+)\\]"); + Matcher matcher = userpattern.matcher(replaced); + while (matcher.find()) { + String key = matcher.group(1); + UsernamePassword usernamePassword = creds.getUsernamePassword(key); + String value = usernamePassword == null ? "undef" : usernamePassword.getUsername(); + replaced = matcher.replaceFirst(value); + matcher = userpattern.matcher(replaced); + } + matcher = passwordpattern.matcher(replaced); + while (matcher.find()) { + String key = matcher.group(1); + UsernamePassword usernamePassword = creds.getUsernamePassword(key); + if (usernamePassword != null) { + passwords.add(usernamePassword.getPassword()); + } + String value = usernamePassword == null ? "undef" : usernamePassword.getPassword(); + replaced = matcher.replaceFirst(value); + matcher = passwordpattern.matcher(replaced); + } + return replaced; + } + + public InterpreterResult hidePasswords(InterpreterResult ret) { + if (ret == null) { + return null; + } + return new InterpreterResult(ret.code(), replacePasswords(ret.message())); + } + + private List replacePasswords(List original) { + List replaced = new ArrayList<>(); + for (InterpreterResultMessage msg : original) { + String replacedMessages = replacePasswords(msg.getData()); + replaced.add(new InterpreterResultMessage(msg.getType(), replacedMessages)); + } + return replaced; + } + + private String replacePasswords(String str) { + String result = str; + for (String password : passwords) { + result = result.replace(password, "###"); + } + return result; + } + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index 57756b8e183..be8f76e7576 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -434,7 +434,12 @@ && isUserAuthorizedToAccessInterpreter(interpreterSetting.getOption()) == false) try { InterpreterContext context = getInterpreterContext(); InterpreterContext.set(context); - InterpreterResult ret = interpreter.interpret(script, context); + UserCredentials creds = context.getAuthenticationInfo().getUserCredentials(); + + CredentialInjector credinjector = new CredentialInjector(creds); + String code = credinjector.replaceCredentials(script); + InterpreterResult ret = interpreter.interpret(code, context); + ret = credinjector.hidePasswords(ret); if (interpreter.getFormType() == FormType.NATIVE) { note.setNoteParams(context.getNoteGui().getParams()); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/CredentialInjectorTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/CredentialInjectorTest.java new file mode 100644 index 00000000000..e5d88aeaf22 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/CredentialInjectorTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.zeppelin.notebook; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.user.UserCredentials; +import org.apache.zeppelin.user.UsernamePassword; +import org.junit.Test; + +public class CredentialInjectorTest { + + private static final String TEMPLATE = + "val jdbcUrl = \"jdbc:mysql://localhost/emp?user=$[user.mysql]&password=$[password.mysql]\""; + private static final String CORRECT_REPLACED = + "val jdbcUrl = \"jdbc:mysql://localhost/emp?user=username&password=pwd\""; + private static final String NOT_REPLACED = + "val jdbcUrl = \"jdbc:mysql://localhost/emp?user=undef&password=undef\""; + + private static final String ANSWER = + "jdbcUrl: String = jdbc:mysql://localhost/employees?user=username&password=pwd"; + private static final String HIDDEN = + "jdbcUrl: String = jdbc:mysql://localhost/employees?user=username&password=###"; + + @Test + public void replaceCredentials() { + UserCredentials userCredentials = mock(UserCredentials.class); + UsernamePassword usernamePassword = new UsernamePassword("username", "pwd"); + when(userCredentials.getUsernamePassword("mysql")).thenReturn(usernamePassword); + CredentialInjector testee = new CredentialInjector(userCredentials); + String actual = testee.replaceCredentials(TEMPLATE); + assertEquals(CORRECT_REPLACED, actual); + + InterpreterResult ret = new InterpreterResult(Code.SUCCESS, ANSWER); + InterpreterResult hiddenResult = testee.hidePasswords(ret); + assertEquals(1, hiddenResult.message().size()); + assertEquals(HIDDEN, hiddenResult.message().get(0).getData()); + } + + @Test + public void replaceCredentialNoTexts() { + UserCredentials userCredentials = mock(UserCredentials.class); + CredentialInjector testee = new CredentialInjector(userCredentials); + String actual = testee.replaceCredentials(null); + assertNull(actual); + } + + @Test + public void replaceCredentialsNotExisting() { + UserCredentials userCredentials = mock(UserCredentials.class); + CredentialInjector testee = new CredentialInjector(userCredentials); + String actual = testee.replaceCredentials(TEMPLATE); + assertEquals(NOT_REPLACED, actual); + + InterpreterResult ret = new InterpreterResult(Code.SUCCESS, ANSWER); + InterpreterResult hiddenResult = testee.hidePasswords(ret); + assertEquals(1, hiddenResult.message().size()); + assertEquals(ANSWER, hiddenResult.message().get(0).getData()); + } + + @Test + public void hidePasswordsNoResult() { + UserCredentials userCredentials = mock(UserCredentials.class); + CredentialInjector testee = new CredentialInjector(userCredentials); + assertNull(testee.hidePasswords(null)); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java index e46b7393ee1..11f16290126 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -30,33 +31,38 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.google.common.collect.Lists; - import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.tuple.Triple; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectBuilder; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.Interpreter.FormType; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterOption; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; import org.apache.zeppelin.interpreter.InterpreterResult.Type; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.InterpreterSetting.Status; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; +import org.apache.zeppelin.user.UserCredentials; +import org.apache.zeppelin.user.UsernamePassword; import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; import org.mockito.Mockito; +import com.google.common.collect.Lists; + public class ParagraphTest extends AbstractInterpreterTest { @Test @@ -299,4 +305,42 @@ public void testCursorPosition() { } } + @Test + public void credentialReplacement() throws Throwable { + Note mockNote = mock(Note.class); + Credentials creds = mock(Credentials.class); + when(mockNote.getCredentials()).thenReturn(creds); + Paragraph spyParagraph = spy(new Paragraph("para_1", mockNote, null, null)); + UserCredentials uc = mock(UserCredentials.class); + when(creds.getUserCredentials(anyString())).thenReturn(uc); + UsernamePassword up = new UsernamePassword("user", "pwd"); + when(uc.getUsernamePassword("ent")).thenReturn(up ); + + Interpreter mockInterpreter = mock(Interpreter.class); + spyParagraph.setInterpreter(mockInterpreter); + doReturn(mockInterpreter).when(spyParagraph).getBindedInterpreter(); + + ManagedInterpreterGroup mockInterpreterGroup = mock(ManagedInterpreterGroup.class); + when(mockInterpreter.getInterpreterGroup()).thenReturn(mockInterpreterGroup); + when(mockInterpreterGroup.getId()).thenReturn("mock_id_1"); + when(mockInterpreterGroup.getAngularObjectRegistry()).thenReturn(mock(AngularObjectRegistry.class)); + when(mockInterpreterGroup.getResourcePool()).thenReturn(mock(ResourcePool.class)); + when(mockInterpreter.getFormType()).thenReturn(FormType.NONE); + + ParagraphJobListener mockJobListener = mock(ParagraphJobListener.class); + doReturn(mockJobListener).when(spyParagraph).getListener(); + doNothing().when(mockJobListener).onOutputUpdateAll(Mockito.any(), Mockito.anyList()); + + InterpreterResult mockInterpreterResult = mock(InterpreterResult.class); + when(mockInterpreter.interpret(anyString(), Mockito.any())).thenReturn(mockInterpreterResult); + when(mockInterpreterResult.code()).thenReturn(Code.SUCCESS); + + AuthenticationInfo user1 = new AuthenticationInfo("user1"); + spyParagraph.setAuthenticationInfo(user1); + + spyParagraph.setText("val x = \"usr=$[user.ent]&pass=$[password.ent]\""); + spyParagraph.jobRun(); + + verify(mockInterpreter).interpret(eq("val x = \"usr=user&pass=pwd\""), any(InterpreterContext.class)); + } }