mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-11 06:48:11 +00:00
feat: initial tailscale backend
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||
@@ -204,7 +205,20 @@ func (app *BootstrapApp) Setup() error {
|
||||
go app.heartbeatRoutine()
|
||||
}
|
||||
|
||||
// If we have an socket path, bind to it
|
||||
// Start listeners and monitor for errors
|
||||
err = app.setupListeners(router)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("server error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) setupListeners(router *gin.Engine) error {
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
// First check socket
|
||||
if app.config.Server.SocketPath != "" {
|
||||
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
|
||||
tlog.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||
@@ -215,21 +229,44 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
|
||||
tlog.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||
if err := router.RunUnix(app.config.Server.SocketPath); err != nil {
|
||||
tlog.App.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
|
||||
return nil
|
||||
go func() {
|
||||
err := router.RunUnix(app.config.Server.SocketPath)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to start server on unix socket: %w", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start server
|
||||
// Then normal TCP listener
|
||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||
tlog.App.Info().Msgf("Starting server on %s", address)
|
||||
if err := router.Run(address); err != nil {
|
||||
tlog.App.Fatal().Err(err).Msg("Failed to start server")
|
||||
|
||||
go func() {
|
||||
err := router.Run(address)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to start server on TCP: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Finally tailscale listener if configured
|
||||
if app.services.tailscaleService.IsConnfigured() {
|
||||
tailscaleListener, err := app.services.tailscaleService.CreateListener()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tailscale listener: %w", err)
|
||||
}
|
||||
|
||||
tlog.App.Info().Msgf("Starting server on Tailscale interface with hostname %s", app.services.tailscaleService.GetHostname())
|
||||
|
||||
go func() {
|
||||
err := router.RunListener(tailscaleListener)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to start server on Tailscale interface: %w", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) heartbeatRoutine() {
|
||||
|
||||
@@ -31,7 +31,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||
|
||||
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
||||
CookieDomain: app.context.cookieDomain,
|
||||
}, app.services.authService, app.services.oauthBrokerService)
|
||||
}, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService)
|
||||
|
||||
err := contextMiddleware.Init()
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||
@@ -13,6 +15,7 @@ type Services struct {
|
||||
ldapService *service.LdapService
|
||||
oauthBrokerService *service.OAuthBrokerService
|
||||
oidcService *service.OIDCService
|
||||
tailscaleService *service.TailscaleService
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) {
|
||||
@@ -68,6 +71,27 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
||||
|
||||
services.oauthBrokerService = oauthBrokerService
|
||||
|
||||
tailscaleHostname := app.config.Tailscale.Hostname
|
||||
|
||||
if tailscaleHostname == "" {
|
||||
tailscaleHostname = fmt.Sprintf("tinyauth-%s", app.context.uuid)
|
||||
}
|
||||
|
||||
tailscaleService := service.NewTailscaleService(service.TailscaleServiceConfig{
|
||||
Dir: app.config.Tailscale.Dir,
|
||||
Hostname: tailscaleHostname,
|
||||
AuthKey: app.config.Tailscale.AuthKey,
|
||||
})
|
||||
|
||||
err = tailscaleService.Init()
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Warn().Err(err).Msg("Failed to setup Tailscale service, starting without it")
|
||||
tailscaleService.Destroy()
|
||||
} else {
|
||||
services.tailscaleService = tailscaleService
|
||||
}
|
||||
|
||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||
Users: app.context.users,
|
||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||
|
||||
@@ -59,6 +59,9 @@ func NewDefaultConfiguration() *Config {
|
||||
Experimental: ExperimentalConfig{
|
||||
ConfigFile: "",
|
||||
},
|
||||
Tailscale: TailscaleConfig{
|
||||
Dir: "./state",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ type Config struct {
|
||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
@@ -209,6 +213,13 @@ type ExperimentalConfig struct {
|
||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||
}
|
||||
|
||||
type TailscaleConfig struct {
|
||||
Dir string `description:"Tailscale state directory." yaml:"dir"`
|
||||
Hostname string `description:"Tailscale hostname." yaml:"hostname"`
|
||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
|
||||
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"`
|
||||
}
|
||||
|
||||
// Config loader options
|
||||
|
||||
const DefaultNamePrefix = "TINYAUTH_"
|
||||
@@ -269,6 +280,13 @@ type UserSearch struct {
|
||||
Type string // local, ldap or unknown
|
||||
}
|
||||
|
||||
type TailscaleWhoisResponse struct {
|
||||
UserID string
|
||||
LoginName string
|
||||
DisplayName string
|
||||
NodeName string
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
Username string
|
||||
Name string
|
||||
@@ -284,6 +302,7 @@ type UserContext struct {
|
||||
OAuthSub string
|
||||
LdapGroups string
|
||||
Attributes UserAttributes
|
||||
Tailscale *TailscaleWhoisResponse
|
||||
}
|
||||
|
||||
// API responses and queries
|
||||
|
||||
@@ -37,16 +37,18 @@ type ContextMiddlewareConfig struct {
|
||||
}
|
||||
|
||||
type ContextMiddleware struct {
|
||||
config ContextMiddlewareConfig
|
||||
auth *service.AuthService
|
||||
broker *service.OAuthBrokerService
|
||||
config ContextMiddlewareConfig
|
||||
auth *service.AuthService
|
||||
broker *service.OAuthBrokerService
|
||||
tailscale *service.TailscaleService
|
||||
}
|
||||
|
||||
func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService) *ContextMiddleware {
|
||||
func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService, tailscale *service.TailscaleService) *ContextMiddleware {
|
||||
return &ContextMiddleware{
|
||||
config: config,
|
||||
auth: auth,
|
||||
broker: broker,
|
||||
config: config,
|
||||
auth: auth,
|
||||
broker: broker,
|
||||
tailscale: tailscale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +71,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if cookie.TotpPending {
|
||||
c.Set("context", &config.UserContext{
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
Email: cookie.Email,
|
||||
@@ -77,6 +79,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
TotpPending: true,
|
||||
TotpEnabled: true,
|
||||
})
|
||||
c.Set("context", &ctx)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -119,7 +122,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
m.auth.RefreshSessionCookie(c)
|
||||
c.Set("context", &config.UserContext{
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
Email: cookie.Email,
|
||||
@@ -128,6 +131,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
LdapGroups: strings.Join(ldapGroups, ","),
|
||||
Attributes: localAttributes,
|
||||
})
|
||||
c.Set("context", &ctx)
|
||||
c.Next()
|
||||
return
|
||||
default:
|
||||
@@ -146,7 +150,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
m.auth.RefreshSessionCookie(c)
|
||||
c.Set("context", &config.UserContext{
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
Email: cookie.Email,
|
||||
@@ -157,6 +161,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
IsLoggedIn: true,
|
||||
OAuth: true,
|
||||
})
|
||||
c.Set("context", &ctx)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -166,7 +171,8 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
|
||||
if basic == nil {
|
||||
tlog.App.Debug().Msg("No basic auth provided")
|
||||
c.Next()
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{})
|
||||
c.Set("context", &ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -218,7 +224,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
email = user.Attributes.Email
|
||||
}
|
||||
|
||||
c.Set("context", &config.UserContext{
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{
|
||||
Username: user.Username,
|
||||
Name: name,
|
||||
Email: email,
|
||||
@@ -227,6 +233,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
IsBasicAuth: true,
|
||||
Attributes: user.Attributes,
|
||||
})
|
||||
c.Set("context", &ctx)
|
||||
c.Next()
|
||||
return
|
||||
case "ldap":
|
||||
@@ -240,7 +247,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("context", &config.UserContext{
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{
|
||||
Username: basic.Username,
|
||||
Name: utils.Capitalize(basic.Username),
|
||||
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||
@@ -249,10 +256,14 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||
IsBasicAuth: true,
|
||||
})
|
||||
c.Set("context", &ctx)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// unreachable but just in case
|
||||
ctx := m.addTailscaleContext(c, config.UserContext{})
|
||||
c.Set("context", &ctx)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -265,3 +276,23 @@ func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *ContextMiddleware) addTailscaleContext(c *gin.Context, ctx config.UserContext) config.UserContext {
|
||||
if !m.tailscale.IsConnfigured() {
|
||||
return ctx
|
||||
}
|
||||
|
||||
ip := c.Request.RemoteAddr
|
||||
|
||||
whois, err := m.tailscale.Whois(c, ip)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Warn().Err(err).Msg("Error performing Tailscale whois")
|
||||
return ctx
|
||||
}
|
||||
|
||||
tlog.App.Trace().Interface("whois", whois).Msg("Tailscale whois result")
|
||||
|
||||
ctx.Tailscale = &whois
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
type TailscaleServiceConfig struct {
|
||||
Dir string
|
||||
Hostname string
|
||||
AuthKey string
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
type TailscaleService struct {
|
||||
config TailscaleServiceConfig
|
||||
srv *tsnet.Server
|
||||
lc *local.Client
|
||||
ln *net.Listener
|
||||
}
|
||||
|
||||
func NewTailscaleService(config TailscaleServiceConfig) *TailscaleService {
|
||||
return &TailscaleService{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) Init() error {
|
||||
srv := new(tsnet.Server)
|
||||
|
||||
// node options
|
||||
srv.Dir = ts.config.Dir
|
||||
srv.Hostname = ts.config.Hostname
|
||||
srv.AuthKey = ts.config.AuthKey
|
||||
srv.Ephemeral = ts.config.Ephemeral
|
||||
|
||||
// redirect logs to zerolog
|
||||
srv.Logf = tlog.App.Printf
|
||||
srv.UserLogf = tlog.App.Printf
|
||||
|
||||
err := srv.Start()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts.srv = srv
|
||||
|
||||
lc, err := srv.LocalClient()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts.lc = lc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) Destroy() error {
|
||||
if ts.ln != nil {
|
||||
(*ts.ln).Close()
|
||||
}
|
||||
if ts.srv != nil {
|
||||
return ts.srv.Close()
|
||||
}
|
||||
ts.ln = nil
|
||||
ts.lc = nil
|
||||
ts.srv = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) Whois(ctx context.Context, addr string) (config.TailscaleWhoisResponse, error) {
|
||||
who, err := ts.lc.WhoIs(ctx, addr)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, local.ErrPeerNotFound) {
|
||||
return config.TailscaleWhoisResponse{}, nil
|
||||
}
|
||||
return config.TailscaleWhoisResponse{}, err
|
||||
}
|
||||
|
||||
res := config.TailscaleWhoisResponse{
|
||||
UserID: who.UserProfile.ID.String(),
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
NodeName: who.Node.Name,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) CreateListener() (net.Listener, error) {
|
||||
if ts.ln != nil {
|
||||
return *ts.ln, nil
|
||||
}
|
||||
ln, err := ts.srv.ListenTLS("tcp", ":443")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts.ln = &ln
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) IsConnfigured() bool {
|
||||
return ts.srv != nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) GetHostname() string {
|
||||
return ts.srv.Hostname
|
||||
}
|
||||
Reference in New Issue
Block a user