mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-10 20:11:13 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62ffd2fd11 | |||
| a3ec07230c | |||
| b4eb7090bd | |||
| 2f24f823eb | |||
| 9a219046ac | |||
| 97d58b376d | |||
| b426a1529e | |||
| c7efb71a5a | |||
| eec75a6f49 |
@@ -84,7 +84,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ jobs:
|
|||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
- name: Upload to code-scanning
|
- name: Upload to code-scanning
|
||||||
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
+3
-3
@@ -38,9 +38,9 @@ COPY ./internal ./internal
|
|||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.Version=${VERSION} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM alpine:3.23 AS runner
|
FROM alpine:3.23 AS runner
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
|||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.Version=${VERSION} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ webui: clean-webui
|
|||||||
# Build the binary
|
# Build the binary
|
||||||
binary: webui
|
binary: webui
|
||||||
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${TAG_NAME} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.Version=${TAG_NAME} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
-X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
-o ${BIN_NAME} ./cmd/tinyauth
|
-o ${BIN_NAME} ./cmd/tinyauth
|
||||||
|
|
||||||
# Build for amd64
|
# Build for amd64
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvEntry struct {
|
type EnvEntry struct {
|
||||||
@@ -20,7 +20,7 @@ type EnvEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateExampleEnv() {
|
func generateExampleEnv() {
|
||||||
cfg := model.NewDefaultConfiguration()
|
cfg := config.NewDefaultConfiguration()
|
||||||
entries := make([]EnvEntry, 0)
|
entries := make([]EnvEntry, 0)
|
||||||
|
|
||||||
root := reflect.TypeOf(cfg).Elem()
|
root := reflect.TypeOf(cfg).Elem()
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarkdownEntry struct {
|
type MarkdownEntry struct {
|
||||||
@@ -21,7 +21,7 @@ type MarkdownEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateMarkdown() {
|
func generateMarkdown() {
|
||||||
cfg := model.NewDefaultConfiguration()
|
cfg := config.NewDefaultConfiguration()
|
||||||
entries := make([]MarkdownEntry, 0)
|
entries := make([]MarkdownEntry, 0)
|
||||||
|
|
||||||
root := reflect.TypeOf(cfg).Elem()
|
root := reflect.TypeOf(cfg).Elem()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ require (
|
|||||||
github.com/weppos/publicsuffix-go v0.50.3
|
github.com/weppos/publicsuffix-go v0.50.3
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
|
gotest.tools/v3 v3.5.2
|
||||||
k8s.io/apimachinery v0.32.2
|
k8s.io/apimachinery v0.32.2
|
||||||
k8s.io/client-go v0.32.2
|
k8s.io/client-go v0.32.2
|
||||||
modernc.org/sqlite v1.49.1
|
modernc.org/sqlite v1.49.1
|
||||||
@@ -132,7 +133,6 @@ require (
|
|||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||||
modernc.org/libc v1.72.0 // indirect
|
modernc.org/libc v1.72.0 // indirect
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type BootstrapApp struct {
|
|||||||
csrfCookieName string
|
csrfCookieName string
|
||||||
redirectCookieName string
|
redirectCookieName string
|
||||||
oauthSessionCookieName string
|
oauthSessionCookieName string
|
||||||
localUsers *[]model.LocalUser
|
localUsers []model.LocalUser
|
||||||
oauthProviders map[string]model.OAuthServiceConfig
|
oauthProviders map[string]model.OAuthServiceConfig
|
||||||
configuredProviders []controller.Provider
|
configuredProviders []controller.Provider
|
||||||
oidcClients []model.OIDCClientConfig
|
oidcClients []model.OIDCClientConfig
|
||||||
@@ -69,7 +69,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
app.context.localUsers = users
|
app.context.localUsers = *users
|
||||||
|
|
||||||
// Setup OAuth providers
|
// Setup OAuth providers
|
||||||
app.context.oauthProviders = app.config.OAuth.Providers
|
app.context.oauthProviders = app.config.OAuth.Providers
|
||||||
@@ -104,13 +104,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get cookie domain
|
// Get cookie domain
|
||||||
cookieDomainResolver := utils.GetCookieDomain
|
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
||||||
if !app.config.Auth.SubdomainsEnabled {
|
|
||||||
tlog.App.Info().Msg("Subdomains disabled, automatic authentication for proxied apps will not work")
|
|
||||||
cookieDomainResolver = utils.GetStandaloneCookieDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieDomain, err := cookieDomainResolver(app.context.appUrl)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
RedirectCookieName: app.context.redirectCookieName,
|
RedirectCookieName: app.context.redirectCookieName,
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
OAuthSessionCookieName: app.context.oauthSessionCookieName,
|
OAuthSessionCookieName: app.context.oauthSessionCookieName,
|
||||||
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
|
|
||||||
}, apiRouter, app.services.authService)
|
}, apiRouter, app.services.authService)
|
||||||
|
|
||||||
oauthController.SetupRoutes()
|
oauthController.SetupRoutes()
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
SessionCookieName: app.context.sessionCookieName,
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
IP: app.config.Auth.IP,
|
IP: app.config.Auth.IP,
|
||||||
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
|
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
|
||||||
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
|
|
||||||
}, services.ldapService, queries, services.oauthBrokerService)
|
}, services.ldapService, queries, services.oauthBrokerService)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
Username: context.GetUsername(),
|
Username: context.GetUsername(),
|
||||||
Name: context.GetName(),
|
Name: context.GetName(),
|
||||||
Email: context.GetEmail(),
|
Email: context.GetEmail(),
|
||||||
Provider: context.GetProviderID(),
|
Provider: context.ProviderName(),
|
||||||
OAuth: context.IsOAuth(),
|
OAuth: context.IsOAuth(),
|
||||||
TOTPPending: context.TOTPPending(),
|
TOTPPending: context.TOTPPending(),
|
||||||
OAuthName: context.OAuthName(),
|
OAuthName: context.OAuthName(),
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContextController(t *testing.T) {
|
func TestContextController(t *testing.T) {
|
||||||
@@ -79,16 +79,12 @@ func TestContextController(t *testing.T) {
|
|||||||
description: "Ensure user context returns when authorized",
|
description: "Ensure user context returns when authorized",
|
||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{
|
||||||
func(c *gin.Context) {
|
func(c *gin.Context) {
|
||||||
c.Set("context", &model.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Authenticated: true,
|
Username: "johndoe",
|
||||||
Provider: model.ProviderLocal,
|
Name: "John Doe",
|
||||||
Local: &model.LocalContext{
|
Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
|
||||||
BaseContext: model.BaseContext{
|
Provider: "local",
|
||||||
Username: "johndoe",
|
IsLoggedIn: true,
|
||||||
Name: "John Doe",
|
|
||||||
Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type OAuthControllerConfig struct {
|
|||||||
SecureCookie bool
|
SecureCookie bool
|
||||||
AppURL string
|
AppURL string
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
SubdomainsEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthController struct {
|
type OAuthController struct {
|
||||||
@@ -106,7 +105,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -136,7 +135,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
||||||
|
|
||||||
@@ -284,10 +283,3 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams)
|
|||||||
params.ClientID != "" &&
|
params.ClientID != "" &&
|
||||||
params.RedirectURI != ""
|
params.RedirectURI != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OAuthController) getCookieDomain() string {
|
|
||||||
if controller.config.SubdomainsEnabled {
|
|
||||||
return "." + controller.config.CookieDomain
|
|
||||||
}
|
|
||||||
return controller.config.CookieDomain
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOIDCController(t *testing.T) {
|
func TestOIDCController(t *testing.T) {
|
||||||
@@ -27,7 +27,7 @@ func TestOIDCController(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
oidcServiceCfg := service.OIDCServiceConfig{
|
oidcServiceCfg := service.OIDCServiceConfig{
|
||||||
Clients: map[string]model.OIDCClientConfig{
|
Clients: map[string]config.OIDCClientConfig{
|
||||||
"test": {
|
"test": {
|
||||||
ClientID: "some-client-id",
|
ClientID: "some-client-id",
|
||||||
ClientSecret: "some-client-secret",
|
ClientSecret: "some-client-secret",
|
||||||
@@ -44,16 +44,12 @@ func TestOIDCController(t *testing.T) {
|
|||||||
controllerCfg := controller.OIDCControllerConfig{}
|
controllerCfg := controller.OIDCControllerConfig{}
|
||||||
|
|
||||||
simpleCtx := func(c *gin.Context) {
|
simpleCtx := func(c *gin.Context) {
|
||||||
c.Set("context", &model.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Authenticated: true,
|
Username: "test",
|
||||||
Provider: model.ProviderLocal,
|
Name: "Test User",
|
||||||
Local: &model.LocalContext{
|
Email: "test@example.com",
|
||||||
BaseContext: model.BaseContext{
|
IsLoggedIn: true,
|
||||||
Username: "test",
|
Provider: "local",
|
||||||
Name: "Test User",
|
|
||||||
Email: "test@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
@@ -852,7 +848,7 @@ func TestOIDCController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(model.Config{})
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -99,12 +99,16 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if acls == nil {
|
||||||
|
acls = &model.App{}
|
||||||
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
if controller.auth.IsBypassedIP(clientIP, acls) {
|
if controller.auth.IsBypassedIP(&acls.IP, clientIP) {
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, *acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -112,7 +116,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls)
|
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, &acls.Path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||||
@@ -122,7 +126,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !authEnabled {
|
if !authEnabled {
|
||||||
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
|
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, *acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -130,7 +134,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.CheckIP(clientIP, acls) {
|
if !controller.auth.CheckIP(&acls.IP, clientIP) {
|
||||||
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,
|
||||||
@@ -209,9 +213,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.auth.IsInOAuthGroup(c, *userContext, acls.OAuth.Groups)
|
||||||
} else {
|
} else {
|
||||||
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
|
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls.LDAP.Groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
@@ -263,7 +267,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuth.Sub))
|
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuth.Sub))
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, *acls)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -296,13 +300,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ProxyController) setHeaders(c *gin.Context, acls *model.App) {
|
func (controller *ProxyController) setHeaders(c *gin.Context, acls model.App) {
|
||||||
c.Header("Authorization", c.Request.Header.Get("Authorization"))
|
c.Header("Authorization", c.Request.Header.Get("Authorization"))
|
||||||
|
|
||||||
if acls == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := utils.ParseHeaders(acls.Response.Headers)
|
headers := utils.ParseHeaders(acls.Response.Headers)
|
||||||
|
|
||||||
for key, value := range headers {
|
for key, value := range headers {
|
||||||
@@ -314,7 +314,7 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls *model.App) {
|
|||||||
|
|
||||||
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
|
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
|
||||||
tlog.App.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
tlog.App.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
|
||||||
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.EncodeBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
|
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProxyController(t *testing.T) {
|
func TestProxyController(t *testing.T) {
|
||||||
@@ -21,7 +21,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
authServiceCfg := service.AuthServiceConfig{
|
authServiceCfg := service.AuthServiceConfig{
|
||||||
LocalUsers: &[]model.LocalUser{
|
Users: []config.User{
|
||||||
{
|
{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
@@ -29,7 +29,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Username: "totpuser",
|
Username: "totpuser",
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
TOTPSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SessionExpiry: 10, // 10 seconds, useful for testing
|
SessionExpiry: 10, // 10 seconds, useful for testing
|
||||||
@@ -43,28 +43,28 @@ func TestProxyController(t *testing.T) {
|
|||||||
AppURL: "https://tinyauth.example.com",
|
AppURL: "https://tinyauth.example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
acls := map[string]model.App{
|
acls := map[string]config.App{
|
||||||
"app_path_allow": {
|
"app_path_allow": {
|
||||||
Config: model.AppConfig{
|
Config: config.AppConfig{
|
||||||
Domain: "path-allow.example.com",
|
Domain: "path-allow.example.com",
|
||||||
},
|
},
|
||||||
Path: model.AppPath{
|
Path: config.AppPath{
|
||||||
Allow: "/allowed",
|
Allow: "/allowed",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"app_user_allow": {
|
"app_user_allow": {
|
||||||
Config: model.AppConfig{
|
Config: config.AppConfig{
|
||||||
Domain: "user-allow.example.com",
|
Domain: "user-allow.example.com",
|
||||||
},
|
},
|
||||||
Users: model.AppUsers{
|
Users: config.AppUsers{
|
||||||
Allow: "testuser",
|
Allow: "testuser",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ip_bypass": {
|
"ip_bypass": {
|
||||||
Config: model.AppConfig{
|
Config: config.AppConfig{
|
||||||
Domain: "ip-bypass.example.com",
|
Domain: "ip-bypass.example.com",
|
||||||
},
|
},
|
||||||
IP: model.AppIP{
|
IP: config.AppIP{
|
||||||
Bypass: []string{"10.10.10.10"},
|
Bypass: []string{"10.10.10.10"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -74,31 +74,24 @@ func TestProxyController(t *testing.T) {
|
|||||||
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`
|
||||||
|
|
||||||
simpleCtx := func(c *gin.Context) {
|
simpleCtx := func(c *gin.Context) {
|
||||||
c.Set("context", &model.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Authenticated: true,
|
Username: "testuser",
|
||||||
Provider: model.ProviderLocal,
|
Name: "Testuser",
|
||||||
Local: &model.LocalContext{
|
Email: "testuser@example.com",
|
||||||
BaseContext: model.BaseContext{
|
IsLoggedIn: true,
|
||||||
Username: "testuser",
|
Provider: "local",
|
||||||
Name: "Testuser",
|
|
||||||
Email: "testuser@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
simpleCtxTotp := func(c *gin.Context) {
|
simpleCtxTotp := func(c *gin.Context) {
|
||||||
c.Set("context", &model.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Authenticated: true,
|
Username: "totpuser",
|
||||||
Provider: model.ProviderLocal,
|
Name: "Totpuser",
|
||||||
Local: &model.LocalContext{
|
Email: "totpuser@example.com",
|
||||||
BaseContext: model.BaseContext{
|
IsLoggedIn: true,
|
||||||
Username: "totpuser",
|
Provider: "local",
|
||||||
Name: "Totpuser",
|
TotpEnabled: true,
|
||||||
Email: "totpuser@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
@@ -398,9 +391,9 @@ func TestProxyController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthBrokerCfgs := make(map[string]model.OAuthServiceConfig)
|
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(model.Config{})
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
|
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
|
||||||
tlog.App.Warn().Err(err).Str("username", req.Username).Msg("Failed to verify password")
|
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
@@ -112,20 +112,16 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlog.App.Info().Str("username", req.Username).Msg("Login successful")
|
||||||
|
tlog.AuditLoginSuccess(c, req.Username, "username")
|
||||||
|
|
||||||
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
var localUser *model.LocalUser
|
var localUser *model.LocalUser
|
||||||
|
|
||||||
if search.Type == model.UserLocal {
|
if search.Type == model.UserLocal {
|
||||||
localUser = controller.auth.GetLocalUser(req.Username)
|
localUser = controller.auth.GetLocalUser(req.Username)
|
||||||
|
|
||||||
if localUser == nil {
|
|
||||||
tlog.App.Warn().Str("username", req.Username).Msg("User disappeared during login")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if localUser.TOTPSecret != "" {
|
if localUser.TOTPSecret != "" {
|
||||||
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||||
|
|
||||||
@@ -202,11 +198,6 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
tlog.App.Info().Str("username", req.Username).Msg("Login successful")
|
|
||||||
tlog.AuditLoginSuccess(c, req.Username, "username")
|
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(req.Username, true)
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
@@ -235,6 +226,17 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to get user context on logout")
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": 500,
|
||||||
|
"message": "Internal Server Error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := controller.auth.DeleteSession(c, uuid)
|
cookie, err := controller.auth.DeleteSession(c, uuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -246,14 +248,7 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
tlog.AuditLogout(c, context.GetUsername(), context.GetProviderID())
|
|
||||||
} else {
|
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to get user context for logout audit, proceeding without username")
|
|
||||||
tlog.AuditLogout(c, "unknown", "unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
@@ -313,15 +308,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
user := controller.auth.GetLocalUser(context.GetUsername())
|
user := controller.auth.GetLocalUser(context.GetUsername())
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
tlog.App.Error().Str("username", context.GetUsername()).Msg("User not found in TOTP handler")
|
|
||||||
c.JSON(401, gin.H{
|
|
||||||
"status": 401,
|
|
||||||
"message": "Unauthorized",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := totp.Validate(req.Code, user.TOTPSecret)
|
ok := totp.Validate(req.Code, user.TOTPSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -335,16 +321,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid, err := c.Cookie(controller.config.SessionCookieName)
|
tlog.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful")
|
||||||
|
tlog.AuditLoginSuccess(c, context.GetUsername(), "totp")
|
||||||
if err == nil {
|
|
||||||
_, err = controller.auth.DeleteSession(c, uuid)
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to delete pending TOTP session")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to retrieve session cookie for pending TOTP session, proceeding without deleting it")
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(context.GetUsername(), true)
|
controller.auth.RecordLoginAttempt(context.GetUsername(), true)
|
||||||
|
|
||||||
@@ -377,9 +355,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
tlog.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful")
|
|
||||||
tlog.AuditLoginSuccess(c, context.GetUsername(), "totp")
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package controller_test
|
package controller_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,14 +10,14 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUserController(t *testing.T) {
|
func TestUserController(t *testing.T) {
|
||||||
@@ -27,7 +25,7 @@ func TestUserController(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
authServiceCfg := service.AuthServiceConfig{
|
authServiceCfg := service.AuthServiceConfig{
|
||||||
LocalUsers: &[]model.LocalUser{
|
Users: []config.User{
|
||||||
{
|
{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
@@ -35,12 +33,12 @@ func TestUserController(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Username: "totpuser",
|
Username: "totpuser",
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
TOTPSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Username: "attruser",
|
Username: "attruser",
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
Attributes: model.UserAttributes{
|
Attributes: config.UserAttributes{
|
||||||
Name: "Alice Smith",
|
Name: "Alice Smith",
|
||||||
Email: "alice@example.com",
|
Email: "alice@example.com",
|
||||||
},
|
},
|
||||||
@@ -48,8 +46,8 @@ func TestUserController(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Username: "attrtotpuser",
|
Username: "attrtotpuser",
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
TOTPSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
Attributes: model.UserAttributes{
|
Attributes: config.UserAttributes{
|
||||||
Name: "Bob Jones",
|
Name: "Bob Jones",
|
||||||
Email: "bob@example.com",
|
Email: "bob@example.com",
|
||||||
},
|
},
|
||||||
@@ -63,63 +61,9 @@ func TestUserController(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userControllerCfg := controller.UserControllerConfig{
|
userControllerCfg := controller.UserControllerConfig{
|
||||||
CookieDomain: "example.com",
|
CookieDomain: "example.com",
|
||||||
SessionCookieName: "tinyauth-session",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totpCtx := func(c *gin.Context) {
|
|
||||||
c.Set("context", &model.UserContext{
|
|
||||||
Authenticated: false,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "totpuser",
|
|
||||||
Name: "Totpuser",
|
|
||||||
Email: "totpuser@example.com",
|
|
||||||
},
|
|
||||||
TOTPPending: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
totpAttrCtx := func(c *gin.Context) {
|
|
||||||
c.Set("context", &model.UserContext{
|
|
||||||
Authenticated: false,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "attrtotpuser",
|
|
||||||
Name: "Bob Jones",
|
|
||||||
Email: "bob@example.com",
|
|
||||||
},
|
|
||||||
TOTPPending: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
simpleCtx := func(c *gin.Context) {
|
|
||||||
c.Set("context", &model.UserContext{
|
|
||||||
Authenticated: true,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "testuser",
|
|
||||||
Name: "Test User",
|
|
||||||
Email: "testuser@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
oauthBrokerCfgs := make(map[string]model.OAuthServiceConfig)
|
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(model.Config{})
|
|
||||||
|
|
||||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
queries := repository.New(db)
|
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
middlewares []gin.HandlerFunc
|
middlewares []gin.HandlerFunc
|
||||||
@@ -150,9 +94,7 @@ func TestUserController(t *testing.T) {
|
|||||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||||
assert.True(t, cookie.HttpOnly)
|
assert.True(t, cookie.HttpOnly)
|
||||||
assert.Equal(t, "example.com", cookie.Domain)
|
assert.Equal(t, "example.com", cookie.Domain)
|
||||||
// 3 seconds should be more than enough for even slow test environments
|
assert.Equal(t, 10, cookie.MaxAge)
|
||||||
assert.GreaterOrEqual(t, cookie.MaxAge, 7)
|
|
||||||
assert.LessOrEqual(t, cookie.MaxAge, 10)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,15 +183,12 @@ func TestUserController(t *testing.T) {
|
|||||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||||
assert.True(t, cookie.HttpOnly)
|
assert.True(t, cookie.HttpOnly)
|
||||||
assert.Equal(t, "example.com", cookie.Domain)
|
assert.Equal(t, "example.com", cookie.Domain)
|
||||||
assert.GreaterOrEqual(t, cookie.MaxAge, 3597)
|
assert.Equal(t, 3600, cookie.MaxAge) // 1 hour, default for totp pending sessions
|
||||||
assert.LessOrEqual(t, cookie.MaxAge, 3600)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Should be able to logout",
|
description: "Should be able to logout",
|
||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{},
|
||||||
simpleCtx,
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
// First login to get a session cookie
|
// First login to get a session cookie
|
||||||
loginReq := controller.LoginRequest{
|
loginReq := controller.LoginRequest{
|
||||||
@@ -265,10 +204,9 @@ func TestUserController(t *testing.T) {
|
|||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, 200, recorder.Code)
|
||||||
cookies := recorder.Result().Cookies()
|
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||||
assert.Len(t, cookies, 1)
|
|
||||||
|
|
||||||
cookie := cookies[0]
|
cookie := recorder.Result().Cookies()[0]
|
||||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||||
|
|
||||||
// Now logout using the session cookie
|
// Now logout using the session cookie
|
||||||
@@ -279,33 +217,18 @@ func TestUserController(t *testing.T) {
|
|||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, 200, recorder.Code)
|
||||||
cookies = recorder.Result().Cookies()
|
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||||
assert.Len(t, cookies, 1)
|
|
||||||
|
|
||||||
cookie = cookies[0]
|
logoutCookie := recorder.Result().Cookies()[0]
|
||||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
assert.Equal(t, "tinyauth-session", logoutCookie.Name)
|
||||||
assert.Equal(t, "", cookie.Value)
|
assert.Equal(t, "", logoutCookie.Value)
|
||||||
assert.Equal(t, -1, cookie.MaxAge) // MaxAge -1 means delete cookie
|
assert.Equal(t, -1, logoutCookie.MaxAge) // MaxAge -1 means delete cookie
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Should be able to login with totp",
|
description: "Should be able to login with totp",
|
||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{},
|
||||||
totpCtx,
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
|
|
||||||
UUID: "test-totp-login-uuid",
|
|
||||||
Username: "test",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Name: "Test",
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: true,
|
|
||||||
Expiry: time.Now().Add(1 * time.Hour).Unix(),
|
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -319,13 +242,7 @@ func TestUserController(t *testing.T) {
|
|||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.AddCookie(&http.Cookie{
|
|
||||||
Name: "tinyauth-session",
|
|
||||||
Value: "test-totp-login-uuid",
|
|
||||||
HttpOnly: true,
|
|
||||||
MaxAge: 3600,
|
|
||||||
Expires: time.Now().Add(1 * time.Hour),
|
|
||||||
})
|
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, 200, recorder.Code)
|
||||||
@@ -336,15 +253,12 @@ func TestUserController(t *testing.T) {
|
|||||||
assert.Equal(t, "tinyauth-session", totpCookie.Name)
|
assert.Equal(t, "tinyauth-session", totpCookie.Name)
|
||||||
assert.True(t, totpCookie.HttpOnly)
|
assert.True(t, totpCookie.HttpOnly)
|
||||||
assert.Equal(t, "example.com", totpCookie.Domain)
|
assert.Equal(t, "example.com", totpCookie.Domain)
|
||||||
assert.GreaterOrEqual(t, totpCookie.MaxAge, 7)
|
assert.Equal(t, 10, totpCookie.MaxAge) // should use the regular session expiry time
|
||||||
assert.LessOrEqual(t, totpCookie.MaxAge, 10)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Totp should rate limit on multiple invalid attempts",
|
description: "Totp should rate limit on multiple invalid attempts",
|
||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{},
|
||||||
totpCtx,
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
for range 3 {
|
for range 3 {
|
||||||
totpReq := controller.TotpRequest{
|
totpReq := controller.TotpRequest{
|
||||||
@@ -414,22 +328,8 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "TOTP completion uses name and email from user attributes",
|
description: "TOTP completion uses name and email from user attributes",
|
||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{},
|
||||||
totpAttrCtx,
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
|
|
||||||
UUID: "test-totp-login-attributes-uuid",
|
|
||||||
Username: "test",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Name: "Test",
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: true,
|
|
||||||
Expiry: time.Now().Add(1 * time.Hour).Unix(),
|
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -439,13 +339,6 @@ func TestUserController(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(body)))
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(body)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.AddCookie(&http.Cookie{
|
|
||||||
Name: "tinyauth-session",
|
|
||||||
Value: "test-totp-login-attributes-uuid",
|
|
||||||
HttpOnly: true,
|
|
||||||
MaxAge: 3600,
|
|
||||||
Expires: time.Now().Add(1 * time.Hour),
|
|
||||||
})
|
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
require.Equal(t, 200, recorder.Code)
|
require.Equal(t, 200, recorder.Code)
|
||||||
@@ -456,6 +349,15 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||||
|
|
||||||
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
|
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
queries := repository.New(db)
|
||||||
|
|
||||||
docker := service.NewDockerService()
|
docker := service.NewDockerService()
|
||||||
err = docker.Init()
|
err = docker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -477,6 +379,33 @@ func TestUserController(t *testing.T) {
|
|||||||
authService.ClearRateLimitsTestingOnly()
|
authService.ClearRateLimitsTestingOnly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTotpMiddlewareOverrides := map[string]config.UserContext{
|
||||||
|
"Should be able to login with totp": {
|
||||||
|
Username: "totpuser",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
"Totp should rate limit on multiple invalid attempts": {
|
||||||
|
Username: "totpuser",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
"TOTP completion uses name and email from user attributes": {
|
||||||
|
Username: "attrtotpuser",
|
||||||
|
Name: "Bob Jones",
|
||||||
|
Email: "bob@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
beforeEach()
|
beforeEach()
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
@@ -486,6 +415,15 @@ func TestUserController(t *testing.T) {
|
|||||||
router.Use(middleware)
|
router.Use(middleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gin is stupid and doesn't allow setting a middleware after the groups
|
||||||
|
// so we need to do some stupid overrides here
|
||||||
|
if ctx, ok := setTotpMiddlewareOverrides[test.description]; ok {
|
||||||
|
ctx := ctx
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("context", &ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWellKnownController(t *testing.T) {
|
func TestWellKnownController(t *testing.T) {
|
||||||
@@ -23,7 +23,7 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
oidcServiceCfg := service.OIDCServiceConfig{
|
oidcServiceCfg := service.OIDCServiceConfig{
|
||||||
Clients: map[string]model.OIDCClientConfig{
|
Clients: map[string]config.OIDCClientConfig{
|
||||||
"test": {
|
"test": {
|
||||||
ClientID: "some-client-id",
|
ClientID: "some-client-id",
|
||||||
ClientSecret: "some-client-secret",
|
ClientSecret: "some-client-secret",
|
||||||
@@ -101,7 +101,7 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(model.Config{})
|
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||||
|
|
||||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -70,24 +70,26 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid)
|
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid)
|
||||||
|
|
||||||
if err == nil {
|
if err != nil {
|
||||||
if cookie != nil {
|
tlog.App.Error().Msgf("Error authenticating session cookie: %v", err)
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlog.App.Trace().Msgf("Authenticated user from session cookie: %s", userContext.GetUsername())
|
|
||||||
c.Set("context", userContext)
|
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
tlog.App.Error().Msgf("Error authenticating session cookie: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cookie != nil {
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Trace().Msgf("Authenticated user from session cookie: %s", userContext.GetUsername())
|
||||||
|
c.Set("context", userContext)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password, ok := c.Request.BasicAuth()
|
basic, err := m.auth.GetBasicAuth(c.Request)
|
||||||
|
|
||||||
if ok {
|
if err == nil {
|
||||||
userContext, headers, err := m.basicAuth(username, password)
|
userContext, headers, err := m.basicAuth(c.Request.Context(), basic)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Msgf("Error authenticating basic auth: %v", err)
|
tlog.App.Error().Msgf("Error authenticating basic auth: %v", err)
|
||||||
@@ -123,6 +125,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
|
|||||||
|
|
||||||
if userContext.Provider == model.ProviderLocal &&
|
if userContext.Provider == model.ProviderLocal &&
|
||||||
userContext.Local.TOTPPending {
|
userContext.Local.TOTPPending {
|
||||||
|
userContext.Local.TOTPEnabled = true
|
||||||
return userContext, nil, nil
|
return userContext, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,39 +188,39 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
|
|||||||
return userContext, cookie, nil
|
return userContext, cookie, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextMiddleware) basicAuth(username string, password string) (*model.UserContext, map[string]string, error) {
|
func (m *ContextMiddleware) basicAuth(ctx context.Context, basic *model.LocalUser) (*model.UserContext, map[string]string, error) {
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
userContext := new(model.UserContext)
|
userContext := new(model.UserContext)
|
||||||
locked, remaining := m.auth.IsAccountLocked(username)
|
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
||||||
|
|
||||||
if locked {
|
if locked {
|
||||||
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", username, remaining)
|
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
||||||
headers["x-tinyauth-lock-locked"] = "true"
|
headers["x-tinyauth-lock-locked"] = "true"
|
||||||
headers["x-tinyauth-lock-reset"] = time.Now().Add(time.Duration(remaining) * time.Second).Format(time.RFC3339)
|
headers["x-tinyauth-lock-reset"] = time.Now().Add(time.Duration(remaining) * time.Second).Format(time.RFC3339)
|
||||||
return nil, headers, nil
|
return nil, headers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
search, err := m.auth.SearchUser(username)
|
search, err := m.auth.SearchUser(basic.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error searching for user: %w", err)
|
return nil, nil, fmt.Errorf("error searching for user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.auth.CheckUserPassword(*search, password)
|
err = m.auth.CheckUserPassword(*search, basic.Password)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.auth.RecordLoginAttempt(username, false)
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
return nil, nil, fmt.Errorf("invalid password for basic auth user: %w", err)
|
return nil, nil, fmt.Errorf("invalid password for basic auth user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.auth.RecordLoginAttempt(username, true)
|
m.auth.RecordLoginAttempt(basic.Username, true)
|
||||||
|
|
||||||
switch search.Type {
|
switch search.Type {
|
||||||
case model.UserLocal:
|
case model.UserLocal:
|
||||||
user := m.auth.GetLocalUser(username)
|
user := m.auth.GetLocalUser(basic.Username)
|
||||||
|
|
||||||
if user.TOTPSecret != "" {
|
if user.TOTPSecret != "" {
|
||||||
return nil, nil, fmt.Errorf("user with totp not allowed to login via basic auth: %s", username)
|
return nil, nil, fmt.Errorf("user with totp not allowed to login via basic auth: %s", basic.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext.Local = &model.LocalContext{
|
userContext.Local = &model.LocalContext{
|
||||||
@@ -230,7 +233,7 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
|
|||||||
}
|
}
|
||||||
userContext.Provider = model.ProviderLocal
|
userContext.Provider = model.ProviderLocal
|
||||||
case model.UserLDAP:
|
case model.UserLDAP:
|
||||||
user, err := m.auth.GetLDAPUser(username)
|
user, err := m.auth.GetLDAPUser(basic.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
|
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
|
||||||
@@ -238,9 +241,9 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
|
|||||||
|
|
||||||
userContext.LDAP = &model.LDAPContext{
|
userContext.LDAP = &model.LDAPContext{
|
||||||
BaseContext: model.BaseContext{
|
BaseContext: model.BaseContext{
|
||||||
Username: username,
|
Username: basic.Username,
|
||||||
Name: utils.Capitalize(username),
|
Name: utils.Capitalize(basic.Username),
|
||||||
Email: utils.CompileUserEmail(username, m.config.CookieDomain),
|
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||||
},
|
},
|
||||||
Groups: user.Groups,
|
Groups: user.Groups,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,328 +0,0 @@
|
|||||||
package middleware_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContextMiddleware(t *testing.T) {
|
|
||||||
tlog.NewTestLogger().Init()
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
authServiceCfg := service.AuthServiceConfig{
|
|
||||||
LocalUsers: &[]model.LocalUser{
|
|
||||||
{
|
|
||||||
Username: "testuser",
|
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Username: "totpuser",
|
|
||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
|
||||||
TOTPSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SessionExpiry: 10, // 10 seconds, useful for testing
|
|
||||||
CookieDomain: "example.com",
|
|
||||||
LoginTimeout: 10, // 10 seconds, useful for testing
|
|
||||||
LoginMaxRetries: 3,
|
|
||||||
SessionCookieName: "tinyauth-session",
|
|
||||||
}
|
|
||||||
|
|
||||||
middlewareCfg := middleware.ContextMiddlewareConfig{
|
|
||||||
CookieDomain: "example.com",
|
|
||||||
SessionCookieName: "tinyauth-session",
|
|
||||||
}
|
|
||||||
|
|
||||||
basicAuthHeader := func(username, password string) string {
|
|
||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
|
||||||
}
|
|
||||||
|
|
||||||
seedSession := func(t *testing.T, queries *repository.Queries, params repository.CreateSessionParams) {
|
|
||||||
t.Helper()
|
|
||||||
_, err := queries.CreateSession(context.Background(), params)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type runArgs struct {
|
|
||||||
do func(req *http.Request) (*model.UserContext, *httptest.ResponseRecorder)
|
|
||||||
queries *repository.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
type testCase struct {
|
|
||||||
description string
|
|
||||||
run func(t *testing.T, args runArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
description: "Skip path bypasses auth processing",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/healthz", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("testuser", "password"))
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "No credentials yields no context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Valid session cookie sets authenticated local context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
uuid := "session-valid-local"
|
|
||||||
seedSession(t, args.queries, repository.CreateSessionParams{
|
|
||||||
UUID: uuid,
|
|
||||||
Username: "testuser",
|
|
||||||
Provider: "local",
|
|
||||||
Expiry: time.Now().Add(10 * time.Second).Unix(),
|
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "tinyauth-session", Value: uuid})
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
require.NotNil(t, userCtx)
|
|
||||||
assert.Equal(t, model.ProviderLocal, userCtx.Provider)
|
|
||||||
assert.Equal(t, "testuser", userCtx.GetUsername())
|
|
||||||
assert.True(t, userCtx.Authenticated)
|
|
||||||
require.NotNil(t, userCtx.Local)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Session cookie with totp pending sets unauthenticated context with totp enabled",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
uuid := "session-totp-pending"
|
|
||||||
seedSession(t, args.queries, repository.CreateSessionParams{
|
|
||||||
UUID: uuid,
|
|
||||||
Username: "totpuser",
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: true,
|
|
||||||
Expiry: time.Now().Add(60 * time.Second).Unix(),
|
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "tinyauth-session", Value: uuid})
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
require.NotNil(t, userCtx)
|
|
||||||
assert.Equal(t, "totpuser", userCtx.GetUsername())
|
|
||||||
assert.False(t, userCtx.Authenticated)
|
|
||||||
require.NotNil(t, userCtx.Local)
|
|
||||||
assert.True(t, userCtx.Local.TOTPPending)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Unknown session cookie yields no context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "tinyauth-session", Value: "does-not-exist"})
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Session for missing local user yields no context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
uuid := "session-deleted-user"
|
|
||||||
seedSession(t, args.queries, repository.CreateSessionParams{
|
|
||||||
UUID: uuid,
|
|
||||||
Username: "ghostuser",
|
|
||||||
Provider: "local",
|
|
||||||
Expiry: time.Now().Add(10 * time.Second).Unix(),
|
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "tinyauth-session", Value: uuid})
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Expired session cookie yields no context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
uuid := "session-expired"
|
|
||||||
seedSession(t, args.queries, repository.CreateSessionParams{
|
|
||||||
UUID: uuid,
|
|
||||||
Username: "testuser",
|
|
||||||
Provider: "local",
|
|
||||||
Expiry: time.Now().Add(-1 * time.Second).Unix(),
|
|
||||||
CreatedAt: time.Now().Add(-10 * time.Second).Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "tinyauth-session", Value: uuid})
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Valid basic auth sets authenticated local context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("testuser", "password"))
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
require.NotNil(t, userCtx)
|
|
||||||
assert.Equal(t, model.ProviderLocal, userCtx.Provider)
|
|
||||||
assert.Equal(t, "testuser", userCtx.GetUsername())
|
|
||||||
assert.True(t, userCtx.Authenticated)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Invalid basic auth password yields no context",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("testuser", "wrongpassword"))
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Basic auth is rejected for users with totp",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("totpuser", "password"))
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Locked account on basic auth sets lock headers",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
for range 3 {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("testuser", "wrongpassword"))
|
|
||||||
args.do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("testuser", "password"))
|
|
||||||
userCtx, recorder := args.do(req)
|
|
||||||
|
|
||||||
assert.Nil(t, userCtx)
|
|
||||||
assert.Equal(t, "true", recorder.Header().Get("x-tinyauth-lock-locked"))
|
|
||||||
assert.NotEmpty(t, recorder.Header().Get("x-tinyauth-lock-reset"))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Cookie auth takes precedence over basic auth",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
uuid := "session-precedence"
|
|
||||||
seedSession(t, args.queries, repository.CreateSessionParams{
|
|
||||||
UUID: uuid,
|
|
||||||
Username: "testuser",
|
|
||||||
Provider: "local",
|
|
||||||
Expiry: time.Now().Add(10 * time.Second).Unix(),
|
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "tinyauth-session", Value: uuid})
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("totpuser", "password"))
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
require.NotNil(t, userCtx)
|
|
||||||
assert.Equal(t, "testuser", userCtx.GetUsername())
|
|
||||||
assert.True(t, userCtx.Authenticated)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Ensure fallback to basic auth when cookie is missing",
|
|
||||||
run: func(t *testing.T, args runArgs) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
||||||
req.Header.Set("Authorization", basicAuthHeader("testuser", "password"))
|
|
||||||
userCtx, _ := args.do(req)
|
|
||||||
|
|
||||||
require.NotNil(t, userCtx)
|
|
||||||
assert.Equal(t, "testuser", userCtx.GetUsername())
|
|
||||||
assert.True(t, userCtx.Authenticated)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
oauthBrokerCfgs := make(map[string]model.OAuthServiceConfig)
|
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(model.Config{})
|
|
||||||
|
|
||||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
queries := repository.New(db)
|
|
||||||
|
|
||||||
ldap := service.NewLdapService(service.LdapServiceConfig{})
|
|
||||||
err = ldap.Init()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(oauthBrokerCfgs)
|
|
||||||
err = broker.Init()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
|
||||||
err = authService.Init()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(middlewareCfg, authService, broker)
|
|
||||||
err = contextMiddleware.Init()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
authService.ClearRateLimitsTestingOnly()
|
|
||||||
t.Run(test.description, func(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
do := func(req *http.Request) (*model.UserContext, *httptest.ResponseRecorder) {
|
|
||||||
var captured *model.UserContext
|
|
||||||
router := gin.New()
|
|
||||||
router.Use(contextMiddleware.Middleware())
|
|
||||||
handler := func(c *gin.Context) {
|
|
||||||
if val, exists := c.Get("context"); exists {
|
|
||||||
captured, _ = val.(*model.UserContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
router.GET("/api/test", handler)
|
|
||||||
router.GET("/api/healthz", handler)
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
return captured, recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
test.run(t, runArgs{do: do, queries: queries})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
err = db.Close()
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
SubdomainsEnabled: true,
|
|
||||||
SessionExpiry: 86400, // 1 day
|
SessionExpiry: 86400, // 1 day
|
||||||
SessionMaxLifetime: 0, // disabled
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300, // 5 minutes
|
LoginTimeout: 300, // 5 minutes
|
||||||
@@ -103,7 +102,6 @@ type ServerConfig struct {
|
|||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||||
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
|
|
||||||
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||||
|
|||||||
+15
-59
@@ -34,6 +34,7 @@ type BaseContext struct {
|
|||||||
type LocalContext struct {
|
type LocalContext struct {
|
||||||
BaseContext
|
BaseContext
|
||||||
TOTPPending bool
|
TOTPPending bool
|
||||||
|
TOTPEnabled bool
|
||||||
Attributes UserAttributes
|
Attributes UserAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,19 +56,19 @@ func (c *UserContext) IsAuthenticated() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) IsLocal() bool {
|
func (c *UserContext) IsLocal() bool {
|
||||||
return c.Provider == ProviderLocal && c.Local != nil
|
return c.Provider == ProviderLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) IsOAuth() bool {
|
func (c *UserContext) IsOAuth() bool {
|
||||||
return c.Provider == ProviderOAuth && c.OAuth != nil
|
return c.Provider == ProviderOAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) IsLDAP() bool {
|
func (c *UserContext) IsLDAP() bool {
|
||||||
return c.Provider == ProviderLDAP && c.LDAP != nil
|
return c.Provider == ProviderLDAP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) IsBasicAuth() bool {
|
func (c *UserContext) IsBasicAuth() bool {
|
||||||
return c.Provider == ProviderBasicAuth && c.Local != nil
|
return c.Provider == ProviderBasicAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
||||||
@@ -79,24 +80,16 @@ func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
|||||||
|
|
||||||
userContext, ok := userContextValue.(*UserContext)
|
userContext, ok := userContextValue.(*UserContext)
|
||||||
|
|
||||||
if !ok || userContext == nil {
|
if !ok {
|
||||||
return nil, errors.New("invalid user context type")
|
return nil, errors.New("invalid user context type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.LDAP == nil && userContext.Local == nil && userContext.OAuth == nil {
|
|
||||||
return nil, errors.New("incomplete user context")
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = *userContext
|
*c = *userContext
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compatability layer until we get an excuse to drop in database migrations
|
// Compatability layer until we get an excuse to drop in database migrations
|
||||||
func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) {
|
func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) {
|
||||||
*c = UserContext{
|
|
||||||
Authenticated: !session.TotpPending,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch session.Provider {
|
switch session.Provider {
|
||||||
case "local":
|
case "local":
|
||||||
c.Provider = ProviderLocal
|
c.Provider = ProviderLocal
|
||||||
@@ -126,42 +119,29 @@ func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext,
|
|||||||
Name: session.Name,
|
Name: session.Name,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
},
|
},
|
||||||
Groups: func() []string {
|
Groups: strings.Split(session.OAuthGroups, ","),
|
||||||
if session.OAuthGroups == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return strings.Split(session.OAuthGroups, ",")
|
|
||||||
}(),
|
|
||||||
Sub: session.OAuthSub,
|
Sub: session.OAuthSub,
|
||||||
DisplayName: session.OAuthName,
|
DisplayName: session.OAuthName,
|
||||||
ID: session.Provider,
|
ID: session.Provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !session.TotpPending {
|
||||||
|
c.Authenticated = true
|
||||||
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) GetUsername() string {
|
func (c *UserContext) GetUsername() string {
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case ProviderLocal:
|
case ProviderLocal:
|
||||||
if c.Local == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.Local.Username
|
return c.Local.Username
|
||||||
case ProviderLDAP:
|
case ProviderLDAP:
|
||||||
if c.LDAP == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.LDAP.Username
|
return c.LDAP.Username
|
||||||
case ProviderBasicAuth:
|
case ProviderBasicAuth:
|
||||||
if c.Local == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.Local.Username
|
return c.Local.Username
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
if c.OAuth == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.OAuth.Username
|
return c.OAuth.Username
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
@@ -171,24 +151,12 @@ func (c *UserContext) GetUsername() string {
|
|||||||
func (c *UserContext) GetEmail() string {
|
func (c *UserContext) GetEmail() string {
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case ProviderLocal:
|
case ProviderLocal:
|
||||||
if c.Local == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.Local.Email
|
return c.Local.Email
|
||||||
case ProviderLDAP:
|
case ProviderLDAP:
|
||||||
if c.LDAP == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.LDAP.Email
|
return c.LDAP.Email
|
||||||
case ProviderBasicAuth:
|
case ProviderBasicAuth:
|
||||||
if c.Local == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.Local.Email
|
return c.Local.Email
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
if c.OAuth == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.OAuth.Email
|
return c.OAuth.Email
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
@@ -198,52 +166,40 @@ func (c *UserContext) GetEmail() string {
|
|||||||
func (c *UserContext) GetName() string {
|
func (c *UserContext) GetName() string {
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case ProviderLocal:
|
case ProviderLocal:
|
||||||
if c.Local == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.Local.Name
|
return c.Local.Name
|
||||||
case ProviderLDAP:
|
case ProviderLDAP:
|
||||||
if c.LDAP == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.LDAP.Name
|
return c.LDAP.Name
|
||||||
case ProviderBasicAuth:
|
case ProviderBasicAuth:
|
||||||
if c.Local == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.Local.Name
|
return c.Local.Name
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
if c.OAuth == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.OAuth.Name
|
return c.OAuth.Name
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) GetProviderID() string {
|
func (c *UserContext) ProviderName() string {
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case ProviderBasicAuth, ProviderLocal:
|
case ProviderBasicAuth, ProviderLocal:
|
||||||
return "local"
|
return "local"
|
||||||
case ProviderLDAP:
|
case ProviderLDAP:
|
||||||
return "ldap"
|
return "ldap"
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
return c.OAuth.ID
|
return c.OAuth.DisplayName // compatability
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) TOTPPending() bool {
|
func (c *UserContext) TOTPPending() bool {
|
||||||
if c.Provider == ProviderLocal && c.Local != nil {
|
if c.Provider == ProviderLocal {
|
||||||
return c.Local.TOTPPending
|
return c.Local.TOTPPending
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) OAuthName() string {
|
func (c *UserContext) OAuthName() string {
|
||||||
if c.Provider == ProviderOAuth && c.OAuth != nil {
|
if c.Provider == ProviderOAuth {
|
||||||
return c.OAuth.DisplayName
|
return c.OAuth.DisplayName
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
package model_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContext(t *testing.T) {
|
|
||||||
newGinCtx := func(value any, set bool) *gin.Context {
|
|
||||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
||||||
if set {
|
|
||||||
c.Set("context", value)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
description string
|
|
||||||
context *model.UserContext
|
|
||||||
run func(*testing.T, *model.UserContext) any
|
|
||||||
expected any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
description: "IsAuthenticated reflects Authenticated field",
|
|
||||||
context: &model.UserContext{Authenticated: true},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsAuthenticated() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsLocal returns true for ProviderLocal",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal, Local: &model.LocalContext{}},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsLocal() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsOAuth returns true for ProviderOAuth",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderOAuth, OAuth: &model.OAuthContext{}},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsOAuth() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsLDAP returns true for ProviderLDAP",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP, LDAP: &model.LDAPContext{}},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsLDAP() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsBasicAuth returns true for ProviderBasicAuth",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth, Local: &model.LocalContext{}},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsBasicAuth() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession local session is authenticated and ProviderLocal",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "alice", Email: "alice@example.com", Name: "Alice",
|
|
||||||
Provider: "local",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
return [2]any{got.Provider, got.Authenticated}
|
|
||||||
},
|
|
||||||
expected: [2]any{model.ProviderLocal, true},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession local session with TotpPending is not authenticated",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "bob", Provider: "local", TotpPending: true,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
return got.Authenticated
|
|
||||||
},
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession ldap session is ProviderLDAP",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "carol", Provider: "ldap",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
return got.Provider
|
|
||||||
},
|
|
||||||
expected: model.ProviderLDAP,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession unknown provider defaults to OAuth and populates oauth fields",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "dave", Provider: "github",
|
|
||||||
OAuthGroups: "devs,admins", OAuthSub: "sub-123", OAuthName: "GitHub",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
return [5]any{got.Provider, got.OAuth.ID, got.OAuth.Sub, got.OAuth.DisplayName, got.OAuth.Groups}
|
|
||||||
},
|
|
||||||
expected: [5]any{model.ProviderOAuth, "github", "sub-123", "GitHub", []string{"devs", "admins"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Local getters return BaseContext fields",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{BaseContext: model.BaseContext{Username: "alice", Email: "alice@example.com", Name: "Alice"}},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
|
||||||
},
|
|
||||||
expected: [3]string{"alice", "alice@example.com", "Alice"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "BasicAuth getters fall back to local fields",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderBasicAuth,
|
|
||||||
Local: &model.LocalContext{BaseContext: model.BaseContext{Username: "bob", Email: "bob@example.com", Name: "Bob"}},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
|
||||||
},
|
|
||||||
expected: [3]string{"bob", "bob@example.com", "Bob"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "LDAP getters return LDAP fields",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderLDAP,
|
|
||||||
LDAP: &model.LDAPContext{BaseContext: model.BaseContext{Username: "carol", Email: "carol@example.com", Name: "Carol"}},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
|
||||||
},
|
|
||||||
expected: [3]string{"carol", "carol@example.com", "Carol"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "OAuth getters return OAuth fields",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{BaseContext: model.BaseContext{Username: "dave", Email: "dave@example.com", Name: "Dave"}},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
|
||||||
},
|
|
||||||
expected: [3]string{"dave", "dave@example.com", "Dave"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns 'local' for ProviderLocal",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
|
||||||
expected: "local",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns 'local' for ProviderBasicAuth",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
|
||||||
expected: "local",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns 'ldap' for ProviderLDAP",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
|
||||||
expected: "ldap",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns OAuth provider ID for ProviderOAuth",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{ID: "github"},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
|
||||||
expected: "github",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "TOTPPending returns true when local context is pending",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{TOTPPending: true},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.TOTPPending() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "TOTPPending returns false when local context is not pending",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{TOTPPending: false},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.TOTPPending() },
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "TOTPPending returns false for non-local providers",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderOAuth, OAuth: &model.OAuthContext{}},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.TOTPPending() },
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "OAuthName returns DisplayName for ProviderOAuth",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{DisplayName: "Google"},
|
|
||||||
},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.OAuthName() },
|
|
||||||
expected: "Google",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "OAuthName returns empty string for non-oauth providers",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal, Local: &model.LocalContext{}},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.OAuthName() },
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromGin populates context from gin value",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
stored := &model.UserContext{
|
|
||||||
Authenticated: true,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{BaseContext: model.BaseContext{Username: "alice"}},
|
|
||||||
}
|
|
||||||
got, err := c.NewFromGin(newGinCtx(stored, true))
|
|
||||||
require.NoError(t, err)
|
|
||||||
return [2]any{got.Authenticated, got.GetUsername()}
|
|
||||||
},
|
|
||||||
expected: [2]any{true, "alice"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromGin returns error when context value is missing",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
_, err := c.NewFromGin(newGinCtx(nil, false))
|
|
||||||
return err.Error()
|
|
||||||
},
|
|
||||||
expected: "failed to get user context",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromGin returns error when context value has wrong type",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
_, err := c.NewFromGin(newGinCtx("not a user context", true))
|
|
||||||
return err.Error()
|
|
||||||
},
|
|
||||||
expected: "invalid user context type",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromGin returns an error when context doesn't include user information",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
_, err := c.NewFromGin(newGinCtx(&model.UserContext{Provider: model.ProviderLocal}, true))
|
|
||||||
return err.Error()
|
|
||||||
},
|
|
||||||
expected: "incomplete user context",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Getters should not panic if provider context is empty",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal},
|
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
|
||||||
},
|
|
||||||
expected: [3]string{"", "", ""},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.description, func(t *testing.T) {
|
|
||||||
assert.Equal(t, test.expected, test.run(t, test.context))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
@@ -27,29 +28,26 @@ func (acls *AccessControlsService) Init() error {
|
|||||||
return nil // No initialization needed
|
return nil // No initialization needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
func (acls *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) {
|
||||||
var appAcls *model.App
|
|
||||||
for app, config := range acls.static {
|
for app, config := range acls.static {
|
||||||
if config.Config.Domain == domain {
|
if config.Config.Domain == domain {
|
||||||
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||||
appAcls = &config
|
return &config, nil
|
||||||
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 {
|
||||||
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||||
appAcls = &config
|
return &config, nil
|
||||||
break // If we find a match by app name, we can stop searching
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return appAcls
|
return nil, errors.New("no results")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||||
// First check in the static config
|
// First check in the static config
|
||||||
app := acls.lookupStaticACLs(domain)
|
app, err := acls.lookupStaticACLs(domain)
|
||||||
|
|
||||||
if app != nil {
|
if err == nil {
|
||||||
tlog.App.Debug().Msg("Using ACls from static configuration")
|
tlog.App.Debug().Msg("Using ACls from static configuration")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ type Lockdown struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthServiceConfig struct {
|
type AuthServiceConfig struct {
|
||||||
LocalUsers *[]model.LocalUser
|
LocalUsers []model.LocalUser
|
||||||
OauthWhitelist []string
|
OauthWhitelist []string
|
||||||
SessionExpiry int
|
SessionExpiry int
|
||||||
SessionMaxLifetime int
|
SessionMaxLifetime int
|
||||||
@@ -84,7 +84,6 @@ type AuthServiceConfig struct {
|
|||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
IP model.IPConfig
|
IP model.IPConfig
|
||||||
LDAPGroupsCacheTTL int
|
LDAPGroupsCacheTTL int
|
||||||
SubdomainsEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
@@ -121,7 +120,7 @@ func (auth *AuthService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {
|
func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {
|
||||||
if auth.GetLocalUser(username) != nil {
|
if auth.GetLocalUser(username).Username != "" {
|
||||||
return &model.UserSearch{
|
return &model.UserSearch{
|
||||||
Username: username,
|
Username: username,
|
||||||
Type: model.UserLocal,
|
Type: model.UserLocal,
|
||||||
@@ -148,9 +147,6 @@ func (auth *AuthService) CheckUserPassword(search model.UserSearch, password str
|
|||||||
switch search.Type {
|
switch search.Type {
|
||||||
case model.UserLocal:
|
case model.UserLocal:
|
||||||
user := auth.GetLocalUser(search.Username)
|
user := auth.GetLocalUser(search.Username)
|
||||||
if user == nil {
|
|
||||||
return ErrUserNotFound
|
|
||||||
}
|
|
||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||||
case model.UserLDAP:
|
case model.UserLDAP:
|
||||||
if auth.ldap.IsConfigured() {
|
if auth.ldap.IsConfigured() {
|
||||||
@@ -173,10 +169,7 @@ func (auth *AuthService) CheckUserPassword(search model.UserSearch, password str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetLocalUser(username string) *model.LocalUser {
|
func (auth *AuthService) GetLocalUser(username string) *model.LocalUser {
|
||||||
if auth.config.LocalUsers == nil {
|
for _, user := range auth.config.LocalUsers {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, user := range *auth.config.LocalUsers {
|
|
||||||
if user.Username == username {
|
if user.Username == username {
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
@@ -302,8 +295,6 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
|||||||
expiry = auth.config.SessionExpiry
|
expiry = auth.config.SessionExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt := time.Now().Add(time.Duration(expiry) * time.Second)
|
|
||||||
|
|
||||||
session := repository.CreateSessionParams{
|
session := repository.CreateSessionParams{
|
||||||
UUID: uuid.String(),
|
UUID: uuid.String(),
|
||||||
Username: data.Username,
|
Username: data.Username,
|
||||||
@@ -312,7 +303,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
|||||||
Provider: data.Provider,
|
Provider: data.Provider,
|
||||||
TotpPending: data.TotpPending,
|
TotpPending: data.TotpPending,
|
||||||
OAuthGroups: data.OAuthGroups,
|
OAuthGroups: data.OAuthGroups,
|
||||||
Expiry: expiresAt.Unix(),
|
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||||
CreatedAt: time.Now().Unix(),
|
CreatedAt: time.Now().Unix(),
|
||||||
OAuthName: data.OAuthName,
|
OAuthName: data.OAuthName,
|
||||||
OAuthSub: data.OAuthSub,
|
OAuthSub: data.OAuthSub,
|
||||||
@@ -329,8 +320,8 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
|||||||
Value: session.UUID,
|
Value: session.UUID,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
||||||
Expires: expiresAt,
|
Expires: time.Now().Add(time.Duration(expiry) * time.Second),
|
||||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
MaxAge: expiry,
|
||||||
Secure: auth.config.SecureCookie,
|
Secure: auth.config.SecureCookie,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
@@ -383,7 +374,7 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
||||||
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
|
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
|
||||||
MaxAge: int(newExpiry - currentTime),
|
MaxAge: auth.config.SessionExpiry,
|
||||||
Secure: auth.config.SecureCookie,
|
Secure: auth.config.SecureCookie,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
@@ -398,12 +389,6 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
|
|||||||
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
|
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.queries.DeleteSession(ctx, uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: auth.config.SessionCookieName,
|
Name: auth.config.SessionCookieName,
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -451,7 +436,7 @@ func (auth *AuthService) GetSession(ctx context.Context, uuid string) (*reposito
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) LocalAuthConfigured() bool {
|
func (auth *AuthService) LocalAuthConfigured() bool {
|
||||||
return auth.config.LocalUsers != nil && len(*auth.config.LocalUsers) > 0
|
return len(auth.config.LocalUsers) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) LDAPAuthConfigured() bool {
|
func (auth *AuthService) LDAPAuthConfigured() bool {
|
||||||
@@ -479,8 +464,8 @@ func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext
|
|||||||
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, requiredGroups string) bool {
|
||||||
if acls == nil {
|
if requiredGroups == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,8 +480,8 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContex
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, userGroup := range context.OAuth.Groups {
|
for _, userGroup := range context.OAuth.Groups {
|
||||||
if utils.CheckFilter(acls.OAuth.Groups, strings.TrimSpace(userGroup)) {
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
tlog.App.Trace().Str("group", userGroup).Str("required", acls.OAuth.Groups).Msg("User group matched")
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,8 +490,8 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContex
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, requiredGroups string) bool {
|
||||||
if acls == nil {
|
if requiredGroups == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,8 +501,8 @@ func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, userGroup := range context.LDAP.Groups {
|
for _, userGroup := range context.LDAP.Groups {
|
||||||
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
tlog.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,14 +511,14 @@ func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsAuthEnabled(uri string, acls *model.App) (bool, error) {
|
func (auth *AuthService) IsAuthEnabled(uri string, path *model.AppPath) (bool, error) {
|
||||||
if acls == nil {
|
if path == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for block list
|
// Check for block list
|
||||||
if acls.Path.Block != "" {
|
if path.Block != "" {
|
||||||
regex, err := regexp.Compile(acls.Path.Block)
|
regex, err := regexp.Compile(path.Block)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
@@ -545,8 +530,8 @@ func (auth *AuthService) IsAuthEnabled(uri string, acls *model.App) (bool, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for allow list
|
// Check for allow list
|
||||||
if acls.Path.Allow != "" {
|
if path.Allow != "" {
|
||||||
regex, err := regexp.Compile(acls.Path.Allow)
|
regex, err := regexp.Compile(path.Allow)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
@@ -560,14 +545,29 @@ func (auth *AuthService) IsAuthEnabled(uri string, acls *model.App) (bool, error
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckIP(ip string, acls *model.App) bool {
|
// local user is used only as a medium to pass the basic auth credentials, user can be ldap too
|
||||||
|
func (auth *AuthService) GetBasicAuth(req *http.Request) (*model.LocalUser, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, errors.New("request is nil")
|
||||||
|
}
|
||||||
|
username, password, ok := req.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("no basic auth credentials provided")
|
||||||
|
}
|
||||||
|
return &model.LocalUser{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) CheckIP(acls *model.AppIP, ip string) bool {
|
||||||
if acls == nil {
|
if acls == nil {
|
||||||
return true
|
acls = &model.AppIP{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge the global and app IP filter
|
// Merge the global and app IP filter
|
||||||
blockedIps := append(auth.config.IP.Block, acls.IP.Block...)
|
blockedIps := append(auth.config.IP.Block, acls.Block...)
|
||||||
allowedIPs := append(auth.config.IP.Allow, acls.IP.Allow...)
|
allowedIPs := append(auth.config.IP.Allow, acls.Allow...)
|
||||||
|
|
||||||
for _, blocked := range blockedIps {
|
for _, blocked := range blockedIps {
|
||||||
res, err := utils.FilterIP(blocked, ip)
|
res, err := utils.FilterIP(blocked, ip)
|
||||||
@@ -602,12 +602,12 @@ func (auth *AuthService) CheckIP(ip string, acls *model.App) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsBypassedIP(ip string, acls *model.App) bool {
|
func (auth *AuthService) IsBypassedIP(acls *model.AppIP, ip string) bool {
|
||||||
if acls == nil {
|
if acls == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bypassed := range acls.IP.Bypass {
|
for _, bypassed := range acls.Bypass {
|
||||||
res, err := utils.FilterIP(bypassed, ip)
|
res, err := utils.FilterIP(bypassed, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
tlog.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||||
@@ -845,10 +845,3 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() {
|
|||||||
}
|
}
|
||||||
auth.loginMutex.Unlock()
|
auth.loginMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) getCookieDomain() string {
|
|
||||||
if auth.config.SubdomainsEnabled {
|
|
||||||
return "." + auth.config.CookieDomain
|
|
||||||
}
|
|
||||||
return auth.config.CookieDomain
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -51,11 +51,19 @@ func (docker *DockerService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (docker *DockerService) getContainers() ([]container.Summary, error) {
|
func (docker *DockerService) getContainers() ([]container.Summary, error) {
|
||||||
return docker.client.ContainerList(docker.context, container.ListOptions{})
|
containers, err := docker.client.ContainerList(docker.context, container.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (docker *DockerService) inspectContainer(containerId string) (container.InspectResponse, error) {
|
func (docker *DockerService) inspectContainer(containerId string) (container.InspectResponse, error) {
|
||||||
return docker.client.ContainerInspect(docker.context, containerId)
|
inspect, err := docker.client.ContainerInspect(docker.context, containerId)
|
||||||
|
if err != nil {
|
||||||
|
return container.InspectResponse{}, err
|
||||||
|
}
|
||||||
|
return inspect, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) {
|
func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) {
|
||||||
|
|||||||
@@ -89,38 +89,36 @@ func (k *KubernetesService) removeIngress(namespace, name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *KubernetesService) getByDomain(domain string) *model.App {
|
func (k *KubernetesService) getByDomain(domain string) (*model.App, bool) {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
|
|
||||||
if appKey, ok := k.domainIndex[domain]; ok {
|
if appKey, ok := k.domainIndex[domain]; ok {
|
||||||
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
||||||
for i := range apps {
|
for _, app := range apps {
|
||||||
app := &apps[i]
|
|
||||||
if app.domain == domain && app.appName == appKey.appName {
|
if app.domain == domain && app.appName == appKey.appName {
|
||||||
return &app.app
|
return &app.app, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *KubernetesService) getByAppName(appName string) *model.App {
|
func (k *KubernetesService) getByAppName(appName string) (*model.App, bool) {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
|
|
||||||
if appKey, ok := k.appNameIndex[appName]; ok {
|
if appKey, ok := k.appNameIndex[appName]; ok {
|
||||||
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
||||||
for i := range apps {
|
for _, app := range apps {
|
||||||
app := &apps[i]
|
|
||||||
if app.appName == appName {
|
if app.appName == appName {
|
||||||
return &app.app
|
return &app.app, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
|
func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
|
||||||
@@ -289,14 +287,12 @@ func (k *KubernetesService) GetLabels(appDomain string) (*model.App, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First check cache
|
// First check cache
|
||||||
app := k.getByDomain(appDomain)
|
if app, found := k.getByDomain(appDomain); found {
|
||||||
if app != nil {
|
|
||||||
tlog.App.Debug().Str("domain", appDomain).Msg("Found labels in cache by domain")
|
tlog.App.Debug().Str("domain", appDomain).Msg("Found labels in cache by domain")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
appName := strings.SplitN(appDomain, ".", 2)[0]
|
appName := strings.SplitN(appDomain, ".", 2)[0]
|
||||||
app = k.getByAppName(appName)
|
if app, found := k.getByAppName(appName); found {
|
||||||
if app != nil {
|
|
||||||
tlog.App.Debug().Str("domain", appDomain).Str("appName", appName).Msg("Found labels in cache by app name")
|
tlog.App.Debug().Str("domain", appDomain).Str("appName", appName).Msg("Found labels in cache by app name")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package service
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKubernetesService(t *testing.T) {
|
func TestKubernetesService(t *testing.T) {
|
||||||
@@ -20,69 +20,69 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "Cache by domain returns app and misses unknown domain",
|
description: "Cache by domain returns app and misses unknown domain",
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
|
app := config.App{Config: config.AppConfig{Domain: "foo.example.com"}}
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
{domain: "foo.example.com", appName: "foo", app: app},
|
{domain: "foo.example.com", appName: "foo", app: app},
|
||||||
})
|
})
|
||||||
|
|
||||||
got := svc.getByDomain("foo.example.com")
|
got, ok := svc.getByDomain("foo.example.com")
|
||||||
require.NotNil(t, got)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
||||||
|
|
||||||
got = svc.getByDomain("notfound.example.com")
|
_, ok = svc.getByDomain("notfound.example.com")
|
||||||
assert.Nil(t, got)
|
assert.False(t, ok)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Cache by app name returns app and misses unknown name",
|
description: "Cache by app name returns app and misses unknown name",
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
app := model.App{Config: model.AppConfig{Domain: "bar.example.com"}}
|
app := config.App{Config: config.AppConfig{Domain: "bar.example.com"}}
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
{domain: "bar.example.com", appName: "bar", app: app},
|
{domain: "bar.example.com", appName: "bar", app: app},
|
||||||
})
|
})
|
||||||
|
|
||||||
got := svc.getByAppName("bar")
|
got, ok := svc.getByAppName("bar")
|
||||||
require.NotNil(t, got)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "bar.example.com", got.Config.Domain)
|
assert.Equal(t, "bar.example.com", got.Config.Domain)
|
||||||
|
|
||||||
got = svc.getByAppName("notfound")
|
_, ok = svc.getByAppName("notfound")
|
||||||
assert.Nil(t, got)
|
assert.False(t, ok)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "RemoveIngress clears domain and app name entries",
|
description: "RemoveIngress clears domain and app name entries",
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
app := model.App{Config: model.AppConfig{Domain: "baz.example.com"}}
|
app := config.App{Config: config.AppConfig{Domain: "baz.example.com"}}
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
{domain: "baz.example.com", appName: "baz", app: app},
|
{domain: "baz.example.com", appName: "baz", app: app},
|
||||||
})
|
})
|
||||||
|
|
||||||
svc.removeIngress("default", "my-ingress")
|
svc.removeIngress("default", "my-ingress")
|
||||||
|
|
||||||
got := svc.getByDomain("baz.example.com")
|
_, ok := svc.getByDomain("baz.example.com")
|
||||||
assert.Nil(t, got)
|
assert.False(t, ok)
|
||||||
got = svc.getByAppName("baz")
|
_, ok = svc.getByAppName("baz")
|
||||||
assert.Nil(t, got)
|
assert.False(t, ok)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "AddIngressApps replaces stale entries for the same ingress",
|
description: "AddIngressApps replaces stale entries for the same ingress",
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
old := model.App{Config: model.AppConfig{Domain: "old.example.com"}}
|
old := config.App{Config: config.AppConfig{Domain: "old.example.com"}}
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
{domain: "old.example.com", appName: "old", app: old},
|
{domain: "old.example.com", appName: "old", app: old},
|
||||||
})
|
})
|
||||||
|
|
||||||
updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}}
|
updated := config.App{Config: config.AppConfig{Domain: "new.example.com"}}
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
{domain: "new.example.com", appName: "new", app: updated},
|
{domain: "new.example.com", appName: "new", app: updated},
|
||||||
})
|
})
|
||||||
|
|
||||||
got := svc.getByDomain("old.example.com")
|
_, ok := svc.getByDomain("old.example.com")
|
||||||
assert.Nil(t, got)
|
assert.False(t, ok)
|
||||||
|
|
||||||
got = svc.getByDomain("new.example.com")
|
got, ok := svc.getByDomain("new.example.com")
|
||||||
require.NotNil(t, got)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "new.example.com", got.Config.Domain)
|
assert.Equal(t, "new.example.com", got.Config.Domain)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -91,7 +91,7 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
svc.started = true
|
svc.started = true
|
||||||
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}}
|
app := config.App{Config: config.AppConfig{Domain: "hit.example.com"}}
|
||||||
svc.addIngressApps("default", "ing", []ingressApp{
|
svc.addIngressApps("default", "ing", []ingressApp{
|
||||||
{domain: "hit.example.com", appName: "hit", app: app},
|
{domain: "hit.example.com", appName: "hit", app: app},
|
||||||
})
|
})
|
||||||
@@ -108,7 +108,7 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
|
|
||||||
got, err := svc.GetLabels("notfound.example.com")
|
got, err := svc.GetLabels("notfound.example.com")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Nil(t, got)
|
assert.Equal(t, config.App{}, got)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,7 +116,7 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
svc.started = true
|
svc.started = true
|
||||||
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "myapp.internal.example.com"}}
|
app := config.App{Config: config.AppConfig{Domain: "myapp.internal.example.com"}}
|
||||||
svc.addIngressApps("default", "ing", []ingressApp{
|
svc.addIngressApps("default", "ing", []ingressApp{
|
||||||
{domain: "myapp.internal.example.com", appName: "myapp", app: app},
|
{domain: "myapp.internal.example.com", appName: "myapp", app: app},
|
||||||
})
|
})
|
||||||
@@ -131,7 +131,7 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
got, err := svc.GetLabels("anything.example.com")
|
got, err := svc.GetLabels("anything.example.com")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Nil(t, got)
|
assert.Equal(t, config.App{}, got)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -147,8 +147,8 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
|
|
||||||
svc.updateFromItem(&item)
|
svc.updateFromItem(&item)
|
||||||
|
|
||||||
got := svc.getByDomain("myapp.example.com")
|
got, ok := svc.getByDomain("myapp.example.com")
|
||||||
require.NotNil(t, got)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "myapp.example.com", got.Config.Domain)
|
assert.Equal(t, "myapp.example.com", got.Config.Domain)
|
||||||
assert.Equal(t, "alice", got.Users.Allow)
|
assert.Equal(t, "alice", got.Users.Allow)
|
||||||
},
|
},
|
||||||
@@ -156,7 +156,7 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "UpdateFromItem with no annotations removes existing cache entries",
|
description: "UpdateFromItem with no annotations removes existing cache entries",
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}}
|
app := config.App{Config: config.AppConfig{Domain: "todelete.example.com"}}
|
||||||
svc.addIngressApps("default", "test-ingress", []ingressApp{
|
svc.addIngressApps("default", "test-ingress", []ingressApp{
|
||||||
{domain: "todelete.example.com", appName: "todelete", app: app},
|
{domain: "todelete.example.com", appName: "todelete", app: app},
|
||||||
})
|
})
|
||||||
@@ -167,8 +167,8 @@ func TestKubernetesService(t *testing.T) {
|
|||||||
|
|
||||||
svc.updateFromItem(&item)
|
svc.updateFromItem(&item)
|
||||||
|
|
||||||
got := svc.getByDomain("todelete.example.com")
|
_, ok := svc.getByDomain("todelete.example.com")
|
||||||
assert.Nil(t, got)
|
assert.False(t, ok)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GithubEmailResponse []struct {
|
type GithubEmailResponse []struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Primary bool `json:"primary"`
|
Primary bool `json:"primary"`
|
||||||
Verified bool `json:"verified"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GithubUserInfoResponse struct {
|
type GithubUserInfoResponse struct {
|
||||||
@@ -27,7 +26,7 @@ func defaultExtractor(client *http.Client, url string) (*model.Claims, error) {
|
|||||||
return simpleReq[model.Claims](client, url, nil)
|
return simpleReq[model.Claims](client, url, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubExtractor(client *http.Client, _ string) (*model.Claims, error) {
|
func githubExtractor(client *http.Client, url string) (*model.Claims, error) {
|
||||||
var user model.Claims
|
var user model.Claims
|
||||||
|
|
||||||
userInfo, err := simpleReq[GithubUserInfoResponse](client, "https://api.github.com/user", map[string]string{
|
userInfo, err := simpleReq[GithubUserInfoResponse](client, "https://api.github.com/user", map[string]string{
|
||||||
@@ -49,7 +48,7 @@ func githubExtractor(client *http.Client, _ string) (*model.Claims, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, email := range *userEmails {
|
for _, email := range *userEmails {
|
||||||
if email.Primary && email.Verified {
|
if email.Primary {
|
||||||
user.Email = email.Email
|
user.Email = email.Email
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -57,16 +56,7 @@ func githubExtractor(client *http.Client, _ string) (*model.Claims, error) {
|
|||||||
|
|
||||||
// Use first available email if no primary email was found
|
// Use first available email if no primary email was found
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
for _, email := range *userEmails {
|
user.Email = (*userEmails)[0].Email
|
||||||
if email.Verified {
|
|
||||||
user.Email = email.Email
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Email == "" {
|
|
||||||
return nil, errors.New("no verified email found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.PreferredUsername = userInfo.Login
|
user.PreferredUsername = userInfo.Login
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestUser() repository.OidcUserinfo {
|
func newTestUser() repository.OidcUserinfo {
|
||||||
addr := model.AddressClaim{
|
addr := config.AddressClaim{
|
||||||
Formatted: "123 Main St",
|
Formatted: "123 Main St",
|
||||||
StreetAddress: "123 Main St",
|
StreetAddress: "123 Main St",
|
||||||
Locality: "Springfield",
|
Locality: "Springfield",
|
||||||
|
|||||||
@@ -47,15 +47,6 @@ func GetCookieDomain(u string) (string, error) {
|
|||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStandaloneCookieDomain(u string) (string, error) {
|
|
||||||
parsed, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed.Hostname(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseFileToLine(content string) string {
|
func ParseFileToLine(content string) string {
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
users := make([]string, 0)
|
users := make([]string, 0)
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetRootDomain(t *testing.T) {
|
func TestGetRootDomain(t *testing.T) {
|
||||||
@@ -12,14 +13,14 @@ func TestGetRootDomain(t *testing.T) {
|
|||||||
domain := "http://sub.tinyauth.app"
|
domain := "http://sub.tinyauth.app"
|
||||||
expected := "tinyauth.app"
|
expected := "tinyauth.app"
|
||||||
result, err := utils.GetCookieDomain(domain)
|
result, err := utils.GetCookieDomain(domain)
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
// Domain with multiple subdomains
|
// Domain with multiple subdomains
|
||||||
domain = "http://b.c.tinyauth.app"
|
domain = "http://b.c.tinyauth.app"
|
||||||
expected = "c.tinyauth.app"
|
expected = "c.tinyauth.app"
|
||||||
result, err = utils.GetCookieDomain(domain)
|
result, err = utils.GetCookieDomain(domain)
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
// Invalid domain (only TLD)
|
// Invalid domain (only TLD)
|
||||||
@@ -41,14 +42,14 @@ func TestGetRootDomain(t *testing.T) {
|
|||||||
domain = "https://sub.tinyauth.app/path"
|
domain = "https://sub.tinyauth.app/path"
|
||||||
expected = "tinyauth.app"
|
expected = "tinyauth.app"
|
||||||
result, err = utils.GetCookieDomain(domain)
|
result, err = utils.GetCookieDomain(domain)
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
// URL with port
|
// URL with port
|
||||||
domain = "http://sub.tinyauth.app:8080"
|
domain = "http://sub.tinyauth.app:8080"
|
||||||
expected = "tinyauth.app"
|
expected = "tinyauth.app"
|
||||||
result, err = utils.GetCookieDomain(domain)
|
result, err = utils.GetCookieDomain(domain)
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
// Domain managed by ICANN
|
// Domain managed by ICANN
|
||||||
@@ -95,35 +96,35 @@ func TestFilter(t *testing.T) {
|
|||||||
testFunc := func(n int) bool { return n%2 == 0 }
|
testFunc := func(n int) bool { return n%2 == 0 }
|
||||||
expected := []int{2, 4}
|
expected := []int{2, 4}
|
||||||
result := utils.Filter(slice, testFunc)
|
result := utils.Filter(slice, testFunc)
|
||||||
assert.Equal(t, expected, result)
|
assert.DeepEqual(t, expected, result)
|
||||||
|
|
||||||
// Case with no matches
|
// Case with no matches
|
||||||
slice = []int{1, 3, 5}
|
slice = []int{1, 3, 5}
|
||||||
testFunc = func(n int) bool { return n%2 == 0 }
|
testFunc = func(n int) bool { return n%2 == 0 }
|
||||||
expected = []int{}
|
expected = []int{}
|
||||||
result = utils.Filter(slice, testFunc)
|
result = utils.Filter(slice, testFunc)
|
||||||
assert.Equal(t, expected, result)
|
assert.DeepEqual(t, expected, result)
|
||||||
|
|
||||||
// Case with all matches
|
// Case with all matches
|
||||||
slice = []int{2, 4, 6}
|
slice = []int{2, 4, 6}
|
||||||
testFunc = func(n int) bool { return n%2 == 0 }
|
testFunc = func(n int) bool { return n%2 == 0 }
|
||||||
expected = []int{2, 4, 6}
|
expected = []int{2, 4, 6}
|
||||||
result = utils.Filter(slice, testFunc)
|
result = utils.Filter(slice, testFunc)
|
||||||
assert.Equal(t, expected, result)
|
assert.DeepEqual(t, expected, result)
|
||||||
|
|
||||||
// Case with empty slice
|
// Case with empty slice
|
||||||
slice = []int{}
|
slice = []int{}
|
||||||
testFunc = func(n int) bool { return n%2 == 0 }
|
testFunc = func(n int) bool { return n%2 == 0 }
|
||||||
expected = []int{}
|
expected = []int{}
|
||||||
result = utils.Filter(slice, testFunc)
|
result = utils.Filter(slice, testFunc)
|
||||||
assert.Equal(t, expected, result)
|
assert.DeepEqual(t, expected, result)
|
||||||
|
|
||||||
// Case with different type (string)
|
// Case with different type (string)
|
||||||
sliceStr := []string{"apple", "banana", "cherry"}
|
sliceStr := []string{"apple", "banana", "cherry"}
|
||||||
testFuncStr := func(s string) bool { return len(s) > 5 }
|
testFuncStr := func(s string) bool { return len(s) > 5 }
|
||||||
expectedStr := []string{"banana", "cherry"}
|
expectedStr := []string{"banana", "cherry"}
|
||||||
resultStr := utils.Filter(sliceStr, testFuncStr)
|
resultStr := utils.Filter(sliceStr, testFuncStr)
|
||||||
assert.Equal(t, expectedStr, resultStr)
|
assert.DeepEqual(t, expectedStr, resultStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsRedirectSafe(t *testing.T) {
|
func TestIsRedirectSafe(t *testing.T) {
|
||||||
@@ -133,50 +134,50 @@ func TestIsRedirectSafe(t *testing.T) {
|
|||||||
// Case with no subdomain
|
// Case with no subdomain
|
||||||
redirectURL := "http://example.com/welcome"
|
redirectURL := "http://example.com/welcome"
|
||||||
result := utils.IsRedirectSafe(redirectURL, domain)
|
result := utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.True(t, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Case with different domain
|
// Case with different domain
|
||||||
redirectURL = "http://malicious.com/phishing"
|
redirectURL = "http://malicious.com/phishing"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.False(t, result)
|
assert.Equal(t, false, result)
|
||||||
|
|
||||||
// Case with subdomain
|
// Case with subdomain
|
||||||
redirectURL = "http://sub.example.com/page"
|
redirectURL = "http://sub.example.com/page"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.True(t, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Case with sub-subdomain
|
// Case with sub-subdomain
|
||||||
redirectURL = "http://a.b.example.com/home"
|
redirectURL = "http://a.b.example.com/home"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.True(t, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Case with empty redirect URL
|
// Case with empty redirect URL
|
||||||
redirectURL = ""
|
redirectURL = ""
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.False(t, result)
|
assert.Equal(t, false, result)
|
||||||
|
|
||||||
// Case with invalid URL
|
// Case with invalid URL
|
||||||
redirectURL = "http://[::1]:namedport"
|
redirectURL = "http://[::1]:namedport"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.False(t, result)
|
assert.Equal(t, false, result)
|
||||||
|
|
||||||
// Case with URL having port
|
// Case with URL having port
|
||||||
redirectURL = "http://sub.example.com:8080/page"
|
redirectURL = "http://sub.example.com:8080/page"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.True(t, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Case with URL having different subdomain
|
// Case with URL having different subdomain
|
||||||
redirectURL = "http://another.example.com/page"
|
redirectURL = "http://another.example.com/page"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.True(t, result)
|
assert.Equal(t, true, result)
|
||||||
|
|
||||||
// Case with URL having different TLD
|
// Case with URL having different TLD
|
||||||
redirectURL = "http://example.org/page"
|
redirectURL = "http://example.org/page"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.False(t, result)
|
assert.Equal(t, false, result)
|
||||||
|
|
||||||
// Case with malicious domain
|
// Case with malicious domain
|
||||||
redirectURL = "https://malicious-example.com/yoyo"
|
redirectURL = "https://malicious-example.com/yoyo"
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.False(t, result)
|
assert.Equal(t, false, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,42 @@ package decoders_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecodeLabels(t *testing.T) {
|
func TestDecodeLabels(t *testing.T) {
|
||||||
// Variables
|
// Variables
|
||||||
expected := model.Apps{
|
expected := config.Apps{
|
||||||
Apps: map[string]model.App{
|
Apps: map[string]config.App{
|
||||||
"foo": {
|
"foo": {
|
||||||
Config: model.AppConfig{
|
Config: config.AppConfig{
|
||||||
Domain: "example.com",
|
Domain: "example.com",
|
||||||
},
|
},
|
||||||
Users: model.AppUsers{
|
Users: config.AppUsers{
|
||||||
Allow: "user1,user2",
|
Allow: "user1,user2",
|
||||||
Block: "user3",
|
Block: "user3",
|
||||||
},
|
},
|
||||||
OAuth: model.AppOAuth{
|
OAuth: config.AppOAuth{
|
||||||
Whitelist: "somebody@example.com",
|
Whitelist: "somebody@example.com",
|
||||||
Groups: "group3",
|
Groups: "group3",
|
||||||
},
|
},
|
||||||
IP: model.AppIP{
|
IP: config.AppIP{
|
||||||
Allow: []string{"10.71.0.1/24", "10.71.0.2"},
|
Allow: []string{"10.71.0.1/24", "10.71.0.2"},
|
||||||
Block: []string{"10.10.10.10", "10.0.0.0/24"},
|
Block: []string{"10.10.10.10", "10.0.0.0/24"},
|
||||||
Bypass: []string{"192.168.1.1"},
|
Bypass: []string{"192.168.1.1"},
|
||||||
},
|
},
|
||||||
Response: model.AppResponse{
|
Response: config.AppResponse{
|
||||||
Headers: []string{"X-Foo=Bar", "X-Baz=Qux"},
|
Headers: []string{"X-Foo=Bar", "X-Baz=Qux"},
|
||||||
BasicAuth: model.AppBasicAuth{
|
BasicAuth: config.AppBasicAuth{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
PasswordFile: "/path/to/passwordfile",
|
PasswordFile: "/path/to/passwordfile",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Path: model.AppPath{
|
Path: config.AppPath{
|
||||||
Allow: "/public",
|
Allow: "/public",
|
||||||
Block: "/private",
|
Block: "/private",
|
||||||
},
|
},
|
||||||
@@ -62,7 +63,7 @@ func TestDecodeLabels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
result, err := decoders.DecodeLabels[model.Apps](test, "apps")
|
result, err := decoders.DecodeLabels[config.Apps](test, "apps")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.DeepEqual(t, expected, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,24 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"gotest.tools/v3/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadFile(t *testing.T) {
|
func TestReadFile(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
file, err := os.Create("/tmp/tinyauth_test_file")
|
file, err := os.Create("/tmp/tinyauth_test_file")
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
_, err = file.WriteString("file content\n")
|
_, err = file.WriteString("file content\n")
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
defer os.Remove("/tmp/tinyauth_test_file")
|
defer os.Remove("/tmp/tinyauth_test_file")
|
||||||
|
|
||||||
// Normal case
|
// Normal case
|
||||||
content, err := ReadFile("/tmp/tinyauth_test_file")
|
content, err := ReadFile("/tmp/tinyauth_test_file")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, "file content\n", content)
|
assert.Equal(t, "file content\n", content)
|
||||||
|
|
||||||
// Non-existing file
|
// Non-existing file
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseHeaders(t *testing.T) {
|
func TestParseHeaders(t *testing.T) {
|
||||||
@@ -17,7 +18,7 @@ func TestParseHeaders(t *testing.T) {
|
|||||||
"X-Custom-Header": "Value",
|
"X-Custom-Header": "Value",
|
||||||
"Another-Header": "AnotherValue",
|
"Another-Header": "AnotherValue",
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, utils.ParseHeaders(headers))
|
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
|
||||||
|
|
||||||
// Case insensitivity and trimming
|
// Case insensitivity and trimming
|
||||||
headers = []string{
|
headers = []string{
|
||||||
@@ -28,7 +29,7 @@ func TestParseHeaders(t *testing.T) {
|
|||||||
"X-Custom-Header": "Value",
|
"X-Custom-Header": "Value",
|
||||||
"Another-Header": "AnotherValue",
|
"Another-Header": "AnotherValue",
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, utils.ParseHeaders(headers))
|
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
|
||||||
|
|
||||||
// Invalid headers (missing '=', empty key/value)
|
// Invalid headers (missing '=', empty key/value)
|
||||||
headers = []string{
|
headers = []string{
|
||||||
@@ -38,7 +39,7 @@ func TestParseHeaders(t *testing.T) {
|
|||||||
" = ",
|
" = ",
|
||||||
}
|
}
|
||||||
expected = map[string]string{}
|
expected = map[string]string{}
|
||||||
assert.Equal(t, expected, utils.ParseHeaders(headers))
|
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
|
||||||
|
|
||||||
// Headers with unsafe characters
|
// Headers with unsafe characters
|
||||||
headers = []string{
|
headers = []string{
|
||||||
@@ -51,7 +52,7 @@ func TestParseHeaders(t *testing.T) {
|
|||||||
"Another-Header": "AnotherValue",
|
"Another-Header": "AnotherValue",
|
||||||
"Good-Header": "GoodValue",
|
"Good-Header": "GoodValue",
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, utils.ParseHeaders(headers))
|
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
|
||||||
|
|
||||||
// Header with spaces in key (should be ignored)
|
// Header with spaces in key (should be ignored)
|
||||||
headers = []string{
|
headers = []string{
|
||||||
@@ -61,7 +62,7 @@ func TestParseHeaders(t *testing.T) {
|
|||||||
expected = map[string]string{
|
expected = map[string]string{
|
||||||
"Valid-Header": "ValidValue",
|
"Valid-Header": "ValidValue",
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, utils.ParseHeaders(headers))
|
assert.DeepEqual(t, expected, utils.ParseHeaders(headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSanitizeHeader(t *testing.T) {
|
func TestSanitizeHeader(t *testing.T) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func ParseSecretFile(contents string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func EncodeBasicAuth(username string, password string) string {
|
func GetBasicAuth(username string, password string) string {
|
||||||
auth := username + ":" + password
|
auth := username + ":" + password
|
||||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetSecret(t *testing.T) {
|
func TestGetSecret(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
file, err := os.Create("/tmp/tinyauth_test_secret")
|
file, err := os.Create("/tmp/tinyauth_test_secret")
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
_, err = file.WriteString(" secret \n")
|
_, err = file.WriteString(" secret \n")
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
defer os.Remove("/tmp/tinyauth_test_secret")
|
defer os.Remove("/tmp/tinyauth_test_secret")
|
||||||
|
|
||||||
// Get from config
|
// Get from config
|
||||||
@@ -55,50 +55,50 @@ func TestParseSecretFile(t *testing.T) {
|
|||||||
assert.Equal(t, "", utils.ParseSecretFile(content))
|
assert.Equal(t, "", utils.ParseSecretFile(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeBasicAuth(t *testing.T) {
|
func TestGetBasicAuth(t *testing.T) {
|
||||||
// Normal case
|
// Normal case
|
||||||
username := "user"
|
username := "user"
|
||||||
password := "pass"
|
password := "pass"
|
||||||
expected := "dXNlcjpwYXNz" // base64 of "user:pass"
|
expected := "dXNlcjpwYXNz" // base64 of "user:pass"
|
||||||
assert.Equal(t, expected, utils.EncodeBasicAuth(username, password))
|
assert.Equal(t, expected, utils.GetBasicAuth(username, password))
|
||||||
|
|
||||||
// Empty username
|
// Empty username
|
||||||
username = ""
|
username = ""
|
||||||
password = "pass"
|
password = "pass"
|
||||||
expected = "OnBhc3M=" // base64 of ":pass"
|
expected = "OnBhc3M=" // base64 of ":pass"
|
||||||
assert.Equal(t, expected, utils.EncodeBasicAuth(username, password))
|
assert.Equal(t, expected, utils.GetBasicAuth(username, password))
|
||||||
|
|
||||||
// Empty password
|
// Empty password
|
||||||
username = "user"
|
username = "user"
|
||||||
password = ""
|
password = ""
|
||||||
expected = "dXNlcjo=" // base64 of "user:"
|
expected = "dXNlcjo=" // base64 of "user:"
|
||||||
assert.Equal(t, expected, utils.EncodeBasicAuth(username, password))
|
assert.Equal(t, expected, utils.GetBasicAuth(username, password))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterIP(t *testing.T) {
|
func TestFilterIP(t *testing.T) {
|
||||||
// Exact match IPv4
|
// Exact match IPv4
|
||||||
ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1")
|
ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, true, ok)
|
assert.Equal(t, true, ok)
|
||||||
|
|
||||||
// Non-match IPv4
|
// Non-match IPv4
|
||||||
ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2")
|
ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
|
|
||||||
// CIDR match IPv4
|
// CIDR match IPv4
|
||||||
ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2")
|
ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, true, ok)
|
assert.Equal(t, true, ok)
|
||||||
|
|
||||||
// CIDR match IPv4 with '-' instead of '/'
|
// CIDR match IPv4 with '-' instead of '/'
|
||||||
ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5")
|
ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, true, ok)
|
assert.Equal(t, true, ok)
|
||||||
|
|
||||||
// CIDR non-match IPv4
|
// CIDR non-match IPv4
|
||||||
ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1")
|
ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, false, ok)
|
assert.Equal(t, false, ok)
|
||||||
|
|
||||||
// Invalid CIDR
|
// Invalid CIDR
|
||||||
@@ -145,5 +145,5 @@ func TestGenerateUUID(t *testing.T) {
|
|||||||
|
|
||||||
// Different output for different input
|
// Different output for different input
|
||||||
id3 := utils.GenerateUUID("differentstring")
|
id3 := utils.GenerateUUID("differentstring")
|
||||||
assert.NotEqual(t, id2, id3)
|
assert.Assert(t, id1 != id3)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCapitalize(t *testing.T) {
|
func TestCapitalize(t *testing.T) {
|
||||||
|
|||||||
@@ -5,75 +5,75 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewLogger(t *testing.T) {
|
func TestNewLogger(t *testing.T) {
|
||||||
cfg := model.LogConfig{
|
cfg := config.LogConfig{
|
||||||
Level: "debug",
|
Level: "debug",
|
||||||
Json: true,
|
Json: true,
|
||||||
Streams: model.LogStreams{
|
Streams: config.LogStreams{
|
||||||
HTTP: model.LogStreamConfig{Enabled: true, Level: "info"},
|
HTTP: config.LogStreamConfig{Enabled: true, Level: "info"},
|
||||||
App: model.LogStreamConfig{Enabled: true, Level: ""},
|
App: config.LogStreamConfig{Enabled: true, Level: ""},
|
||||||
Audit: model.LogStreamConfig{Enabled: false, Level: ""},
|
Audit: config.LogStreamConfig{Enabled: false, Level: ""},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := tlog.NewLogger(cfg)
|
logger := tlog.NewLogger(cfg)
|
||||||
|
|
||||||
assert.NotNil(t, logger)
|
assert.Assert(t, logger != nil)
|
||||||
assert.Equal(t, zerolog.InfoLevel, logger.HTTP.GetLevel())
|
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.InfoLevel)
|
||||||
assert.Equal(t, zerolog.DebugLevel, logger.App.GetLevel())
|
assert.Assert(t, logger.App.GetLevel() == zerolog.DebugLevel)
|
||||||
assert.Equal(t, zerolog.Disabled, logger.Audit.GetLevel())
|
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewSimpleLogger(t *testing.T) {
|
func TestNewSimpleLogger(t *testing.T) {
|
||||||
logger := tlog.NewSimpleLogger()
|
logger := tlog.NewSimpleLogger()
|
||||||
assert.NotNil(t, logger)
|
assert.Assert(t, logger != nil)
|
||||||
assert.Equal(t, zerolog.InfoLevel, logger.HTTP.GetLevel())
|
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.InfoLevel)
|
||||||
assert.Equal(t, zerolog.InfoLevel, logger.App.GetLevel())
|
assert.Assert(t, logger.App.GetLevel() == zerolog.InfoLevel)
|
||||||
assert.Equal(t, zerolog.Disabled, logger.Audit.GetLevel())
|
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerInit(t *testing.T) {
|
func TestLoggerInit(t *testing.T) {
|
||||||
logger := tlog.NewSimpleLogger()
|
logger := tlog.NewSimpleLogger()
|
||||||
logger.Init()
|
logger.Init()
|
||||||
|
|
||||||
assert.NotEqual(t, zerolog.Disabled, tlog.App.GetLevel())
|
assert.Assert(t, tlog.App.GetLevel() != zerolog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerWithDisabledStreams(t *testing.T) {
|
func TestLoggerWithDisabledStreams(t *testing.T) {
|
||||||
cfg := model.LogConfig{
|
cfg := config.LogConfig{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
Json: false,
|
Json: false,
|
||||||
Streams: model.LogStreams{
|
Streams: config.LogStreams{
|
||||||
HTTP: model.LogStreamConfig{Enabled: false},
|
HTTP: config.LogStreamConfig{Enabled: false},
|
||||||
App: model.LogStreamConfig{Enabled: false},
|
App: config.LogStreamConfig{Enabled: false},
|
||||||
Audit: model.LogStreamConfig{Enabled: false},
|
Audit: config.LogStreamConfig{Enabled: false},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := tlog.NewLogger(cfg)
|
logger := tlog.NewLogger(cfg)
|
||||||
|
|
||||||
assert.Equal(t, zerolog.Disabled, logger.HTTP.GetLevel())
|
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.Disabled)
|
||||||
assert.Equal(t, zerolog.Disabled, logger.App.GetLevel())
|
assert.Assert(t, logger.App.GetLevel() == zerolog.Disabled)
|
||||||
assert.Equal(t, zerolog.Disabled, logger.Audit.GetLevel())
|
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogStreamField(t *testing.T) {
|
func TestLogStreamField(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
cfg := model.LogConfig{
|
cfg := config.LogConfig{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
Json: true,
|
Json: true,
|
||||||
Streams: model.LogStreams{
|
Streams: config.LogStreams{
|
||||||
HTTP: model.LogStreamConfig{Enabled: true},
|
HTTP: config.LogStreamConfig{Enabled: true},
|
||||||
App: model.LogStreamConfig{Enabled: true},
|
App: config.LogStreamConfig{Enabled: true},
|
||||||
Audit: model.LogStreamConfig{Enabled: true},
|
Audit: config.LogStreamConfig{Enabled: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ func TestLogStreamField(t *testing.T) {
|
|||||||
|
|
||||||
var logEntry map[string]interface{}
|
var logEntry map[string]interface{}
|
||||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "http", logEntry["log_stream"])
|
assert.Equal(t, "http", logEntry["log_stream"])
|
||||||
assert.Equal(t, "test message", logEntry["message"])
|
assert.Equal(t, "test message", logEntry["message"])
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]mod
|
|||||||
var usersStr []string
|
var usersStr []string
|
||||||
|
|
||||||
if len(usersCfg) == 0 && usersPath == "" {
|
if len(usersCfg) == 0 && usersPath == "" {
|
||||||
return nil, nil
|
return &[]model.LocalUser{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(usersCfg) > 0 {
|
if len(usersCfg) > 0 {
|
||||||
|
|||||||
@@ -4,76 +4,74 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetUsers(t *testing.T) {
|
func TestGetUsers(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
hash := "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"
|
hash := "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
file, err := os.Create(tmpDir + "/tinyauth_users_test.txt")
|
file, err := os.Create("/tmp/tinyauth_users_test.txt")
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
_, err = file.WriteString(" user1:" + hash + " \n user2:" + hash + " ") // Spacing is on purpose
|
_, err = file.WriteString(" user1:" + hash + " \n user2:" + hash + " ") // Spacing is on purpose
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
require.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
defer os.Remove(tmpDir + "/tinyauth_users_test.txt")
|
defer os.Remove("/tmp/tinyauth_users_test.txt")
|
||||||
|
|
||||||
noAttrs := map[string]model.UserAttributes{}
|
noAttrs := map[string]config.UserAttributes{}
|
||||||
|
|
||||||
// Test file only
|
// Test file only
|
||||||
users, err := utils.GetUsers([]string{}, tmpDir+"/tinyauth_users_test.txt", noAttrs)
|
users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt", noAttrs)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.NotNil(t, users)
|
|
||||||
assert.Len(t, *users, 2)
|
|
||||||
|
|
||||||
assert.Equal(t, "user1", (*users)[0].Username)
|
assert.Equal(t, 2, len(users))
|
||||||
assert.Equal(t, hash, (*users)[0].Password)
|
|
||||||
assert.Equal(t, "user2", (*users)[1].Username)
|
assert.Equal(t, "user1", users[0].Username)
|
||||||
assert.Equal(t, hash, (*users)[1].Password)
|
assert.Equal(t, hash, users[0].Password)
|
||||||
|
assert.Equal(t, "user2", users[1].Username)
|
||||||
|
assert.Equal(t, hash, users[1].Password)
|
||||||
|
|
||||||
// Test inline config only
|
// Test inline config only
|
||||||
users, err = utils.GetUsers([]string{"user3:" + hash, "user4:" + hash}, "", noAttrs)
|
users, err = utils.GetUsers([]string{"user3:" + hash, "user4:" + hash}, "", noAttrs)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Len(t, *users, 2)
|
assert.Equal(t, 2, len(users))
|
||||||
assert.Equal(t, "user3", (*users)[0].Username)
|
assert.Equal(t, "user3", users[0].Username)
|
||||||
assert.Equal(t, "user4", (*users)[1].Username)
|
assert.Equal(t, "user4", users[1].Username)
|
||||||
|
|
||||||
// Test both
|
// Test both
|
||||||
users, err = utils.GetUsers([]string{"user5:" + hash}, tmpDir+"/tinyauth_users_test.txt", noAttrs)
|
users, err = utils.GetUsers([]string{"user5:" + hash}, "/tmp/tinyauth_users_test.txt", noAttrs)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Len(t, *users, 3)
|
assert.Equal(t, 3, len(users))
|
||||||
|
|
||||||
usernames := map[string]bool{}
|
usernames := map[string]bool{}
|
||||||
for _, u := range *users {
|
for _, u := range users {
|
||||||
usernames[u.Username] = true
|
usernames[u.Username] = true
|
||||||
}
|
}
|
||||||
assert.True(t, usernames["user1"])
|
assert.Assert(t, usernames["user1"])
|
||||||
assert.True(t, usernames["user2"])
|
assert.Assert(t, usernames["user2"])
|
||||||
assert.True(t, usernames["user5"])
|
assert.Assert(t, usernames["user5"])
|
||||||
|
|
||||||
// Test attributes applied from userAttributes map
|
// Test attributes applied from userAttributes map
|
||||||
attrs := map[string]model.UserAttributes{
|
attrs := map[string]config.UserAttributes{
|
||||||
"user1": {Name: "User One", Email: "user1@example.com"},
|
"user1": {Name: "User One", Email: "user1@example.com"},
|
||||||
}
|
}
|
||||||
users, err = utils.GetUsers([]string{}, tmpDir+"/tinyauth_users_test.txt", attrs)
|
users, err = utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt", attrs)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Len(t, *users, 2)
|
assert.Equal(t, 2, len(users))
|
||||||
|
|
||||||
for _, u := range *users {
|
for _, u := range users {
|
||||||
if u.Username == "user1" {
|
if u.Username == "user1" {
|
||||||
assert.Equal(t, "User One", u.Attributes.Name)
|
assert.Equal(t, "User One", u.Attributes.Name)
|
||||||
assert.Equal(t, "user1@example.com", u.Attributes.Email)
|
assert.Equal(t, "user1@example.com", u.Attributes.Email)
|
||||||
@@ -86,14 +84,16 @@ func TestGetUsers(t *testing.T) {
|
|||||||
// Test empty
|
// Test empty
|
||||||
users, err = utils.GetUsers([]string{}, "", noAttrs)
|
users, err = utils.GetUsers([]string{}, "", noAttrs)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Nil(t, users)
|
|
||||||
|
assert.Equal(t, 0, len(users))
|
||||||
|
|
||||||
// Test non-existent file
|
// Test non-existent file
|
||||||
users, err = utils.GetUsers([]string{}, tmpDir+"/non_existent_file.txt", noAttrs)
|
users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt", noAttrs)
|
||||||
|
|
||||||
assert.ErrorContains(t, err, "no such file or directory")
|
assert.ErrorContains(t, err, "no such file or directory")
|
||||||
assert.Nil(t, users)
|
|
||||||
|
assert.Equal(t, 0, len(users))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseUser(t *testing.T) {
|
func TestParseUser(t *testing.T) {
|
||||||
@@ -102,38 +102,38 @@ func TestParseUser(t *testing.T) {
|
|||||||
// Valid user without TOTP
|
// Valid user without TOTP
|
||||||
user, err := utils.ParseUser("user1:" + hash)
|
user, err := utils.ParseUser("user1:" + hash)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "user1", user.Username)
|
assert.Equal(t, "user1", user.Username)
|
||||||
assert.Equal(t, hash, user.Password)
|
assert.Equal(t, hash, user.Password)
|
||||||
assert.Equal(t, "", user.TOTPSecret)
|
assert.Equal(t, "", user.TotpSecret)
|
||||||
|
|
||||||
// Valid user with TOTP
|
// Valid user with TOTP
|
||||||
user, err = utils.ParseUser("user2:" + hash + ":ABCDEF")
|
user, err = utils.ParseUser("user2:" + hash + ":ABCDEF")
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "user2", user.Username)
|
assert.Equal(t, "user2", user.Username)
|
||||||
assert.Equal(t, hash, user.Password)
|
assert.Equal(t, hash, user.Password)
|
||||||
assert.Equal(t, "ABCDEF", user.TOTPSecret)
|
assert.Equal(t, "ABCDEF", user.TotpSecret)
|
||||||
|
|
||||||
// Valid user with $$ in password
|
// Valid user with $$ in password
|
||||||
user, err = utils.ParseUser("user3:pa$$word123")
|
user, err = utils.ParseUser("user3:pa$$word123")
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "user3", user.Username)
|
assert.Equal(t, "user3", user.Username)
|
||||||
assert.Equal(t, "pa$word123", user.Password)
|
assert.Equal(t, "pa$word123", user.Password)
|
||||||
assert.Equal(t, "", user.TOTPSecret)
|
assert.Equal(t, "", user.TotpSecret)
|
||||||
|
|
||||||
// User with spaces
|
// User with spaces
|
||||||
user, err = utils.ParseUser(" user4 : password123 : TOTPSECRET ")
|
user, err = utils.ParseUser(" user4 : password123 : TOTPSECRET ")
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "user4", user.Username)
|
assert.Equal(t, "user4", user.Username)
|
||||||
assert.Equal(t, "password123", user.Password)
|
assert.Equal(t, "password123", user.Password)
|
||||||
assert.Equal(t, "TOTPSECRET", user.TOTPSecret)
|
assert.Equal(t, "TOTPSECRET", user.TotpSecret)
|
||||||
|
|
||||||
// Invalid users
|
// Invalid users
|
||||||
_, err = utils.ParseUser("user1") // Missing password
|
_, err = utils.ParseUser("user1") // Missing password
|
||||||
|
|||||||
Reference in New Issue
Block a user