[ 
https://issues.apache.org/jira/browse/KNOX-2631?focusedWorklogId=815627&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-815627
 ]

ASF GitHub Bot logged work on KNOX-2631:
----------------------------------------

                Author: ASF GitHub Bot
            Created on: 11/Oct/22 11:33
            Start Date: 11/Oct/22 11:33
    Worklog Time Spent: 10m 
      Work Description: smolnar82 commented on code in PR #639:
URL: https://github.com/apache/knox/pull/639#discussion_r992149578


##########
gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java:
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.knox.gateway.websockets;
+
+import org.apache.knox.gateway.audit.api.AuditService;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.GatewaySpiMessages;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
+import org.easymock.EasyMock;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.net.URI;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.easymock.EasyMock.isA;

Review Comment:
   nit: static imports should go first



##########
gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.knox.gateway.webshell;
+
+import org.apache.knox.gateway.audit.api.AuditService;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.websockets.JWTValidator;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.easymock.EasyMock.isA;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({WebshellWebSocketAdapter.class, MessagesFactory.class, 
AuditServiceFactory.class})
+public class WebshellWebsocketAdapterTest extends EasyMockSupport {
+
+    private GatewayConfig setupGatewayConfig(){
+        GatewayConfig gatewayConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
EasyMock.expect(gatewayConfig.getGatewayPIDDir()).andReturn("testPIDDir").anyTimes();
+        
EasyMock.expect(gatewayConfig.getWebShellReadBufferSize()).andReturn(1024).anyTimes();
+        
EasyMock.expect(gatewayConfig.isWebShellAuditLoggingEnabled()).andReturn(false).anyTimes();
+        EasyMock.replay(gatewayConfig);
+        return gatewayConfig;
+    }
+    private void setupMessagesFactory(){
+        PowerMock.mockStatic(MessagesFactory.class);
+        
EasyMock.expect(MessagesFactory.get(JWTMessages.class)).andReturn(EasyMock.createNiceMock(JWTMessages.class)).anyTimes();
+        
EasyMock.expect(MessagesFactory.get(WebsocketLogMessages.class)).andReturn(EasyMock.createNiceMock(WebsocketLogMessages.class)).anyTimes();
+        PowerMock.replay(MessagesFactory.class);
+    }
+    private void setupAuditor(){
+        PowerMock.mockStatic(AuditServiceFactory.class);
+        AuditService auditService = 
EasyMock.createNiceMock(AuditService.class);
+        
EasyMock.expect(AuditServiceFactory.getAuditService()).andReturn(auditService).anyTimes();
+        Auditor auditor = EasyMock.createNiceMock(Auditor.class);
+        
EasyMock.expect(auditService.getAuditor(isA(String.class),isA(String.class),isA(String.class))).andReturn(auditor).anyTimes();
+        PowerMock.replay(AuditServiceFactory.class);
+        EasyMock.replay(auditService,auditor);
+    }
+
+    @Test
+    public void testOnWebSocketConnect() throws Exception{
+
+        GatewayConfig gatewayConfig = setupGatewayConfig();
+        setupMessagesFactory();
+        setupAuditor();
+
+        JWTValidator jwtValidator = 
EasyMock.createNiceMock(JWTValidator.class);
+        
EasyMock.expect(jwtValidator.getUsername()).andReturn("Alice").anyTimes();
+        EasyMock.replay(jwtValidator);
+
+        ConnectionInfo connectionInfo = 
PowerMock.createMock(ConnectionInfo.class);
+        PowerMock.expectNew(ConnectionInfo.class, isA(String.class) , 
isA(String.class), isA(AtomicInteger.class)).andReturn(connectionInfo);
+        connectionInfo.connect();
+
+        String bashOuput = "fake bash output";
+        InputStream inputStream = new 
ByteArrayInputStream(bashOuput.getBytes(StandardCharsets.UTF_8));
+        
EasyMock.expect(connectionInfo.getInputStream()).andReturn(inputStream).times(1);
+        InputStream inputStreamEmpty = new 
ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8));
+        
EasyMock.expect(connectionInfo.getInputStream()).andReturn(inputStreamEmpty).times(1);
+
+        Session session = EasyMock.createNiceMock(Session.class);
+        RemoteEndpoint remote = EasyMock.createNiceMock(RemoteEndpoint.class);
+        // send to client
+        EasyMock.expect(session.getRemote()).andReturn(remote).anyTimes();
+        remote.sendString(bashOuput);
+        // clean up
+        EasyMock.expect(session.isOpen()).andReturn(true).anyTimes();
+        session.close();
+        EasyMock.replay(session, remote);
+
+        connectionInfo.disconnect();
+        PowerMock.replay(connectionInfo, ConnectionInfo.class);
+
+        ExecutorService pool = Executors.newFixedThreadPool(10);
+        AtomicInteger concurrentWebshells = new AtomicInteger(0);
+        WebshellWebSocketAdapter webshellWebSocketAdapter = new 
WebshellWebSocketAdapter(pool, gatewayConfig, jwtValidator, 
concurrentWebshells);
+        webshellWebSocketAdapter.onWebSocketConnect(session);
+        verifyAll();
+    }
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void testOnWebSocketConnectWithoutUsername() throws Exception {
+        thrown.expect(RuntimeException.class);
+        thrown.expectMessage("Needs user name in JWT to use WebShell");
+        GatewayConfig gatewayConfig = setupGatewayConfig();
+        setupMessagesFactory();
+        setupAuditor();
+
+        JWTValidator jwtValidator = 
EasyMock.createNiceMock(JWTValidator.class);
+        EasyMock.expect(jwtValidator.getUsername()).andReturn(null).anyTimes();
+        EasyMock.replay(jwtValidator);
+
+        ExecutorService pool = Executors.newFixedThreadPool(10);
+        AtomicInteger concurrentWebshells = new AtomicInteger(0);
+        new WebshellWebSocketAdapter(pool, gatewayConfig, jwtValidator, 
concurrentWebshells);
+        verifyAll();
+    }
+
+
+    @Test
+    public void testOnWebSocketText() throws Exception{
+
+        GatewayConfig gatewayConfig = setupGatewayConfig();
+        setupMessagesFactory();
+        setupAuditor();
+
+        JWTValidator jwtValidator = 
EasyMock.createNiceMock(JWTValidator.class);
+        
EasyMock.expect(jwtValidator.getUsername()).andReturn("Alice").anyTimes();
+        
EasyMock.expect(jwtValidator.tokenIsStillValid()).andReturn(true).anyTimes();
+        EasyMock.replay(jwtValidator);
+
+        ConnectionInfo connectionInfo = 
PowerMock.createMock(ConnectionInfo.class);
+        PowerMock.expectNew(ConnectionInfo.class, isA(String.class) , 
isA(String.class), isA(AtomicInteger.class)).andReturn(connectionInfo);
+        connectionInfo.connect();
+
+        OutputStream outputStream = 
EasyMock.createNiceMock(OutputStream.class);
+        
EasyMock.expect(connectionInfo.getOutputStream()).andReturn(outputStream).times(2);
+        outputStream.write("fake user input".getBytes(StandardCharsets.UTF_8));
+        outputStream.flush();
+        PowerMock.replay(connectionInfo, ConnectionInfo.class);
+        EasyMock.replay(outputStream);
+
+        ExecutorService pool = Executors.newFixedThreadPool(10);
+        AtomicInteger concurrentWebshells = new AtomicInteger(0);
+        WebshellWebSocketAdapter webshellWebSocketAdapter = new 
WebshellWebSocketAdapter(pool, gatewayConfig, jwtValidator, 
concurrentWebshells);
+        webshellWebSocketAdapter.onWebSocketText("{ \"userInput\" : \"fake 
user input\"}");
+        verifyAll();
+    }
+
+    @Test
+    public void testOnWebSocketTextWithExpiredToken() throws Exception {
+        GatewayConfig gatewayConfig = setupGatewayConfig();
+        setupMessagesFactory();
+        setupAuditor();
+
+        JWTValidator jwtValidator = 
EasyMock.createNiceMock(JWTValidator.class);
+        
EasyMock.expect(jwtValidator.getUsername()).andReturn("Alice").anyTimes();
+        
EasyMock.expect(jwtValidator.tokenIsStillValid()).andReturn(false).anyTimes();
+        EasyMock.replay(jwtValidator);
+
+        ConnectionInfo connectionInfo = 
PowerMock.createMock(ConnectionInfo.class);
+        PowerMock.expectNew(ConnectionInfo.class, isA(String.class) , 
isA(String.class), isA(AtomicInteger.class)).andReturn(connectionInfo);
+        connectionInfo.disconnect();
+        PowerMock.replay(connectionInfo, ConnectionInfo.class);
+
+        ExecutorService pool = Executors.newFixedThreadPool(10);
+        AtomicInteger concurrentWebshells = new AtomicInteger(0);
+        WebshellWebSocketAdapter webshellWebSocketAdapter = new 
WebshellWebSocketAdapter(pool, gatewayConfig, jwtValidator, 
concurrentWebshells);
+        webshellWebSocketAdapter.onWebSocketText("{ \"userInput\" : \"fake 
user input\"}");
+        verifyAll();
+    }
+

