mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-12 15:28:20 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d38784715d | |||
| 11b6155b9e | |||
| e739aa8fd0 | |||
| d5009070e3 | |||
| 548d97fa62 | |||
| 3d9c81d7a0 | |||
| 4e760e8397 | |||
| 02b48aa165 | |||
| 886f9a84d6 | |||
| 74aca0f521 | |||
| a76141a99d | |||
| c7e9fade03 | |||
| 9fccb63097 | |||
| 8c8d56f87c | |||
| 71ddfbbdba | |||
| b73a9db061 | |||
| 0958c3b864 | |||
| e214d6d8d4 | |||
| 55b53c77bf | |||
| 112a30f6b2 | |||
| 592c221b2d | |||
| cc357f35ef |
@@ -38,6 +38,6 @@ jobs:
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
|
||||
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -65,7 +65,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
A big thank you to the following people for providing me with more coffee:
|
||||
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <a href="https://github.com/apearson"><img src="https://github.com/apearson.png" width="64px" alt="User avatar: apearson" /></a> <!-- sponsors -->
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <!-- sponsors -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ func (app *BootstrapApp) setupServices() error {
|
||||
labelProvider = dockerService
|
||||
}
|
||||
|
||||
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
||||
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
|
||||
app.services.accessControlService = accessControlsService
|
||||
|
||||
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
||||
|
||||
@@ -101,7 +101,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if controller.acls.IsIPBypassed(clientIP, acls) {
|
||||
if controller.auth.IsBypassedIP(clientIP, acls) {
|
||||
controller.setHeaders(c, acls)
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
@@ -110,7 +110,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
authEnabled := controller.acls.IsAuthEnabled(proxyCtx.Path, acls)
|
||||
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls)
|
||||
|
||||
if err != nil {
|
||||
controller.log.App.Error().Err(err).Msg("Failed to determine if authentication is enabled for resource")
|
||||
controller.handleError(c, proxyCtx)
|
||||
return
|
||||
}
|
||||
|
||||
if !authEnabled {
|
||||
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
||||
@@ -122,7 +128,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.acls.IsIPAllowed(clientIP, acls) {
|
||||
if !controller.auth.CheckIP(clientIP, acls) {
|
||||
queries, err := query.Values(UnauthorizedQuery{
|
||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||
IP: clientIP,
|
||||
@@ -159,7 +165,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if userContext.Authenticated {
|
||||
userAllowed := controller.acls.IsUserAllowed(*userContext, acls)
|
||||
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
||||
|
||||
if !userAllowed {
|
||||
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
||||
@@ -199,9 +205,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
var groupOK bool
|
||||
|
||||
if userContext.IsOAuth() {
|
||||
groupOK = controller.acls.IsInOAuthGroup(*userContext, acls)
|
||||
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls)
|
||||
} else {
|
||||
groupOK = controller.acls.IsInLDAPGroup(*userContext, acls)
|
||||
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
|
||||
}
|
||||
|
||||
if !groupOK {
|
||||
|
||||
@@ -24,6 +24,33 @@ func TestProxyController(t *testing.T) {
|
||||
|
||||
cfg, runtime := test.CreateTestConfigs(t)
|
||||
|
||||
acls := map[string]model.App{
|
||||
"app_path_allow": {
|
||||
Config: model.AppConfig{
|
||||
Domain: "path-allow.example.com",
|
||||
},
|
||||
Path: model.AppPath{
|
||||
Allow: "/allowed",
|
||||
},
|
||||
},
|
||||
"app_user_allow": {
|
||||
Config: model.AppConfig{
|
||||
Domain: "user-allow.example.com",
|
||||
},
|
||||
Users: model.AppUsers{
|
||||
Allow: "testuser",
|
||||
},
|
||||
},
|
||||
"ip_bypass": {
|
||||
Config: model.AppConfig{
|
||||
Domain: "ip-bypass.example.com",
|
||||
},
|
||||
IP: model.AppIP{
|
||||
Bypass: []string{"10.10.10.10"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const browserUserAgent = `
|
||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
|
||||
|
||||
@@ -364,7 +391,7 @@ func TestProxyController(t *testing.T) {
|
||||
|
||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||
aclsService := service.NewAccessControlsService(log, cfg, nil)
|
||||
aclsService := service.NewAccessControlsService(log, nil, acls)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
|
||||
@@ -189,9 +189,6 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
|
||||
if search.Type == model.UserLDAP {
|
||||
sessionCookie.Provider = "ldap"
|
||||
if search.Email != "" {
|
||||
sessionCookie.Email = search.Email
|
||||
}
|
||||
}
|
||||
|
||||
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
||||
|
||||
@@ -160,12 +160,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
|
||||
|
||||
userContext.LDAP.Groups = user.Groups
|
||||
userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username)
|
||||
|
||||
userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.runtime.CookieDomain)
|
||||
if search.Email != "" {
|
||||
userContext.LDAP.Email = search.Email
|
||||
}
|
||||
|
||||
case model.ProviderOAuth:
|
||||
_, exists := m.broker.GetService(userContext.OAuth.ID)
|
||||
|
||||
@@ -243,15 +238,11 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
|
||||
BaseContext: model.BaseContext{
|
||||
Username: username,
|
||||
Name: utils.Capitalize(username),
|
||||
Email: utils.CompileUserEmail(username, m.runtime.CookieDomain),
|
||||
},
|
||||
Groups: user.Groups,
|
||||
}
|
||||
userContext.Provider = model.ProviderLDAP
|
||||
|
||||
userContext.LDAP.Email = utils.CompileUserEmail(username, m.runtime.CookieDomain)
|
||||
if search.Email != "" {
|
||||
userContext.LDAP.Email = search.Email
|
||||
}
|
||||
}
|
||||
|
||||
userContext.Authenticated = true
|
||||
|
||||
@@ -24,9 +24,6 @@ func NewDefaultConfiguration() *Config {
|
||||
SessionMaxLifetime: 0, // disabled
|
||||
LoginTimeout: 300, // 5 minutes
|
||||
LoginMaxRetries: 3,
|
||||
ACLs: ACLsConfig{
|
||||
Policy: "allow",
|
||||
},
|
||||
},
|
||||
UI: UIConfig{
|
||||
Title: "Tinyauth",
|
||||
@@ -117,7 +114,6 @@ type AuthConfig struct {
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
||||
}
|
||||
|
||||
type UserAttributes struct {
|
||||
@@ -227,10 +223,6 @@ type OIDCClientConfig struct {
|
||||
Name string `description:"Client name in UI." yaml:"name"`
|
||||
}
|
||||
|
||||
type ACLsConfig struct {
|
||||
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
|
||||
}
|
||||
|
||||
// ACLs
|
||||
|
||||
type Apps struct {
|
||||
|
||||
@@ -21,6 +21,5 @@ type LocalUser struct {
|
||||
|
||||
type UserSearch struct {
|
||||
Username string
|
||||
Email string // used for LDAP, we can't throw it to LDAPUser because it would need another cache or an LDAP lookup every time
|
||||
Type UserSearchType
|
||||
}
|
||||
|
||||
@@ -1,82 +1,44 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
type AccessControlPolicy string
|
||||
|
||||
const (
|
||||
PolicyAllow AccessControlPolicy = "allow"
|
||||
PolicyDeny AccessControlPolicy = "deny"
|
||||
)
|
||||
|
||||
func accessControlPolicyFromString(s string) (AccessControlPolicy, bool) {
|
||||
switch strings.ToLower(s) {
|
||||
case "allow":
|
||||
return PolicyAllow, true
|
||||
case "deny":
|
||||
return PolicyDeny, true
|
||||
default:
|
||||
return PolicyAllow, false
|
||||
}
|
||||
}
|
||||
|
||||
type LabelProvider interface {
|
||||
GetLabels(appDomain string) (*model.App, error)
|
||||
}
|
||||
|
||||
type AccessControlsService struct {
|
||||
log *logger.Logger
|
||||
config model.Config
|
||||
labelProvider *LabelProvider
|
||||
policy AccessControlPolicy
|
||||
static map[string]model.App
|
||||
}
|
||||
|
||||
func NewAccessControlsService(
|
||||
log *logger.Logger,
|
||||
config model.Config,
|
||||
labelProvider *LabelProvider) *AccessControlsService {
|
||||
|
||||
service := AccessControlsService{
|
||||
labelProvider *LabelProvider,
|
||||
static map[string]model.App) *AccessControlsService {
|
||||
return &AccessControlsService{
|
||||
log: log,
|
||||
config: config,
|
||||
labelProvider: labelProvider,
|
||||
static: static,
|
||||
}
|
||||
}
|
||||
|
||||
policy, ok := accessControlPolicyFromString(config.Auth.ACLs.Policy)
|
||||
|
||||
if !ok {
|
||||
log.App.Warn().Str("policy", config.Auth.ACLs.Policy).Msg("Invalid ACL policy in config, defaulting to 'allow'")
|
||||
}
|
||||
|
||||
if policy == PolicyAllow {
|
||||
log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
|
||||
} else {
|
||||
log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
|
||||
}
|
||||
|
||||
service.policy = policy
|
||||
|
||||
return &service
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||
var appAcls *model.App
|
||||
for app, config := range service.config.Apps {
|
||||
for app, config := range acls.static {
|
||||
if config.Config.Domain == domain {
|
||||
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||
acls.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||
appAcls = &config
|
||||
break // If we find a match by domain, we can stop searching
|
||||
}
|
||||
|
||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||
service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||
acls.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||
appAcls = &config
|
||||
break // If we find a match by app name, we can stop searching
|
||||
}
|
||||
@@ -84,193 +46,20 @@ func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App
|
||||
return appAcls
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||
// First check in the static config
|
||||
app := service.lookupStaticACLs(domain)
|
||||
app := acls.lookupStaticACLs(domain)
|
||||
|
||||
if app != nil {
|
||||
service.log.App.Debug().Msg("Using static ACLs for app")
|
||||
acls.log.App.Debug().Msg("Using static ACLs for app")
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// If we have a label provider configured, try to get ACLs from it
|
||||
if service.labelProvider != nil {
|
||||
return (*service.labelProvider).GetLabels(domain)
|
||||
if acls.labelProvider != nil {
|
||||
return (*acls.labelProvider).GetLabels(domain)
|
||||
}
|
||||
|
||||
// no labels
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) IsUserAllowed(context model.UserContext, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return service.policyResult(true)
|
||||
}
|
||||
|
||||
if context.Provider == model.ProviderOAuth {
|
||||
service.log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
|
||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
||||
}
|
||||
|
||||
if acls.Users.Block != "" {
|
||||
service.log.App.Debug().Msg("Checking users block list")
|
||||
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
service.log.App.Debug().Msg("Checking users allow list")
|
||||
return service.policyResult(utils.CheckFilter(acls.Users.Allow, context.GetUsername()))
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) IsInOAuthGroup(context model.UserContext, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if !context.IsOAuth() {
|
||||
service.log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
||||
service.log.App.Debug().Str("provider", context.OAuth.ID).Msg("Provider override detected, skipping group check")
|
||||
return true
|
||||
}
|
||||
|
||||
for _, userGroup := range context.OAuth.Groups {
|
||||
if utils.CheckFilter(acls.OAuth.Groups, strings.TrimSpace(userGroup)) {
|
||||
service.log.App.Trace().Str("group", userGroup).Str("required", acls.OAuth.Groups).Msg("User group matched")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
service.log.App.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) IsInLDAPGroup(context model.UserContext, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if !context.IsLDAP() {
|
||||
service.log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, userGroup := range context.LDAP.Groups {
|
||||
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
||||
service.log.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
service.log.App.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) IsAuthEnabled(uri string, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if acls.Path.Block != "" {
|
||||
regex, err := regexp.Compile(acls.Path.Block)
|
||||
|
||||
if err != nil {
|
||||
service.log.App.Error().Err(err).Msg("Failed to compile block regex")
|
||||
return true
|
||||
}
|
||||
|
||||
if !regex.MatchString(uri) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if acls.Path.Allow != "" {
|
||||
regex, err := regexp.Compile(acls.Path.Allow)
|
||||
|
||||
if err != nil {
|
||||
service.log.App.Error().Err(err).Msg("Failed to compile allow regex")
|
||||
return true
|
||||
}
|
||||
|
||||
if regex.MatchString(uri) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) IsIPAllowed(ip string, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return service.policyResult(true)
|
||||
}
|
||||
|
||||
// Merge the global and app IP filter
|
||||
blockedIps := append(acls.IP.Block, service.config.Auth.IP.Block...)
|
||||
allowedIPs := append(acls.IP.Allow, service.config.Auth.IP.Allow...)
|
||||
|
||||
for _, blocked := range blockedIps {
|
||||
res, err := utils.FilterIP(blocked, ip)
|
||||
if err != nil {
|
||||
service.log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
service.log.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in block list, denying access")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, allowed := range allowedIPs {
|
||||
res, err := utils.FilterIP(allowed, ip)
|
||||
if err != nil {
|
||||
service.log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
service.log.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowedIPs) > 0 {
|
||||
service.log.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||
return false
|
||||
}
|
||||
|
||||
service.log.App.Debug().Str("ip", ip).Msg("IP not in block or allow list, allowing access")
|
||||
return service.policyResult(true)
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) IsIPBypassed(ip string, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, bypassed := range acls.IP.Bypass {
|
||||
res, err := utils.FilterIP(bypassed, ip)
|
||||
if err != nil {
|
||||
service.log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
service.log.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
service.log.App.Debug().Str("ip", ip).Msg("IP not in bypass list, proceeding with authentication")
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *AccessControlsService) policyResult(result bool) bool {
|
||||
if service.policy == PolicyAllow {
|
||||
return result
|
||||
} else {
|
||||
return !result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -128,7 +130,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error)
|
||||
}
|
||||
|
||||
if auth.ldap != nil {
|
||||
userDN, email, err := auth.ldap.GetUserInfo(username)
|
||||
userDN, err := auth.ldap.GetUserDN(username)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ldap user: %w", err)
|
||||
@@ -136,7 +138,6 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error)
|
||||
|
||||
return &model.UserSearch{
|
||||
Username: userDN,
|
||||
Email: email,
|
||||
Type: model.UserLDAP,
|
||||
}, nil
|
||||
}
|
||||
@@ -452,6 +453,171 @@ func (auth *AuthService) LDAPAuthConfigured() bool {
|
||||
return auth.ldap != nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if context.Provider == model.ProviderOAuth {
|
||||
auth.log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
|
||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
||||
}
|
||||
|
||||
if acls.Users.Block != "" {
|
||||
auth.log.App.Debug().Msg("Checking users block list")
|
||||
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
auth.log.App.Debug().Msg("Checking users allow list")
|
||||
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if !context.IsOAuth() {
|
||||
auth.log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
||||
auth.log.App.Debug().Str("provider", context.OAuth.ID).Msg("Provider override detected, skipping group check")
|
||||
return true
|
||||
}
|
||||
|
||||
for _, userGroup := range context.OAuth.Groups {
|
||||
if utils.CheckFilter(acls.OAuth.Groups, strings.TrimSpace(userGroup)) {
|
||||
auth.log.App.Trace().Str("group", userGroup).Str("required", acls.OAuth.Groups).Msg("User group matched")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
auth.log.App.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if !context.IsLDAP() {
|
||||
auth.log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, userGroup := range context.LDAP.Groups {
|
||||
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
||||
auth.log.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
auth.log.App.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsAuthEnabled(uri string, acls *model.App) (bool, error) {
|
||||
if acls == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check for block list
|
||||
if acls.Path.Block != "" {
|
||||
regex, err := regexp.Compile(acls.Path.Block)
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if !regex.MatchString(uri) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for allow list
|
||||
if acls.Path.Allow != "" {
|
||||
regex, err := regexp.Compile(acls.Path.Allow)
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if regex.MatchString(uri) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) CheckIP(ip string, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Merge the global and app IP filter
|
||||
blockedIps := append(auth.config.Auth.IP.Block, acls.IP.Block...)
|
||||
allowedIPs := append(auth.config.Auth.IP.Allow, acls.IP.Allow...)
|
||||
|
||||
for _, blocked := range blockedIps {
|
||||
res, err := utils.FilterIP(blocked, ip)
|
||||
if err != nil {
|
||||
auth.log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
auth.log.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in block list, denying access")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, allowed := range allowedIPs {
|
||||
res, err := utils.FilterIP(allowed, ip)
|
||||
if err != nil {
|
||||
auth.log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
auth.log.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowedIPs) > 0 {
|
||||
auth.log.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||
return false
|
||||
}
|
||||
|
||||
auth.log.App.Debug().Str("ip", ip).Msg("IP not in any block or allow list, allowing access by default")
|
||||
return true
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsBypassedIP(ip string, acls *model.App) bool {
|
||||
if acls == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, bypassed := range acls.IP.Bypass {
|
||||
res, err := utils.FilterIP(bypassed, ip)
|
||||
if err != nil {
|
||||
auth.log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||
continue
|
||||
}
|
||||
if res {
|
||||
auth.log.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
auth.log.App.Debug().Str("ip", ip).Msg("IP not in bypass list, proceeding with authentication")
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
||||
auth.ensureOAuthSessionLimit()
|
||||
|
||||
|
||||
@@ -134,7 +134,8 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
|
||||
return ldap.conn, nil
|
||||
}
|
||||
|
||||
func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, err error) {
|
||||
func (ldap *LdapService) GetUserDN(username string) (string, error) {
|
||||
// Escape the username to prevent LDAP injection
|
||||
escapedUsername := ldapgo.EscapeFilter(username)
|
||||
filter := fmt.Sprintf(ldap.config.LDAP.SearchFilter, escapedUsername)
|
||||
|
||||
@@ -142,7 +143,7 @@ func (ldap *LdapService) GetUserInfo(username string) (dn string, email string,
|
||||
ldap.config.LDAP.BaseDN,
|
||||
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{"dn", "mail"},
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
@@ -151,15 +152,15 @@ func (ldap *LdapService) GetUserInfo(username string) (dn string, email string,
|
||||
|
||||
searchResult, err := ldap.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) != 1 {
|
||||
return "", "", fmt.Errorf("multiple or no entries found for user %s", username)
|
||||
return "", fmt.Errorf("multiple or no entries found for user %s", username)
|
||||
}
|
||||
|
||||
entry := searchResult.Entries[0]
|
||||
return entry.DN, entry.GetAttributeValue("mail"), nil
|
||||
userDN := searchResult.Entries[0].DN
|
||||
return userDN, nil
|
||||
}
|
||||
|
||||
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
||||
|
||||
@@ -40,9 +40,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
||||
SessionExpiry: 10,
|
||||
LoginTimeout: 10,
|
||||
LoginMaxRetries: 3,
|
||||
ACLs: model.ACLsConfig{
|
||||
Policy: "allow",
|
||||
},
|
||||
},
|
||||
Database: model.DatabaseConfig{
|
||||
Path: filepath.Join(tempDir, "test.db"),
|
||||
@@ -51,32 +48,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
||||
Enabled: true,
|
||||
Path: filepath.Join(tempDir, "resources"),
|
||||
},
|
||||
Apps: map[string]model.App{
|
||||
"app_path_allow": {
|
||||
Config: model.AppConfig{
|
||||
Domain: "path-allow.example.com",
|
||||
},
|
||||
Path: model.AppPath{
|
||||
Allow: "/allowed",
|
||||
},
|
||||
},
|
||||
"app_user_allow": {
|
||||
Config: model.AppConfig{
|
||||
Domain: "user-allow.example.com",
|
||||
},
|
||||
Users: model.AppUsers{
|
||||
Allow: "testuser",
|
||||
},
|
||||
},
|
||||
"ip_bypass": {
|
||||
Config: model.AppConfig{
|
||||
Domain: "ip-bypass.example.com",
|
||||
},
|
||||
IP: model.AppIP{
|
||||
Bypass: []string{"10.10.10.10"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
passwd, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
||||
|
||||
Reference in New Issue
Block a user