зеркало из https://github.com/Azure/ARO-RP.git
362 строки
9.6 KiB
Go
362 строки
9.6 KiB
Go
package portal
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the Apache License 2.0.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/csrf"
|
|
"github.com/gorilla/mux"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/Azure/ARO-RP/pkg/api/validate"
|
|
"github.com/Azure/ARO-RP/pkg/database"
|
|
"github.com/Azure/ARO-RP/pkg/env"
|
|
frontendmiddleware "github.com/Azure/ARO-RP/pkg/frontend/middleware"
|
|
"github.com/Azure/ARO-RP/pkg/metrics"
|
|
"github.com/Azure/ARO-RP/pkg/portal/cluster"
|
|
"github.com/Azure/ARO-RP/pkg/portal/kubeconfig"
|
|
"github.com/Azure/ARO-RP/pkg/portal/middleware"
|
|
"github.com/Azure/ARO-RP/pkg/portal/prometheus"
|
|
"github.com/Azure/ARO-RP/pkg/portal/ssh"
|
|
"github.com/Azure/ARO-RP/pkg/proxy"
|
|
"github.com/Azure/ARO-RP/pkg/util/heartbeat"
|
|
"github.com/Azure/ARO-RP/pkg/util/oidc"
|
|
)
|
|
|
|
type Runnable interface {
|
|
Run(context.Context) error
|
|
}
|
|
|
|
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
|
|
|
|
hostname string
|
|
servingKey *rsa.PrivateKey
|
|
servingCerts []*x509.Certificate
|
|
clientID string
|
|
clientKey *rsa.PrivateKey
|
|
clientCerts []*x509.Certificate
|
|
sessionKey []byte
|
|
sshKey *rsa.PrivateKey
|
|
|
|
groupIDs []string
|
|
elevatedGroupIDs []string
|
|
|
|
dbPortal database.Portal
|
|
dbOpenShiftClusters database.OpenShiftClusters
|
|
|
|
dialer proxy.Dialer
|
|
|
|
templateV1 *template.Template
|
|
templateV2 *template.Template
|
|
|
|
aad middleware.AAD
|
|
|
|
m metrics.Emitter
|
|
}
|
|
|
|
func NewPortal(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,
|
|
servingCerts []*x509.Certificate,
|
|
clientID string,
|
|
clientKey *rsa.PrivateKey,
|
|
clientCerts []*x509.Certificate,
|
|
sessionKey []byte,
|
|
sshKey *rsa.PrivateKey,
|
|
groupIDs []string,
|
|
elevatedGroupIDs []string,
|
|
dbOpenShiftClusters database.OpenShiftClusters,
|
|
dbPortal database.Portal,
|
|
dialer proxy.Dialer,
|
|
m metrics.Emitter,
|
|
) Runnable {
|
|
return &portal{
|
|
env: env,
|
|
audit: audit,
|
|
log: log,
|
|
baseAccessLog: baseAccessLog,
|
|
l: l,
|
|
sshl: sshl,
|
|
verifier: verifier,
|
|
|
|
hostname: hostname,
|
|
servingKey: servingKey,
|
|
servingCerts: servingCerts,
|
|
clientID: clientID,
|
|
clientKey: clientKey,
|
|
clientCerts: clientCerts,
|
|
sessionKey: sessionKey,
|
|
sshKey: sshKey,
|
|
|
|
groupIDs: groupIDs,
|
|
elevatedGroupIDs: elevatedGroupIDs,
|
|
|
|
dbOpenShiftClusters: dbOpenShiftClusters,
|
|
dbPortal: dbPortal,
|
|
|
|
dialer: dialer,
|
|
|
|
m: m,
|
|
}
|
|
}
|
|
|
|
func (p *portal) setupRouter() error {
|
|
if p.baseRouter != nil {
|
|
return fmt.Errorf("can't setup twice")
|
|
}
|
|
|
|
r := mux.NewRouter()
|
|
r.Use(middleware.Panic(p.log))
|
|
|
|
assetv1, err := Asset("v1/build/index.html")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
assetv2, err := Asset("v2/build/index.html")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.templateV1, err = template.New("index.html").Parse(string(assetv1))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.templateV2, err = template.New("index.html").Parse(string(assetv2))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
unauthenticatedRouter := r.NewRoute().Subrouter()
|
|
p.unauthenticatedRoutes(unauthenticatedRouter)
|
|
|
|
allGroups := append([]string{}, p.groupIDs...)
|
|
allGroups = append(allGroups, p.elevatedGroupIDs...)
|
|
|
|
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
|
|
}
|
|
|
|
aadAuthenticatedRouter := r.NewRoute().Subrouter()
|
|
aadAuthenticatedRouter.Use(p.aad.AAD)
|
|
aadAuthenticatedRouter.Use(middleware.Log(p.env, p.audit, p.baseAccessLog))
|
|
aadAuthenticatedRouter.Use(p.aad.CheckAuthentication)
|
|
aadAuthenticatedRouter.Use(csrf.Protect(p.sessionKey, csrf.SameSite(csrf.SameSiteStrictMode), csrf.MaxAge(0), csrf.Path("/")))
|
|
|
|
p.aadAuthenticatedRoutes(aadAuthenticatedRouter)
|
|
|
|
p.baseRouter = r
|
|
p.publicRouter = unauthenticatedRouter
|
|
p.authenticatedRouter = aadAuthenticatedRouter
|
|
|
|
return 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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ssh.Run()
|
|
if err != nil {
|
|
return 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)
|
|
|
|
prometheus.New(p.log, p.dbOpenShiftClusters, p.dialer, p.authenticatedRouter)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *portal) Run(ctx context.Context) error {
|
|
config := &tls.Config{
|
|
Certificates: []tls.Certificate{
|
|
{
|
|
PrivateKey: p.servingKey,
|
|
},
|
|
},
|
|
NextProtos: []string{"h2", "http/1.1"},
|
|
CipherSuites: []uint16{
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
},
|
|
PreferServerCipherSuites: true,
|
|
SessionTicketsDisabled: true,
|
|
MinVersion: tls.VersionTLS12,
|
|
CurvePreferences: []tls.CurveID{
|
|
tls.CurveP256,
|
|
tls.X25519,
|
|
},
|
|
}
|
|
|
|
for _, cert := range p.servingCerts {
|
|
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
|
|
}
|
|
}
|
|
|
|
s := &http.Server{
|
|
Handler: frontendmiddleware.Lowercase(p.baseRouter),
|
|
ReadTimeout: 10 * time.Second,
|
|
IdleTimeout: 2 * time.Minute,
|
|
ErrorLog: log.New(p.log.Writer(), "", 0),
|
|
BaseContext: func(net.Listener) context.Context { return ctx },
|
|
}
|
|
|
|
go heartbeat.EmitHeartbeat(p.log, p.m, "portal.heartbeat", nil, func() bool { return true })
|
|
|
|
return s.Serve(tls.NewListener(p.l, config))
|
|
}
|
|
|
|
func (p *portal) unauthenticatedRoutes(r *mux.Router) {
|
|
logger := middleware.Log(p.env, p.audit, p.baseAccessLog)
|
|
|
|
r.NewRoute().Methods(http.MethodGet).Path("/healthz/ready").Handler(logger(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})))
|
|
}
|
|
|
|
func (p *portal) aadAuthenticatedRoutes(r *mux.Router) {
|
|
for _, name := range AssetNames() {
|
|
if name == "v1/build/index.html" {
|
|
r.NewRoute().Methods(http.MethodGet).Path("/").HandlerFunc(p.index)
|
|
continue
|
|
}
|
|
|
|
if name == "v2/build/index.html" {
|
|
r.NewRoute().Methods(http.MethodGet).Path("/v2").HandlerFunc(p.indexV2)
|
|
continue
|
|
}
|
|
|
|
fmtName := strings.TrimPrefix(name, "v1/build/")
|
|
fmtName = strings.TrimPrefix(fmtName, "v2/build/")
|
|
|
|
r.NewRoute().Methods(http.MethodGet).Path("/" + fmtName).HandlerFunc(p.serve(name))
|
|
}
|
|
|
|
r.NewRoute().Methods(http.MethodGet).Path("/api/clusters").HandlerFunc(p.clusters)
|
|
r.NewRoute().Methods(http.MethodGet).Path("/api/info").HandlerFunc(p.info)
|
|
|
|
// Cluster-specific routes
|
|
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{name}/clusteroperators").HandlerFunc(p.clusterOperators)
|
|
r.NewRoute().Methods(http.MethodGet).Path("/api/{subscription}/{resourceGroup}/{name}").HandlerFunc(p.clusterInfo)
|
|
}
|
|
|
|
func (p *portal) index(w http.ResponseWriter, r *http.Request) {
|
|
buf := &bytes.Buffer{}
|
|
|
|
err := p.templateV1.ExecuteTemplate(buf, "index.html", map[string]interface{}{
|
|
"location": p.env.Location(),
|
|
csrf.TemplateTag: csrf.TemplateField(r),
|
|
})
|
|
if err != nil {
|
|
p.internalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(buf.Bytes()))
|
|
}
|
|
|
|
func (p *portal) indexV2(w http.ResponseWriter, r *http.Request) {
|
|
buf := &bytes.Buffer{}
|
|
|
|
err := p.templateV2.ExecuteTemplate(buf, "index.html", map[string]interface{}{
|
|
"location": p.env.Location(),
|
|
csrf.TemplateTag: csrf.TemplateField(r),
|
|
})
|
|
if err != nil {
|
|
p.internalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(buf.Bytes()))
|
|
}
|
|
|
|
// makeFetcher creates a cluster.FetchClient suitable for use by the Portal REST API
|
|
func (p *portal) makeFetcher(ctx context.Context, r *http.Request) (cluster.FetchClient, error) {
|
|
resourceID := strings.Join(strings.Split(r.URL.Path, "/")[:9], "/")
|
|
if !validate.RxClusterID.MatchString(resourceID) {
|
|
return nil, fmt.Errorf("invalid resource ID")
|
|
}
|
|
|
|
doc, err := p.dbOpenShiftClusters.Get(ctx, resourceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// In development mode, we can have localhost "fake" APIServers which don't
|
|
// get proxied, so use a direct dialer for this
|
|
var dialer proxy.Dialer
|
|
if p.env.IsLocalDevelopmentMode() && doc.OpenShiftCluster.Properties.APIServerProfile.IP == "127.0.0.1" {
|
|
dialer, err = proxy.NewDialer(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
dialer = p.dialer
|
|
}
|
|
|
|
return cluster.NewFetchClient(p.log, dialer, doc)
|
|
}
|
|
|
|
func (p *portal) serve(path string) func(w http.ResponseWriter, r *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
b, err := Asset(path)
|
|
if err != nil {
|
|
p.internalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
http.ServeContent(w, r, path, time.Time{}, bytes.NewReader(b))
|
|
}
|
|
}
|
|
|
|
func (p *portal) internalServerError(w http.ResponseWriter, err error) {
|
|
p.log.Warn(err)
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|