Review Comment:
   nit: remove 2 empty lines



##########
knox-homepage-ui/home/app/generalProxyInformation/general.proxy.information.component.ts:
##########
@@ -108,6 +116,7 @@ export class GeneralProxyInformationComponent implements 
OnInit {
         this['showAdminAPI'] = (profile['gpi_admin_api'] === 'true');
         this['showMetadataAPI'] = (profile['gpi_md_api'] === 'true');
         this['showTokens'] = (profile['gpi_tokens'] === 'true');
+        this['showWebShell'] = (profile['gpi_webshell'] === 'true');

Review Comment:
   It will not work since you are missing the relevant profile declaration from 
`org.apache.knox.gateway.dto.HomePageProfile`



##########
pom.xml:
##########
@@ -2473,11 +2524,17 @@
 
             <dependency>
                 <groupId>org.apache.velocity</groupId>
-                <artifactId>velocity-engine-core</artifactId>
+                <artifactId>velocity</artifactId>

Review Comment:
   We need to remove `velocity`; see KNOX-2741 for more information.



##########
gateway-server/src/main/java/org/apache/knox/gateway/webshell/ConnectionInfo.java:
##########
@@ -0,0 +1,157 @@
+/*
+ * 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.knox.gateway.webshell;
+
+import com.pty4j.PtyProcess;
+import com.pty4j.PtyProcessBuilder;
+import de.thetaphi.forbiddenapis.SuppressForbidden;
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.audit.api.Action;
+import org.apache.knox.gateway.audit.api.ActionOutcome;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+* data structure to store a connection session
+*/
+public class ConnectionInfo {
+    private static final Auditor auditor = 
AuditServiceFactory.getAuditService().getAuditor(
+            AuditConstants.DEFAULT_AUDITOR_NAME, 
AuditConstants.KNOX_SERVICE_NAME,
+            AuditConstants.KNOX_COMPONENT_NAME );
+    private static final WebsocketLogMessages LOG = 
MessagesFactory.get(WebsocketLogMessages.class);
+
+    private InputStream inputStream;
+    private OutputStream outputStream;
+    private PtyProcess ptyProcess;
+    private final String username;
+
+    private final String gatewayPIDDir;
+    @SuppressWarnings("PMD.DoNotUseThreads") //we need to define a Thread to 
clean up resources using shutdown hook
+    private final Thread shutdownHook;
+    private final AtomicInteger concurrentWebshells;
+    private long pid;
+    /* set up webshell environment variables */
+    private final Map<String, String> env = new HashMap();

Review Comment:
   Instead of adding a comment what this class member is called we may have a 
better name. E.g. `webshellEnvVariables`.



##########
gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java:
##########
@@ -910,6 +930,28 @@ public boolean isWebsocketEnabled() {
     return Boolean.parseBoolean(result);
   }
 
+  @Override
+  public boolean isWebShellEnabled() {
+    final String result = get( WEBSHELL_FEATURE_ENABLED, 
Boolean.toString(DEFAULT_WEBSHELL_FEATURE_ENABLED));
+    return Boolean.parseBoolean(result);
+  }
+
+  @Override
+  public boolean isWebShellAuditLoggingEnabled(){
+    final String result = get( WEBSHELL_AUDIT_LOGGING_ENABLED, 
Boolean.toString(DEFAULT_WEBSHELL_AUDIT_LOGGING_ENABLED));

Review Comment:
   There is a `getBoolean(String name, boolean defaultValue)` that would make 
the code less complex.



##########
gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java:
##########
@@ -260,7 +260,13 @@ private static void addMissingDefaultProviders(Topology 
topology) {
     // for some tests the number of providers are limited to the classpath of 
the module
     // and exceptions will be thrown as topologies are deployed even though 
they will
     // work fine at actual server runtime.
-    if (PROVIDER_CONTRIBUTOR_MAP.get("identity-assertion") != null) {
+
+    // SRM: Only add "Default" provider iff it is found in the provider 
contributor map
+    // There could be cases where service loader might find other
+    // identity-assertion providers e.g. JWTAuthCodeAsserter
+    if (PROVIDER_CONTRIBUTOR_MAP.get("identity-assertion") != null &&
+            PROVIDER_CONTRIBUTOR_MAP.get("identity-assertion").keySet() != 
null &&
+            
PROVIDER_CONTRIBUTOR_MAP.get("identity-assertion").keySet().contains("Default"))
 {

Review Comment:
   That NPE could be the result of a missing service loader declaration from 
one of the resources. I checked that 
`org.apache.knox.gateway.provider.federation.jwt.deploy.JWTAuthCodeAssertionContributor`
 is added into the appropriate resource file.
   I'd handle it in a separate JIRA to not to mix this with WebShell.



##########
gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java:
##########
@@ -910,6 +930,28 @@ public boolean isWebsocketEnabled() {
     return Boolean.parseBoolean(result);
   }
 
+  @Override
+  public boolean isWebShellEnabled() {
+    final String result = get( WEBSHELL_FEATURE_ENABLED, 
Boolean.toString(DEFAULT_WEBSHELL_FEATURE_ENABLED));

Review Comment:
   There is a `getBoolean(String name, boolean defaultValue)` that would make 
the code less complex.



##########
gateway-server/src/test/java/org/apache/knox/gateway/webshell/ConnectionInfoTest.java:
##########
@@ -0,0 +1,149 @@
+/*
+ * 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.knox.gateway.webshell;
+
+import com.pty4j.PtyProcess;
+import com.pty4j.PtyProcessBuilder;
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.audit.api.AuditService;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.easymock.EasyMock.anyInt;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
+import static org.easymock.EasyMock.isA;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Runtime.class, MessagesFactory.class, 
AuditServiceFactory.class, ConnectionInfo.class})
+public class ConnectionInfoTest extends EasyMockSupport {
+    private static String testPIDdir = "webshell-test";
+    private static String testUserName = "Alice";
+    private static long testPID = 12345;
+
+    private static void setupMessagesFactory(){
+        PowerMock.mockStatic(MessagesFactory.class);
+        
EasyMock.expect(MessagesFactory.get(WebsocketLogMessages.class)).andReturn(EasyMock.createNiceMock(WebsocketLogMessages.class)).anyTimes();
+        PowerMock.replay(MessagesFactory.class);
+    }
+    private static void setupAuditService(){
+        PowerMock.mockStatic(AuditServiceFactory.class);
+        AuditService auditService = 
EasyMock.createNiceMock(AuditService.class);
+        
EasyMock.expect(AuditServiceFactory.getAuditService()).andReturn(auditService).anyTimes();
+        Auditor auditor = EasyMock.createNiceMock(Auditor.class);
+        
EasyMock.expect(auditService.getAuditor(isA(String.class),isA(String.class),isA(String.class))).andReturn(auditor).anyTimes();
+        PowerMock.replay(AuditServiceFactory.class);
+        EasyMock.replay(auditService, auditor);
+    }
+
+
+    @SuppressWarnings("PMD.DoNotUseThreads")
+    private static void setupRuntime(){
+        PowerMock.mockStatic(Runtime.class);
+        Runtime runtime = EasyMock.createMock(Runtime.class);
+        runtime.addShutdownHook(isA(Thread.class));
+        
EasyMock.expect(runtime.removeShutdownHook(isA(Thread.class))).andReturn(true).anyTimes();
+        EasyMock.replay(runtime);
+        EasyMock.expect(Runtime.getRuntime()).andReturn(runtime).anyTimes();
+        PowerMock.replay(Runtime.class);
+    }
+
+    @BeforeClass
+    public static void setupBeforeClass(){
+        setupMessagesFactory();
+        setupAuditService();
+        setupRuntime();
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() throws Exception{
+        //remove testPIDdir directory
+        FileUtils.deleteDirectory(new File(testPIDdir));
+
+    }
+
+    @Test
+    public void testInstantiation() {
+        new ConnectionInfo(testUserName, testPIDdir, new AtomicInteger(0));
+        verifyAll();
+    }
+
+    @Test
+    public void testConnectSuccess() throws Exception {
+        AtomicInteger concurrentWebshell = 
EasyMock.createNiceMock(AtomicInteger.class);
+        concurrentWebshell.incrementAndGet();
+        concurrentWebshell.decrementAndGet();
+        EasyMock.replay(concurrentWebshell);
+
+        PtyProcess ptyProcess = EasyMock.createNiceMock(PtyProcess.class);
+        EasyMock.expect(ptyProcess.pid()).andReturn(testPID);
+        EasyMock.replay(ptyProcess);
+
+        PtyProcessBuilder ptyProcessBuilder = 
PowerMock.createNiceMock(PtyProcessBuilder.class);
+        String[] cmd = { "sudo","--user", testUserName ,"bash","-i"};
+        
EasyMock.expect(ptyProcessBuilder.setCommand(cmd)).andReturn(ptyProcessBuilder);
+        
EasyMock.expect(ptyProcessBuilder.setRedirectErrorStream(true)).andReturn(ptyProcessBuilder);
+        
EasyMock.expect(ptyProcessBuilder.setWindowsAnsiColorEnabled(true)).andReturn(ptyProcessBuilder);
+        
EasyMock.expect(ptyProcessBuilder.setInitialColumns(anyInt())).andReturn(ptyProcessBuilder);
+        
EasyMock.expect(ptyProcessBuilder.setInitialRows(anyInt())).andReturn(ptyProcessBuilder);
+        
EasyMock.expect(ptyProcessBuilder.setDirectory(anyString())).andReturn(ptyProcessBuilder);
+        
EasyMock.expect(ptyProcessBuilder.setEnvironment(anyObject())).andReturn(ptyProcessBuilder);
+        EasyMock.expect(ptyProcessBuilder.start()).andReturn(ptyProcess);
+        
PowerMock.expectNew(PtyProcessBuilder.class).andReturn(ptyProcessBuilder);
+        PowerMock.replay(ptyProcessBuilder,PtyProcessBuilder.class);
+
+        ConnectionInfo connectionInfo = new ConnectionInfo(testUserName, 
testPIDdir, concurrentWebshell);
+        connectionInfo.connect();
+        connectionInfo.disconnect();
+        verifyAll();
+    }
+
+    @Rule

Review Comment:
   Rules should be defined above in the class (i.e. not between test methods).



##########
gateway-server/src/main/java/org/apache/knox/gateway/webshell/ConnectionInfo.java:
##########
@@ -0,0 +1,157 @@
+/*
+ * 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.knox.gateway.webshell;
+
+import com.pty4j.PtyProcess;
+import com.pty4j.PtyProcessBuilder;
+import de.thetaphi.forbiddenapis.SuppressForbidden;
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.audit.api.Action;
+import org.apache.knox.gateway.audit.api.ActionOutcome;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.eclipse.jetty.io.RuntimeIOException;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+* data structure to store a connection session
+*/
+public class ConnectionInfo {
+    private static final Auditor auditor = 
AuditServiceFactory.getAuditService().getAuditor(
+            AuditConstants.DEFAULT_AUDITOR_NAME, 
AuditConstants.KNOX_SERVICE_NAME,
+            AuditConstants.KNOX_COMPONENT_NAME );
+    private static final WebsocketLogMessages LOG = 
MessagesFactory.get(WebsocketLogMessages.class);
+
+    private InputStream inputStream;
+    private OutputStream outputStream;
+    private PtyProcess ptyProcess;
+    private final String username;
+
+    private final String gatewayPIDDir;
+    @SuppressWarnings("PMD.DoNotUseThreads") //we need to define a Thread to 
clean up resources using shutdown hook
+    private final Thread shutdownHook;
+    private final AtomicInteger concurrentWebshells;
+    private long pid;
+    /* set up webshell environment variables */
+    private final Map<String, String> env = new HashMap();
+
+    @SuppressWarnings("PMD.DoNotUseThreads") //we need to define a Thread to 
clean up resources using shutdown hook
+    public ConnectionInfo(String username, String gatewayPIDDir, AtomicInteger 
concurrentWebshells) {
+        this.username = username;
+        this.gatewayPIDDir = gatewayPIDDir;
+        this.concurrentWebshells = concurrentWebshells;
+        shutdownHook = new Thread(this::disconnect);
+        Runtime.getRuntime().addShutdownHook(shutdownHook);
+        env.put("TERM", "xterm");

Review Comment:
   `TERM` and `xterm` texts could be constants.



##########
gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.knox.gateway.webshell;
+
+import org.apache.knox.gateway.audit.api.AuditService;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.websockets.JWTValidator;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.easymock.EasyMock.isA;

Review Comment:
   nit: static imports should go first



##########
gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java:
##########
@@ -0,0 +1,239 @@
+/*
+ * 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.knox.gateway.webshell;
+
+import org.apache.knox.gateway.audit.api.AuditService;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.websockets.JWTValidator;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.easymock.EasyMock.isA;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({WebshellWebSocketAdapter.class, MessagesFactory.class, 
AuditServiceFactory.class})
+public class WebshellWebsocketAdapterTest extends EasyMockSupport {
+
+    private GatewayConfig setupGatewayConfig(){
+        GatewayConfig gatewayConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
EasyMock.expect(gatewayConfig.getGatewayPIDDir()).andReturn("testPIDDir").anyTimes();
+        
EasyMock.expect(gatewayConfig.getWebShellReadBufferSize()).andReturn(1024).anyTimes();
+        
EasyMock.expect(gatewayConfig.isWebShellAuditLoggingEnabled()).andReturn(false).anyTimes();
+        EasyMock.replay(gatewayConfig);
+        return gatewayConfig;
+    }
+    private void setupMessagesFactory(){
+        PowerMock.mockStatic(MessagesFactory.class);
+        
EasyMock.expect(MessagesFactory.get(JWTMessages.class)).andReturn(EasyMock.createNiceMock(JWTMessages.class)).anyTimes();
+        
EasyMock.expect(MessagesFactory.get(WebsocketLogMessages.class)).andReturn(EasyMock.createNiceMock(WebsocketLogMessages.class)).anyTimes();
+        PowerMock.replay(MessagesFactory.class);
+    }
+    private void setupAuditor(){
+        PowerMock.mockStatic(AuditServiceFactory.class);
+        AuditService auditService = 
EasyMock.createNiceMock(AuditService.class);
+        
EasyMock.expect(AuditServiceFactory.getAuditService()).andReturn(auditService).anyTimes();
+        Auditor auditor = EasyMock.createNiceMock(Auditor.class);
+        
EasyMock.expect(auditService.getAuditor(isA(String.class),isA(String.class),isA(String.class))).andReturn(auditor).anyTimes();
+        PowerMock.replay(AuditServiceFactory.class);
+        EasyMock.replay(auditService,auditor);
+    }
+
+    @Test
+    public void testOnWebSocketConnect() throws Exception{
+
+        GatewayConfig gatewayConfig = setupGatewayConfig();
+        setupMessagesFactory();
+        setupAuditor();
+
+        JWTValidator jwtValidator = 
EasyMock.createNiceMock(JWTValidator.class);
+        
EasyMock.expect(jwtValidator.getUsername()).andReturn("Alice").anyTimes();
+        EasyMock.replay(jwtValidator);
+
+        ConnectionInfo connectionInfo = 
PowerMock.createMock(ConnectionInfo.class);
+        PowerMock.expectNew(ConnectionInfo.class, isA(String.class) , 
isA(String.class), isA(AtomicInteger.class)).andReturn(connectionInfo);
+        connectionInfo.connect();
+
+        String bashOuput = "fake bash output";
+        InputStream inputStream = new 
ByteArrayInputStream(bashOuput.getBytes(StandardCharsets.UTF_8));
+        
EasyMock.expect(connectionInfo.getInputStream()).andReturn(inputStream).times(1);
+        InputStream inputStreamEmpty = new 
ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8));
+        
EasyMock.expect(connectionInfo.getInputStream()).andReturn(inputStreamEmpty).times(1);
+
+        Session session = EasyMock.createNiceMock(Session.class);
+        RemoteEndpoint remote = EasyMock.createNiceMock(RemoteEndpoint.class);
+        // send to client
+        EasyMock.expect(session.getRemote()).andReturn(remote).anyTimes();
+        remote.sendString(bashOuput);
+        // clean up
+        EasyMock.expect(session.isOpen()).andReturn(true).anyTimes();
+        session.close();
+        EasyMock.replay(session, remote);
+
+        connectionInfo.disconnect();
+        PowerMock.replay(connectionInfo, ConnectionInfo.class);
+
+        ExecutorService pool = Executors.newFixedThreadPool(10);
+        AtomicInteger concurrentWebshells = new AtomicInteger(0);
+        WebshellWebSocketAdapter webshellWebSocketAdapter = new 
WebshellWebSocketAdapter(pool, gatewayConfig, jwtValidator, 
concurrentWebshells);
+        webshellWebSocketAdapter.onWebSocketConnect(session);
+        verifyAll();
+    }
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();

Review Comment:
   Rules should be defined above in the class (i.e. not between test methods).



##########
knox-webshell-ui/webshell-ui/assets/.gitkeep:
##########
@@ -0,0 +1,17 @@
+##########################################################################

Review Comment:
   What's the purpose of this file/folder? If there are no assets now, we 
should not add the folder and remove all references.



##########
gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidatorFactory.java:
##########
@@ -0,0 +1,131 @@
+/*
+ * 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.knox.gateway.websockets;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import 
org.apache.knox.gateway.provider.federation.jwt.filter.SignatureVerificationCache;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.Service;
+import org.apache.knox.gateway.topology.Topology;
+import org.apache.knox.gateway.util.CertificateUtils;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import javax.servlet.ServletException;
+import java.net.HttpCookie;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidatorFactory {

Review Comment:
   See my comment above on this topic. I'm with Larry here, we should not 
duplicate code at this scale.



##########
gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java:
##########
@@ -0,0 +1,193 @@
+/*
+ * 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.knox.gateway.websockets;
+
+import com.nimbusds.jose.JWSHeader;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import 
org.apache.knox.gateway.provider.federation.jwt.filter.SignatureVerificationCache;
+import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.util.Tokens;
+
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.Date;
+
+public class JWTValidator {

Review Comment:
   I believe there has to be a more elegant way that copy-pasting validation 
code from an already existing class. I got that you tried to avoid refactoring, 
but - IMO - this refactoring should not be saved here.





Issue Time Tracking
-------------------

    Worklog Id:     (was: 815627)
    Time Spent: 7h 10m  (was: 7h)

> KnoxSSO for Secure Shell Access
> -------------------------------
>
>                 Key: KNOX-2631
>                 URL: https://issues.apache.org/jira/browse/KNOX-2631
>             Project: Apache Knox
>          Issue Type: New Feature
>          Components: Homepage, KnoxSSO
>    Affects Versions: 1.5.0
>            Reporter: Lu Liu
>            Assignee: Lu Liu
>            Priority: Minor
>             Fix For: 2.0.0
>
>          Time Spent: 7h 10m
>  Remaining Estimate: 0h
>
> While Apache Knox aids in helping to lessen the need to gain physical access
> to deployment machines, there are still numerous compelling reasons for users
> to require such access:
> 1. Debugging, log access, etc
> 2. CLI use and automation
> 3. beeline and other clients that are deployed to gateway machines
> Gaining access to a shell for these purposes currently requires the user to 
> have
> an ssh client installed and valid credentials that can be used for ssh, such 
> as:
> username and password or SSH keys. Separate management of credentials for this
> access introduces some additional complexities which may even violate 
> enterprise
> infosec policies and require the secure distribution and management of keys.
> The intent of this proposed improvement is to add a browser based terminal 
> application
> that will provide secure access to a shell on the Knox machine. Just as any 
> resource
> exposed by or hosted by Knox, you would be able to protect access to this 
> terminal
> with any of the available security providers. We would also like to make this
> available out of the box as available from the Knox Homepage. This would make 
> the
> terminal/shell available via KnoxSSO thus providing shell access with your 
> existing
> enterprise credential authenticated SSO session.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to