mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-17 01:30:13 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8b0188776 | |||
| 7b5d882ee8 | |||
| 8932f2ad46 | |||
| 482ba9d99f | |||
| 1bcd1bb59a | |||
| 5349f21212 | |||
| e8071a9d80 | |||
| 1f67797605 | |||
| ca06099466 | |||
| d4b4245017 | |||
| 4c741a5990 | |||
| b9abab2f17 | |||
| 3fd56272d2 |
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help improve Tinyauth
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: bug
|
|
||||||
assignees:
|
|
||||||
- steveiliop56
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Logs**
|
|
||||||
Please include the Tinyauth logs below, make sure to not include sensitive info.
|
|
||||||
|
|
||||||
**Device (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Tinyauth [e.g. v2.1.1]
|
|
||||||
- Docker [e.g. 27.3.1]
|
|
||||||
|
|
||||||
**
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve this project
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees:
|
||||||
|
- steveiliop56
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for reporting a bug! Please provide detailed information below.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the Bug
|
||||||
|
description: "A clear and concise description of what the bug is."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: How to Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
value: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: "A clear and concise description of what you expected to happen."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: "Additional Context"
|
||||||
|
description: "If applicable add screenshots to help explain your problem."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: "Logs"
|
||||||
|
description: "Please include the Tinyauth logs, make sure to not include sensitive info."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
placeholder: "e.g. iOS, Android, Windows, Linux, etc"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser
|
||||||
|
placeholder: "e.g. Chrome, Firefox, Safari, Edge, etc"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: tinyauth
|
||||||
|
attributes:
|
||||||
|
label: Tinyauth Version
|
||||||
|
placeholder: "e.g. v5.0.0"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: docker
|
||||||
|
attributes:
|
||||||
|
label: Docker Version (if applicable)
|
||||||
|
placeholder: "e.g. 27.3.1"
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: not-llm
|
||||||
|
attributes:
|
||||||
|
label: Human Written Confirmation
|
||||||
|
options:
|
||||||
|
- label: I confirm this issue was written by me and not generated by an LLM or AI assistant.
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Tinyauth Community Support on Discord
|
||||||
|
url: https://discord.gg/eHzVaCzRRd
|
||||||
|
about: Please ask and answer questions here.
|
||||||
|
- name: Tinyauth Documentation
|
||||||
|
url: https://tinyauth.app/docs/getting-started/
|
||||||
|
about: Please check the documentation here.
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: "[FEATURE]"
|
|
||||||
labels: enhancement
|
|
||||||
assignees:
|
|
||||||
- steveiliop56
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: "[FEATURE]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees:
|
||||||
|
- steveiliop56
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for suggesting a feature! Please provide detailed information below.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem? Please describe.
|
||||||
|
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like.
|
||||||
|
description: "A clear and concise description of what you want to happen."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered.
|
||||||
|
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: "Add any other context or screenshots about the feature request here."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: not-llm
|
||||||
|
attributes:
|
||||||
|
label: Human Written Confirmation
|
||||||
|
options:
|
||||||
|
- label: I confirm this request was written by me and not generated by an LLM or AI assistant.
|
||||||
|
required: true
|
||||||
@@ -16,36 +16,13 @@ func (app *BootstrapApp) setupServices() error {
|
|||||||
|
|
||||||
app.services.ldapService = ldapService
|
app.services.ldapService = ldapService
|
||||||
|
|
||||||
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
labelProvider, err := app.getLabelProvider()
|
||||||
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
|
||||||
|
|
||||||
var labelProvider service.LabelProvider
|
|
||||||
|
|
||||||
if useKubernetes {
|
|
||||||
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
|
||||||
|
|
||||||
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
return fmt.Errorf("failed to initialize label provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.services.kubernetesService = kubernetesService
|
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
||||||
labelProvider = kubernetesService
|
|
||||||
} else {
|
|
||||||
app.log.App.Debug().Msg("Using Docker label provider")
|
|
||||||
|
|
||||||
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize docker service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.services.dockerService = dockerService
|
|
||||||
labelProvider = dockerService
|
|
||||||
}
|
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
|
|
||||||
app.services.accessControlService = accessControlsService
|
app.services.accessControlService = accessControlsService
|
||||||
|
|
||||||
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
||||||
@@ -64,3 +41,36 @@ func (app *BootstrapApp) setupServices() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
|
||||||
|
if app.config.LabelProvider == "none" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
||||||
|
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
||||||
|
|
||||||
|
if useKubernetes {
|
||||||
|
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
||||||
|
|
||||||
|
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.services.kubernetesService = kubernetesService
|
||||||
|
return kubernetesService, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
app.log.App.Debug().Msg("Using Docker label provider")
|
||||||
|
|
||||||
|
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.services.dockerService = dockerService
|
||||||
|
return dockerService, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,7 +208,12 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
name = user.Name
|
name = user.Name
|
||||||
} else {
|
} else {
|
||||||
controller.log.App.Debug().Msg("No name from OAuth provider, generating from email")
|
controller.log.App.Debug().Msg("No name from OAuth provider, generating from email")
|
||||||
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
parts := strings.SplitN(user.Email, "@", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1])
|
||||||
|
} else {
|
||||||
|
name = utils.Capitalize(user.Email)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var username string
|
var username string
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
client, ok := controller.oidc.GetClient(req.ClientID)
|
client, ok := controller.oidc.GetClient(req.ClientID)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.authorizeError(c, err, "Client not found", "The client ID is invalid", "", "", "")
|
controller.authorizeError(c, fmt.Errorf("client not found: %s", req.ClientID), "Client not found", "The client ID is invalid", "", "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code), client.ClientID)
|
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code), client.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := controller.oidc.DeleteTokenByCodeHash(c, controller.oidc.Hash(req.Code)); err != nil {
|
if err := controller.oidc.DeleteTokenByCodeHash(c, controller.oidc.Hash(req.Code)); err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to delete code")
|
controller.log.App.Error().Err(err).Msg("Failed to revoke tokens for replayed code")
|
||||||
}
|
}
|
||||||
if errors.Is(err, service.ErrCodeNotFound) {
|
if errors.Is(err, service.ErrCodeNotFound) {
|
||||||
controller.log.App.Warn().Msg("Code not found")
|
controller.log.App.Warn().Msg("Code not found")
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
if controller.auth.IsBypassedIP(clientIP, acls) {
|
if controller.acls.IsIPBypassed(clientIP, acls) {
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -110,13 +110,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls)
|
authEnabled := controller.acls.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 {
|
if !authEnabled {
|
||||||
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
||||||
@@ -128,7 +122,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.CheckIP(clientIP, acls) {
|
if !controller.acls.IsIPAllowed(clientIP, acls) {
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
IP: clientIP,
|
IP: clientIP,
|
||||||
@@ -144,9 +138,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !controller.useBrowserResponse(proxyCtx) {
|
if !controller.useBrowserResponse(proxyCtx) {
|
||||||
c.Header("x-tinyauth-location", redirectURL)
|
c.Header("x-tinyauth-location", redirectURL)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(403, gin.H{
|
||||||
"status": 401,
|
"status": 403,
|
||||||
"message": "Unauthorized",
|
"message": "Forbidden",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -165,7 +159,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userContext.Authenticated {
|
if userContext.Authenticated {
|
||||||
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
userAllowed := controller.acls.IsUserAllowed(*userContext, acls)
|
||||||
|
|
||||||
if !userAllowed {
|
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")
|
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
||||||
@@ -205,9 +199,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
var groupOK bool
|
var groupOK bool
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.IsOAuth() {
|
||||||
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls)
|
groupOK = controller.acls.IsInOAuthGroup(*userContext, acls)
|
||||||
} else {
|
} else {
|
||||||
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
|
groupOK = controller.acls.IsInLDAPGroup(*userContext, acls)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
|
|||||||
@@ -24,33 +24,6 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
cfg, runtime := test.CreateTestConfigs(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 = `
|
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`
|
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`
|
||||||
|
|
||||||
@@ -391,7 +364,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||||
aclsService := service.NewAccessControlsService(log, nil, acls)
|
aclsService := service.NewAccessControlsService(log, cfg, nil)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
|
|||||||
if controller.config.Resources.Path == "" {
|
if controller.config.Resources.Path == "" {
|
||||||
c.JSON(404, gin.H{
|
c.JSON(404, gin.H{
|
||||||
"status": 404,
|
"status": 404,
|
||||||
"message": "Resources not found",
|
"message": "Resource not found",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ func NewDefaultConfiguration() *Config {
|
|||||||
SessionMaxLifetime: 0, // disabled
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300, // 5 minutes
|
LoginTimeout: 300, // 5 minutes
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
|
ACLs: ACLsConfig{
|
||||||
|
Policy: "allow",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
@@ -78,7 +81,7 @@ type Config struct {
|
|||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, or kubernetes). auto detects the environment." yaml:"labelProvider"`
|
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
|
||||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +117,7 @@ type AuthConfig struct {
|
|||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||||
|
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAttributes struct {
|
type UserAttributes struct {
|
||||||
@@ -223,6 +227,10 @@ type OIDCClientConfig struct {
|
|||||||
Name string `description:"Client name in UI." yaml:"name"`
|
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
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
|
|||||||
@@ -1,44 +1,82 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"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 {
|
type LabelProvider interface {
|
||||||
GetLabels(appDomain string) (*model.App, error)
|
GetLabels(appDomain string) (*model.App, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
config model.Config
|
||||||
labelProvider *LabelProvider
|
labelProvider *LabelProvider
|
||||||
static map[string]model.App
|
policy AccessControlPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(
|
func NewAccessControlsService(
|
||||||
log *logger.Logger,
|
log *logger.Logger,
|
||||||
labelProvider *LabelProvider,
|
config model.Config,
|
||||||
static map[string]model.App) *AccessControlsService {
|
labelProvider *LabelProvider) *AccessControlsService {
|
||||||
return &AccessControlsService{
|
|
||||||
|
service := AccessControlsService{
|
||||||
log: log,
|
log: log,
|
||||||
|
config: config,
|
||||||
labelProvider: labelProvider,
|
labelProvider: labelProvider,
|
||||||
static: static,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
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 {
|
||||||
var appAcls *model.App
|
var appAcls *model.App
|
||||||
for app, config := range acls.static {
|
for app, config := range service.config.Apps {
|
||||||
if config.Config.Domain == domain {
|
if config.Config.Domain == domain {
|
||||||
acls.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||||
appAcls = &config
|
appAcls = &config
|
||||||
break // If we find a match by domain, we can stop searching
|
break // If we find a match by domain, we can stop searching
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||||
acls.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||||
appAcls = &config
|
appAcls = &config
|
||||||
break // If we find a match by app name, we can stop searching
|
break // If we find a match by app name, we can stop searching
|
||||||
}
|
}
|
||||||
@@ -46,20 +84,193 @@ func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
|||||||
return appAcls
|
return appAcls
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||||
// First check in the static config
|
// First check in the static config
|
||||||
app := acls.lookupStaticACLs(domain)
|
app := service.lookupStaticACLs(domain)
|
||||||
|
|
||||||
if app != nil {
|
if app != nil {
|
||||||
acls.log.App.Debug().Msg("Using static ACLs for app")
|
service.log.App.Debug().Msg("Using static ACLs for app")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a label provider configured, try to get ACLs from it
|
// If we have a label provider configured, try to get ACLs from it
|
||||||
if acls.labelProvider != nil {
|
if service.labelProvider != nil {
|
||||||
return (*acls.labelProvider).GetLabels(domain)
|
return (*service.labelProvider).GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// no labels
|
// no labels
|
||||||
return nil, nil
|
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,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,7 +17,6 @@ import (
|
|||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -454,171 +452,6 @@ func (auth *AuthService) LDAPAuthConfigured() bool {
|
|||||||
return auth.ldap != nil
|
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) {
|
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
||||||
auth.ensureOAuthSessionLimit()
|
auth.ensureOAuthSessionLimit()
|
||||||
|
|
||||||
@@ -773,46 +606,49 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
|
|||||||
auth.oauthMutex.Lock()
|
auth.oauthMutex.Lock()
|
||||||
defer auth.oauthMutex.Unlock()
|
defer auth.oauthMutex.Unlock()
|
||||||
|
|
||||||
if len(auth.oauthPendingSessions) >= MaxOAuthPendingSessions {
|
if len(auth.oauthPendingSessions) <= MaxOAuthPendingSessions {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cleanupIds := make([]string, 0, OAuthCleanupCount)
|
type entry struct {
|
||||||
|
id string
|
||||||
for range OAuthCleanupCount {
|
expiresAt int64
|
||||||
oldestId := ""
|
}
|
||||||
oldestTime := int64(0)
|
|
||||||
|
|
||||||
|
entries := make([]entry, 0, len(auth.oauthPendingSessions))
|
||||||
for id, session := range auth.oauthPendingSessions {
|
for id, session := range auth.oauthPendingSessions {
|
||||||
if oldestTime == 0 {
|
entries = append(entries, entry{id, session.ExpiresAt.Unix()})
|
||||||
oldestId = id
|
|
||||||
oldestTime = session.ExpiresAt.Unix()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if slices.Contains(cleanupIds, id) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if session.ExpiresAt.Unix() < oldestTime {
|
|
||||||
oldestId = id
|
|
||||||
oldestTime = session.ExpiresAt.Unix()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupIds = append(cleanupIds, oldestId)
|
slices.SortFunc(entries, func(a, b entry) int {
|
||||||
|
if a.expiresAt < b.expiresAt {
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
|
if a.expiresAt > b.expiresAt {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
for _, id := range cleanupIds {
|
for _, e := range entries[:OAuthCleanupCount] {
|
||||||
delete(auth.oauthPendingSessions, id)
|
delete(auth.oauthPendingSessions, e.id)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) lockdownMode() {
|
func (auth *AuthService) lockdownMode() {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
|
||||||
auth.lockdownCtx = ctx
|
|
||||||
auth.lockdownCancelFunc = cancel
|
|
||||||
|
|
||||||
auth.loginMutex.Lock()
|
auth.loginMutex.Lock()
|
||||||
|
|
||||||
|
if auth.lockdown != nil && auth.lockdown.Active {
|
||||||
|
auth.loginMutex.Unlock()
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.lockdownCtx = ctx
|
||||||
|
auth.lockdownCancelFunc = cancel
|
||||||
|
|
||||||
auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode")
|
auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode")
|
||||||
|
|
||||||
auth.lockdown = &Lockdown{
|
auth.lockdown = &Lockdown{
|
||||||
@@ -825,10 +661,12 @@ func (auth *AuthService) lockdownMode() {
|
|||||||
auth.loginAttempts = make(map[string]*LoginAttempt)
|
auth.loginAttempts = make(map[string]*LoginAttempt)
|
||||||
|
|
||||||
timer := time.NewTimer(time.Until(auth.lockdown.ActiveUntil))
|
timer := time.NewTimer(time.Until(auth.lockdown.ActiveUntil))
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
auth.loginMutex.Unlock()
|
auth.loginMutex.Unlock()
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
// Timer expired, end lockdown
|
// Timer expired, end lockdown
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func NewOAuthService(config model.OAuthServiceConfig, id string, ctx context.Con
|
|||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: config.Insecure,
|
InsecureSkipVerify: config.Insecure,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ type OIDCService struct {
|
|||||||
|
|
||||||
clients map[string]model.OIDCClientConfig
|
clients map[string]model.OIDCClientConfig
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
publicKey crypto.PublicKey
|
publicKey *rsa.PublicKey
|
||||||
issuer string
|
issuer string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +239,16 @@ func NewOIDCService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rPublicKey, ok := publicKey.(*rsa.PublicKey)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("public key is not an rsa public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rPublicKey.N.Cmp(privateKey.N) != 0 || rPublicKey.E != privateKey.E {
|
||||||
|
return nil, fmt.Errorf("public key does not pair with private key")
|
||||||
|
}
|
||||||
|
|
||||||
// We will reorganize the client into a map with the client ID as the key
|
// We will reorganize the client into a map with the client ID as the key
|
||||||
clients := make(map[string]model.OIDCClientConfig)
|
clients := make(map[string]model.OIDCClientConfig)
|
||||||
|
|
||||||
@@ -271,7 +281,7 @@ func NewOIDCService(
|
|||||||
|
|
||||||
clients: clients,
|
clients: clients,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
publicKey: publicKey,
|
publicKey: rPublicKey,
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +465,7 @@ func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user
|
|||||||
|
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
|
|
||||||
der := x509.MarshalPKCS1PublicKey(&service.privateKey.PublicKey)
|
der := x509.MarshalPKCS1PublicKey(service.publicKey)
|
||||||
|
|
||||||
if der == nil {
|
if der == nil {
|
||||||
return "", errors.New("failed to marshal public key")
|
return "", errors.New("failed to marshal public key")
|
||||||
@@ -813,7 +823,7 @@ func (service *OIDCService) cleanupRoutine() {
|
|||||||
func (service *OIDCService) GetJWK() ([]byte, error) {
|
func (service *OIDCService) GetJWK() ([]byte, error) {
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
|
|
||||||
der := x509.MarshalPKCS1PublicKey(&service.privateKey.PublicKey)
|
der := x509.MarshalPKCS1PublicKey(service.publicKey)
|
||||||
|
|
||||||
if der == nil {
|
if der == nil {
|
||||||
return nil, errors.New("failed to marshal public key")
|
return nil, errors.New("failed to marshal public key")
|
||||||
@@ -822,13 +832,13 @@ func (service *OIDCService) GetJWK() ([]byte, error) {
|
|||||||
hasher.Write(der)
|
hasher.Write(der)
|
||||||
|
|
||||||
jwk := jose.JSONWebKey{
|
jwk := jose.JSONWebKey{
|
||||||
Key: service.privateKey,
|
Key: service.publicKey,
|
||||||
Algorithm: string(jose.RS256),
|
Algorithm: string(jose.RS256),
|
||||||
Use: "sig",
|
Use: "sig",
|
||||||
KeyID: base64.URLEncoding.EncodeToString(hasher.Sum(nil)),
|
KeyID: base64.URLEncoding.EncodeToString(hasher.Sum(nil)),
|
||||||
}
|
}
|
||||||
|
|
||||||
return jwk.Public().MarshalJSON()
|
return jwk.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) ValidatePKCE(codeChallenge string, codeVerifier string) bool {
|
func (service *OIDCService) ValidatePKCE(codeChallenge string, codeVerifier string) bool {
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
SessionExpiry: 10,
|
SessionExpiry: 10,
|
||||||
LoginTimeout: 10,
|
LoginTimeout: 10,
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
|
ACLs: model.ACLsConfig{
|
||||||
|
Policy: "allow",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Database: model.DatabaseConfig{
|
Database: model.DatabaseConfig{
|
||||||
Path: filepath.Join(tempDir, "test.db"),
|
Path: filepath.Join(tempDir, "test.db"),
|
||||||
@@ -48,6 +51,32 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Path: filepath.Join(tempDir, "resources"),
|
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)
|
passwd, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
||||||
|
|||||||
Reference in New Issue
Block a user