You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@knox.apache.org by GitBox <gi...@apache.org> on 2021/12/27 23:17:33 UTC

[GitHub] [knox] moresandeep commented on a change in pull request #477: [WIP] KNOX-2631

moresandeep commented on a change in pull request #477:
URL: https://github.com/apache/knox/pull/477#discussion_r775231179



##########
File path: gateway-release/home/conf/gateway-site.xml
##########
@@ -88,6 +88,30 @@ limitations under the License.
         <value>60</value>
         <description>The interval (in seconds) for polling Ambari for cluster configuration changes.</description>
     </property>
+    <!-- @since 2.0.0 WebShell configs -->
+    <!-- must have websocket enabled to use webshell --> 
+    <property>
+        <name>gateway.webshell.feature.enabled</name>
+        <value>false</value>
+        <description>Enable/Disable webshell feature.</description>
+    </property>
+    <property>
+        <name>webshell.max.concurrent.sessions</name>
+        <value>3</value>
+        <description>maximum number of concurrent webshell sessions</description>
+    </property>
+    <property>
+        <name>gateway.webshell.logging.enabled</name>
+        <value>false</value>

Review comment:
       This is for audit logging right? in which case we should be specific, something like `gateway.webshell.audit.logging.enabled`

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.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.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.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.apache.knox.gateway.util.Tokens;
+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.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidator {
+    private static final String KNOXSSO_COOKIE_NAME = "knoxsso.cookie.name";
+    private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+    private static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
+    private static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
+    private static final String JWT_DEFAULT_SIGALG = "RS256";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final JWTMessages log = MessagesFactory.get(JWTMessages.class);
+    private String cookieName;
+    private String expectedIssuer;
+    private String expectedSigAlg;
+    private RSAPublicKey publicKey;
+    private String providerParamValue;
+
+    private TokenStateService tokenStateService;
+    private JWTokenAuthority authorityService;
+    private SignatureVerificationCache signatureVerificationCache;
+    private JWT token;
+    private String displayableTokenId;
+    private String displayableToken;
+    private final GatewayConfig gatewayConfig;
+    private final GatewayServices gatewayServices;
+    private static final WebsocketLogMessages websocketLog = MessagesFactory
+            .get(WebsocketLogMessages.class);
+
+    JWTValidator(ServletUpgradeRequest req, GatewayServices gatewayServices,
+                 GatewayConfig gatewayConfig){
+        this.gatewayConfig = gatewayConfig;
+        this.gatewayServices = gatewayServices;
+        configureParameters();
+        extractToken(req);
+    }
+
+    private Map<String,String> getParams(){
+        Map<String,String> params = new LinkedHashMap<>();
+        TopologyService ts = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        for (Topology topology : ts.getTopologies()) {
+            if (topology.getName().equals("knoxsso")) {
+                for (Service service : topology.getServices()) {
+                    if (service.getRole().equals("KNOXSSO")) {
+                        params = service.getParams();
+                    }
+                    break;
+                }
+                break;
+            }
+        }
+        return params;
+    }
+    private void configureParameters() {
+        Map<String,String> params = getParams();
+        // token verification pem
+        String verificationPEM = params.get(SSO_VERIFICATION_PEM);
+        // setup the public key of the token issuer for verification
+        if (verificationPEM != null) {
+            try {
+                publicKey = CertificateUtils.parseRSAPublicKey(verificationPEM);
+            } catch (ServletException e){
+                throw new RuntimeException("Failed to obtain public key: "+e);
+            }
+        }
+
+        //provider-level configuration
+        providerParamValue = params.get(TokenStateService.CONFIG_SERVER_MANAGED);
+
+        cookieName = params.get(KNOXSSO_COOKIE_NAME);
+        if (cookieName == null) {
+            cookieName = DEFAULT_SSO_COOKIE_NAME;
+        }
+        expectedIssuer =  params.get(JWT_EXPECTED_ISSUER);
+        if (expectedIssuer == null) {
+            expectedIssuer = JWT_DEFAULT_ISSUER;
+        }
+        expectedSigAlg =  params.get(JWT_EXPECTED_SIGALG);
+        if (expectedSigAlg == null) {
+            expectedSigAlg = JWT_DEFAULT_SIGALG;
+        }
+        authorityService = gatewayServices.getService(ServiceType.TOKEN_SERVICE);
+        if (isServerManagedTokenStateEnabled()) {
+            tokenStateService = gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE);
+        }
+        // Setup the verified tokens cache
+        signatureVerificationCache = SignatureVerificationCache.getInstance(
+                "knoxsso", new WebSocketFilterConfig(params));
+    }
+
+    private void extractToken(ServletUpgradeRequest req){
+        List<HttpCookie> ssoCookies = req.getCookies();
+        if (ssoCookies != null){
+            for (HttpCookie ssoCookie : ssoCookies) {
+                if (cookieName.equals(ssoCookie.getName())) {
+                    try {
+                        token = new JWTToken(ssoCookie.getValue());
+                        displayableTokenId = Tokens.getTokenIDDisplayText(TokenUtils.getTokenId(token));
+                        displayableToken = Tokens.getTokenDisplayText(token.toString());
+                        websocketLog.debugLog("found token:"+displayableToken+" id:"+displayableTokenId);
+                        return;
+                    } catch (ParseException e) {
+                        // Fall through to keep checking if there are more cookies

Review comment:
       Need a log statement here to indicate what failed.

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

Review comment:
       Does this file needs to be checked in SVN?

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.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.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.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.apache.knox.gateway.util.Tokens;
+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.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidator {
+    private static final String KNOXSSO_COOKIE_NAME = "knoxsso.cookie.name";
+    private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+    private static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
+    private static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
+    private static final String JWT_DEFAULT_SIGALG = "RS256";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final JWTMessages log = MessagesFactory.get(JWTMessages.class);
+    private String cookieName;
+    private String expectedIssuer;
+    private String expectedSigAlg;
+    private RSAPublicKey publicKey;
+    private String providerParamValue;
+
+    private TokenStateService tokenStateService;
+    private JWTokenAuthority authorityService;
+    private SignatureVerificationCache signatureVerificationCache;
+    private JWT token;
+    private String displayableTokenId;
+    private String displayableToken;
+    private final GatewayConfig gatewayConfig;
+    private final GatewayServices gatewayServices;
+    private static final WebsocketLogMessages websocketLog = MessagesFactory
+            .get(WebsocketLogMessages.class);
+
+    JWTValidator(ServletUpgradeRequest req, GatewayServices gatewayServices,
+                 GatewayConfig gatewayConfig){
+        this.gatewayConfig = gatewayConfig;
+        this.gatewayServices = gatewayServices;
+        configureParameters();
+        extractToken(req);
+    }
+
+    private Map<String,String> getParams(){
+        Map<String,String> params = new LinkedHashMap<>();
+        TopologyService ts = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        for (Topology topology : ts.getTopologies()) {
+            if (topology.getName().equals("knoxsso")) {
+                for (Service service : topology.getServices()) {
+                    if (service.getRole().equals("KNOXSSO")) {
+                        params = service.getParams();
+                    }
+                    break;

Review comment:
       Why not just `return params` here? 

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/webshell/ConnectionInfo.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 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.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.eclipse.jetty.io.RuntimeIOException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+* data structure to store a connection session
+*/
+public class ConnectionInfo {
+
+    private InputStream inputStream;
+    private OutputStream outputStream;
+    private PtyProcess ptyProcess;
+    private final String username;
+    private final Auditor auditor;
+    private final WebsocketLogMessages LOG;
+    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;
+
+    @SuppressWarnings("PMD.DoNotUseThreads") //we need to define a Thread to clean up resources using shutdown hook
+    public ConnectionInfo(String username, String gatewayPIDDir, AtomicInteger concurrentWebshells, Auditor auditor, WebsocketLogMessages LOG) {
+        this.username = username;
+        this.auditor = auditor;
+        this.LOG = LOG;
+        this.gatewayPIDDir = gatewayPIDDir;
+        this.concurrentWebshells = concurrentWebshells;
+        shutdownHook = new Thread(() -> {
+            LOG.debugLog("running webshell shutdown hook");
+            disconnect();
+        });
+        Runtime.getRuntime().addShutdownHook(shutdownHook);
+    }
+
+    private void saveProcessPID(long pid){
+        File file = new File(gatewayPIDDir + "/" + "webshell_" + pid + ".pid");
+        try {
+            FileUtils.writeStringToFile(file, String.valueOf(pid), StandardCharsets.UTF_8);
+        } catch (IOException e){
+            LOG.onError("error saving PID for webshell:" + e);
+        }
+        auditor.audit( Action.WEBSHELL, username+':'+pid,
+                ResourceType.PROCESS, ActionOutcome.SUCCESS,"Started Bash process");
+    }
+
+    @SuppressForbidden // we need to spawn a bash process for authenticated user
+    @SuppressWarnings("PMD.DoNotUseThreads") // we need to define a Thread to register a shutdown hook
+    public void connect(){
+        // sudoers file needs to be configured for this to work.
+        // refer to design doc for details
+        String[] cmd = { "sudo","--user", username,"bash"};
+        // todo: make environment configurable through gateway-site.xml
+        // if do not set environment variable, env = System.getenv() is used by default
+        // Map<String,String> env = System.getenv();
+        // env.put("TEST_ENV","test_env");
+        // env.forEach((key, value) -> LOG.debugLog(key + ":" + value));
+        try {
+            ptyProcess = new PtyProcessBuilder()
+                    .setCommand(cmd)
+                    //.setEnvironment(env)
+                    .setRedirectErrorStream(true)
+                    .setWindowsAnsiColorEnabled(true)
+                    .setInitialColumns(150)
+                    .setInitialRows(50)
+                    .start();
+        } catch (IOException e) {
+            LOG.onError("Error starting ptyProcess: " + e.getMessage());
+            disconnect();
+            throw new RuntimeIOException(e);
+        }
+        outputStream = ptyProcess.getOutputStream();
+        inputStream = ptyProcess.getInputStream();
+        pid = ptyProcess.pid();
+        saveProcessPID(pid);
+        concurrentWebshells.incrementAndGet();
+        LOG.debugLog("incremented concurrent webshells:"+concurrentWebshells);
+    }
+
+    public String getUsername(){
+        return this.username;
+    }
+    public long getPid(){ return this.pid; }

Review comment:
       Formatting is off here.

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.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.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.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.apache.knox.gateway.util.Tokens;
+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.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidator {
+    private static final String KNOXSSO_COOKIE_NAME = "knoxsso.cookie.name";
+    private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+    private static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
+    private static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
+    private static final String JWT_DEFAULT_SIGALG = "RS256";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final JWTMessages log = MessagesFactory.get(JWTMessages.class);
+    private String cookieName;
+    private String expectedIssuer;
+    private String expectedSigAlg;
+    private RSAPublicKey publicKey;
+    private String providerParamValue;
+
+    private TokenStateService tokenStateService;
+    private JWTokenAuthority authorityService;
+    private SignatureVerificationCache signatureVerificationCache;
+    private JWT token;
+    private String displayableTokenId;
+    private String displayableToken;
+    private final GatewayConfig gatewayConfig;
+    private final GatewayServices gatewayServices;
+    private static final WebsocketLogMessages websocketLog = MessagesFactory
+            .get(WebsocketLogMessages.class);
+
+    JWTValidator(ServletUpgradeRequest req, GatewayServices gatewayServices,
+                 GatewayConfig gatewayConfig){
+        this.gatewayConfig = gatewayConfig;
+        this.gatewayServices = gatewayServices;
+        configureParameters();
+        extractToken(req);
+    }
+
+    private Map<String,String> getParams(){
+        Map<String,String> params = new LinkedHashMap<>();
+        TopologyService ts = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        for (Topology topology : ts.getTopologies()) {
+            if (topology.getName().equals("knoxsso")) {
+                for (Service service : topology.getServices()) {
+                    if (service.getRole().equals("KNOXSSO")) {
+                        params = service.getParams();
+                    }
+                    break;
+                }
+                break;
+            }
+        }
+        return params;
+    }
+    private void configureParameters() {
+        Map<String,String> params = getParams();
+        // token verification pem
+        String verificationPEM = params.get(SSO_VERIFICATION_PEM);
+        // setup the public key of the token issuer for verification
+        if (verificationPEM != null) {
+            try {
+                publicKey = CertificateUtils.parseRSAPublicKey(verificationPEM);
+            } catch (ServletException e){
+                throw new RuntimeException("Failed to obtain public key: "+e);
+            }
+        }
+
+        //provider-level configuration
+        providerParamValue = params.get(TokenStateService.CONFIG_SERVER_MANAGED);
+
+        cookieName = params.get(KNOXSSO_COOKIE_NAME);
+        if (cookieName == null) {
+            cookieName = DEFAULT_SSO_COOKIE_NAME;
+        }
+        expectedIssuer =  params.get(JWT_EXPECTED_ISSUER);
+        if (expectedIssuer == null) {
+            expectedIssuer = JWT_DEFAULT_ISSUER;
+        }
+        expectedSigAlg =  params.get(JWT_EXPECTED_SIGALG);
+        if (expectedSigAlg == null) {
+            expectedSigAlg = JWT_DEFAULT_SIGALG;
+        }
+        authorityService = gatewayServices.getService(ServiceType.TOKEN_SERVICE);
+        if (isServerManagedTokenStateEnabled()) {
+            tokenStateService = gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE);
+        }
+        // Setup the verified tokens cache
+        signatureVerificationCache = SignatureVerificationCache.getInstance(
+                "knoxsso", new WebSocketFilterConfig(params));
+    }
+
+    private void extractToken(ServletUpgradeRequest req){
+        List<HttpCookie> ssoCookies = req.getCookies();
+        if (ssoCookies != null){
+            for (HttpCookie ssoCookie : ssoCookies) {
+                if (cookieName.equals(ssoCookie.getName())) {
+                    try {
+                        token = new JWTToken(ssoCookie.getValue());
+                        displayableTokenId = Tokens.getTokenIDDisplayText(TokenUtils.getTokenId(token));
+                        displayableToken = Tokens.getTokenDisplayText(token.toString());
+                        websocketLog.debugLog("found token:"+displayableToken+" id:"+displayableTokenId);
+                        return;
+                    } catch (ParseException e) {
+                        // Fall through to keep checking if there are more cookies
+                    }
+                }
+            }
+        }
+        log.missingBearerToken();
+        throw new RuntimeException("no Valid JWT found");
+    }
+
+    public JWT getToken(){
+        return token;
+    }
+
+    public String getUsername(){
+        return token.getSubject();
+    }
+
+    public boolean validate() {
+        // confirm that issuer matches the intended target
+        if (expectedIssuer.equals(token.getIssuer())) {
+            // if there is no expiration data then the lifecycle is tied entirely to
+            // the cookie validity - otherwise ensure that the current time is before
+            // the designated expiration time
+            try {
+                if (tokenIsStillValid()) {
+                    Date nbf = token.getNotBeforeDate();
+                    if (nbf == null || new Date().after(nbf)) {

Review comment:
       Why are we letting tokens with `null` nbf through? do we do this elsewhere?

##########
File path: gateway-server/src/test/java/org/apache/knox/gateway/websockets/JWTValidatorTest.java
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
+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.JWTokenAuthority;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+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.X509CertificateUtil;
+import org.easymock.EasyMock;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.net.HttpCookie;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+public class JWTValidatorTest {
+    private static final String dnTemplate = "CN={0},OU=Test,O=Hadoop,L=Test,ST=Test,C=US";
+    private static final String PASSCODE_CLAIM = "passcode";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+
+    private static SignedJWT jwt;
+    private static Map<String, String> params = new HashMap<>();
+    private static RSAPublicKey publicKey;
+    private static RSAPrivateKey privateKey;
+    private static String pem;
+    private static GatewayConfig gatewayConfig;
+    private static GatewayServices gatewayServices;
+    private static JWTokenAuthority authorityService;
+    private static TokenStateService tokenStateService;
+
+    private JWTValidator jwtValidator;
+
+    private static String buildDistinguishedName(String hostname) {
+        final String cn = Character.isAlphabetic(hostname.charAt(0)) ? hostname : "localhost";
+        String[] paramArray = new String[1];
+        paramArray[0] = cn;
+        return new MessageFormat(dnTemplate, Locale.ROOT).format(paramArray);
+    }
+
+    private static void setupJWT() throws Exception{
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+        kpg.initialize(2048);
+        KeyPair KPair = kpg.generateKeyPair();
+        String dn = buildDistinguishedName(InetAddress.getLocalHost().getHostName());
+        Certificate cert = X509CertificateUtil.generateCertificate(dn, KPair, 365, "SHA1withRSA");
+        byte[] data = cert.getEncoded();
+        Base64 encoder = new Base64( 76, "\n".getBytes( StandardCharsets.US_ASCII ) );
+        pem = new String(encoder.encodeToString( data ).getBytes( StandardCharsets.US_ASCII ), StandardCharsets.US_ASCII).trim();
+
+        publicKey = (RSAPublicKey) KPair.getPublic();
+        privateKey = (RSAPrivateKey) KPair.getPrivate();
+
+        jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER,
+                "alice",
+                new Date(new Date().getTime() + TimeUnit.MINUTES.toMillis(10)),
+                new Date(),
+                privateKey,
+                JWSAlgorithm.RS512.getName());
+
+        params.put(SSO_VERIFICATION_PEM, pem);
+        params.put(JWT_EXPECTED_SIGALG, jwt.getHeader().getAlgorithm().getName());
+
+    }
+
+    private static void setupGatewayConfig() {
+        gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+        EasyMock.expect(gatewayConfig.isServerManagedTokenStateEnabled())
+                .andReturn(false).anyTimes();
+
+        gatewayServices = EasyMock.createNiceMock(GatewayServices.class);
+        TopologyService ts = EasyMock.createNiceMock(TopologyService.class);
+        Topology topology = EasyMock.createNiceMock(Topology.class);
+        Service service = EasyMock.createNiceMock(Service.class);
+
+        EasyMock.expect(gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE))
+                .andReturn(ts).anyTimes();
+        EasyMock.expect(ts.getTopologies())
+                .andReturn(Arrays.asList(topology)).anyTimes();
+        EasyMock.expect(topology.getName())
+                .andReturn("knoxsso").anyTimes();
+        EasyMock.expect(topology.getServices())
+                .andReturn(Arrays.asList(service)).anyTimes();
+        EasyMock.expect(service.getRole())
+                .andReturn("KNOXSSO").anyTimes();
+        EasyMock.expect(service.getParams())
+                .andReturn(params).anyTimes();
+
+        authorityService = EasyMock.createNiceMock(JWTokenAuthority.class);
+        EasyMock.expect(gatewayServices.getService(ServiceType.TOKEN_SERVICE))
+                .andReturn(authorityService).anyTimes();
+        tokenStateService = EasyMock.createNiceMock(TokenStateService.class);
+        EasyMock.expect(gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE))
+                .andReturn(tokenStateService).anyTimes();
+        EasyMock.replay(gatewayConfig, gatewayServices, ts, topology, service);
+
+    }
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        setupJWT();
+        setupGatewayConfig();
+    }
+
+    @After
+    public void tearDown() {
+        try {
+            Field f = jwtValidator.getClass().getDeclaredField("signatureVerificationCache");
+            f.setAccessible(true);
+            ((SignatureVerificationCache) f.get(jwtValidator)).clear();
+        } catch (Exception e) {
+            //
+        }
+    }
+
+    private void setTokenOnRequest(ServletUpgradeRequest request, SignedJWT jwt){
+        HttpCookie cookie1 = new HttpCookie("hadoop-jwt", "garbage");
+        HttpCookie cookie2 = new HttpCookie("hadoop-jwt", "ljm" + jwt.serialize());// garbled jwt
+        HttpCookie cookie3 = new HttpCookie("hadoop-jwt", jwt.serialize());
+        EasyMock.expect(request.getCookies()).andReturn(Arrays.asList(cookie1, cookie2, cookie3)).anyTimes();
+    }
+
+    private static SignedJWT getJWT(final String issuer,
+                       final String sub,
+                       final Date expires,
+                       final Date nbf,
+                       final RSAPrivateKey privateKey,
+                       final String signatureAlgorithm)
+            throws Exception {
+        JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
+        builder.issuer(issuer)
+                .subject(sub)
+                .expirationTime(expires)
+                .notBeforeTime(nbf)
+                .claim("scope", "openid")
+                .claim(PASSCODE_CLAIM, UUID.randomUUID().toString());
+        JWTClaimsSet claims = builder.build();
+
+        JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signatureAlgorithm)).build();
+
+        SignedJWT signedJWT = new SignedJWT(header, claims);
+        JWSSigner signer = new RSASSASigner(privateKey);
+
+        signedJWT.sign(signer);
+
+        return signedJWT;
+    }
+
+    @Test
+    public void testGetToken(){
+        ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+        setTokenOnRequest(request, jwt);
+        EasyMock.replay(request);
+        jwtValidator = new JWTValidator(request, gatewayServices, gatewayConfig);
+        Assert.assertEquals(jwt.serialize(), jwtValidator.getToken().toString());
+    }
+
+    @Test
+    public void testGetUsername(){
+        ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+        setTokenOnRequest(request, jwt);
+        EasyMock.replay(request);
+        jwtValidator = new JWTValidator(request, gatewayServices, gatewayConfig);
+        Assert.assertEquals("alice",jwtValidator.getUsername());
+    }
+
+    @Test
+    public void testValidToken() throws Exception{
+        ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+        setTokenOnRequest(request, jwt);
+        EasyMock.replay(request);
+        jwtValidator = new JWTValidator(request, gatewayServices, gatewayConfig);
+        EasyMock.expect(authorityService.verifyToken(jwtValidator.getToken(), publicKey)).andReturn(true).anyTimes();
+        EasyMock.replay(authorityService);
+        Assert.assertTrue(jwtValidator.validate());
+    }

Review comment:
       We need negative tests i.e. test where an invalid token is used.

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/webshell/WebshellWebSocketAdapter.java
##########
@@ -0,0 +1,176 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.CharMatcher;
+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.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.websockets.JWTValidator;
+import org.apache.knox.gateway.websockets.ProxyWebSocketAdapter;
+import org.eclipse.jetty.websocket.api.Session;
+
+public class WebshellWebSocketAdapter extends ProxyWebSocketAdapter  {
+    private Session session;
+    private final ConnectionInfo connectionInfo;
+    private final JWTValidator jwtValidator;
+    private final StringBuilder auditBuffer;
+    private final Auditor auditor;
+    private final ObjectMapper objectMapper;
+
+    public WebshellWebSocketAdapter(ExecutorService pool, GatewayConfig config, JWTValidator jwtValidator, AtomicInteger concurrentWebshells) {
+        super(null, pool, null, config);
+        this.jwtValidator = jwtValidator;
+        auditBuffer = new StringBuilder(); // buffer for audit log
+        auditor = AuditServiceFactory.getAuditService().getAuditor(
+                AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME,
+                AuditConstants.KNOX_COMPONENT_NAME );
+        connectionInfo = new ConnectionInfo(jwtValidator.getUsername(),config.getGatewayPIDDir(), concurrentWebshells, auditor, LOG);
+        objectMapper = new ObjectMapper();
+    }
+
+    @SuppressWarnings("PMD.DoNotUseThreads")
+    @Override
+    public void onWebSocketConnect(final Session session) {
+        this.session = session;
+        if (jwtValidator.getUsername() == null){
+            throw new RuntimeException("Needs user name in JWT to use WebShell");
+        }
+        connectionInfo.connect();
+        pool.execute(this::blockingReadFromHost);
+
+    }
+
+    private void blockingReadFromHost(){
+        byte[] buffer = new byte[1024];

Review comment:
       Let's make this buffer configurable. We have noticed that for websocket we often need to change the buffer size. 

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
##########
@@ -109,18 +116,51 @@ public void configure(final WebSocketServletFactory factory) {
 
   }
 
+  private Boolean isWebshellRequest(URI requestURI){
+    return requestURI.toString().matches(REGEX_WEBSHELL_REQUEST_PATH);
+  }
+
+  private WebshellWebSocketAdapter handleWebshellRequest(ServletUpgradeRequest req){
+      if (config.isWebShellEnabled()){
+        if (concurrentWebshells.get() >= config.getMaximumConcurrentWebshells()){
+          LOG.onError("number of allowed concurrent webshell sessions exceeded");

Review comment:
       Nit: Number instead of number.

##########
File path: gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
##########
@@ -521,6 +537,26 @@ public boolean isWebsocketEnabled() {
     return DEFAULT_WEBSOCKET_FEATURE_ENABLED;
   }
 
+  @Override
+  public boolean isWebShellEnabled() {
+    return DEFAULT_WEBSHELL_FEATURE_ENABLED;
+  }
+
+  @Override
+  public boolean isWebShellLoggingEnabled() { return DEFAULT_WEBSHELL_LOGGING_ENABLED; }

Review comment:
       Format, should be in the same format as other.

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.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.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.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.apache.knox.gateway.util.Tokens;
+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.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidator {
+    private static final String KNOXSSO_COOKIE_NAME = "knoxsso.cookie.name";
+    private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+    private static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
+    private static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
+    private static final String JWT_DEFAULT_SIGALG = "RS256";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final JWTMessages log = MessagesFactory.get(JWTMessages.class);
+    private String cookieName;
+    private String expectedIssuer;
+    private String expectedSigAlg;
+    private RSAPublicKey publicKey;
+    private String providerParamValue;
+
+    private TokenStateService tokenStateService;
+    private JWTokenAuthority authorityService;
+    private SignatureVerificationCache signatureVerificationCache;
+    private JWT token;
+    private String displayableTokenId;
+    private String displayableToken;
+    private final GatewayConfig gatewayConfig;
+    private final GatewayServices gatewayServices;
+    private static final WebsocketLogMessages websocketLog = MessagesFactory
+            .get(WebsocketLogMessages.class);
+
+    JWTValidator(ServletUpgradeRequest req, GatewayServices gatewayServices,
+                 GatewayConfig gatewayConfig){
+        this.gatewayConfig = gatewayConfig;
+        this.gatewayServices = gatewayServices;
+        configureParameters();
+        extractToken(req);
+    }
+
+    private Map<String,String> getParams(){
+        Map<String,String> params = new LinkedHashMap<>();
+        TopologyService ts = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        for (Topology topology : ts.getTopologies()) {
+            if (topology.getName().equals("knoxsso")) {
+                for (Service service : topology.getServices()) {
+                    if (service.getRole().equals("KNOXSSO")) {
+                        params = service.getParams();
+                    }
+                    break;
+                }
+                break;
+            }
+        }
+        return params;
+    }
+    private void configureParameters() {
+        Map<String,String> params = getParams();
+        // token verification pem
+        String verificationPEM = params.get(SSO_VERIFICATION_PEM);
+        // setup the public key of the token issuer for verification
+        if (verificationPEM != null) {
+            try {
+                publicKey = CertificateUtils.parseRSAPublicKey(verificationPEM);
+            } catch (ServletException e){
+                throw new RuntimeException("Failed to obtain public key: "+e);
+            }
+        }
+
+        //provider-level configuration
+        providerParamValue = params.get(TokenStateService.CONFIG_SERVER_MANAGED);
+
+        cookieName = params.get(KNOXSSO_COOKIE_NAME);
+        if (cookieName == null) {
+            cookieName = DEFAULT_SSO_COOKIE_NAME;
+        }
+        expectedIssuer =  params.get(JWT_EXPECTED_ISSUER);
+        if (expectedIssuer == null) {
+            expectedIssuer = JWT_DEFAULT_ISSUER;
+        }
+        expectedSigAlg =  params.get(JWT_EXPECTED_SIGALG);
+        if (expectedSigAlg == null) {
+            expectedSigAlg = JWT_DEFAULT_SIGALG;
+        }
+        authorityService = gatewayServices.getService(ServiceType.TOKEN_SERVICE);
+        if (isServerManagedTokenStateEnabled()) {
+            tokenStateService = gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE);
+        }
+        // Setup the verified tokens cache
+        signatureVerificationCache = SignatureVerificationCache.getInstance(

Review comment:
       We should setup the cache in constructor and add params here. All initialization things should go in constructor or init method.

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.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.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.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.apache.knox.gateway.util.Tokens;
+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.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidator {
+    private static final String KNOXSSO_COOKIE_NAME = "knoxsso.cookie.name";
+    private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+    private static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
+    private static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
+    private static final String JWT_DEFAULT_SIGALG = "RS256";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final JWTMessages log = MessagesFactory.get(JWTMessages.class);
+    private String cookieName;
+    private String expectedIssuer;
+    private String expectedSigAlg;
+    private RSAPublicKey publicKey;
+    private String providerParamValue;
+
+    private TokenStateService tokenStateService;
+    private JWTokenAuthority authorityService;
+    private SignatureVerificationCache signatureVerificationCache;
+    private JWT token;
+    private String displayableTokenId;
+    private String displayableToken;
+    private final GatewayConfig gatewayConfig;
+    private final GatewayServices gatewayServices;
+    private static final WebsocketLogMessages websocketLog = MessagesFactory
+            .get(WebsocketLogMessages.class);
+
+    JWTValidator(ServletUpgradeRequest req, GatewayServices gatewayServices,
+                 GatewayConfig gatewayConfig){
+        this.gatewayConfig = gatewayConfig;
+        this.gatewayServices = gatewayServices;
+        configureParameters();
+        extractToken(req);
+    }
+
+    private Map<String,String> getParams(){
+        Map<String,String> params = new LinkedHashMap<>();
+        TopologyService ts = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        for (Topology topology : ts.getTopologies()) {
+            if (topology.getName().equals("knoxsso")) {
+                for (Service service : topology.getServices()) {
+                    if (service.getRole().equals("KNOXSSO")) {
+                        params = service.getParams();
+                    }
+                    break;
+                }
+                break;
+            }
+        }
+        return params;
+    }
+    private void configureParameters() {
+        Map<String,String> params = getParams();
+        // token verification pem
+        String verificationPEM = params.get(SSO_VERIFICATION_PEM);
+        // setup the public key of the token issuer for verification
+        if (verificationPEM != null) {
+            try {
+                publicKey = CertificateUtils.parseRSAPublicKey(verificationPEM);
+            } catch (ServletException e){
+                throw new RuntimeException("Failed to obtain public key: "+e);
+            }
+        }
+
+        //provider-level configuration
+        providerParamValue = params.get(TokenStateService.CONFIG_SERVER_MANAGED);
+
+        cookieName = params.get(KNOXSSO_COOKIE_NAME);
+        if (cookieName == null) {
+            cookieName = DEFAULT_SSO_COOKIE_NAME;
+        }
+        expectedIssuer =  params.get(JWT_EXPECTED_ISSUER);
+        if (expectedIssuer == null) {
+            expectedIssuer = JWT_DEFAULT_ISSUER;
+        }
+        expectedSigAlg =  params.get(JWT_EXPECTED_SIGALG);
+        if (expectedSigAlg == null) {
+            expectedSigAlg = JWT_DEFAULT_SIGALG;
+        }
+        authorityService = gatewayServices.getService(ServiceType.TOKEN_SERVICE);
+        if (isServerManagedTokenStateEnabled()) {
+            tokenStateService = gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE);
+        }
+        // Setup the verified tokens cache
+        signatureVerificationCache = SignatureVerificationCache.getInstance(
+                "knoxsso", new WebSocketFilterConfig(params));
+    }
+
+    private void extractToken(ServletUpgradeRequest req){
+        List<HttpCookie> ssoCookies = req.getCookies();
+        if (ssoCookies != null){
+            for (HttpCookie ssoCookie : ssoCookies) {
+                if (cookieName.equals(ssoCookie.getName())) {
+                    try {
+                        token = new JWTToken(ssoCookie.getValue());
+                        displayableTokenId = Tokens.getTokenIDDisplayText(TokenUtils.getTokenId(token));
+                        displayableToken = Tokens.getTokenDisplayText(token.toString());
+                        websocketLog.debugLog("found token:"+displayableToken+" id:"+displayableTokenId);

Review comment:
       We should only be logging token id and not the actual token!

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidator.java
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.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.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.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.apache.knox.gateway.util.Tokens;
+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.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JWTValidator {
+    private static final String KNOXSSO_COOKIE_NAME = "knoxsso.cookie.name";
+    private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+    private static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
+    private static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
+    private static final String JWT_DEFAULT_SIGALG = "RS256";
+    private static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
+    public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
+    private static final JWTMessages log = MessagesFactory.get(JWTMessages.class);
+    private String cookieName;
+    private String expectedIssuer;
+    private String expectedSigAlg;
+    private RSAPublicKey publicKey;
+    private String providerParamValue;
+
+    private TokenStateService tokenStateService;
+    private JWTokenAuthority authorityService;
+    private SignatureVerificationCache signatureVerificationCache;
+    private JWT token;
+    private String displayableTokenId;
+    private String displayableToken;
+    private final GatewayConfig gatewayConfig;
+    private final GatewayServices gatewayServices;
+    private static final WebsocketLogMessages websocketLog = MessagesFactory
+            .get(WebsocketLogMessages.class);
+
+    JWTValidator(ServletUpgradeRequest req, GatewayServices gatewayServices,
+                 GatewayConfig gatewayConfig){
+        this.gatewayConfig = gatewayConfig;
+        this.gatewayServices = gatewayServices;
+        configureParameters();
+        extractToken(req);
+    }
+
+    private Map<String,String> getParams(){
+        Map<String,String> params = new LinkedHashMap<>();
+        TopologyService ts = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        for (Topology topology : ts.getTopologies()) {
+            if (topology.getName().equals("knoxsso")) {
+                for (Service service : topology.getServices()) {
+                    if (service.getRole().equals("KNOXSSO")) {
+                        params = service.getParams();
+                    }
+                    break;
+                }
+                break;
+            }
+        }
+        return params;
+    }
+    private void configureParameters() {
+        Map<String,String> params = getParams();
+        // token verification pem
+        String verificationPEM = params.get(SSO_VERIFICATION_PEM);
+        // setup the public key of the token issuer for verification
+        if (verificationPEM != null) {
+            try {
+                publicKey = CertificateUtils.parseRSAPublicKey(verificationPEM);
+            } catch (ServletException e){
+                throw new RuntimeException("Failed to obtain public key: "+e);
+            }
+        }
+
+        //provider-level configuration
+        providerParamValue = params.get(TokenStateService.CONFIG_SERVER_MANAGED);
+
+        cookieName = params.get(KNOXSSO_COOKIE_NAME);
+        if (cookieName == null) {
+            cookieName = DEFAULT_SSO_COOKIE_NAME;
+        }
+        expectedIssuer =  params.get(JWT_EXPECTED_ISSUER);
+        if (expectedIssuer == null) {
+            expectedIssuer = JWT_DEFAULT_ISSUER;
+        }
+        expectedSigAlg =  params.get(JWT_EXPECTED_SIGALG);
+        if (expectedSigAlg == null) {
+            expectedSigAlg = JWT_DEFAULT_SIGALG;
+        }
+        authorityService = gatewayServices.getService(ServiceType.TOKEN_SERVICE);
+        if (isServerManagedTokenStateEnabled()) {
+            tokenStateService = gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE);
+        }
+        // Setup the verified tokens cache
+        signatureVerificationCache = SignatureVerificationCache.getInstance(
+                "knoxsso", new WebSocketFilterConfig(params));
+    }
+
+    private void extractToken(ServletUpgradeRequest req){
+        List<HttpCookie> ssoCookies = req.getCookies();
+        if (ssoCookies != null){
+            for (HttpCookie ssoCookie : ssoCookies) {
+                if (cookieName.equals(ssoCookie.getName())) {
+                    try {
+                        token = new JWTToken(ssoCookie.getValue());
+                        displayableTokenId = Tokens.getTokenIDDisplayText(TokenUtils.getTokenId(token));
+                        displayableToken = Tokens.getTokenDisplayText(token.toString());
+                        websocketLog.debugLog("found token:"+displayableToken+" id:"+displayableTokenId);
+                        return;
+                    } catch (ParseException e) {
+                        // Fall through to keep checking if there are more cookies
+                    }
+                }
+            }
+        }
+        log.missingBearerToken();
+        throw new RuntimeException("no Valid JWT found");
+    }
+
+    public JWT getToken(){
+        return token;
+    }
+
+    public String getUsername(){
+        return token.getSubject();
+    }
+
+    public boolean validate() {
+        // confirm that issuer matches the intended target
+        if (expectedIssuer.equals(token.getIssuer())) {
+            // if there is no expiration data then the lifecycle is tied entirely to
+            // the cookie validity - otherwise ensure that the current time is before
+            // the designated expiration time
+            try {
+                if (tokenIsStillValid()) {
+                    Date nbf = token.getNotBeforeDate();
+                    if (nbf == null || new Date().after(nbf)) {
+                        if (isTokenEnabled()){
+                            if (verifyTokenSignature()) {
+                                return true;
+                            }
+                        }
+                    } else {
+                        log.notBeforeCheckFailed();
+                    }
+                }
+            } catch (UnknownTokenException e){
+                return false;
+            }
+        }
+        log.unexpectedTokenIssuer(displayableToken, displayableTokenId);
+        return false;
+    }
+
+    // adapted from TokenUtils.isServerManagedTokenStateEnabled(FilterConfig)
+    private boolean isServerManagedTokenStateEnabled() {
+        boolean isServerManaged = false;
+        // If there is no provider-level configuration
+        if (providerParamValue == null || providerParamValue.isEmpty()) {
+            // Fall back to the gateway-level default
+            isServerManaged = (gatewayConfig != null) && gatewayConfig.isServerManagedTokenStateEnabled();
+        } else {
+            // Otherwise, apply the provider-level configuration
+            isServerManaged = Boolean.parseBoolean(providerParamValue);
+        }
+        return isServerManaged;
+    }
+
+    public boolean tokenIsStillValid() throws UnknownTokenException {
+        Date expires = getServerManagedStateExpiration();
+        if (expires == null) {
+            // if there is no expiration date then the lifecycle is tied entirely to
+            // the cookie validity - otherwise ensure that the current time is before
+            // the designated expiration time
+            expires = token.getExpiresDate();
+        }
+        if (expires == null || new Date().before(expires)){

Review comment:
       Same as before.

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/webshell/ConnectionInfo.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 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.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.eclipse.jetty.io.RuntimeIOException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+* data structure to store a connection session
+*/
+public class ConnectionInfo {
+
+    private InputStream inputStream;
+    private OutputStream outputStream;
+    private PtyProcess ptyProcess;
+    private final String username;
+    private final Auditor auditor;
+    private final WebsocketLogMessages LOG;
+    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;
+
+    @SuppressWarnings("PMD.DoNotUseThreads") //we need to define a Thread to clean up resources using shutdown hook
+    public ConnectionInfo(String username, String gatewayPIDDir, AtomicInteger concurrentWebshells, Auditor auditor, WebsocketLogMessages LOG) {
+        this.username = username;
+        this.auditor = auditor;
+        this.LOG = LOG;
+        this.gatewayPIDDir = gatewayPIDDir;
+        this.concurrentWebshells = concurrentWebshells;
+        shutdownHook = new Thread(() -> {
+            LOG.debugLog("running webshell shutdown hook");
+            disconnect();
+        });
+        Runtime.getRuntime().addShutdownHook(shutdownHook);
+    }
+
+    private void saveProcessPID(long pid){
+        File file = new File(gatewayPIDDir + "/" + "webshell_" + pid + ".pid");
+        try {
+            FileUtils.writeStringToFile(file, String.valueOf(pid), StandardCharsets.UTF_8);
+        } catch (IOException e){
+            LOG.onError("error saving PID for webshell:" + e);
+        }
+        auditor.audit( Action.WEBSHELL, username+':'+pid,
+                ResourceType.PROCESS, ActionOutcome.SUCCESS,"Started Bash process");
+    }
+
+    @SuppressForbidden // we need to spawn a bash process for authenticated user
+    @SuppressWarnings("PMD.DoNotUseThreads") // we need to define a Thread to register a shutdown hook
+    public void connect(){
+        // sudoers file needs to be configured for this to work.
+        // refer to design doc for details
+        String[] cmd = { "sudo","--user", username,"bash"};
+        // todo: make environment configurable through gateway-site.xml

Review comment:
       Remove todo

##########
File path: knox-webshell-ui/package-lock.json
##########
@@ -0,0 +1,9803 @@
+{
+  "name": "ng-knox-webshell-ui",

Review comment:
       I don't think we need to check this file in git.

##########
File path: gateway-server/src/main/java/org/apache/knox/gateway/webshell/WebshellWebSocketAdapter.java
##########
@@ -0,0 +1,176 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.CharMatcher;
+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.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.websockets.JWTValidator;
+import org.apache.knox.gateway.websockets.ProxyWebSocketAdapter;
+import org.eclipse.jetty.websocket.api.Session;
+
+public class WebshellWebSocketAdapter extends ProxyWebSocketAdapter  {
+    private Session session;
+    private final ConnectionInfo connectionInfo;
+    private final JWTValidator jwtValidator;
+    private final StringBuilder auditBuffer;
+    private final Auditor auditor;
+    private final ObjectMapper objectMapper;
+
+    public WebshellWebSocketAdapter(ExecutorService pool, GatewayConfig config, JWTValidator jwtValidator, AtomicInteger concurrentWebshells) {
+        super(null, pool, null, config);
+        this.jwtValidator = jwtValidator;
+        auditBuffer = new StringBuilder(); // buffer for audit log
+        auditor = AuditServiceFactory.getAuditService().getAuditor(
+                AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME,
+                AuditConstants.KNOX_COMPONENT_NAME );
+        connectionInfo = new ConnectionInfo(jwtValidator.getUsername(),config.getGatewayPIDDir(), concurrentWebshells, auditor, LOG);
+        objectMapper = new ObjectMapper();
+    }
+
+    @SuppressWarnings("PMD.DoNotUseThreads")
+    @Override
+    public void onWebSocketConnect(final Session session) {
+        this.session = session;
+        if (jwtValidator.getUsername() == null){
+            throw new RuntimeException("Needs user name in JWT to use WebShell");
+        }
+        connectionInfo.connect();
+        pool.execute(this::blockingReadFromHost);
+
+    }
+
+    private void blockingReadFromHost(){
+        byte[] buffer = new byte[1024];

Review comment:
       property name could be `webshell.read.buffer.size`.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@knox.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org