This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/outpost/pkg/server/api.go
2021-01-16 19:20:27 +01:00

226 lines
5.9 KiB
Go

package server
import (
"crypto/sha512"
"encoding/hex"
"fmt"
"math/rand"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/BeryJu/authentik/outpost/pkg"
"github.com/BeryJu/authentik/outpost/pkg/client"
"github.com/BeryJu/authentik/outpost/pkg/client/outposts"
"github.com/getsentry/sentry-go"
"github.com/go-openapi/runtime"
"github.com/recws-org/recws"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
log "github.com/sirupsen/logrus"
)
const ConfigLogLevel = "log_level"
const ConfigErrorReportingEnabled = "error_reporting_enabled"
const ConfigErrorReportingEnvironment = "error_reporting_environment"
// APIController main controller which connects to the authentik api via http and ws
type APIController struct {
client *client.Authentik
auth runtime.ClientAuthInfoWriter
token string
server *Server
commonOpts *options.Options
lastBundleHash string
logger *log.Entry
reloadOffset time.Duration
wsConn *recws.RecConn
}
func getCommonOptions() *options.Options {
commonOpts := options.NewOptions()
commonOpts.Cookie.Name = "authentik_proxy"
commonOpts.Cookie.Expire = 24 * time.Hour
commonOpts.EmailDomains = []string{"*"}
commonOpts.ProviderType = "oidc"
commonOpts.ProxyPrefix = "/akprox"
commonOpts.Logging.SilencePing = true
commonOpts.SetAuthorization = false
commonOpts.Scope = "openid email profile ak_proxy"
return commonOpts
}
func doGlobalSetup(config map[string]interface{}) {
log.SetFormatter(&log.JSONFormatter{})
switch config[ConfigLogLevel].(string) {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warning":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
default:
log.SetLevel(log.DebugLevel)
}
log.WithField("version", pkg.VERSION).Info("Starting authentik proxy")
var dsn string
if config[ConfigErrorReportingEnabled].(bool) {
dsn = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
log.Debug("Error reporting enabled")
}
err := sentry.Init(sentry.ClientOptions{
Dsn: dsn,
Environment: config[ConfigErrorReportingEnvironment].(string),
})
if err != nil {
log.Fatalf("sentry.Init: %s", err)
}
defer sentry.Flush(2 * time.Second)
}
func getTLSTransport() http.RoundTripper {
value, set := os.LookupEnv("AUTHENTIK_INSECURE")
if !set {
value = "false"
}
tlsTransport, err := httptransport.TLSTransport(httptransport.TLSClientOptions{
InsecureSkipVerify: strings.ToLower(value) == "true",
})
if err != nil {
panic(err)
}
return tlsTransport
}
// NewAPIController initialise new API Controller instance from URL and API token
func NewAPIController(pbURL url.URL, token string) *APIController {
transport := httptransport.New(pbURL.Host, client.DefaultBasePath, []string{pbURL.Scheme})
transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION))
// create the transport
auth := httptransport.BasicAuth("", token)
// create the API client, with the transport
apiClient := client.New(transport, strfmt.Default)
// Because we don't know the outpost UUID, we simply do a list and pick the first
// The service account this token belongs to should only have access to a single outpost
outposts, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth)
if err != nil {
panic(err)
}
outpost := outposts.Payload.Results[0]
doGlobalSetup(outpost.Config.(map[string]interface{}))
ac := &APIController{
client: apiClient,
auth: auth,
token: token,
logger: log.WithField("component", "api-controller"),
commonOpts: getCommonOptions(),
server: NewServer(),
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
lastBundleHash: "",
}
ac.logger.Debugf("HA Reload offset: %s", ac.reloadOffset)
ac.initWS(pbURL, outpost.Pk)
return ac
}
func (a *APIController) bundleProviders() ([]*providerBundle, error) {
providers, err := a.client.Outposts.OutpostsProxyList(outposts.NewOutpostsProxyListParams(), a.auth)
if err != nil {
a.logger.WithError(err).Error("Failed to fetch providers")
return nil, err
}
// Check provider hash to see if anything is changed
hasher := sha512.New()
bin, _ := providers.Payload.MarshalBinary()
hash := hex.EncodeToString(hasher.Sum(bin))
if hash == a.lastBundleHash {
return nil, nil
}
a.lastBundleHash = hash
bundles := make([]*providerBundle, len(providers.Payload.Results))
for idx, provider := range providers.Payload.Results {
externalHost, err := url.Parse(*provider.ExternalHost)
if err != nil {
log.WithError(err).Warning("Failed to parse URL, skipping provider")
}
bundles[idx] = &providerBundle{
a: a,
Host: externalHost.Host,
}
bundles[idx].Build(provider)
}
return bundles, nil
}
func (a *APIController) updateHTTPServer(bundles []*providerBundle) {
newMap := make(map[string]*providerBundle)
for _, bundle := range bundles {
newMap[bundle.Host] = bundle
}
a.logger.Debug("Swapped maps")
a.server.Handlers = newMap
}
// UpdateIfRequired Updates the HTTP Server config if required, automatically swaps the handlers
func (a *APIController) UpdateIfRequired() error {
bundles, err := a.bundleProviders()
if err != nil {
return err
}
if bundles == nil {
a.logger.Debug("Providers have not changed, not updating")
return nil
}
a.updateHTTPServer(bundles)
return nil
}
// Start Starts all handlers, non-blocking
func (a *APIController) Start() error {
err := a.UpdateIfRequired()
if err != nil {
return err
}
go func() {
a.logger.Debug("Starting HTTP Server...")
a.server.ServeHTTP()
}()
go func() {
a.logger.Debug("Starting HTTPs Server...")
a.server.ServeHTTPS()
}()
go func() {
a.logger.Debug("Starting WS Handler...")
a.startWSHandler()
}()
go func() {
a.logger.Debug("Starting WS Health notifier...")
a.startWSHealth()
}()
return nil
}