Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions internal/bootstrap/app_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,14 @@ func (app *BootstrapApp) Setup() error {

// Create services
dockerService := service.NewDockerService()
aclsService := service.NewAccessControlsService(dockerService)
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)

// Initialize services
// Initialize services (order matters)
services := []Service{
dockerService,
aclsService,
authService,
oauthBrokerService,
}
Expand Down Expand Up @@ -243,7 +245,7 @@ func (app *BootstrapApp) Setup() error {

proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL,
}, apiRouter, dockerService, authService)
}, apiRouter, aclsService, authService)

userController := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: cookieDomain,
Expand Down
41 changes: 21 additions & 20 deletions internal/controller/proxy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ type ProxyControllerConfig struct {
type ProxyController struct {
config ProxyControllerConfig
router *gin.RouterGroup
docker *service.DockerService
acls *service.AccessControlsService
auth *service.AuthService
}

func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, docker *service.DockerService, auth *service.AuthService) *ProxyController {
func NewProxyController(config ProxyControllerConfig, router *gin.RouterGroup, acls *service.AccessControlsService, auth *service.AuthService) *ProxyController {
return &ProxyController{
config: config,
router: router,
docker: docker,
acls: acls,
auth: auth,
}
}
Expand Down Expand Up @@ -76,28 +76,29 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host")

labels, err := controller.docker.GetLabels(host)
// Get acls
acls, err := controller.acls.GetAccessControls(host)

if err != nil {
log.Error().Err(err).Msg("Failed to get labels from Docker")
log.Error().Err(err).Msg("Failed to get access controls for resource")
controller.handleError(c, req, isBrowser)
return
}

log.Trace().Interface("labels", labels).Msg("Labels for resource")
log.Trace().Interface("acls", acls).Msg("ACLs for resource")

clientIP := c.ClientIP()

if controller.auth.IsBypassedIP(labels.IP, clientIP) {
controller.setHeaders(c, labels)
if controller.auth.IsBypassedIP(acls.IP, clientIP) {
controller.setHeaders(c, acls)
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
})
return
}

authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path)
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)

if err != nil {
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
Expand All @@ -107,15 +108,15 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {

if !authEnabled {
log.Debug().Msg("Authentication disabled for resource, allowing access")
controller.setHeaders(c, labels)
controller.setHeaders(c, acls)
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
})
return
}

if !controller.auth.CheckIP(labels.IP, clientIP) {
if !controller.auth.CheckIP(acls.IP, clientIP) {
if req.Proxy == "nginx" || !isBrowser {
c.JSON(401, gin.H{
"status": 401,
Expand Down Expand Up @@ -160,7 +161,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}

if userContext.IsLoggedIn {
appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels)
appAllowed := controller.auth.IsResourceAllowed(c, userContext, acls)

if !appAllowed {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
Expand Down Expand Up @@ -194,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}

if userContext.OAuth {
groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups)
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)

if !groupOK {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
Expand Down Expand Up @@ -234,7 +235,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))

controller.setHeaders(c, labels)
controller.setHeaders(c, acls)

c.JSON(200, gin.H{
"status": 200,
Expand Down Expand Up @@ -264,21 +265,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode()))
}

func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) {
func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
c.Header("Authorization", c.Request.Header.Get("Authorization"))

headers := utils.ParseHeaders(labels.Response.Headers)
headers := utils.ParseHeaders(acls.Response.Headers)

for key, value := range headers {
log.Debug().Str("header", key).Msg("Setting header")
c.Header(key, value)
}

basicPassword := utils.GetSecret(labels.Response.BasicAuth.Password, labels.Response.BasicAuth.PasswordFile)
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)

if labels.Response.BasicAuth.Username != "" && basicPassword != "" {
log.Debug().Str("username", labels.Response.BasicAuth.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(labels.Response.BasicAuth.Username, basicPassword)))
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
log.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
}
}

Expand Down
7 changes: 6 additions & 1 deletion internal/controller/proxy_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En

assert.NilError(t, dockerService.Init())

// Access controls
accessControlsService := service.NewAccessControlsService(dockerService)

assert.NilError(t, accessControlsService.Init())

// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{
Expand All @@ -59,7 +64,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
// Controller
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: "http://localhost:8080",
}, group, dockerService, authService)
}, group, accessControlsService, authService)
ctrl.SetupRoutes()

return router, recorder, authService
Expand Down
103 changes: 103 additions & 0 deletions internal/service/access_controls_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package service

import (
"os"
"strings"
"tinyauth/internal/config"
"tinyauth/internal/utils/decoders"

"github.com/rs/zerolog/log"
)

type AccessControlsService struct {
docker *DockerService
envACLs config.Apps
}

func NewAccessControlsService(docker *DockerService) *AccessControlsService {
return &AccessControlsService{
docker: docker,
}
}

func (acls *AccessControlsService) Init() error {
acls.envACLs = config.Apps{}
env := os.Environ()
appEnvVars := []string{}

for _, e := range env {
if strings.HasPrefix(e, "TINYAUTH_APPS_") {
appEnvVars = append(appEnvVars, e)
}
}

err := acls.loadEnvACLs(appEnvVars)

if err != nil {
return err
}

return nil
}

func (acls *AccessControlsService) loadEnvACLs(appEnvVars []string) error {
if len(appEnvVars) == 0 {
return nil
}

envAcls := map[string]string{}

for _, e := range appEnvVars {
parts := strings.SplitN(e, "=", 2)
if len(parts) != 2 {
continue
}

// Normalize key, this should use the same normalization logic as in utils/decoders/decoders.go
key := parts[0]
key = strings.ToLower(key)
key = strings.ReplaceAll(key, "_", ".")
value := parts[1]
envAcls[key] = value
}

apps, err := decoders.DecodeLabels(envAcls)

if err != nil {
return err
}

acls.envACLs = apps
return nil
}
Comment thread
steveiliop56 marked this conversation as resolved.

func (acls *AccessControlsService) lookupEnvACLs(appDomain string) *config.App {
if len(acls.envACLs.Apps) == 0 {
return nil
}

for appName, appACLs := range acls.envACLs.Apps {
if appACLs.Config.Domain == appDomain {
return &appACLs
}

if strings.SplitN(appDomain, ".", 2)[0] == appName {
return &appACLs
}
}

return nil
}

func (acls *AccessControlsService) GetAccessControls(appDomain string) (config.App, error) {
// First check environment variables
envACLs := acls.lookupEnvACLs(appDomain)

if envACLs != nil {
log.Debug().Str("domain", appDomain).Msg("Found matching access controls in environment variables")
return *envACLs, nil
}

// Fallback to Docker labels
return acls.docker.GetLabels(appDomain)
}
22 changes: 11 additions & 11 deletions internal/service/auth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,21 +287,21 @@ func (auth *AuthService) UserAuthConfigured() bool {
return len(auth.config.Users) > 0 || auth.ldap != nil
}

func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool {
func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
if context.OAuth {
log.Debug().Msg("Checking OAuth whitelist")
return utils.CheckFilter(labels.OAuth.Whitelist, context.Email)
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
}

if labels.Users.Block != "" {
if acls.Users.Block != "" {
log.Debug().Msg("Checking blocked users")
if utils.CheckFilter(labels.Users.Block, context.Username) {
if utils.CheckFilter(acls.Users.Block, context.Username) {
return false
}
}

log.Debug().Msg("Checking users")
return utils.CheckFilter(labels.Users.Allow, context.Username)
return utils.CheckFilter(acls.Users.Allow, context.Username)
}

func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
Expand Down Expand Up @@ -369,8 +369,8 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
}
}

func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
for _, blocked := range labels.Block {
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
for _, blocked := range acls.Block {
res, err := utils.FilterIP(blocked, ip)
if err != nil {
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
Expand All @@ -382,7 +382,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
}
}

for _, allowed := range labels.Allow {
for _, allowed := range acls.Allow {
res, err := utils.FilterIP(allowed, ip)
if err != nil {
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
Expand All @@ -394,7 +394,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
}
}

if len(labels.Allow) > 0 {
if len(acls.Allow) > 0 {
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
return false
}
Expand All @@ -403,8 +403,8 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool {
return true
}

func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool {
for _, bypassed := range labels.Bypass {
func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
for _, bypassed := range acls.Bypass {
res, err := utils.FilterIP(bypassed, ip)
if err != nil {
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
Expand Down