Repository: Azure/ARO-RP
Pull request #2740 (2023-02-22 14:17:20)

clean up of portal
### Which issue this PR addresses:
some rework of portal before removing gorilla

this centralizes the route definitions in the portal.go file instead of having some extra route definitions added to the routers from other packages. 
<!--
Please include a link to the ADO work item as well as any GitHub issues.

Usage: `Fixes #<GitHub issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes

### What this PR does / why we need it:

<!--
Include a brief summary of what the PR is intended to accomplish and how the PR
does it. (2-3 sentences)
-->

### Test plan for issue:

<!--
How did you test that this PR works?

- Are there unit tests?
- Are there integration/e2e tests?
- If it is not possible to write automated tests, explain why and document how
  to manually test and verify the feature.
-->

### Is there any documentation that needs to be updated for this PR?

<!--
- If yes and the docs are in GitHub, include doc updates in the PR.
- If yes and the docs are not in GitHub (i.e. ADO wiki), include a link to the
  docs.
- If no, explain why (e.g. "tech debt cleanup, N/A").
-->


jaitaiwan: I have a couple of suggestions if I may?
 - It would be good if the functions that are now being exported as a result of this PR be documented so we're able to see at a glance what functions they perform in their contexts; I'm not sure if this is going against your mandate of making this a small PR with as little changes as possible tho.
 - I would like to see slightly more specific commit messages if at all possible e.g. "Centralise route definitions for portal" would be more meaningful to me than "clean up of portal".
 
 Apart from these couple things, looks great!
facchettos: @jaitaiwan I think that's fair. 
Also, you may always give suggestions or request changes on the PRs, worst case scenario is that your wrong and you learn something new :D , and people should be able to justify what they are doing 

Patch:
diff --git a/pkg/portal/kubeconfig/kubeconfig.go b/pkg/portal/kubeconfig/kubeconfig.go
index 5d1a36b9f..ec9808ffd 100644
--- a/pkg/portal/kubeconfig/kubeconfig.go
+++ b/pkg/portal/kubeconfig/kubeconfig.go
@@ -14,7 +14,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gorilla/mux"
 	"github.com/sirupsen/logrus"
 	clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
 
@@ -32,18 +31,22 @@ const (
 	kubeconfigNewTimeout = 6 * time.Hour
 )
 
-type kubeconfig struct {
-	log           *logrus.Entry
-	baseAccessLog *logrus.Entry
+type Kubeconfig struct {
+	Log           *logrus.Entry
+	BaseAccessLog *logrus.Entry
+	Audit         *logrus.Entry
 
 	servingCert      *x509.Certificate
 	elevatedGroupIDs []string
 
 	dbOpenShiftClusters database.OpenShiftClusters
-	dbPortal            database.Portal
+	DbPortal            database.Portal
 
 	dialer      proxy.Dialer
 	clientCache clientcache.ClientCache
+	Env         env.Core
+
+	ReverseProxy *httputil.ReverseProxy
 }
 
 func New(baseLog *logrus.Entry,
@@ -55,42 +58,35 @@ func New(baseLog *logrus.Entry,
 	dbOpenShiftClusters database.OpenShiftClusters,
 	dbPortal database.Portal,
 	dialer proxy.Dialer,
-	aadAuthenticatedRouter,
-	unauthenticatedRouter *mux.Router) *kubeconfig {
-	k := &kubeconfig{
-		log:           baseLog,
-		baseAccessLog: baseAccessLog,
+) *Kubeconfig {
+	k := &Kubeconfig{
+		Log:           baseLog,
+		BaseAccessLog: baseAccessLog,
+		Audit:         audit,
 
 		servingCert:      servingCert,
 		elevatedGroupIDs: elevatedGroupIDs,
 
 		dbOpenShiftClusters: dbOpenShiftClusters,
-		dbPortal:            dbPortal,
+		DbPortal:            dbPortal,
 
 		dialer:      dialer,
 		clientCache: clientcache.New(time.Hour),
+		Env:         env,
 	}
 
-	rp := &httputil.ReverseProxy{
+	k.ReverseProxy = &httputil.ReverseProxy{
 		Director:  k.director,
 		Transport: roundtripper.RoundTripperFunc(k.roundTripper),
-		ErrorLog:  log.New(k.log.Writer(), "", 0),
+		ErrorLog:  log.New(k.Log.Writer(), "", 0),
 	}
 
-	aadAuthenticatedRouter.NewRoute().Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/new").HandlerFunc(k.new)
-
-	bearerAuthenticatedRouter := unauthenticatedRouter.NewRoute().Subrouter()
-	bearerAuthenticatedRouter.Use(middleware.Bearer(k.dbPortal))
-	bearerAuthenticatedRouter.Use(middleware.Log(env, audit, k.baseAccessLog))
-
-	bearerAuthenticatedRouter.PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/proxy/").Handler(rp)
-
 	return k
 }
 
-// new creates a new PortalDocument allowing kubeconfig access to a cluster for
+// New creates a New PortalDocument allowing kubeconfig access to a cluster for
 // 6 hours and returns a kubeconfig with the temporary credentials
-func (k *kubeconfig) new(w http.ResponseWriter, r *http.Request) {
+func (k *Kubeconfig) New(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 
 	resourceID := strings.Join(strings.Split(r.URL.Path, "/")[:9], "/")
@@ -101,7 +97,7 @@ func (k *kubeconfig) new(w http.ResponseWriter, r *http.Request) {
 
 	elevated := len(middleware.GroupsIntersect(k.elevatedGroupIDs, ctx.Value(middleware.ContextKeyGroups).([]string))) > 0
 
-	token := k.dbPortal.NewUUID()
+	token := k.DbPortal.NewUUID()
 	portalDoc := &api.PortalDocument{
 		ID:  token,
 		TTL: int(kubeconfigNewTimeout / time.Second),
@@ -114,7 +110,7 @@ func (k *kubeconfig) new(w http.ResponseWriter, r *http.Request) {
 		},
 	}
 
-	_, err := k.dbPortal.Create(ctx, portalDoc)
+	_, err := k.DbPortal.Create(ctx, portalDoc)
 	if err != nil {
 		k.internalServerError(w, err)
 		return
@@ -136,12 +132,12 @@ func (k *kubeconfig) new(w http.ResponseWriter, r *http.Request) {
 	_, _ = w.Write(b)
 }
 
-func (k *kubeconfig) internalServerError(w http.ResponseWriter, err error) {
-	k.log.Warn(err)
+func (k *Kubeconfig) internalServerError(w http.ResponseWriter, err error) {
+	k.Log.Warn(err)
 	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 }
 
-func (k *kubeconfig) makeKubeconfig(server, token string) ([]byte, error) {
+func (k *Kubeconfig) makeKubeconfig(server, token string) ([]byte, error) {
 	return json.MarshalIndent(&clientcmdv1.Config{
 		APIVersion: "v1",
 		Kind:       "Config",
diff --git a/pkg/portal/kubeconfig/proxy.go b/pkg/portal/kubeconfig/proxy.go
index ac2b50796..e7eb386c2 100644
--- a/pkg/portal/kubeconfig/proxy.go
+++ b/pkg/portal/kubeconfig/proxy.go
@@ -42,7 +42,7 @@ const (
 // Unfortunately the signature of httputil.ReverseProxy.Director does not allow
 // us to return values.  We get around this limitation slightly naughtily by
 // storing return information in the request context.
-func (k *kubeconfig) director(r *http.Request) {
+func (k *Kubeconfig) director(r *http.Request) {
 	ctx := r.Context()
 
 	portalDoc, _ := ctx.Value(middleware.ContextKeyPortalDoc).(*api.PortalDocument)
@@ -93,7 +93,7 @@ func (k *kubeconfig) director(r *http.Request) {
 
 // cli returns an appropriately configured HTTP client for forwarding the
 // incoming request to a cluster
-func (k *kubeconfig) cli(ctx context.Context, resourceID string, elevated bool) (*http.Client, error) {
+func (k *Kubeconfig) cli(ctx context.Context, resourceID string, elevated bool) (*http.Client, error) {
 	openShiftDoc, err := k.dbOpenShiftClusters.Get(ctx, resourceID)
 	if err != nil {
 		return nil, err
@@ -154,7 +154,7 @@ func (k *kubeconfig) cli(ctx context.Context, resourceID string, elevated bool)
 // roundTripper is called by ReverseProxy to make the onward request happen.  We
 // check if we had an error earlier and return that if we did.  Otherwise we dig
 // out the client and call it.
-func (k *kubeconfig) roundTripper(r *http.Request) (*http.Response, error) {
+func (k *Kubeconfig) roundTripper(r *http.Request) (*http.Response, error) {
 	if resp, ok := r.Context().Value(contextKeyResponse).(*http.Response); ok {
 		return resp, nil
 	}
@@ -172,9 +172,9 @@ func (k *kubeconfig) roundTripper(r *http.Request) (*http.Response, error) {
 	return resp, err
 }
 
-func (k *kubeconfig) error(r *http.Request, statusCode int, err error) {
+func (k *Kubeconfig) error(r *http.Request, statusCode int, err error) {
 	if err != nil {
-		k.log.Warn(err)
+		k.Log.Warn(err)
 	}
 
 	w := responsewriter.New(r)
diff --git a/pkg/portal/portal.go b/pkg/portal/portal.go
index e9bbba8ac..6f2902a6e 100644
--- a/pkg/portal/portal.go
+++ b/pkg/portal/portal.go
@@ -44,16 +44,13 @@ type Runnable interface {
 }
 
 type portal struct {
-	env                 env.Core
-	audit               *logrus.Entry
-	log                 *logrus.Entry
-	baseAccessLog       *logrus.Entry
-	l                   net.Listener
-	sshl                net.Listener
-	verifier            oidc.Verifier
-	baseRouter          *mux.Router
-	authenticatedRouter *mux.Router
-	publicRouter        *mux.Router
+	env           env.Core
+	audit         *logrus.Entry
+	log           *logrus.Entry
+	baseAccessLog *logrus.Entry
+	l             net.Listener
+	sshl          net.Listener
+	verifier      oidc.Verifier
 
 	hostname     string
 	servingKey   *rsa.PrivateKey
@@ -132,35 +129,32 @@ func NewPortal(env env.Core,
 	}
 }
 
-func (p *portal) setupRouter() error {
-	if p.baseRouter != nil {
-		return fmt.Errorf("can't setup twice")
-	}
-
+func (p *portal) setupRouter(kconfig *kubeconfig.Kubeconfig, prom *prometheus.Prometheus, sshStruct *ssh.SSH) (*mux.Router, error) {
 	r := mux.NewRouter()
 	r.Use(middleware.Panic(p.log))
 
 	assetv1, err := assets.EmbeddedFiles.ReadFile("v1/build/index.html")
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	assetv2, err := assets.EmbeddedFiles.ReadFile("v2/build/index.html")
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	p.templateV1, err = template.New("index.html").Parse(string(assetv1))
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	p.templateV2, err = template.New("index.html").Parse(string(assetv2))
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	unauthenticatedRouter := r.NewRoute().Subrouter()
+	bearerRoutes(unauthenticatedRouter, kconfig)
 	p.unauthenticatedRoutes(unauthenticatedRouter)
 
 	allGroups := append([]string{}, p.groupIDs...)
@@ -168,7 +162,7 @@ func (p *portal) setupRouter() error {
 
 	p.aad, err = middleware.NewAAD(p.log, p.audit, p.env, p.baseAccessLog, p.hostname, p.sessionKey, p.clientID, p.clientKey, p.clientCerts, allGroups, unauthenticatedRouter, p.verifier)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	aadAuthenticatedRouter := r.NewRoute().Subrouter()
@@ -177,31 +171,27 @@ func (p *portal) setupRouter() error {
 	aadAuthenticatedRouter.Use(p.aad.CheckAuthentication)
 	aadAuthenticatedRouter.Use(csrf.Protect(p.sessionKey, csrf.SameSite(csrf.SameSiteStrictMode), csrf.MaxAge(0), csrf.Path("/")))
 
-	p.aadAuthenticatedRoutes(aadAuthenticatedRouter)
+	p.aadAuthenticatedRoutes(aadAuthenticatedRouter, prom, kconfig, sshStruct)
 
-	p.baseRouter = r
-	p.publicRouter = unauthenticatedRouter
-	p.authenticatedRouter = aadAuthenticatedRouter
-
-	return nil
+	return r, nil
 }
 
-func (p *portal) setupServices() error {
-	ssh, err := ssh.New(p.env, p.log, p.baseAccessLog, p.sshl, p.sshKey, p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer, p.authenticatedRouter)
+func (p *portal) setupServices() (*kubeconfig.Kubeconfig, *prometheus.Prometheus, *ssh.SSH, error) {
+	ssh, err := ssh.New(p.env, p.log, p.baseAccessLog, p.sshl, p.sshKey, p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer)
 	if err != nil {
-		return err
+		return nil, nil, nil, err
 	}
 
 	err = ssh.Run()
 	if err != nil {
-		return err
+		return nil, nil, nil, err
 	}
 
-	kubeconfig.New(p.log, p.audit, p.env, p.baseAccessLog, p.servingCerts[0], p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer, p.authenticatedRouter, p.publicRouter)
+	k := kubeconfig.New(p.log, p.audit, p.env, p.baseAccessLog, p.servingCerts[0], p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer)
 
-	prometheus.New(p.log, p.dbOpenShiftClusters, p.dialer, p.authenticatedRouter)
+	prom := prometheus.New(p.log, p.dbOpenShiftClusters, p.dialer)
 
-	return nil
+	return k, prom, ssh, nil
 }
 
 func (p *portal) Run(ctx context.Context) error {
@@ -232,19 +222,17 @@ func (p *portal) Run(ctx context.Context) error {
 		config.Certificates[0].Certificate = append(config.Certificates[0].Certificate, cert.Raw)
 	}
 
-	if p.baseRouter == nil {
-		err := p.setupRouter()
-		if err != nil {
-			return err
-		}
-		err = p.setupServices()
-		if err != nil {
-			return err
-		}
+	k, prom, sshStruct, err := p.setupServices()
+	if err != nil {
+		return err
+	}
+	router, err := p.setupRouter(k, prom, sshStruct)
+	if err != nil {
+		return err
 	}
 
 	s := &http.Server{
-		Handler:     frontendmiddleware.Lowercase(p.baseRouter),
+		Handler:     frontendmiddleware.Lowercase(router),
 		ReadTimeout: 10 * time.Second,
 		IdleTimeout: 2 * time.Minute,
 		ErrorLog:    log.New(p.log.Writer(), "", 0),
@@ -256,13 +244,23 @@ func (p *portal) Run(ctx context.Context) error {
 	return s.Serve(tls.NewListener(p.l, config))
 }
 
+func bearerRoutes(r *mux.Router, k *kubeconfig.Kubeconfig) {
+	if k != nil {
+		bearerAuthenticatedRouter := r.NewRoute().Subrouter()
+		bearerAuthenticatedRouter.Use(middleware.Bearer(k.DbPortal))
+		bearerAuthenticatedRouter.Use(middleware.Log(k.Env, k.Audit, k.BaseAccessLog))
+
+		bearerAuthenticatedRouter.PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/proxy/").Handler(k.ReverseProxy)
+	}
+}
+
 func (p *portal) unauthenticatedRoutes(r *mux.Router) {
 	logger := middleware.Log(p.env, p.audit, p.baseAccessLog)
 
 	r.Methods(http.MethodGet).Path("/healthz/ready").Handler(logger(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})))
 }
 
-func (p *portal) aadAuthenticatedRoutes(r *mux.Router) {
+func (p *portal) aadAuthenticatedRoutes(r *mux.Router, prom *prometheus.Prometheus, kconfig *kubeconfig.Kubeconfig, sshStruct *ssh.SSH) {
 	var names []string
 
 	err := fs.WalkDir(assets.EmbeddedFiles, ".", func(path string, entry fs.DirEntry, err error) error {
@@ -308,6 +306,24 @@ func (p *portal) aadAuthenticatedRoutes(r *mux.Router) {
 	r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/machines").HandlerFunc(p.machines)
 	r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/machine-sets").HandlerFunc(p.machineSets)
 	r.Path("/api/{subscription}/{resourceGroup}/{clusterName}").HandlerFunc(p.clusterInfo)
+
+	// prometheus
+	if prom != nil {
+		r.Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/prometheus").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			r.URL.Path += "/"
+			http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
+		})
+
+		r.PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/prometheus/").Handler(prom.ReverseProxy)
+	}
+
+	//kubeconfig
+	if kconfig != nil {
+		r.Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/new").HandlerFunc(kconfig.New)
+	}
+
+	// ssh
+	r.Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/ssh/new").HandlerFunc(sshStruct.New)
 }
 
 func (p *portal) index(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/portal/prometheus/prometheus.go b/pkg/portal/prometheus/prometheus.go
index 367c7831a..f30fb702e 100644
--- a/pkg/portal/prometheus/prometheus.go
+++ b/pkg/portal/prometheus/prometheus.go
@@ -5,11 +5,9 @@ package prometheus
 
 import (
 	"log"
-	"net/http"
 	"net/http/httputil"
 	"time"
 
-	"github.com/gorilla/mux"
 	"github.com/sirupsen/logrus"
 
 	"github.com/Azure/ARO-RP/pkg/database"
@@ -18,20 +16,22 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/roundtripper"
 )
 
-type prometheus struct {
+type Prometheus struct {
 	log *logrus.Entry
 
 	dbOpenShiftClusters database.OpenShiftClusters
 
 	dialer      proxy.Dialer
 	clientCache clientcache.ClientCache
+
+	ReverseProxy *httputil.ReverseProxy
 }
 
 func New(baseLog *logrus.Entry,
 	dbOpenShiftClusters database.OpenShiftClusters,
 	dialer proxy.Dialer,
-	aadAuthenticatedRouter *mux.Router) *prometheus {
-	p := &prometheus{
+) *Prometheus {
+	p := &Prometheus{
 		log: baseLog,
 
 		dbOpenShiftClusters: dbOpenShiftClusters,
@@ -39,20 +39,12 @@ func New(baseLog *logrus.Entry,
 		dialer:      dialer,
 		clientCache: clientcache.New(time.Hour),
 	}
-
-	rp := &httputil.ReverseProxy{
-		Director:       p.director,
-		Transport:      roundtripper.RoundTripperFunc(p.roundTripper),
-		ModifyResponse: p.modifyResponse,
+	p.ReverseProxy = &httputil.ReverseProxy{
+		Director:       p.Director,
+		Transport:      roundtripper.RoundTripperFunc(p.RoundTripper),
+		ModifyResponse: p.ModifyResponse,
 		ErrorLog:       log.New(p.log.Writer(), "", 0),
 	}
 
-	aadAuthenticatedRouter.NewRoute().Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/prometheus").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		r.URL.Path += "/"
-		http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
-	})
-
-	aadAuthenticatedRouter.NewRoute().PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/prometheus/").Handler(rp)
-
 	return p
 }
diff --git a/pkg/portal/prometheus/proxy.go b/pkg/portal/prometheus/proxy.go
index 60e3e0e98..66129b773 100644
--- a/pkg/portal/prometheus/proxy.go
+++ b/pkg/portal/prometheus/proxy.go
@@ -33,7 +33,8 @@ const (
 	contextKeyResponse
 )
 
-func (p *prometheus) director(r *http.Request) {
+// Director modifies the request to point to the clusters prometheus instance
+func (p *Prometheus) Director(r *http.Request) {
 	ctx := r.Context()
 
 	resourceID := strings.Join(strings.Split(r.URL.Path, "/")[:9], "/")
@@ -68,7 +69,7 @@ func (p *prometheus) director(r *http.Request) {
 	*r = *r.WithContext(context.WithValue(ctx, contextKeyClient, cli))
 }
 
-func (p *prometheus) cli(ctx context.Context, resourceID string) (*http.Client, error) {
+func (p *Prometheus) cli(ctx context.Context, resourceID string) (*http.Client, error) {
 	openShiftDoc, err := p.dbOpenShiftClusters.Get(ctx, resourceID)
 	if err != nil {
 		return nil, err
@@ -88,7 +89,7 @@ func (p *prometheus) cli(ctx context.Context, resourceID string) (*http.Client,
 	}, nil
 }
 
-func (p *prometheus) roundTripper(r *http.Request) (*http.Response, error) {
+func (p *Prometheus) RoundTripper(r *http.Request) (*http.Response, error) {
 	if resp, ok := r.Context().Value(contextKeyResponse).(*http.Response); ok {
 		return resp, nil
 	}
@@ -97,12 +98,12 @@ func (p *prometheus) roundTripper(r *http.Request) (*http.Response, error) {
 	return cli.Do(r)
 }
 
-// modifyResponse: unfortunately Prometheus serves HTML files containing just a
+// ModifyResponse: unfortunately Prometheus serves HTML files containing just a
 // couple of absolute links.  Given that we're serving Prometheus under
 // /subscriptions/.../clusterName/prometheus, we need to dig these out and
 // rewrite them.  This is a hack which hopefully goes away once we forward all
 // metrics to Kusto.
-func (p *prometheus) modifyResponse(r *http.Response) error {
+func (p *Prometheus) ModifyResponse(r *http.Response) error {
 	mediaType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
 	if mediaType != "text/html" {
 		return nil
@@ -169,7 +170,7 @@ func walk(n *html.Node, f func(*html.Node)) {
 	}
 }
 
-func (p *prometheus) error(r *http.Request, statusCode int, err error) {
+func (p *Prometheus) error(r *http.Request, statusCode int, err error) {
 	if err != nil {
 		p.log.Print(err)
 	}
diff --git a/pkg/portal/ssh/proxy.go b/pkg/portal/ssh/proxy.go
index dcffaaa43..676e91f38 100644
--- a/pkg/portal/ssh/proxy.go
+++ b/pkg/portal/ssh/proxy.go
@@ -58,7 +58,7 @@ const (
 	sshTimeout = time.Hour // never allow a connection to live longer than an hour.
 )
 
-func (s *ssh) Run() error {
+func (s *SSH) Run() error {
 	go func() {
 		defer recover.Panic(s.log)
 
@@ -79,7 +79,7 @@ func (s *ssh) Run() error {
 	return nil
 }
 
-func (s *ssh) newConn(ctx context.Context, clientConn net.Conn) error {
+func (s *SSH) newConn(ctx context.Context, clientConn net.Conn) error {
 	defer clientConn.Close()
 
 	config := &cryptossh.ServerConfig{}
@@ -195,7 +195,7 @@ func (s *ssh) newConn(ctx context.Context, clientConn net.Conn) error {
 
 // proxyConn handles incoming new channel and administrative requests.  It calls
 // newChannel to handle new channels, each on a new goroutine.
-func (s *ssh) proxyConn(ctx context.Context, accessLog *logrus.Entry, keyring agent.Agent, upstreamConn, downstreamConn cryptossh.Conn, upstreamNewChannels, downstreamNewChannels <-chan cryptossh.NewChannel, upstreamRequests, downstreamRequests <-chan *cryptossh.Request) error {
+func (s *SSH) proxyConn(ctx context.Context, accessLog *logrus.Entry, keyring agent.Agent, upstreamConn, downstreamConn cryptossh.Conn, upstreamNewChannels, downstreamNewChannels <-chan cryptossh.NewChannel, upstreamRequests, downstreamRequests <-chan *cryptossh.Request) error {
 	timer := time.NewTimer(sshTimeout)
 	defer timer.Stop()
 
@@ -256,7 +256,7 @@ func (s *ssh) proxyConn(ctx context.Context, accessLog *logrus.Entry, keyring ag
 	}
 }
 
-func (s *ssh) handleAgent(accessLog *logrus.Entry, nc cryptossh.NewChannel, keyring agent.Agent) error {
+func (s *SSH) handleAgent(accessLog *logrus.Entry, nc cryptossh.NewChannel, keyring agent.Agent) error {
 	ch, rs, err := nc.Accept()
 	if err != nil {
 		return err
@@ -277,7 +277,7 @@ func (s *ssh) handleAgent(accessLog *logrus.Entry, nc cryptossh.NewChannel, keyr
 // newChannel handles an incoming request to create a new channel.  If the
 // channel creation is successful, it calls proxyChannel to proxy the channel
 // between SRE and cluster.
-func (s *ssh) newChannel(ctx context.Context, accessLog *logrus.Entry, nc cryptossh.NewChannel, upstreamConn, downstreamConn cryptossh.Conn, firstSession bool) error {
+func (s *SSH) newChannel(ctx context.Context, accessLog *logrus.Entry, nc cryptossh.NewChannel, upstreamConn, downstreamConn cryptossh.Conn, firstSession bool) error {
 	defer recover.Panic(s.log)
 
 	ch2, rs2, err := downstreamConn.OpenChannel(nc.ChannelType(), nc.ExtraData())
@@ -310,7 +310,7 @@ func (s *ssh) newChannel(ctx context.Context, accessLog *logrus.Entry, nc crypto
 	return s.proxyChannel(ch1, ch2, rs1, rs2)
 }
 
-func (s *ssh) proxyGlobalRequest(r *cryptossh.Request, c cryptossh.Conn) error {
+func (s *SSH) proxyGlobalRequest(r *cryptossh.Request, c cryptossh.Conn) error {
 	ok, payload, err := c.SendRequest(r.Type, r.WantReply, r.Payload)
 	if err != nil {
 		return err
@@ -319,7 +319,7 @@ func (s *ssh) proxyGlobalRequest(r *cryptossh.Request, c cryptossh.Conn) error {
 	return r.Reply(ok, payload)
 }
 
-func (s *ssh) proxyRequest(r *cryptossh.Request, ch cryptossh.Channel) error {
+func (s *SSH) proxyRequest(r *cryptossh.Request, ch cryptossh.Channel) error {
 	ok, err := ch.SendRequest(r.Type, r.WantReply, r.Payload)
 	if err != nil {
 		return err
@@ -328,7 +328,7 @@ func (s *ssh) proxyRequest(r *cryptossh.Request, ch cryptossh.Channel) error {
 	return r.Reply(ok, nil)
 }
 
-func (s *ssh) proxyChannel(ch1, ch2 cryptossh.Channel, rs1, rs2 <-chan *cryptossh.Request) error {
+func (s *SSH) proxyChannel(ch1, ch2 cryptossh.Channel, rs1, rs2 <-chan *cryptossh.Request) error {
 	g := errgroup.Group{}
 
 	g.Go(func() error {
@@ -382,7 +382,7 @@ func (s *ssh) proxyChannel(ch1, ch2 cryptossh.Channel, rs1, rs2 <-chan *cryptoss
 	return g.Wait()
 }
 
-func (s *ssh) keepAliveConn(ctx context.Context, channel cryptossh.Channel) {
+func (s *SSH) keepAliveConn(ctx context.Context, channel cryptossh.Channel) {
 	ticker := time.NewTicker(keepAliveInterval)
 	defer ticker.Stop()
 
diff --git a/pkg/portal/ssh/ssh.go b/pkg/portal/ssh/ssh.go
index e4ddd30c7..962ea88cf 100644
--- a/pkg/portal/ssh/ssh.go
+++ b/pkg/portal/ssh/ssh.go
@@ -13,7 +13,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gorilla/mux"
 	"github.com/sirupsen/logrus"
 	cryptossh "golang.org/x/crypto/ssh"
 
@@ -29,7 +28,7 @@ const (
 	sshNewTimeout = time.Minute
 )
 
-type ssh struct {
+type SSH struct {
 	env           env.Core
 	log           *logrus.Entry
 	baseAccessLog *logrus.Entry
@@ -54,8 +53,8 @@ func New(env env.Core,
 	dbOpenShiftClusters database.OpenShiftClusters,
 	dbPortal database.Portal,
 	dialer proxy.Dialer,
-	aadAuthenticatedRouter *mux.Router) (*ssh, error) {
-	s := &ssh{
+) (*SSH, error) {
+	s := &SSH{
 		env:           env,
 		log:           log,
 		baseAccessLog: baseAccessLog,
@@ -78,8 +77,6 @@ func New(env env.Core,
 
 	s.baseServerConfig.AddHostKey(signer)
 
-	aadAuthenticatedRouter.NewRoute().Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/ssh/new").HandlerFunc(s.new)
-
 	return s, nil
 }
 
@@ -93,7 +90,9 @@ type response struct {
 	Error    string `json:"error,omitempty"`
 }
 
-func (s *ssh) new(w http.ResponseWriter, r *http.Request) {
+// New creates a new temporary password from the request params and sends it
+// through the writer
+func (s *SSH) New(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 
 	parts := strings.Split(r.URL.Path, "/")
@@ -171,7 +170,7 @@ func (s *ssh) new(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *ssh) sendResponse(w http.ResponseWriter, resp *response) {
+func (s *SSH) sendResponse(w http.ResponseWriter, resp *response) {
 	b, err := json.MarshalIndent(resp, "", "    ")
 	if err != nil {
 		s.internalServerError(w, err)
@@ -182,7 +181,7 @@ func (s *ssh) sendResponse(w http.ResponseWriter, resp *response) {
 	_, _ = w.Write(b)
 }
 
-func (s *ssh) internalServerError(w http.ResponseWriter, err error) {
+func (s *SSH) internalServerError(w http.ResponseWriter, err error) {
 	s.log.Warn(err)
 	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 }


Test patch:
diff --git a/pkg/portal/cluster_test.go b/pkg/portal/cluster_test.go
index 5b011ca7a..cc0eb76ad 100644
--- a/pkg/portal/cluster_test.go
+++ b/pkg/portal/cluster_test.go
@@ -76,7 +76,7 @@ func TestClusterList(t *testing.T) {
 	}
 
 	aadAuthenticatedRouter := mux.NewRouter()
-	p.aadAuthenticatedRoutes(aadAuthenticatedRouter)
+	p.aadAuthenticatedRoutes(aadAuthenticatedRouter, nil, nil, nil)
 	w := httptest.NewRecorder()
 	aadAuthenticatedRouter.ServeHTTP(w, req)
 
@@ -197,7 +197,7 @@ func TestClusterDetail(t *testing.T) {
 	}
 
 	aadAuthenticatedRouter := mux.NewRouter()
-	p.aadAuthenticatedRoutes(aadAuthenticatedRouter)
+	p.aadAuthenticatedRoutes(aadAuthenticatedRouter, nil, nil, nil)
 	w := httptest.NewRecorder()
 	aadAuthenticatedRouter.ServeHTTP(w, req)
 
diff --git a/pkg/portal/http_test.go b/pkg/portal/http_test.go
index 5969a51f3..0d49fb6ba 100644
--- a/pkg/portal/http_test.go
+++ b/pkg/portal/http_test.go
@@ -50,13 +50,13 @@ func (p *testPortal) DumpLogs(t *testing.T) {
 }
 
 func (p *testPortal) Run(ctx context.Context) error {
-	err := p.p.setupRouter()
+	router, err := p.p.setupRouter(nil, nil, nil)
 	if err != nil {
 		return err
 	}
 
 	s := &http.Server{
-		Handler:     frontendmiddleware.Lowercase(p.p.baseRouter),
+		Handler:     frontendmiddleware.Lowercase(router),
 		ReadTimeout: 10 * time.Second,
 		IdleTimeout: 2 * time.Minute,
 		ErrorLog:    log.New(p.p.log.Writer(), "", 0),
diff --git a/pkg/portal/kubeconfig/kubeconfig_test.go b/pkg/portal/kubeconfig/kubeconfig_test.go
index 170257712..6d54e6c9d 100644
--- a/pkg/portal/kubeconfig/kubeconfig_test.go
+++ b/pkg/portal/kubeconfig/kubeconfig_test.go
@@ -148,7 +148,7 @@ func TestNew(t *testing.T) {
 			_, audit := testlog.NewAudit()
 			_, baseLog := testlog.New()
 			_, baseAccessLog := testlog.New()
-			_ = New(baseLog, audit, _env, baseAccessLog, servingCert, elevatedGroupIDs, nil, dbPortal, nil, aadAuthenticatedRouter, &mux.Router{})
+			k := New(baseLog, audit, _env, baseAccessLog, servingCert, elevatedGroupIDs, nil, dbPortal, nil)
 
 			if tt.r != nil {
 				tt.r(r)
@@ -156,6 +156,8 @@ func TestNew(t *testing.T) {
 
 			w := responsewriter.New(r)
 
+			aadAuthenticatedRouter.NewRoute().Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/new").HandlerFunc(k.New)
+
 			aadAuthenticatedRouter.ServeHTTP(w, r)
 
 			portalClient.SetError(nil)
diff --git a/pkg/portal/kubeconfig/proxy_test.go b/pkg/portal/kubeconfig/proxy_test.go
index 071d5ec7a..0c150dea3 100644
--- a/pkg/portal/kubeconfig/proxy_test.go
+++ b/pkg/portal/kubeconfig/proxy_test.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
+	"github.com/Azure/ARO-RP/pkg/portal/middleware"
 	"github.com/Azure/ARO-RP/pkg/portal/util/responsewriter"
 	"github.com/Azure/ARO-RP/pkg/util/azureclient"
 	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
@@ -392,12 +393,16 @@ func TestProxy(t *testing.T) {
 				tt.mocks(dialer)
 			}
 
-			unauthenticatedRouter := &mux.Router{}
-
 			_, audit := testlog.NewAudit()
 			_, baseLog := testlog.New()
 			_, baseAccessLog := testlog.New()
-			_ = New(baseLog, audit, _env, baseAccessLog, nil, nil, dbOpenShiftClusters, dbPortal, dialer, &mux.Router{}, unauthenticatedRouter)
+			k := New(baseLog, audit, _env, baseAccessLog, nil, nil, dbOpenShiftClusters, dbPortal, dialer)
+
+			unauthenticatedRouter := &mux.Router{}
+			unauthenticatedRouter.Use(middleware.Bearer(k.DbPortal))
+			unauthenticatedRouter.Use(middleware.Log(k.Env, audit, k.BaseAccessLog))
+
+			unauthenticatedRouter.PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/proxy/").Handler(k.ReverseProxy)
 
 			if tt.r != nil {
 				tt.r(r)
diff --git a/pkg/portal/prometheus/proxy_test.go b/pkg/portal/prometheus/proxy_test.go
index f45514dbd..72223a71e 100644
--- a/pkg/portal/prometheus/proxy_test.go
+++ b/pkg/portal/prometheus/proxy_test.go
@@ -277,16 +277,22 @@ func TestProxy(t *testing.T) {
 				tt.mocks(dialer)
 			}
 
+			prom := New(logrus.NewEntry(logrus.StandardLogger()), dbOpenShiftClusters, dialer)
 			aadAuthenticatedRouter := &mux.Router{}
 
-			New(logrus.NewEntry(logrus.StandardLogger()), dbOpenShiftClusters, dialer, aadAuthenticatedRouter)
-
 			if tt.r != nil {
 				tt.r(r)
 			}
 
 			w := responsewriter.New(r)
 
+			aadAuthenticatedRouter.NewRoute().Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/prometheus").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				r.URL.Path += "/"
+				http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
+			})
+
+			aadAuthenticatedRouter.NewRoute().PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/prometheus/").Handler(prom.ReverseProxy)
+
 			aadAuthenticatedRouter.ServeHTTP(w, r)
 
 			resp := w.Response()
@@ -346,9 +352,9 @@ func TestModifyResponse(t *testing.T) {
 				Body: io.NopCloser(strings.NewReader(tt.body)),
 			}
 
-			p := &prometheus{}
+			p := &Prometheus{}
 
-			err := p.modifyResponse(r)
+			err := p.ModifyResponse(r)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/portal/regions_test.go b/pkg/portal/regions_test.go
index 3715cd758..e73abbeda 100644
--- a/pkg/portal/regions_test.go
+++ b/pkg/portal/regions_test.go
@@ -40,7 +40,7 @@ func TestRegionListPublic(t *testing.T) {
 	}
 
 	aadAuthenticatedRouter := mux.NewRouter()
-	p.aadAuthenticatedRoutes(aadAuthenticatedRouter)
+	p.aadAuthenticatedRoutes(aadAuthenticatedRouter, nil, nil, nil)
 	w := httptest.NewRecorder()
 	aadAuthenticatedRouter.ServeHTTP(w, req)
 
@@ -251,7 +251,7 @@ func TestRegionListFF(t *testing.T) {
 	}
 
 	aadAuthenticatedRouter := mux.NewRouter()
-	p.aadAuthenticatedRoutes(aadAuthenticatedRouter)
+	p.aadAuthenticatedRoutes(aadAuthenticatedRouter, nil, nil, nil)
 	w := httptest.NewRecorder()
 	aadAuthenticatedRouter.ServeHTTP(w, req)
 
diff --git a/pkg/portal/ssh/proxy_test.go b/pkg/portal/ssh/proxy_test.go
index 532d85019..6992635af 100644
--- a/pkg/portal/ssh/proxy_test.go
+++ b/pkg/portal/ssh/proxy_test.go
@@ -10,6 +10,7 @@ import (
 	"crypto/x509"
 	"fmt"
 	"net"
+	"net/http"
 	"strings"
 	"testing"
 
@@ -412,11 +413,14 @@ func TestProxy(t *testing.T) {
 
 			hook, log := testlog.New()
 
-			s, err := New(nil, nil, log, nil, hostKey, nil, dbOpenShiftClusters, dbPortal, dialer, &mux.Router{})
+			s, err := New(nil, nil, log, nil, hostKey, nil, dbOpenShiftClusters, dbPortal, dialer)
 			if err != nil {
 				t.Fatal(err)
 			}
 
+			r := mux.NewRouter()
+			r.Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/ssh/new").HandlerFunc(s.New)
+
 			done := make(chan struct{})
 
 			go func() {
diff --git a/pkg/portal/ssh/ssh_test.go b/pkg/portal/ssh/ssh_test.go
index c37ca9843..852b3f5dd 100644
--- a/pkg/portal/ssh/ssh_test.go
+++ b/pkg/portal/ssh/ssh_test.go
@@ -138,20 +138,21 @@ func TestNew(t *testing.T) {
 			env := mock_env.NewMockCore(ctrl)
 			env.EXPECT().IsLocalDevelopmentMode().AnyTimes().Return(false)
 
-			aadAuthenticatedRouter := &mux.Router{}
-
-			_, err = New(env, logrus.NewEntry(logrus.StandardLogger()), nil, nil, hostKey, elevatedGroupIDs, nil, dbPortal, nil, aadAuthenticatedRouter)
+			s, err := New(env, logrus.NewEntry(logrus.StandardLogger()), nil, nil, hostKey, elevatedGroupIDs, nil, dbPortal, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
 
+			router := mux.NewRouter()
+			router.Methods(http.MethodPost).Path("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/ssh/new").HandlerFunc(s.New)
+
 			if tt.r != nil {
 				tt.r(r)
 			}
 
 			w := responsewriter.New(r)
 
-			aadAuthenticatedRouter.ServeHTTP(w, r)
+			router.ServeHTTP(w, r)
 
 			portalClient.SetError(nil)