mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-05 03:48:14 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0c38b023c | |||
| 11e5fab6fe |
@@ -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
|
||||||
|
|
||||||
|
|||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Connect to server",
|
||||||
|
"type": "go",
|
||||||
|
"request": "attach",
|
||||||
|
"mode": "remote",
|
||||||
|
"remotePath": "/tinyauth",
|
||||||
|
"port": 4000,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"debugAdapter": "legacy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+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
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
|||||||
|
|
||||||
# Deps
|
# Deps
|
||||||
deps:
|
deps:
|
||||||
bun install --frozen-lockfile --cwd frontend
|
bun install --cwd frontend
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
# Clean data
|
# Clean data
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<a href="https://scorecard.dev/viewer/?uri=github.com/tinyauthapp/tinyauth" target="_blank" title="OpenSSF Scorecard">
|
<a href="https://scorecard.dev/viewer/?uri=github.com/tinyauthapp/tinyauth" target="_blank" title="OpenSSF Scorecard">
|
||||||
<img src="https://api.scorecard.dev/projects/github.com/tinyauthapp/tinyauth/badge">
|
<img src="https://api.scorecard.dev/projects/github.com/tinyauthapp/tinyauth/badge">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.bestpractices.dev/projects/12681" target="_blank" title="OSSF Best Practices"><img src="https://www.bestpractices.dev/projects/12681/baseline"></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
docker = true
|
docker = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.TOTPSecret != "" {
|
if user.TotpSecret != "" {
|
||||||
return fmt.Errorf("user already has a TOTP secret")
|
return fmt.Errorf("user already has a TOTP secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +102,14 @@ func generateTotpCmd() *cli.Command {
|
|||||||
|
|
||||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||||
|
|
||||||
user.TOTPSecret = secret
|
user.TotpSecret = secret
|
||||||
|
|
||||||
// If using docker escape re-escape it
|
// If using docker escape re-escape it
|
||||||
if docker {
|
if docker {
|
||||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
tConfig := model.NewDefaultConfiguration()
|
tConfig := config.NewDefaultConfiguration()
|
||||||
|
|
||||||
loaders := []cli.ResourceLoader{
|
loaders := []cli.ResourceLoader{
|
||||||
&loaders.FileLoader{},
|
&loaders.FileLoader{},
|
||||||
@@ -108,11 +108,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCmd(cfg model.Config) error {
|
func runCmd(cfg config.Config) error {
|
||||||
logger := tlog.NewLogger(cfg.Log)
|
logger := tlog.NewLogger(cfg.Log)
|
||||||
logger.Init()
|
logger.Init()
|
||||||
|
|
||||||
tlog.App.Info().Str("version", model.Version).Msg("Starting tinyauth")
|
tlog.App.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(cfg)
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
return fmt.Errorf("password is incorrect: %w", err)
|
return fmt.Errorf("password is incorrect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.TOTPSecret == "" {
|
if user.TotpSecret == "" {
|
||||||
if tCfg.Totp != "" {
|
if tCfg.Totp != "" {
|
||||||
tlog.App.Warn().Msg("User does not have TOTP secret")
|
tlog.App.Warn().Msg("User does not have TOTP secret")
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
ok := totp.Validate(tCfg.Totp, user.TotpSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("TOTP code incorrect")
|
return fmt.Errorf("TOTP code incorrect")
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func versionCmd() *cli.Command {
|
func versionCmd() *cli.Command {
|
||||||
@@ -14,9 +15,9 @@ func versionCmd() *cli.Command {
|
|||||||
Configuration: nil,
|
Configuration: nil,
|
||||||
Resources: nil,
|
Resources: nil,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
fmt.Printf("Version: %s\n", model.Version)
|
fmt.Printf("Version: %s\n", config.Version)
|
||||||
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
fmt.Printf("Commit Hash: %s\n", config.CommitHash)
|
||||||
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ WORKDIR /frontend
|
|||||||
COPY ./frontend/package.json ./
|
COPY ./frontend/package.json ./
|
||||||
COPY ./frontend/bun.lock ./
|
COPY ./frontend/bun.lock ./
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile
|
RUN bun install
|
||||||
|
|
||||||
COPY ./frontend/public ./public
|
COPY ./frontend/public ./public
|
||||||
COPY ./frontend/src ./src
|
COPY ./frontend/src ./src
|
||||||
|
|||||||
+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()
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ require (
|
|||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
||||||
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/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
k8s.io/apimachinery v0.32.2
|
gotest.tools/v3 v3.5.2
|
||||||
k8s.io/client-go v0.32.2
|
|
||||||
modernc.org/sqlite v1.49.1
|
modernc.org/sqlite v1.49.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +63,6 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
@@ -74,9 +73,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
@@ -95,7 +92,6 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
@@ -110,7 +106,6 @@ require (
|
|||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
@@ -122,24 +117,15 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/term v0.42.0 // indirect
|
golang.org/x/term v0.42.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
|
||||||
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/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/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
|
||||||
modernc.org/libc v1.72.0 // indirect
|
modernc.org/libc v1.72.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -97,14 +97,10 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
|||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
@@ -122,12 +118,6 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
|
||||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
|
||||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
|
||||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
|
||||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -140,23 +130,14 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
|
||||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
|
||||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -181,12 +162,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
|||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -199,8 +176,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -234,8 +209,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -269,8 +242,6 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
|||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -290,12 +261,8 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
|||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
@@ -322,54 +289,29 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
@@ -382,27 +324,11 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
|
||||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
|
|
||||||
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
|
|
||||||
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
|
|
||||||
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
|
||||||
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
|
|
||||||
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
|
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
|
||||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
|
||||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
|
||||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
|
||||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
|
||||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||||
@@ -433,9 +359,3 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
|
||||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
|
||||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config model.Config
|
config config.Config
|
||||||
context struct {
|
context struct {
|
||||||
appUrl string
|
appUrl string
|
||||||
uuid string
|
uuid string
|
||||||
@@ -29,15 +29,15 @@ type BootstrapApp struct {
|
|||||||
csrfCookieName string
|
csrfCookieName string
|
||||||
redirectCookieName string
|
redirectCookieName string
|
||||||
oauthSessionCookieName string
|
oauthSessionCookieName string
|
||||||
localUsers []model.LocalUser
|
users []config.User
|
||||||
oauthProviders map[string]model.OAuthServiceConfig
|
oauthProviders map[string]config.OAuthServiceConfig
|
||||||
configuredProviders []controller.Provider
|
configuredProviders []controller.Provider
|
||||||
oidcClients []model.OIDCClientConfig
|
oidcClients []config.OIDCClientConfig
|
||||||
}
|
}
|
||||||
services Services
|
services Services
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
func NewBootstrapApp(config config.Config) *BootstrapApp {
|
||||||
return &BootstrapApp{
|
return &BootstrapApp{
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
app.context.localUsers = *users
|
app.context.users = users
|
||||||
|
|
||||||
// Setup OAuth providers
|
// Setup OAuth providers
|
||||||
app.context.oauthProviders = app.config.OAuth.Providers
|
app.context.oauthProviders = app.config.OAuth.Providers
|
||||||
@@ -88,7 +88,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
for id, provider := range app.context.oauthProviders {
|
for id, provider := range app.context.oauthProviders {
|
||||||
if provider.Name == "" {
|
if provider.Name == "" {
|
||||||
if name, ok := model.OverrideProviders[id]; ok {
|
if name, ok := config.OverrideProviders[id]; ok {
|
||||||
provider.Name = name
|
provider.Name = name
|
||||||
} else {
|
} else {
|
||||||
provider.Name = utils.Capitalize(id)
|
provider.Name = utils.Capitalize(id)
|
||||||
@@ -115,14 +115,14 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
// Cookie names
|
// Cookie names
|
||||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", model.SessionCookieName, cookieId)
|
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||||
app.context.csrfCookieName = fmt.Sprintf("%s-%s", model.CSRFCookieName, cookieId)
|
app.context.csrfCookieName = fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
|
||||||
app.context.redirectCookieName = fmt.Sprintf("%s-%s", model.RedirectCookieName, cookieId)
|
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||||
app.context.oauthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId)
|
app.context.oauthSessionCookieName = fmt.Sprintf("%s-%s", config.OAuthSessionCookieName, cookieId)
|
||||||
|
|
||||||
// Dumps
|
// Dumps
|
||||||
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
||||||
tlog.App.Trace().Interface("users", app.context.localUsers).Msg("Users dump")
|
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
|
||||||
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||||
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||||
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||||
@@ -171,7 +171,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if services.authService.LDAPAuthConfigured() {
|
if services.authService.LdapAuthConfigured() {
|
||||||
configuredProviders = append(configuredProviders, controller.Provider{
|
configuredProviders = append(configuredProviders, controller.Provider{
|
||||||
Name: "LDAP",
|
Name: "LDAP",
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
@@ -244,7 +244,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
|||||||
var body heartbeat
|
var body heartbeat
|
||||||
|
|
||||||
body.UUID = app.context.uuid
|
body.UUID = app.context.uuid
|
||||||
body.Version = model.Version
|
body.Version = config.Version
|
||||||
|
|
||||||
bodyJson, err := json.Marshal(body)
|
bodyJson, err := json.Marshal(body)
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
|||||||
Timeout: 30 * time.Second, // The server should never take more than 30 seconds to respond
|
Timeout: 30 * time.Second, // The server should never take more than 30 seconds to respond
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeatURL := model.APIServer + "/v1/instances/heartbeat"
|
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
tlog.App.Debug().Msg("Sending heartbeat")
|
tlog.App.Debug().Msg("Sending heartbeat")
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
var DEV_MODES = []string{"main", "test", "development"}
|
var DEV_MODES = []string{"main", "test", "development"}
|
||||||
|
|
||||||
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||||
if !slices.Contains(DEV_MODES, model.Version) {
|
if !slices.Contains(DEV_MODES, config.Version) {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,6 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
SessionCookieName: app.context.sessionCookieName,
|
|
||||||
}, app.services.authService, app.services.oauthBrokerService)
|
}, app.services.authService, app.services.oauthBrokerService)
|
||||||
|
|
||||||
err := contextMiddleware.Init()
|
err := contextMiddleware.Init()
|
||||||
@@ -100,7 +99,6 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
userController := controller.NewUserController(controller.UserControllerConfig{
|
userController := controller.NewUserController(controller.UserControllerConfig{
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
SessionCookieName: app.context.sessionCookieName,
|
|
||||||
}, apiRouter, app.services.authService)
|
}, apiRouter, app.services.authService)
|
||||||
|
|
||||||
userController.SetupRoutes()
|
userController.SetupRoutes()
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"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"
|
||||||
@@ -12,7 +10,6 @@ type Services struct {
|
|||||||
accessControlService *service.AccessControlsService
|
accessControlService *service.AccessControlsService
|
||||||
authService *service.AuthService
|
authService *service.AuthService
|
||||||
dockerService *service.DockerService
|
dockerService *service.DockerService
|
||||||
kubernetesService *service.KubernetesService
|
|
||||||
ldapService *service.LdapService
|
ldapService *service.LdapService
|
||||||
oauthBrokerService *service.OAuthBrokerService
|
oauthBrokerService *service.OAuthBrokerService
|
||||||
oidcService *service.OIDCService
|
oidcService *service.OIDCService
|
||||||
@@ -22,14 +19,14 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
services := Services{}
|
services := Services{}
|
||||||
|
|
||||||
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
||||||
Address: app.config.LDAP.Address,
|
Address: app.config.Ldap.Address,
|
||||||
BindDN: app.config.LDAP.BindDN,
|
BindDN: app.config.Ldap.BindDN,
|
||||||
BindPassword: app.config.LDAP.BindPassword,
|
BindPassword: app.config.Ldap.BindPassword,
|
||||||
BaseDN: app.config.LDAP.BaseDN,
|
BaseDN: app.config.Ldap.BaseDN,
|
||||||
Insecure: app.config.LDAP.Insecure,
|
Insecure: app.config.Ldap.Insecure,
|
||||||
SearchFilter: app.config.LDAP.SearchFilter,
|
SearchFilter: app.config.Ldap.SearchFilter,
|
||||||
AuthCert: app.config.LDAP.AuthCert,
|
AuthCert: app.config.Ldap.AuthCert,
|
||||||
AuthKey: app.config.LDAP.AuthKey,
|
AuthKey: app.config.Ldap.AuthKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
err := ldapService.Init()
|
err := ldapService.Init()
|
||||||
@@ -41,34 +38,17 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
|
|
||||||
services.ldapService = ldapService
|
services.ldapService = ldapService
|
||||||
|
|
||||||
var labelProvider service.LabelProvider
|
dockerService := service.NewDockerService()
|
||||||
var dockerService *service.DockerService
|
|
||||||
var kubernetesService *service.KubernetesService
|
|
||||||
|
|
||||||
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
|
||||||
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
|
||||||
|
|
||||||
if useKubernetes {
|
|
||||||
tlog.App.Debug().Msg("Using Kubernetes label provider")
|
|
||||||
kubernetesService = service.NewKubernetesService()
|
|
||||||
err = kubernetesService.Init()
|
|
||||||
if err != nil {
|
|
||||||
return Services{}, err
|
|
||||||
}
|
|
||||||
services.kubernetesService = kubernetesService
|
|
||||||
labelProvider = kubernetesService
|
|
||||||
} else {
|
|
||||||
tlog.App.Debug().Msg("Using Docker label provider")
|
|
||||||
dockerService = service.NewDockerService()
|
|
||||||
err = dockerService.Init()
|
err = dockerService.Init()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Services{}, err
|
return Services{}, err
|
||||||
}
|
}
|
||||||
services.dockerService = dockerService
|
|
||||||
labelProvider = dockerService
|
|
||||||
}
|
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(labelProvider, app.config.Apps)
|
services.dockerService = dockerService
|
||||||
|
|
||||||
|
accessControlsService := service.NewAccessControlsService(dockerService, app.config.Apps)
|
||||||
|
|
||||||
err = accessControlsService.Init()
|
err = accessControlsService.Init()
|
||||||
|
|
||||||
@@ -89,7 +69,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
services.oauthBrokerService = oauthBrokerService
|
services.oauthBrokerService = oauthBrokerService
|
||||||
|
|
||||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||||
LocalUsers: app.context.localUsers,
|
Users: app.context.users,
|
||||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
||||||
@@ -99,8 +79,8 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||||
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,
|
||||||
}, services.ldapService, queries, services.oauthBrokerService)
|
}, dockerService, services.ldapService, queries, services.oauthBrokerService)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package model
|
package config
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
func NewDefaultConfiguration() *Config {
|
func NewDefaultConfiguration() *Config {
|
||||||
@@ -29,7 +29,7 @@ func NewDefaultConfiguration() *Config {
|
|||||||
BackgroundImage: "/background.jpg",
|
BackgroundImage: "/background.jpg",
|
||||||
WarningsEnabled: true,
|
WarningsEnabled: true,
|
||||||
},
|
},
|
||||||
LDAP: LDAPConfig{
|
Ldap: LdapConfig{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
SearchFilter: "(uid=%s)",
|
SearchFilter: "(uid=%s)",
|
||||||
GroupCacheTTL: 900, // 15 minutes
|
GroupCacheTTL: 900, // 15 minutes
|
||||||
@@ -59,10 +59,24 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Experimental: ExperimentalConfig{
|
Experimental: ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
LabelProvider: "auto",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version information, set at build time
|
||||||
|
|
||||||
|
var Version = "development"
|
||||||
|
var CommitHash = "development"
|
||||||
|
var BuildTimestamp = "0000-00-00T00:00:00Z"
|
||||||
|
|
||||||
|
// Cookie name templates
|
||||||
|
|
||||||
|
var SessionCookieName = "tinyauth-session"
|
||||||
|
var CSRFCookieName = "tinyauth-csrf"
|
||||||
|
var RedirectCookieName = "tinyauth-redirect"
|
||||||
|
var OAuthSessionCookieName = "tinyauth-oauth"
|
||||||
|
|
||||||
|
// Main app config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||||
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
||||||
@@ -74,9 +88,8 @@ type Config struct {
|
|||||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, or kubernetes). auto detects the environment." yaml:"labelProvider"`
|
|
||||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +176,7 @@ type UIConfig struct {
|
|||||||
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPConfig struct {
|
type LdapConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
||||||
@@ -196,6 +209,20 @@ type ExperimentalConfig struct {
|
|||||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config loader options
|
||||||
|
|
||||||
|
const DefaultNamePrefix = "TINYAUTH_"
|
||||||
|
|
||||||
|
// OAuth/OIDC config
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Groups any `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
||||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
||||||
@@ -218,6 +245,60 @@ type OIDCClientConfig struct {
|
|||||||
Name string `description:"Client name in UI." yaml:"name"`
|
Name string `description:"Client name in UI." yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var OverrideProviders = map[string]string{
|
||||||
|
"google": "Google",
|
||||||
|
"github": "GitHub",
|
||||||
|
}
|
||||||
|
|
||||||
|
// User/session related stuff
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
TotpSecret string
|
||||||
|
Attributes UserAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type LdapUser struct {
|
||||||
|
DN string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSearch struct {
|
||||||
|
Username string
|
||||||
|
Type string // local, ldap or unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserContext struct {
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
IsLoggedIn bool
|
||||||
|
IsBasicAuth bool
|
||||||
|
OAuth bool
|
||||||
|
Provider string
|
||||||
|
TotpPending bool
|
||||||
|
OAuthGroups string
|
||||||
|
TotpEnabled bool
|
||||||
|
OAuthName string
|
||||||
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
|
Attributes UserAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// API responses and queries
|
||||||
|
|
||||||
|
type UnauthorizedQuery struct {
|
||||||
|
Username string `url:"username"`
|
||||||
|
Resource string `url:"resource"`
|
||||||
|
GroupErr bool `url:"groupErr"`
|
||||||
|
IP string `url:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedirectQuery struct {
|
||||||
|
RedirectURI string `url:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
@@ -273,3 +354,7 @@ type AppPath struct {
|
|||||||
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
||||||
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API server
|
||||||
|
|
||||||
|
var ApiServer = "https://api.tinyauth.app"
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -19,7 +19,7 @@ type UserContextResponse struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
OAuth bool `json:"oauth"`
|
OAuth bool `json:"oauth"`
|
||||||
TOTPPending bool `json:"totpPending"`
|
TotpPending bool `json:"totpPending"`
|
||||||
OAuthName string `json:"oauthName"`
|
OAuthName string `json:"oauthName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,29 +76,28 @@ func (controller *ContextController) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Debug().Err(err).Msg("No user context found in request")
|
|
||||||
c.JSON(200, UserContextResponse{
|
|
||||||
Status: 401,
|
|
||||||
Message: "Unauthorized",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext := UserContextResponse{
|
userContext := UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
IsLoggedIn: context.Authenticated,
|
IsLoggedIn: context.IsLoggedIn,
|
||||||
Username: context.GetUsername(),
|
Username: context.Username,
|
||||||
Name: context.GetName(),
|
Name: context.Name,
|
||||||
Email: context.GetEmail(),
|
Email: context.Email,
|
||||||
Provider: context.ProviderName(),
|
Provider: context.Provider,
|
||||||
OAuth: context.IsOAuth(),
|
OAuth: context.OAuth,
|
||||||
TOTPPending: context.TOTPPending(),
|
TotpPending: context.TotpPending,
|
||||||
OAuthName: context.OAuthName(),
|
OAuthName: context.OAuthName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Msg("No user context found in request")
|
||||||
|
userContext.Status = 401
|
||||||
|
userContext.Message = "Unauthorized"
|
||||||
|
userContext.IsLoggedIn = false
|
||||||
|
c.JSON(200, userContext)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, userContext)
|
c.JSON(200, userContext)
|
||||||
|
|||||||
@@ -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,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "johndoe",
|
Username: "johndoe",
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
|
Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
|
||||||
},
|
Provider: "local",
|
||||||
},
|
IsLoggedIn: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
type UnauthorizedQuery struct {
|
|
||||||
Username string `url:"username"`
|
|
||||||
Resource string `url:"resource"`
|
|
||||||
GroupErr bool `url:"groupErr"`
|
|
||||||
IP string `url:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedirectQuery struct {
|
|
||||||
RedirectURI string `url:"redirect_uri"`
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
@@ -175,7 +176,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||||
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
|
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
|
||||||
|
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(config.UnauthorizedQuery{
|
||||||
Username: user.Email,
|
Username: user.Email,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -235,7 +236,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
@@ -243,8 +244,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
|
|
||||||
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
||||||
|
|
||||||
if controller.isOidcRequest(oauthPendingSession.CallbackParams) {
|
if controller.isOidcRequest(oauthPendingSession.CallbackParams) {
|
||||||
@@ -260,7 +259,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oauthPendingSession.CallbackParams.RedirectURI != "" {
|
if oauthPendingSession.CallbackParams.RedirectURI != "" {
|
||||||
queries, err := query.Values(RedirectQuery{
|
queries, err := query.Values(config.RedirectQuery{
|
||||||
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
|
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"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"
|
||||||
@@ -112,14 +111,14 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
userContext, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userContext.Authenticated {
|
if !userContext.IsLoggedIn {
|
||||||
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "")
|
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -152,7 +151,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
||||||
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.GetUsername(), client.ID))
|
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.Username, client.ID))
|
||||||
code := utils.GenerateString(32)
|
code := utils.GenerateString(32)
|
||||||
|
|
||||||
// Before storing the code, delete old session
|
// Before storing the code, delete old session
|
||||||
@@ -171,7 +170,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
|
|
||||||
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
||||||
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
||||||
err = controller.oidc.StoreUserinfo(c, sub, *userContext, req)
|
err = controller.oidc.StoreUserinfo(c, sub, userContext, req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to insert user info into database")
|
tlog.App.Error().Err(err).Msg("Failed to insert user info into database")
|
||||||
@@ -430,7 +429,7 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrTokenNotFound) {
|
if err == service.ErrTokenNotFound {
|
||||||
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"error": "invalid_grant",
|
"error": "invalid_grant",
|
||||||
|
|||||||
@@ -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,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "test",
|
Username: "test",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
},
|
IsLoggedIn: true,
|
||||||
},
|
Provider: "local",
|
||||||
})
|
})
|
||||||
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)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"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"
|
||||||
@@ -103,7 +103,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -112,7 +112,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")
|
||||||
@@ -130,8 +130,8 @@ 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(config.UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
IP: clientIP,
|
IP: clientIP,
|
||||||
})
|
})
|
||||||
@@ -157,24 +157,28 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
var userContext config.UserContext
|
||||||
|
|
||||||
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Debug().Err(err).Msg("No user context found in request, treating as unauthenticated")
|
tlog.App.Debug().Msg("No user context found in request, treating as not logged in")
|
||||||
userContext = &model.UserContext{
|
userContext = config.UserContext{
|
||||||
Authenticated: false,
|
IsLoggedIn: false,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
userContext = context
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||||
|
|
||||||
if userContext.Authenticated {
|
if userContext.IsLoggedIn {
|
||||||
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
||||||
|
|
||||||
if !userAllowed {
|
if !userAllowed {
|
||||||
tlog.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
|
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
|
||||||
|
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(config.UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -184,10 +188,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.OAuth {
|
||||||
queries.Set("username", userContext.GetEmail())
|
queries.Set("username", userContext.Email)
|
||||||
} else {
|
} else {
|
||||||
queries.Set("username", userContext.GetUsername())
|
queries.Set("username", userContext.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
||||||
@@ -205,19 +209,19 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsOAuth() || userContext.IsLDAP() {
|
if userContext.OAuth || userContext.Provider == "ldap" {
|
||||||
var groupOK bool
|
var groupOK bool
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.OAuth {
|
||||||
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 {
|
||||||
tlog.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
|
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
|
||||||
|
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(config.UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
GroupErr: true,
|
GroupErr: true,
|
||||||
})
|
})
|
||||||
@@ -228,10 +232,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.OAuth {
|
||||||
queries.Set("username", userContext.GetEmail())
|
queries.Set("username", userContext.Email)
|
||||||
} else {
|
} else {
|
||||||
queries.Set("username", userContext.GetUsername())
|
queries.Set("username", userContext.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
||||||
@@ -250,18 +254,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Remote-User", utils.SanitizeHeader(userContext.GetUsername()))
|
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
||||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.GetName()))
|
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.GetEmail()))
|
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||||
|
|
||||||
if userContext.IsLDAP() {
|
if userContext.Provider == "ldap" {
|
||||||
c.Header("Remote-Groups", utils.SanitizeHeader(strings.Join(userContext.LDAP.Groups, ",")))
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.LdapGroups))
|
||||||
|
} else if userContext.Provider != "local" {
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
||||||
c.Header("Remote-Groups", utils.SanitizeHeader(strings.Join(userContext.OAuth.Groups, ",")))
|
|
||||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuth.Sub))
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
|
|
||||||
@@ -272,7 +275,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
queries, err := query.Values(RedirectQuery{
|
queries, err := query.Values(config.RedirectQuery{
|
||||||
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
|
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -296,13 +299,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 config.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 +313,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,32 +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,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Name: "Testuser",
|
Name: "Testuser",
|
||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
},
|
IsLoggedIn: true,
|
||||||
},
|
Provider: "local",
|
||||||
})
|
})
|
||||||
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,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "totpuser",
|
Username: "totpuser",
|
||||||
Name: "Totpuser",
|
Name: "Totpuser",
|
||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
},
|
IsLoggedIn: true,
|
||||||
TOTPEnabled: true,
|
Provider: "local",
|
||||||
},
|
TotpEnabled: true,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
@@ -399,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)
|
||||||
@@ -420,7 +412,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
err = broker.Init()
|
err = broker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
@@ -27,7 +25,6 @@ type TotpRequest struct {
|
|||||||
|
|
||||||
type UserControllerConfig struct {
|
type UserControllerConfig struct {
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
SessionCookieName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserController struct {
|
type UserController struct {
|
||||||
@@ -80,10 +77,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
search, err := controller.auth.SearchUser(req.Username)
|
userSearch := controller.auth.SearchUser(req.Username)
|
||||||
|
|
||||||
if err != nil {
|
if userSearch.Type == "unknown" {
|
||||||
if errors.Is(err, service.ErrUserNotFound) {
|
|
||||||
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
||||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
||||||
@@ -93,15 +89,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tlog.App.Error().Err(err).Str("username", req.Username).Msg("Error searching for user")
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
|
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
||||||
tlog.App.Warn().Str("username", req.Username).Msg("Invalid 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")
|
||||||
@@ -117,26 +106,30 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
controller.auth.RecordLoginAttempt(req.Username, true)
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
var localUser *model.LocalUser
|
var localUser *config.User
|
||||||
|
if userSearch.Type == "local" {
|
||||||
|
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||||
|
localUser = &user
|
||||||
|
}
|
||||||
|
|
||||||
if search.Type == model.UserLocal {
|
if userSearch.Type == "local" && localUser != nil {
|
||||||
localUser = controller.auth.GetLocalUser(req.Username)
|
user := *localUser
|
||||||
|
|
||||||
if localUser.TOTPSecret != "" {
|
if user.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")
|
||||||
|
|
||||||
name := localUser.Attributes.Name
|
name := user.Attributes.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = utils.Capitalize(localUser.Username)
|
name = utils.Capitalize(user.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
email := localUser.Attributes.Email
|
email := user.Attributes.Email
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = utils.CompileUserEmail(localUser.Username, controller.config.CookieDomain)
|
email = utils.CompileUserEmail(user.Username, controller.config.CookieDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := controller.auth.CreateSession(c, repository.Session{
|
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: localUser.Username,
|
Username: user.Username,
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: email,
|
Email: email,
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
@@ -152,8 +145,6 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "TOTP required",
|
"message": "TOTP required",
|
||||||
@@ -170,7 +161,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
Provider: "local",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
if search.Type == model.UserLocal {
|
if userSearch.Type == "local" && localUser != nil {
|
||||||
if localUser.Attributes.Name != "" {
|
if localUser.Attributes.Name != "" {
|
||||||
sessionCookie.Name = localUser.Attributes.Name
|
sessionCookie.Name = localUser.Attributes.Name
|
||||||
}
|
}
|
||||||
@@ -179,13 +170,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if search.Type == model.UserLDAP {
|
if userSearch.Type == "ldap" {
|
||||||
sessionCookie.Provider = "ldap"
|
sessionCookie.Provider = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
@@ -196,8 +187,6 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
@@ -207,50 +196,12 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
func (controller *UserController) logoutHandler(c *gin.Context) {
|
func (controller *UserController) logoutHandler(c *gin.Context) {
|
||||||
tlog.App.Debug().Msg("Logout request received")
|
tlog.App.Debug().Msg("Logout request received")
|
||||||
|
|
||||||
uuid, err := c.Cookie(controller.config.SessionCookieName)
|
controller.auth.DeleteSessionCookie(c)
|
||||||
|
|
||||||
if err != nil {
|
context, err := utils.GetContext(c)
|
||||||
if errors.Is(err, http.ErrNoCookie) {
|
if err == nil && context.IsLoggedIn {
|
||||||
tlog.App.Warn().Msg("No session cookie found on logout request")
|
tlog.AuditLogout(c, context.Username, context.Provider)
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": 200,
|
|
||||||
"message": "Logout successful",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
tlog.App.Error().Err(err).Msg("Error retrieving session cookie on logout")
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Error().Err(err).Msg("Error deleting session on logout")
|
|
||||||
c.JSON(500, gin.H{
|
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
|
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -271,7 +222,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
||||||
@@ -282,7 +233,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.TOTPPending() {
|
if !context.TotpPending {
|
||||||
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
@@ -291,12 +242,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Debug().Str("username", context.GetUsername()).Msg("TOTP verification attempt")
|
tlog.App.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
||||||
|
|
||||||
isLocked, remaining := controller.auth.IsAccountLocked(context.GetUsername())
|
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
||||||
|
|
||||||
if isLocked {
|
if isLocked {
|
||||||
tlog.App.Warn().Str("username", context.GetUsername()).Msg("Account is locked due to too many failed TOTP attempts")
|
tlog.App.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.JSON(429, gin.H{
|
c.JSON(429, gin.H{
|
||||||
@@ -306,14 +257,14 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := controller.auth.GetLocalUser(context.GetUsername())
|
user := controller.auth.GetLocalUser(context.Username)
|
||||||
|
|
||||||
ok := totp.Validate(req.Code, user.TOTPSecret)
|
ok := totp.Validate(req.Code, user.TotpSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
tlog.App.Warn().Str("username", context.GetUsername()).Msg("Invalid TOTP code")
|
tlog.App.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
||||||
controller.auth.RecordLoginAttempt(context.GetUsername(), false)
|
controller.auth.RecordLoginAttempt(context.Username, false)
|
||||||
tlog.AuditLoginFailure(c, context.GetUsername(), "totp", "invalid totp code")
|
tlog.AuditLoginFailure(c, context.Username, "totp", "invalid totp code")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -321,10 +272,10 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful")
|
tlog.App.Info().Str("username", context.Username).Msg("TOTP verification successful")
|
||||||
tlog.AuditLoginSuccess(c, context.GetUsername(), "totp")
|
tlog.AuditLoginSuccess(c, context.Username, "totp")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(context.GetUsername(), true)
|
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||||
|
|
||||||
sessionCookie := repository.Session{
|
sessionCookie := repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
@@ -342,7 +293,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
@@ -353,8 +304,6 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
|
|||||||
@@ -10,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) {
|
||||||
@@ -25,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
|
||||||
@@ -33,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",
|
||||||
},
|
},
|
||||||
@@ -46,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",
|
||||||
},
|
},
|
||||||
@@ -62,53 +62,6 @@ 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: true,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "totpuser",
|
|
||||||
Name: "Totpuser",
|
|
||||||
Email: "totpuser@example.com",
|
|
||||||
},
|
|
||||||
TOTPPending: true,
|
|
||||||
TOTPEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
totpAttrCtx := func(c *gin.Context) {
|
|
||||||
c.Set("context", &model.UserContext{
|
|
||||||
Authenticated: true,
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{
|
|
||||||
BaseContext: model.BaseContext{
|
|
||||||
Username: "attrtotpuser",
|
|
||||||
Name: "Bob Jones",
|
|
||||||
Email: "bob@example.com",
|
|
||||||
},
|
|
||||||
TOTPPending: true,
|
|
||||||
TOTPEnabled: 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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
@@ -235,9 +188,7 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
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{
|
||||||
@@ -253,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
|
||||||
@@ -267,20 +217,17 @@ 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) {
|
||||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -311,9 +258,7 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
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{
|
||||||
@@ -383,9 +328,7 @@ 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) {
|
||||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -406,9 +349,9 @@ func TestUserController(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)
|
||||||
@@ -427,7 +370,7 @@ func TestUserController(t *testing.T) {
|
|||||||
err = broker.Init()
|
err = broker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -436,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) {
|
||||||
@@ -445,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)
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"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"
|
||||||
@@ -37,7 +34,6 @@ var (
|
|||||||
|
|
||||||
type ContextMiddlewareConfig struct {
|
type ContextMiddlewareConfig struct {
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
SessionCookieName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextMiddleware struct {
|
type ContextMiddleware struct {
|
||||||
@@ -65,193 +61,200 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid, err := c.Cookie(m.config.SessionCookieName)
|
cookie, err := m.auth.GetSessionCookie(c)
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Msgf("Error authenticating session cookie: %v", err)
|
tlog.App.Debug().Err(err).Msg("No valid session cookie found")
|
||||||
|
goto basic
|
||||||
|
}
|
||||||
|
|
||||||
|
if cookie.TotpPending {
|
||||||
|
c.Set("context", &config.UserContext{
|
||||||
|
Username: cookie.Username,
|
||||||
|
Name: cookie.Name,
|
||||||
|
Email: cookie.Email,
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookie != nil {
|
switch cookie.Provider {
|
||||||
http.SetCookie(c.Writer, cookie)
|
case "local", "ldap":
|
||||||
|
userSearch := m.auth.SearchUser(cookie.Username)
|
||||||
|
|
||||||
|
if userSearch.Type == "unknown" {
|
||||||
|
tlog.App.Debug().Msg("User from session cookie not found")
|
||||||
|
m.auth.DeleteSessionCookie(c)
|
||||||
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Msgf("Authenticated user from session cookie: %s", userContext.GetUsername())
|
if userSearch.Type != cookie.Provider {
|
||||||
c.Set("context", userContext)
|
tlog.App.Warn().Msg("User type from session cookie does not match user search type")
|
||||||
|
m.auth.DeleteSessionCookie(c)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password, ok := c.Request.BasicAuth()
|
var ldapGroups []string
|
||||||
|
var localAttributes config.UserAttributes
|
||||||
|
|
||||||
if ok {
|
if cookie.Provider == "ldap" {
|
||||||
userContext, headers, err := m.basicAuth(username, password)
|
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Msgf("Error authenticating basic auth: %v", err)
|
tlog.App.Error().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range headers {
|
ldapGroups = ldapUser.Groups
|
||||||
c.Header(k, v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set("context", userContext)
|
if cookie.Provider == "local" {
|
||||||
|
localUser := m.auth.GetLocalUser(cookie.Username)
|
||||||
|
localAttributes = localUser.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
m.auth.RefreshSessionCookie(c)
|
||||||
|
c.Set("context", &config.UserContext{
|
||||||
|
Username: cookie.Username,
|
||||||
|
Name: cookie.Name,
|
||||||
|
Email: cookie.Email,
|
||||||
|
Provider: cookie.Provider,
|
||||||
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: strings.Join(ldapGroups, ","),
|
||||||
|
Attributes: localAttributes,
|
||||||
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
default:
|
||||||
|
_, exists := m.broker.GetService(cookie.Provider)
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model.UserContext, *http.Cookie, error) {
|
|
||||||
session, err := m.auth.GetSession(ctx, uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error retrieving session: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext, err := new(model.UserContext).NewFromSession(session)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error creating user context from session: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if userContext.Provider == model.ProviderLocal &&
|
|
||||||
userContext.Local.TOTPPending {
|
|
||||||
userContext.Local.TOTPEnabled = true
|
|
||||||
return userContext, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch userContext.Provider {
|
|
||||||
case model.ProviderLocal:
|
|
||||||
user := m.auth.GetLocalUser(userContext.Local.Username)
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
return nil, nil, fmt.Errorf("local user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext.Local.Attributes = user.Attributes
|
|
||||||
|
|
||||||
if userContext.Local.Attributes.Name == "" {
|
|
||||||
userContext.Local.Attributes.Name = utils.Capitalize(user.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
if userContext.Local.Attributes.Email == "" {
|
|
||||||
userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.config.CookieDomain)
|
|
||||||
}
|
|
||||||
case model.ProviderLDAP:
|
|
||||||
search, err := m.auth.SearchUser(userContext.LDAP.Username)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error searching for ldap user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if search.Type != model.UserLDAP {
|
|
||||||
return nil, nil, fmt.Errorf("user from session cookie is not ldap")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := m.auth.GetLDAPUser(search.Username)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext.LDAP.Groups = user.Groups
|
|
||||||
userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username)
|
|
||||||
userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.config.CookieDomain)
|
|
||||||
case model.ProviderOAuth:
|
|
||||||
_, exists := m.broker.GetService(userContext.OAuth.ID)
|
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, nil, fmt.Errorf("oauth provider from session cookie not found: %s", userContext.OAuth.ID)
|
tlog.App.Debug().Msg("OAuth provider from session cookie not found")
|
||||||
|
m.auth.DeleteSessionCookie(c)
|
||||||
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.IsEmailWhitelisted(userContext.OAuth.Email) {
|
if !m.auth.IsEmailWhitelisted(cookie.Email) {
|
||||||
m.auth.DeleteSession(ctx, uuid)
|
tlog.App.Debug().Msg("Email from session cookie not whitelisted")
|
||||||
return nil, nil, fmt.Errorf("email from session cookie not whitelisted: %s", userContext.OAuth.Email)
|
m.auth.DeleteSessionCookie(c)
|
||||||
}
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := m.auth.RefreshSession(ctx, uuid)
|
m.auth.RefreshSessionCookie(c)
|
||||||
|
c.Set("context", &config.UserContext{
|
||||||
if err != nil {
|
Username: cookie.Username,
|
||||||
return nil, nil, fmt.Errorf("error refreshing session: %w", err)
|
Name: cookie.Name,
|
||||||
|
Email: cookie.Email,
|
||||||
|
Provider: cookie.Provider,
|
||||||
|
OAuthGroups: cookie.OAuthGroups,
|
||||||
|
OAuthName: cookie.OAuthName,
|
||||||
|
OAuthSub: cookie.OAuthSub,
|
||||||
|
IsLoggedIn: true,
|
||||||
|
OAuth: true,
|
||||||
|
})
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return userContext, cookie, nil
|
basic:
|
||||||
}
|
basic := m.auth.GetBasicAuth(c)
|
||||||
|
|
||||||
func (m *ContextMiddleware) basicAuth(username string, password string) (*model.UserContext, map[string]string, error) {
|
if basic == nil {
|
||||||
headers := make(map[string]string)
|
tlog.App.Debug().Msg("No basic auth provided")
|
||||||
userContext := new(model.UserContext)
|
c.Next()
|
||||||
locked, remaining := m.auth.IsAccountLocked(username)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||||
headers["x-tinyauth-lock-reset"] = time.Now().Add(time.Duration(remaining) * time.Second).Format(time.RFC3339)
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
return nil, headers, nil
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
search, err := m.auth.SearchUser(username)
|
userSearch := m.auth.SearchUser(basic.Username)
|
||||||
|
|
||||||
if err != nil {
|
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||||
return nil, nil, fmt.Errorf("error searching for user: %w", err)
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
|
tlog.App.Debug().Msg("User from basic auth not found")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.auth.CheckUserPassword(*search, password)
|
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
if err != nil {
|
tlog.App.Debug().Msg("Invalid password for basic auth user")
|
||||||
m.auth.RecordLoginAttempt(username, false)
|
c.Next()
|
||||||
return nil, nil, fmt.Errorf("invalid password for basic auth user: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.auth.RecordLoginAttempt(username, true)
|
m.auth.RecordLoginAttempt(basic.Username, true)
|
||||||
|
|
||||||
switch search.Type {
|
switch userSearch.Type {
|
||||||
case model.UserLocal:
|
case "local":
|
||||||
user := m.auth.GetLocalUser(username)
|
tlog.App.Debug().Msg("Basic auth user is local")
|
||||||
|
|
||||||
if user.TOTPSecret != "" {
|
user := m.auth.GetLocalUser(basic.Username)
|
||||||
return nil, nil, fmt.Errorf("user with totp not allowed to login via basic auth: %s", username)
|
|
||||||
|
if user.TotpSecret != "" {
|
||||||
|
tlog.App.Debug().Msg("User with TOTP not allowed to login via basic auth")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext.Local = &model.LocalContext{
|
name := utils.Capitalize(user.Username)
|
||||||
BaseContext: model.BaseContext{
|
if user.Attributes.Name != "" {
|
||||||
|
name = user.Attributes.Name
|
||||||
|
}
|
||||||
|
email := utils.CompileUserEmail(user.Username, m.config.CookieDomain)
|
||||||
|
if user.Attributes.Email != "" {
|
||||||
|
email = user.Attributes.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("context", &config.UserContext{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: name,
|
||||||
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
Email: email,
|
||||||
},
|
Provider: "local",
|
||||||
|
IsLoggedIn: true,
|
||||||
|
IsBasicAuth: true,
|
||||||
Attributes: user.Attributes,
|
Attributes: user.Attributes,
|
||||||
}
|
})
|
||||||
userContext.Provider = model.ProviderLocal
|
c.Next()
|
||||||
case model.UserLDAP:
|
return
|
||||||
user, err := m.auth.GetLDAPUser(username)
|
case "ldap":
|
||||||
|
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
||||||
|
|
||||||
|
ldapUser, err := m.auth.GetLdapUser(basic.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
|
tlog.App.Debug().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext.LDAP = &model.LDAPContext{
|
c.Set("context", &config.UserContext{
|
||||||
BaseContext: model.BaseContext{
|
Username: basic.Username,
|
||||||
Username: username,
|
Name: utils.Capitalize(basic.Username),
|
||||||
Name: utils.Capitalize(username),
|
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||||
Email: utils.CompileUserEmail(username, m.config.CookieDomain),
|
Provider: "ldap",
|
||||||
},
|
IsLoggedIn: true,
|
||||||
Groups: user.Groups,
|
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||||
}
|
IsBasicAuth: true,
|
||||||
userContext.Provider = model.ProviderLDAP
|
})
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext.Authenticated = true
|
c.Next()
|
||||||
return userContext, nil, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
||||||
|
|||||||
@@ -1,318 +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)
|
|
||||||
assert.False(t, userCtx.Local.TOTPEnabled)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
assert.True(t, userCtx.Local.TOTPEnabled)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
const DefaultNamePrefix = "TINYAUTH_"
|
|
||||||
|
|
||||||
const APIServer = "https://api.tinyauth.app"
|
|
||||||
|
|
||||||
type Claims struct {
|
|
||||||
Sub string `json:"sub"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
PreferredUsername string `json:"preferred_username"`
|
|
||||||
Groups any `json:"groups"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var OverrideProviders = map[string]string{
|
|
||||||
"google": "Google",
|
|
||||||
"github": "GitHub",
|
|
||||||
}
|
|
||||||
|
|
||||||
const SessionCookieName = "tinyauth-session"
|
|
||||||
const CSRFCookieName = "tinyauth-csrf"
|
|
||||||
const RedirectCookieName = "tinyauth-redirect"
|
|
||||||
const OAuthSessionCookieName = "tinyauth-oauth"
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProviderType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProviderLocal ProviderType = iota
|
|
||||||
ProviderBasicAuth
|
|
||||||
ProviderOAuth
|
|
||||||
ProviderLDAP
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserContext struct {
|
|
||||||
Authenticated bool
|
|
||||||
Provider ProviderType
|
|
||||||
Local *LocalContext
|
|
||||||
OAuth *OAuthContext
|
|
||||||
LDAP *LDAPContext
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseContext struct {
|
|
||||||
Username string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalContext struct {
|
|
||||||
BaseContext
|
|
||||||
TOTPPending bool
|
|
||||||
TOTPEnabled bool
|
|
||||||
Attributes UserAttributes
|
|
||||||
}
|
|
||||||
|
|
||||||
type OAuthContext struct {
|
|
||||||
BaseContext
|
|
||||||
Groups []string
|
|
||||||
Sub string
|
|
||||||
DisplayName string
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LDAPContext struct {
|
|
||||||
BaseContext
|
|
||||||
Groups []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) IsAuthenticated() bool {
|
|
||||||
return c.Authenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) IsLocal() bool {
|
|
||||||
return c.Provider == ProviderLocal
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) IsOAuth() bool {
|
|
||||||
return c.Provider == ProviderOAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) IsLDAP() bool {
|
|
||||||
return c.Provider == ProviderLDAP
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) IsBasicAuth() bool {
|
|
||||||
return c.Provider == ProviderBasicAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
|
||||||
userContextValue, exists := ginctx.Get("context")
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return nil, errors.New("failed to get user context")
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext, ok := userContextValue.(*UserContext)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("invalid user context type")
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = *userContext
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compatability layer until we get an excuse to drop in database migrations
|
|
||||||
func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) {
|
|
||||||
switch session.Provider {
|
|
||||||
case "local":
|
|
||||||
c.Provider = ProviderLocal
|
|
||||||
c.Local = &LocalContext{
|
|
||||||
BaseContext: BaseContext{
|
|
||||||
Username: session.Username,
|
|
||||||
Name: session.Name,
|
|
||||||
Email: session.Email,
|
|
||||||
},
|
|
||||||
TOTPPending: session.TotpPending,
|
|
||||||
}
|
|
||||||
case "ldap":
|
|
||||||
c.Provider = ProviderLDAP
|
|
||||||
c.LDAP = &LDAPContext{
|
|
||||||
BaseContext: BaseContext{
|
|
||||||
Username: session.Username,
|
|
||||||
Name: session.Name,
|
|
||||||
Email: session.Email,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// By default we assume an unkown name which is oauth
|
|
||||||
default:
|
|
||||||
c.Provider = ProviderOAuth
|
|
||||||
c.OAuth = &OAuthContext{
|
|
||||||
BaseContext: BaseContext{
|
|
||||||
Username: session.Username,
|
|
||||||
Name: session.Name,
|
|
||||||
Email: session.Email,
|
|
||||||
},
|
|
||||||
Groups: strings.Split(session.OAuthGroups, ","),
|
|
||||||
Sub: session.OAuthSub,
|
|
||||||
DisplayName: session.OAuthName,
|
|
||||||
ID: session.Provider,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !session.TotpPending {
|
|
||||||
c.Authenticated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) GetUsername() string {
|
|
||||||
switch c.Provider {
|
|
||||||
case ProviderLocal:
|
|
||||||
return c.Local.Username
|
|
||||||
case ProviderLDAP:
|
|
||||||
return c.LDAP.Username
|
|
||||||
case ProviderBasicAuth:
|
|
||||||
return c.Local.Username
|
|
||||||
case ProviderOAuth:
|
|
||||||
return c.OAuth.Username
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) GetEmail() string {
|
|
||||||
switch c.Provider {
|
|
||||||
case ProviderLocal:
|
|
||||||
return c.Local.Email
|
|
||||||
case ProviderLDAP:
|
|
||||||
return c.LDAP.Email
|
|
||||||
case ProviderBasicAuth:
|
|
||||||
return c.Local.Email
|
|
||||||
case ProviderOAuth:
|
|
||||||
return c.OAuth.Email
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) GetName() string {
|
|
||||||
switch c.Provider {
|
|
||||||
case ProviderLocal:
|
|
||||||
return c.Local.Name
|
|
||||||
case ProviderLDAP:
|
|
||||||
return c.LDAP.Name
|
|
||||||
case ProviderBasicAuth:
|
|
||||||
return c.Local.Name
|
|
||||||
case ProviderOAuth:
|
|
||||||
return c.OAuth.Name
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) ProviderName() string {
|
|
||||||
switch c.Provider {
|
|
||||||
case ProviderBasicAuth, ProviderLocal:
|
|
||||||
return "local"
|
|
||||||
case ProviderLDAP:
|
|
||||||
return "ldap"
|
|
||||||
case ProviderOAuth:
|
|
||||||
return c.OAuth.DisplayName // compatability
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) TOTPPending() bool {
|
|
||||||
if c.Provider == ProviderLocal {
|
|
||||||
return c.Local.TOTPPending
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UserContext) OAuthName() string {
|
|
||||||
if c.Provider == ProviderOAuth {
|
|
||||||
return c.OAuth.DisplayName
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
package model_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"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(*model.UserContext) any
|
|
||||||
expected any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
description: "IsAuthenticated reflects Authenticated field",
|
|
||||||
context: &model.UserContext{Authenticated: true},
|
|
||||||
run: func(c *model.UserContext) any { return c.IsAuthenticated() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsLocal returns true for ProviderLocal",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal},
|
|
||||||
run: func(c *model.UserContext) any { return c.IsLocal() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsOAuth returns true for ProviderOAuth",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderOAuth},
|
|
||||||
run: func(c *model.UserContext) any { return c.IsOAuth() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsLDAP returns true for ProviderLDAP",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP},
|
|
||||||
run: func(c *model.UserContext) any { return c.IsLDAP() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "IsBasicAuth returns true for ProviderBasicAuth",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth},
|
|
||||||
run: func(c *model.UserContext) any { return c.IsBasicAuth() },
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession local session is authenticated and ProviderLocal",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(c *model.UserContext) any {
|
|
||||||
got, _ := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "alice", Email: "alice@example.com", Name: "Alice",
|
|
||||||
Provider: "local",
|
|
||||||
})
|
|
||||||
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(c *model.UserContext) any {
|
|
||||||
got, _ := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "bob", Provider: "local", TotpPending: true,
|
|
||||||
})
|
|
||||||
return got.Authenticated
|
|
||||||
},
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession ldap session is ProviderLDAP",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(c *model.UserContext) any {
|
|
||||||
got, _ := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "carol", Provider: "ldap",
|
|
||||||
})
|
|
||||||
return got.Provider
|
|
||||||
},
|
|
||||||
expected: model.ProviderLDAP,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromSession unknown provider defaults to OAuth and populates oauth fields",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(c *model.UserContext) any {
|
|
||||||
got, _ := c.NewFromSession(&repository.Session{
|
|
||||||
Username: "dave", Provider: "github",
|
|
||||||
OAuthGroups: "devs,admins", OAuthSub: "sub-123", OAuthName: "GitHub",
|
|
||||||
})
|
|
||||||
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(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(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(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(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(c *model.UserContext) any { return c.ProviderName() },
|
|
||||||
expected: "local",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns 'local' for ProviderBasicAuth",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth},
|
|
||||||
run: func(c *model.UserContext) any { return c.ProviderName() },
|
|
||||||
expected: "local",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns 'ldap' for ProviderLDAP",
|
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP},
|
|
||||||
run: func(c *model.UserContext) any { return c.ProviderName() },
|
|
||||||
expected: "ldap",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "ProviderName returns OAuth DisplayName for ProviderOAuth",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderOAuth,
|
|
||||||
OAuth: &model.OAuthContext{DisplayName: "GitHub"},
|
|
||||||
},
|
|
||||||
run: func(c *model.UserContext) any { return c.ProviderName() },
|
|
||||||
expected: "GitHub",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "TOTPPending returns true when local context is pending",
|
|
||||||
context: &model.UserContext{
|
|
||||||
Provider: model.ProviderLocal,
|
|
||||||
Local: &model.LocalContext{TOTPPending: true},
|
|
||||||
},
|
|
||||||
run: func(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(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(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(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(c *model.UserContext) any { return c.OAuthName() },
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "NewFromGin populates context from gin value",
|
|
||||||
context: &model.UserContext{},
|
|
||||||
run: func(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))
|
|
||||||
if err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
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(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(c *model.UserContext) any {
|
|
||||||
_, err := c.NewFromGin(newGinCtx("not a user context", true))
|
|
||||||
return err.Error()
|
|
||||||
},
|
|
||||||
expected: "invalid user context type",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.description, func(t *testing.T) {
|
|
||||||
assert.Equal(t, test.expected, test.run(test.context))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type UserSearchType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
UserLocal UserSearchType = iota
|
|
||||||
UserLDAP
|
|
||||||
)
|
|
||||||
|
|
||||||
type LDAPUser struct {
|
|
||||||
DN string
|
|
||||||
Groups []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalUser struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
TOTPSecret string
|
|
||||||
Attributes UserAttributes
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSearch struct {
|
|
||||||
Username string
|
|
||||||
Type UserSearchType
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
var Version = "development"
|
|
||||||
var CommitHash = "development"
|
|
||||||
var BuildTimestamp = "0000-00-00T00:00:00Z"
|
|
||||||
@@ -1,24 +1,21 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LabelProvider interface {
|
|
||||||
GetLabels(appDomain string) (*model.App, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
labelProvider LabelProvider
|
docker *DockerService
|
||||||
static map[string]model.App
|
static map[string]config.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(labelProvider LabelProvider, static map[string]model.App) *AccessControlsService {
|
func NewAccessControlsService(docker *DockerService, static map[string]config.App) *AccessControlsService {
|
||||||
return &AccessControlsService{
|
return &AccessControlsService{
|
||||||
labelProvider: labelProvider,
|
docker: docker,
|
||||||
static: static,
|
static: static,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,31 +24,31 @@ 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) (config.App, error) {
|
||||||
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")
|
||||||
return &config
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
return &config
|
return config, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return config.App{}, errors.New("no results")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
func (acls *AccessControlsService) GetAccessControls(domain string) (config.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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to label provider
|
// Fallback to Docker labels
|
||||||
tlog.App.Debug().Msg("Falling back to label provider for ACLs")
|
tlog.App.Debug().Msg("Falling back to Docker labels for ACLs")
|
||||||
return acls.labelProvider.GetLabels(domain)
|
return acls.docker.GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|||||||
+156
-152
@@ -5,22 +5,20 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,10 +28,6 @@ const MaxOAuthPendingSessions = 256
|
|||||||
const OAuthCleanupCount = 16
|
const OAuthCleanupCount = 16
|
||||||
const MaxLoginAttemptRecords = 256
|
const MaxLoginAttemptRecords = 256
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUserNotFound = errors.New("user not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// slightly modified version of the AuthorizeRequest from the OIDC service to basically accept all
|
// slightly modified version of the AuthorizeRequest from the OIDC service to basically accept all
|
||||||
// parameters and pass them to the authorize page if needed
|
// parameters and pass them to the authorize page if needed
|
||||||
type OAuthURLParams struct {
|
type OAuthURLParams struct {
|
||||||
@@ -73,7 +67,7 @@ type Lockdown struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthServiceConfig struct {
|
type AuthServiceConfig struct {
|
||||||
LocalUsers []model.LocalUser
|
Users []config.User
|
||||||
OauthWhitelist []string
|
OauthWhitelist []string
|
||||||
SessionExpiry int
|
SessionExpiry int
|
||||||
SessionMaxLifetime int
|
SessionMaxLifetime int
|
||||||
@@ -82,12 +76,13 @@ type AuthServiceConfig struct {
|
|||||||
LoginTimeout int
|
LoginTimeout int
|
||||||
LoginMaxRetries int
|
LoginMaxRetries int
|
||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
IP model.IPConfig
|
IP config.IPConfig
|
||||||
LDAPGroupsCacheTTL int
|
LDAPGroupsCacheTTL int
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
config AuthServiceConfig
|
config AuthServiceConfig
|
||||||
|
docker *DockerService
|
||||||
loginAttempts map[string]*LoginAttempt
|
loginAttempts map[string]*LoginAttempt
|
||||||
ldapGroupsCache map[string]*LdapGroupsCache
|
ldapGroupsCache map[string]*LdapGroupsCache
|
||||||
oauthPendingSessions map[string]*OAuthPendingSession
|
oauthPendingSessions map[string]*OAuthPendingSession
|
||||||
@@ -102,9 +97,10 @@ type AuthService struct {
|
|||||||
lockdownCancelFunc context.CancelFunc
|
lockdownCancelFunc context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(config AuthServiceConfig, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
config: config,
|
config: config,
|
||||||
|
docker: docker,
|
||||||
loginAttempts: make(map[string]*LoginAttempt),
|
loginAttempts: make(map[string]*LoginAttempt),
|
||||||
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||||
oauthPendingSessions: make(map[string]*OAuthPendingSession),
|
oauthPendingSessions: make(map[string]*OAuthPendingSession),
|
||||||
@@ -119,67 +115,79 @@ func (auth *AuthService) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {
|
func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
||||||
if auth.GetLocalUser(username).Username != "" {
|
if auth.GetLocalUser(username).Username != "" {
|
||||||
return &model.UserSearch{
|
return config.UserSearch{
|
||||||
Username: username,
|
Username: username,
|
||||||
Type: model.UserLocal,
|
Type: "local",
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.ldap.IsConfigured() {
|
if auth.ldap.IsConfigured() {
|
||||||
userDN, err := auth.ldap.GetUserDN(username)
|
userDN, err := auth.ldap.GetUserDN(username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ldap user: %w", err)
|
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
||||||
|
return config.UserSearch{
|
||||||
|
Type: "unknown",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.UserSearch{
|
return config.UserSearch{
|
||||||
Username: userDN,
|
Username: userDN,
|
||||||
Type: model.UserLDAP,
|
Type: "ldap",
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrUserNotFound
|
return config.UserSearch{
|
||||||
|
Type: "unknown",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckUserPassword(search model.UserSearch, password string) error {
|
func (auth *AuthService) VerifyUser(search config.UserSearch, password string) bool {
|
||||||
switch search.Type {
|
switch search.Type {
|
||||||
case model.UserLocal:
|
case "local":
|
||||||
user := auth.GetLocalUser(search.Username)
|
user := auth.GetLocalUser(search.Username)
|
||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
return auth.CheckPassword(user, password)
|
||||||
case model.UserLDAP:
|
case "ldap":
|
||||||
if auth.ldap.IsConfigured() {
|
if auth.ldap.IsConfigured() {
|
||||||
err := auth.ldap.Bind(search.Username, password)
|
err := auth.ldap.Bind(search.Username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to bind to ldap user: %w", err)
|
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.ldap.BindService(true)
|
err = auth.ldap.BindService(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to bind to ldap service account: %w", err)
|
tlog.App.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return true
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return errors.New("unknown user search type")
|
tlog.App.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return errors.New("user authentication failed")
|
|
||||||
|
tlog.App.Warn().Str("username", search.Username).Msg("User authentication failed")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetLocalUser(username string) *model.LocalUser {
|
func (auth *AuthService) GetLocalUser(username string) config.User {
|
||||||
for _, user := range auth.config.LocalUsers {
|
for _, user := range auth.config.Users {
|
||||||
if user.Username == username {
|
if user.Username == username {
|
||||||
return &user
|
return user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
tlog.App.Warn().Str("username", username).Msg("Local user not found")
|
||||||
|
return config.User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
|
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
||||||
if !auth.ldap.IsConfigured() {
|
if !auth.ldap.IsConfigured() {
|
||||||
return nil, errors.New("ldap service not configured")
|
return config.LdapUser{}, errors.New("LDAP service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.ldapGroupsMutex.RLock()
|
auth.ldapGroupsMutex.RLock()
|
||||||
@@ -187,7 +195,7 @@ func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
|
|||||||
auth.ldapGroupsMutex.RUnlock()
|
auth.ldapGroupsMutex.RUnlock()
|
||||||
|
|
||||||
if exists && time.Now().Before(entry.Expires) {
|
if exists && time.Now().Before(entry.Expires) {
|
||||||
return &model.LDAPUser{
|
return config.LdapUser{
|
||||||
DN: userDN,
|
DN: userDN,
|
||||||
Groups: entry.Groups,
|
Groups: entry.Groups,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -196,7 +204,7 @@ func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
|
|||||||
groups, err := auth.ldap.GetUserGroups(userDN)
|
groups, err := auth.ldap.GetUserGroups(userDN)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ldap groups: %w", err)
|
return config.LdapUser{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.ldapGroupsMutex.Lock()
|
auth.ldapGroupsMutex.Lock()
|
||||||
@@ -206,12 +214,16 @@ func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
|
|||||||
}
|
}
|
||||||
auth.ldapGroupsMutex.Unlock()
|
auth.ldapGroupsMutex.Unlock()
|
||||||
|
|
||||||
return &model.LDAPUser{
|
return config.LdapUser{
|
||||||
DN: userDN,
|
DN: userDN,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
|
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
|
||||||
auth.loginMutex.RLock()
|
auth.loginMutex.RLock()
|
||||||
defer auth.loginMutex.RUnlock()
|
defer auth.loginMutex.RUnlock()
|
||||||
@@ -280,11 +292,11 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
|||||||
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
|
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate session uuid: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var expiry int
|
var expiry int
|
||||||
@@ -309,30 +321,28 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
|||||||
OAuthSub: data.OAuthSub,
|
OAuthSub: data.OAuthSub,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = auth.queries.CreateSession(ctx, session)
|
_, err = auth.queries.CreateSession(c, session)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Cookie{
|
c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||||
Name: auth.config.SessionCookieName,
|
|
||||||
Value: session.UUID,
|
return nil
|
||||||
Path: "/",
|
|
||||||
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
|
||||||
Expires: time.Now().Add(time.Duration(expiry) * time.Second),
|
|
||||||
MaxAge: expiry,
|
|
||||||
Secure: auth.config.SecureCookie,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http.Cookie, error) {
|
func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
||||||
session, err := auth.queries.GetSession(ctx, uuid)
|
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve session: %w", err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := auth.queries.GetSession(c, cookie)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
@@ -346,12 +356,12 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http
|
|||||||
}
|
}
|
||||||
|
|
||||||
if session.Expiry-currentTime > refreshThreshold {
|
if session.Expiry-currentTime > refreshThreshold {
|
||||||
return nil, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newExpiry := session.Expiry + refreshThreshold
|
newExpiry := session.Expiry + refreshThreshold
|
||||||
|
|
||||||
_, err = auth.queries.UpdateSession(ctx, repository.UpdateSessionParams{
|
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
|
||||||
Username: session.Username,
|
Username: session.Username,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
Name: session.Name,
|
Name: session.Name,
|
||||||
@@ -365,123 +375,122 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to update session expiry: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Cookie{
|
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||||
Name: auth.config.SessionCookieName,
|
tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
|
||||||
Value: session.UUID,
|
|
||||||
Path: "/",
|
|
||||||
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
|
||||||
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
|
|
||||||
MaxAge: auth.config.SessionExpiry,
|
|
||||||
Secure: auth.config.SecureCookie,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.Cookie, error) {
|
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
||||||
err := auth.queries.DeleteSession(ctx, uuid)
|
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Cookie{
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
Name: auth.config.SessionCookieName,
|
|
||||||
Value: "",
|
if err != nil {
|
||||||
Path: "/",
|
return err
|
||||||
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
}
|
||||||
Expires: time.Now(),
|
|
||||||
MaxAge: -1,
|
c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
||||||
Secure: auth.config.SecureCookie,
|
|
||||||
HttpOnly: true,
|
return nil
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetSession(ctx context.Context, uuid string) (*repository.Session, error) {
|
func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
|
||||||
session, err := auth.queries.GetSession(ctx, uuid)
|
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return repository.Session{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := auth.queries.GetSession(c, cookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, errors.New("session not found")
|
return repository.Session{}, fmt.Errorf("session not found")
|
||||||
}
|
}
|
||||||
return nil, err
|
return repository.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
|
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
|
||||||
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
||||||
err = auth.queries.DeleteSession(ctx, uuid)
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to delete expired session: %w", err)
|
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("session max lifetime exceeded")
|
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentTime > session.Expiry {
|
if currentTime > session.Expiry {
|
||||||
err = auth.queries.DeleteSession(ctx, uuid)
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to delete expired session: %w", err)
|
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("session expired")
|
return repository.Session{}, fmt.Errorf("session expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &session, nil
|
return repository.Session{
|
||||||
|
UUID: session.UUID,
|
||||||
|
Username: session.Username,
|
||||||
|
Email: session.Email,
|
||||||
|
Name: session.Name,
|
||||||
|
Provider: session.Provider,
|
||||||
|
TotpPending: session.TotpPending,
|
||||||
|
OAuthGroups: session.OAuthGroups,
|
||||||
|
OAuthName: session.OAuthName,
|
||||||
|
OAuthSub: session.OAuthSub,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) LocalAuthConfigured() bool {
|
func (auth *AuthService) LocalAuthConfigured() bool {
|
||||||
return len(auth.config.LocalUsers) > 0
|
return len(auth.config.Users) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) LDAPAuthConfigured() bool {
|
func (auth *AuthService) LdapAuthConfigured() bool {
|
||||||
return auth.ldap.IsConfigured()
|
return auth.ldap.IsConfigured()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||||
if acls == nil {
|
if context.OAuth {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.Provider == model.ProviderOAuth {
|
|
||||||
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
||||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if acls.Users.Block != "" {
|
if acls.Users.Block != "" {
|
||||||
tlog.App.Debug().Msg("Checking blocked users")
|
tlog.App.Debug().Msg("Checking blocked users")
|
||||||
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
if utils.CheckFilter(acls.Users.Block, context.Username) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Debug().Msg("Checking users")
|
tlog.App.Debug().Msg("Checking users")
|
||||||
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
|
||||||
if acls == nil {
|
if requiredGroups == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.IsOAuth() {
|
for id := range config.OverrideProviders {
|
||||||
tlog.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
if context.Provider == id {
|
||||||
return false
|
tlog.App.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
|
||||||
tlog.App.Debug().Msg("Provider override for OAuth groups enabled, skipping group check")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, userGroup := range context.OAuth.Groups {
|
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,19 +499,14 @@ 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 config.UserContext, requiredGroups string) bool {
|
||||||
if acls == nil {
|
if requiredGroups == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.IsLDAP() {
|
for userGroup := range strings.SplitSeq(context.LdapGroups, ",") {
|
||||||
tlog.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
return false
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
}
|
|
||||||
|
|
||||||
for _, userGroup := range context.LDAP.Groups {
|
|
||||||
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
|
||||||
tlog.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,14 +515,10 @@ 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 config.AppPath) (bool, error) {
|
||||||
if acls == 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
|
||||||
@@ -530,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
|
||||||
@@ -545,14 +545,22 @@ 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 {
|
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
||||||
if acls == nil {
|
username, password, ok := c.Request.BasicAuth()
|
||||||
return true
|
if !ok {
|
||||||
|
tlog.App.Debug().Msg("No basic auth provided")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
return &config.User{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
||||||
// 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)
|
||||||
@@ -587,12 +595,8 @@ 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 config.AppIP, ip string) bool {
|
||||||
if acls == nil {
|
for _, bypassed := range acls.Bypass {
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bypassed := range acls.IP.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")
|
||||||
@@ -671,21 +675,21 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, error) {
|
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, error) {
|
||||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return config.Claims{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.Token == nil {
|
if session.Token == nil {
|
||||||
return nil, fmt.Errorf("oauth token not found for session: %s", sessionId)
|
return config.Claims{}, fmt.Errorf("oauth token not found for session: %s", sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
userinfo, err := (*session.Service).GetUserinfo(session.Token)
|
userinfo, err := (*session.Service).GetUserinfo(session.Token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get userinfo: %w", err)
|
return config.Claims{}, fmt.Errorf("failed to get userinfo: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
@@ -51,48 +51,56 @@ 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) (config.App, error) {
|
||||||
if !docker.isConnected {
|
if !docker.isConnected {
|
||||||
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
|
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
|
||||||
return nil, nil
|
return config.App{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err := docker.getContainers()
|
containers, err := docker.getContainers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return config.App{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
inspect, err := docker.inspectContainer(ctr.ID)
|
inspect, err := docker.inspectContainer(ctr.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return config.App{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
labels, err := decoders.DecodeLabels[model.Apps](inspect.Config.Labels, "apps")
|
labels, err := decoders.DecodeLabels[config.Apps](inspect.Config.Labels, "apps")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return config.App{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for appName, appLabels := range labels.Apps {
|
for appName, appLabels := range labels.Apps {
|
||||||
if appLabels.Config.Domain == appDomain {
|
if appLabels.Config.Domain == appDomain {
|
||||||
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
||||||
return &appLabels, nil
|
return appLabels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
||||||
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
||||||
return &appLabels, nil
|
return appLabels, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Debug().Msg("No matching container found, returning empty labels")
|
tlog.App.Debug().Msg("No matching container found, returning empty labels")
|
||||||
return nil, nil
|
return config.App{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
"k8s.io/client-go/dynamic"
|
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ingressKey struct {
|
|
||||||
namespace string
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ingressAppKey struct {
|
|
||||||
ingressKey
|
|
||||||
appName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ingressApp struct {
|
|
||||||
domain string
|
|
||||||
appName string
|
|
||||||
app model.App
|
|
||||||
}
|
|
||||||
|
|
||||||
type KubernetesService struct {
|
|
||||||
client dynamic.Interface
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
started bool
|
|
||||||
mu sync.RWMutex
|
|
||||||
ingressApps map[ingressKey][]ingressApp
|
|
||||||
domainIndex map[string]ingressAppKey
|
|
||||||
appNameIndex map[string]ingressAppKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKubernetesService() *KubernetesService {
|
|
||||||
return &KubernetesService{
|
|
||||||
ingressApps: make(map[ingressKey][]ingressApp),
|
|
||||||
domainIndex: make(map[string]ingressAppKey),
|
|
||||||
appNameIndex: make(map[string]ingressAppKey),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) addIngressApps(namespace, name string, apps []ingressApp) {
|
|
||||||
k.mu.Lock()
|
|
||||||
defer k.mu.Unlock()
|
|
||||||
|
|
||||||
key := ingressKey{namespace, name}
|
|
||||||
// Remove existing entries for this ingress
|
|
||||||
if existing, ok := k.ingressApps[key]; ok {
|
|
||||||
for _, app := range existing {
|
|
||||||
delete(k.domainIndex, app.domain)
|
|
||||||
delete(k.appNameIndex, app.appName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add new entries
|
|
||||||
k.ingressApps[key] = apps
|
|
||||||
for _, app := range apps {
|
|
||||||
appKey := ingressAppKey{key, app.appName}
|
|
||||||
k.domainIndex[app.domain] = appKey
|
|
||||||
k.appNameIndex[app.appName] = appKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) removeIngress(namespace, name string) {
|
|
||||||
k.mu.Lock()
|
|
||||||
defer k.mu.Unlock()
|
|
||||||
|
|
||||||
key := ingressKey{namespace, name}
|
|
||||||
if apps, ok := k.ingressApps[key]; ok {
|
|
||||||
for _, app := range apps {
|
|
||||||
delete(k.domainIndex, app.domain)
|
|
||||||
delete(k.appNameIndex, app.appName)
|
|
||||||
}
|
|
||||||
delete(k.ingressApps, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) getByDomain(domain string) *model.App {
|
|
||||||
k.mu.RLock()
|
|
||||||
defer k.mu.RUnlock()
|
|
||||||
|
|
||||||
if appKey, ok := k.domainIndex[domain]; ok {
|
|
||||||
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
|
||||||
for _, app := range apps {
|
|
||||||
if app.domain == domain && app.appName == appKey.appName {
|
|
||||||
return &app.app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) getByAppName(appName string) *model.App {
|
|
||||||
k.mu.RLock()
|
|
||||||
defer k.mu.RUnlock()
|
|
||||||
|
|
||||||
if appKey, ok := k.appNameIndex[appName]; ok {
|
|
||||||
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
|
||||||
for _, app := range apps {
|
|
||||||
if app.appName == appName {
|
|
||||||
return &app.app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
|
|
||||||
namespace := item.GetNamespace()
|
|
||||||
name := item.GetName()
|
|
||||||
annotations := item.GetAnnotations()
|
|
||||||
if annotations == nil {
|
|
||||||
k.removeIngress(namespace, name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Debug().Err(err).Msg("Failed to decode labels from annotations")
|
|
||||||
k.removeIngress(namespace, name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var apps []ingressApp
|
|
||||||
for appName, appLabels := range labels.Apps {
|
|
||||||
if appLabels.Config.Domain == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
apps = append(apps, ingressApp{
|
|
||||||
domain: appLabels.Config.Domain,
|
|
||||||
appName: appName,
|
|
||||||
app: appLabels,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(apps) == 0 {
|
|
||||||
k.removeIngress(namespace, name)
|
|
||||||
} else {
|
|
||||||
k.addIngressApps(namespace, name, apps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource) error {
|
|
||||||
ctx, cancel := context.WithTimeout(k.ctx, 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
list, err := k.client.Resource(gvr).List(ctx, metav1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Debug().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to list ingresses during resync")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i := range list.Items {
|
|
||||||
k.updateFromItem(&list.Items[i])
|
|
||||||
}
|
|
||||||
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Int("count", len(list.Items)).Msg("Resynced ingress cache")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// runWatcher drains events from an active watcher until it closes or the context is done.
|
|
||||||
// Returns true if the caller should restart the watcher, false if it should exit.
|
|
||||||
func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch.Interface, resyncTicker *time.Ticker) bool {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-k.ctx.Done():
|
|
||||||
w.Stop()
|
|
||||||
return false
|
|
||||||
case event, ok := <-w.ResultChan():
|
|
||||||
if !ok {
|
|
||||||
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher channel closed, restarting in 5 seconds")
|
|
||||||
w.Stop()
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
item, ok := event.Object.(*unstructured.Unstructured)
|
|
||||||
if !ok {
|
|
||||||
tlog.App.Warn().Str("api", gvr.GroupVersion().String()).Msg("Failed to cast watched object")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch event.Type {
|
|
||||||
case watch.Added, watch.Modified:
|
|
||||||
k.updateFromItem(item)
|
|
||||||
case watch.Deleted:
|
|
||||||
k.removeIngress(item.GetNamespace(), item.GetName())
|
|
||||||
}
|
|
||||||
case <-resyncTicker.C:
|
|
||||||
if err := k.resyncGVR(gvr); err != nil {
|
|
||||||
tlog.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource) {
|
|
||||||
resyncTicker := time.NewTicker(5 * time.Minute)
|
|
||||||
defer resyncTicker.Stop()
|
|
||||||
|
|
||||||
if err := k.resyncGVR(gvr); err != nil {
|
|
||||||
tlog.App.Error().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Initial resync failed, retrying in 30 seconds")
|
|
||||||
time.Sleep(30 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-k.ctx.Done():
|
|
||||||
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Stopping watcher")
|
|
||||||
return
|
|
||||||
case <-resyncTicker.C:
|
|
||||||
if err := k.resyncGVR(gvr); err != nil {
|
|
||||||
tlog.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
ctx, cancel := context.WithCancel(k.ctx)
|
|
||||||
watcher, err := k.client.Resource(gvr).Watch(ctx, metav1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Error().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to start watcher")
|
|
||||||
cancel()
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher started")
|
|
||||||
if !k.runWatcher(gvr, watcher, resyncTicker) {
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) Init() error {
|
|
||||||
var cfg *rest.Config
|
|
||||||
var err error
|
|
||||||
|
|
||||||
cfg, err = rest.InClusterConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get in-cluster Kubernetes config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := dynamic.NewForConfig(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create Kubernetes client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
k.client = client
|
|
||||||
k.ctx, k.cancel = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
gvr := schema.GroupVersionResource{
|
|
||||||
Group: "networking.k8s.io",
|
|
||||||
Version: "v1",
|
|
||||||
Resource: "ingresses",
|
|
||||||
}
|
|
||||||
|
|
||||||
accessCtx, accessCancel := context.WithTimeout(k.ctx, 5*time.Second)
|
|
||||||
defer accessCancel()
|
|
||||||
_, err = k.client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Warn().Err(err).Msg("Insufficient permissions for networking.k8s.io/v1 Ingress, Kubernetes label provider will not work")
|
|
||||||
k.started = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tlog.App.Debug().Msg("networking.k8s.io/v1 Ingress API accessible")
|
|
||||||
go k.watchGVR(gvr)
|
|
||||||
|
|
||||||
k.started = true
|
|
||||||
tlog.App.Info().Msg("Kubernetes label provider initialized")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *KubernetesService) GetLabels(appDomain string) (*model.App, error) {
|
|
||||||
if !k.started {
|
|
||||||
tlog.App.Debug().Msg("Kubernetes not connected, returning empty labels")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check cache
|
|
||||||
app := k.getByDomain(appDomain)
|
|
||||||
if app != nil {
|
|
||||||
tlog.App.Debug().Str("domain", appDomain).Msg("Found labels in cache by domain")
|
|
||||||
return app, nil
|
|
||||||
}
|
|
||||||
appName := strings.SplitN(appDomain, ".", 2)[0]
|
|
||||||
app = k.getByAppName(appName)
|
|
||||||
if app != nil {
|
|
||||||
tlog.App.Debug().Str("domain", appDomain).Str("appName", appName).Msg("Found labels in cache by app name")
|
|
||||||
return app, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tlog.App.Debug().Str("domain", appDomain).Msg("Cache miss, no matching ingress found")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestKubernetesService(t *testing.T) {
|
|
||||||
type testCase struct {
|
|
||||||
description string
|
|
||||||
run func(t *testing.T, svc *KubernetesService)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []testCase{
|
|
||||||
{
|
|
||||||
description: "Cache by domain returns app and misses unknown domain",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
|
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
|
||||||
{domain: "foo.example.com", appName: "foo", app: app},
|
|
||||||
})
|
|
||||||
|
|
||||||
got := svc.getByDomain("foo.example.com")
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
|
||||||
|
|
||||||
got = svc.getByDomain("notfound.example.com")
|
|
||||||
assert.Nil(t, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Cache by app name returns app and misses unknown name",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "bar.example.com"}}
|
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
|
||||||
{domain: "bar.example.com", appName: "bar", app: app},
|
|
||||||
})
|
|
||||||
|
|
||||||
got := svc.getByAppName("bar")
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, "bar.example.com", got.Config.Domain)
|
|
||||||
|
|
||||||
got = svc.getByAppName("notfound")
|
|
||||||
assert.Nil(t, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "RemoveIngress clears domain and app name entries",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "baz.example.com"}}
|
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
|
||||||
{domain: "baz.example.com", appName: "baz", app: app},
|
|
||||||
})
|
|
||||||
|
|
||||||
svc.removeIngress("default", "my-ingress")
|
|
||||||
|
|
||||||
got := svc.getByDomain("baz.example.com")
|
|
||||||
assert.Nil(t, got)
|
|
||||||
got = svc.getByAppName("baz")
|
|
||||||
assert.Nil(t, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "AddIngressApps replaces stale entries for the same ingress",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
old := model.App{Config: model.AppConfig{Domain: "old.example.com"}}
|
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
|
||||||
{domain: "old.example.com", appName: "old", app: old},
|
|
||||||
})
|
|
||||||
|
|
||||||
updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}}
|
|
||||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
|
||||||
{domain: "new.example.com", appName: "new", app: updated},
|
|
||||||
})
|
|
||||||
|
|
||||||
got := svc.getByDomain("old.example.com")
|
|
||||||
assert.Nil(t, got)
|
|
||||||
|
|
||||||
got = svc.getByDomain("new.example.com")
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, "new.example.com", got.Config.Domain)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "GetLabels returns app from cache when started",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
svc.started = true
|
|
||||||
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}}
|
|
||||||
svc.addIngressApps("default", "ing", []ingressApp{
|
|
||||||
{domain: "hit.example.com", appName: "hit", app: app},
|
|
||||||
})
|
|
||||||
|
|
||||||
got, err := svc.GetLabels("hit.example.com")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "hit.example.com", got.Config.Domain)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "GetLabels returns empty app on cache miss when started",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
svc.started = true
|
|
||||||
|
|
||||||
got, err := svc.GetLabels("notfound.example.com")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "GetLabels resolves app by app name",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
svc.started = true
|
|
||||||
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "myapp.internal.example.com"}}
|
|
||||||
svc.addIngressApps("default", "ing", []ingressApp{
|
|
||||||
{domain: "myapp.internal.example.com", appName: "myapp", app: app},
|
|
||||||
})
|
|
||||||
|
|
||||||
got, err := svc.GetLabels("myapp.internal.example.com")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "myapp.internal.example.com", got.Config.Domain)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "GetLabels returns empty app when service not yet started",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
got, err := svc.GetLabels("anything.example.com")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "UpdateFromItem parses annotations and populates cache",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
item := unstructured.Unstructured{}
|
|
||||||
item.SetNamespace("default")
|
|
||||||
item.SetName("test-ingress")
|
|
||||||
item.SetAnnotations(map[string]string{
|
|
||||||
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
|
||||||
"tinyauth.apps.myapp.users.allow": "alice",
|
|
||||||
})
|
|
||||||
|
|
||||||
svc.updateFromItem(&item)
|
|
||||||
|
|
||||||
got := svc.getByDomain("myapp.example.com")
|
|
||||||
require.NotNil(t, got)
|
|
||||||
assert.Equal(t, "myapp.example.com", got.Config.Domain)
|
|
||||||
assert.Equal(t, "alice", got.Users.Allow)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "UpdateFromItem with no annotations removes existing cache entries",
|
|
||||||
run: func(t *testing.T, svc *KubernetesService) {
|
|
||||||
app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}}
|
|
||||||
svc.addIngressApps("default", "test-ingress", []ingressApp{
|
|
||||||
{domain: "todelete.example.com", appName: "todelete", app: app},
|
|
||||||
})
|
|
||||||
|
|
||||||
item := unstructured.Unstructured{}
|
|
||||||
item.SetNamespace("default")
|
|
||||||
item.SetName("test-ingress")
|
|
||||||
|
|
||||||
svc.updateFromItem(&item)
|
|
||||||
|
|
||||||
got := svc.getByDomain("todelete.example.com")
|
|
||||||
assert.Nil(t, got)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.description, func(t *testing.T) {
|
|
||||||
svc := &KubernetesService{
|
|
||||||
ingressApps: make(map[ingressKey][]ingressApp),
|
|
||||||
domainIndex: make(map[string]ingressAppKey),
|
|
||||||
appNameIndex: make(map[string]ingressAppKey),
|
|
||||||
}
|
|
||||||
test.run(t, svc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,20 +14,20 @@ type OAuthServiceImpl interface {
|
|||||||
NewRandom() string
|
NewRandom() string
|
||||||
GetAuthURL(state string, verifier string) string
|
GetAuthURL(state string, verifier string) string
|
||||||
GetToken(code string, verifier string) (*oauth2.Token, error)
|
GetToken(code string, verifier string) (*oauth2.Token, error)
|
||||||
GetUserinfo(token *oauth2.Token) (*model.Claims, error)
|
GetUserinfo(token *oauth2.Token) (config.Claims, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthBrokerService struct {
|
type OAuthBrokerService struct {
|
||||||
services map[string]OAuthServiceImpl
|
services map[string]OAuthServiceImpl
|
||||||
configs map[string]model.OAuthServiceConfig
|
configs map[string]config.OAuthServiceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
var presets = map[string]func(config model.OAuthServiceConfig) *OAuthService{
|
var presets = map[string]func(config config.OAuthServiceConfig) *OAuthService{
|
||||||
"github": newGitHubOAuthService,
|
"github": newGitHubOAuthService,
|
||||||
"google": newGoogleOAuthService,
|
"google": newGoogleOAuthService,
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOAuthBrokerService(configs map[string]model.OAuthServiceConfig) *OAuthBrokerService {
|
func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
|
||||||
return &OAuthBrokerService{
|
return &OAuthBrokerService{
|
||||||
services: make(map[string]OAuthServiceImpl),
|
services: make(map[string]OAuthServiceImpl),
|
||||||
configs: configs,
|
configs: configs,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GithubEmailResponse []struct {
|
type GithubEmailResponse []struct {
|
||||||
@@ -22,32 +22,32 @@ type GithubUserInfoResponse struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultExtractor(client *http.Client, url string) (*model.Claims, error) {
|
func defaultExtractor(client *http.Client, url string) (config.Claims, error) {
|
||||||
return simpleReq[model.Claims](client, url, nil)
|
return simpleReq[config.Claims](client, url, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubExtractor(client *http.Client, url string) (*model.Claims, error) {
|
func githubExtractor(client *http.Client, url string) (config.Claims, error) {
|
||||||
var user model.Claims
|
var user config.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{
|
||||||
"accept": "application/vnd.github+json",
|
"accept": "application/vnd.github+json",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return config.Claims{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userEmails, err := simpleReq[GithubEmailResponse](client, "https://api.github.com/user/emails", map[string]string{
|
userEmails, err := simpleReq[GithubEmailResponse](client, "https://api.github.com/user/emails", map[string]string{
|
||||||
"accept": "application/vnd.github+json",
|
"accept": "application/vnd.github+json",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return config.Claims{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(*userEmails) == 0 {
|
if len(userEmails) == 0 {
|
||||||
return nil, errors.New("no emails found")
|
return user, errors.New("no emails found")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, email := range *userEmails {
|
for _, email := range userEmails {
|
||||||
if email.Primary {
|
if email.Primary {
|
||||||
user.Email = email.Email
|
user.Email = email.Email
|
||||||
break
|
break
|
||||||
@@ -56,22 +56,22 @@ func githubExtractor(client *http.Client, url 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 == "" {
|
||||||
user.Email = (*userEmails)[0].Email
|
user.Email = userEmails[0].Email
|
||||||
}
|
}
|
||||||
|
|
||||||
user.PreferredUsername = userInfo.Login
|
user.PreferredUsername = userInfo.Login
|
||||||
user.Name = userInfo.Name
|
user.Name = userInfo.Name
|
||||||
user.Sub = strconv.Itoa(userInfo.ID)
|
user.Sub = strconv.Itoa(userInfo.ID)
|
||||||
|
|
||||||
return &user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (*T, error) {
|
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (T, error) {
|
||||||
var decodedRes T
|
var decodedRes T
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return decodedRes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range headers {
|
for key, value := range headers {
|
||||||
@@ -80,23 +80,23 @@ func simpleReq[T any](client *http.Client, url string, headers map[string]string
|
|||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return decodedRes, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
return nil, fmt.Errorf("request failed with status: %s", res.Status)
|
return decodedRes, fmt.Errorf("request failed with status: %s", res.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return decodedRes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, &decodedRes)
|
err = json.Unmarshal(body, &decodedRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return decodedRes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &decodedRes, nil
|
return decodedRes, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"golang.org/x/oauth2/endpoints"
|
"golang.org/x/oauth2/endpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newGoogleOAuthService(config model.OAuthServiceConfig) *OAuthService {
|
func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||||
scopes := []string{"openid", "email", "profile"}
|
scopes := []string{"openid", "email", "profile"}
|
||||||
config.Scopes = scopes
|
config.Scopes = scopes
|
||||||
config.AuthURL = endpoints.Google.AuthURL
|
config.AuthURL = endpoints.Google.AuthURL
|
||||||
@@ -14,7 +14,7 @@ func newGoogleOAuthService(config model.OAuthServiceConfig) *OAuthService {
|
|||||||
return NewOAuthService(config, "google")
|
return NewOAuthService(config, "google")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGitHubOAuthService(config model.OAuthServiceConfig) *OAuthService {
|
func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||||
scopes := []string{"read:user", "user:email"}
|
scopes := []string{"read:user", "user:email"}
|
||||||
config.Scopes = scopes
|
config.Scopes = scopes
|
||||||
config.AuthURL = endpoints.GitHub.AuthURL
|
config.AuthURL = endpoints.GitHub.AuthURL
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserinfoExtractor func(client *http.Client, url string) (*model.Claims, error)
|
type UserinfoExtractor func(client *http.Client, url string) (config.Claims, error)
|
||||||
|
|
||||||
type OAuthService struct {
|
type OAuthService struct {
|
||||||
serviceCfg model.OAuthServiceConfig
|
serviceCfg config.OAuthServiceConfig
|
||||||
config *oauth2.Config
|
config *oauth2.Config
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
userinfoExtractor UserinfoExtractor
|
userinfoExtractor UserinfoExtractor
|
||||||
id string
|
id string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOAuthService(config model.OAuthServiceConfig, id string) *OAuthService {
|
func NewOAuthService(config config.OAuthServiceConfig, id string) *OAuthService {
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
@@ -78,7 +78,7 @@ func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, er
|
|||||||
return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
|
return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
|
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (config.Claims, error) {
|
||||||
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
|
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
|
||||||
return s.userinfoExtractor(client, s.serviceCfg.UserinfoURL)
|
return s.userinfoExtractor(client, s.serviceCfg.UserinfoURL)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-jose/go-jose/v4"
|
"github.com/go-jose/go-jose/v4"
|
||||||
"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/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -87,7 +86,7 @@ type UserinfoResponse struct {
|
|||||||
EmailVerified bool `json:"email_verified,omitempty"`
|
EmailVerified bool `json:"email_verified,omitempty"`
|
||||||
PhoneNumber string `json:"phone_number,omitempty"`
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
|
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
|
||||||
Address *model.AddressClaim `json:"address,omitempty"`
|
Address *config.AddressClaim `json:"address,omitempty"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +111,7 @@ type AuthorizeRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OIDCServiceConfig struct {
|
type OIDCServiceConfig struct {
|
||||||
Clients map[string]model.OIDCClientConfig
|
Clients map[string]config.OIDCClientConfig
|
||||||
PrivateKeyPath string
|
PrivateKeyPath string
|
||||||
PublicKeyPath string
|
PublicKeyPath string
|
||||||
Issuer string
|
Issuer string
|
||||||
@@ -122,7 +121,7 @@ type OIDCServiceConfig struct {
|
|||||||
type OIDCService struct {
|
type OIDCService struct {
|
||||||
config OIDCServiceConfig
|
config OIDCServiceConfig
|
||||||
queries *repository.Queries
|
queries *repository.Queries
|
||||||
clients map[string]model.OIDCClientConfig
|
clients map[string]config.OIDCClientConfig
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
publicKey crypto.PublicKey
|
publicKey crypto.PublicKey
|
||||||
issuer string
|
issuer string
|
||||||
@@ -255,7 +254,7 @@ func (service *OIDCService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We will reorganize the client into a map with the client ID as the key
|
// We will reorganize the client into a map with the client ID as the key
|
||||||
service.clients = make(map[string]model.OIDCClientConfig)
|
service.clients = make(map[string]config.OIDCClientConfig)
|
||||||
|
|
||||||
for id, client := range service.config.Clients {
|
for id, client := range service.config.Clients {
|
||||||
client.ID = id
|
client.ID = id
|
||||||
@@ -283,7 +282,7 @@ func (service *OIDCService) GetIssuer() string {
|
|||||||
return service.issuer
|
return service.issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) GetClient(id string) (model.OIDCClientConfig, bool) {
|
func (service *OIDCService) GetClient(id string) (config.OIDCClientConfig, bool) {
|
||||||
client, ok := service.clients[id]
|
client, ok := service.clients[id]
|
||||||
return client, ok
|
return client, ok
|
||||||
}
|
}
|
||||||
@@ -367,45 +366,43 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext model.UserContext, req AuthorizeRequest) error {
|
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext config.UserContext, req AuthorizeRequest) error {
|
||||||
userInfoParams := repository.CreateOidcUserInfoParams{
|
addressJSON, err := json.Marshal(userContext.Attributes.Address)
|
||||||
Sub: sub,
|
|
||||||
Name: userContext.GetName(),
|
|
||||||
Email: userContext.GetEmail(),
|
|
||||||
PreferredUsername: userContext.GetUsername(),
|
|
||||||
UpdatedAt: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if userContext.IsLocal() {
|
|
||||||
addressJSON, err := json.Marshal(userContext.Local.Attributes.Address)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfoParams.GivenName = userContext.Local.Attributes.GivenName
|
|
||||||
userInfoParams.FamilyName = userContext.Local.Attributes.FamilyName
|
userInfoParams := repository.CreateOidcUserInfoParams{
|
||||||
userInfoParams.MiddleName = userContext.Local.Attributes.MiddleName
|
Sub: sub,
|
||||||
userInfoParams.Nickname = userContext.Local.Attributes.Nickname
|
Name: userContext.Name,
|
||||||
userInfoParams.Profile = userContext.Local.Attributes.Profile
|
Email: userContext.Email,
|
||||||
userInfoParams.Picture = userContext.Local.Attributes.Picture
|
PreferredUsername: userContext.Username,
|
||||||
userInfoParams.Website = userContext.Local.Attributes.Website
|
UpdatedAt: time.Now().Unix(),
|
||||||
userInfoParams.Gender = userContext.Local.Attributes.Gender
|
GivenName: userContext.Attributes.GivenName,
|
||||||
userInfoParams.Birthdate = userContext.Local.Attributes.Birthdate
|
FamilyName: userContext.Attributes.FamilyName,
|
||||||
userInfoParams.Zoneinfo = userContext.Local.Attributes.Zoneinfo
|
MiddleName: userContext.Attributes.MiddleName,
|
||||||
userInfoParams.Locale = userContext.Local.Attributes.Locale
|
Nickname: userContext.Attributes.Nickname,
|
||||||
userInfoParams.PhoneNumber = userContext.Local.Attributes.PhoneNumber
|
Profile: userContext.Attributes.Profile,
|
||||||
userInfoParams.Address = string(addressJSON)
|
Picture: userContext.Attributes.Picture,
|
||||||
|
Website: userContext.Attributes.Website,
|
||||||
|
Gender: userContext.Attributes.Gender,
|
||||||
|
Birthdate: userContext.Attributes.Birthdate,
|
||||||
|
Zoneinfo: userContext.Attributes.Zoneinfo,
|
||||||
|
Locale: userContext.Attributes.Locale,
|
||||||
|
PhoneNumber: userContext.Attributes.PhoneNumber,
|
||||||
|
Address: string(addressJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server
|
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server
|
||||||
if userContext.IsLDAP() {
|
if userContext.Provider == "ldap" {
|
||||||
userInfoParams.Groups = strings.Join(userContext.LDAP.Groups, ",")
|
userInfoParams.Groups = userContext.LdapGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.OAuth && len(userContext.OAuthGroups) > 0 {
|
||||||
userInfoParams.Groups = strings.Join(userContext.OAuth.Groups, ",")
|
userInfoParams.Groups = userContext.OAuthGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := service.queries.CreateOidcUserInfo(c, userInfoParams)
|
_, err = service.queries.CreateOidcUserInfo(c, userInfoParams)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -447,7 +444,7 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, client
|
|||||||
return oidcCode, nil
|
return oidcCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
|
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
|
||||||
createdAt := time.Now().Unix()
|
createdAt := time.Now().Unix()
|
||||||
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
@@ -513,7 +510,7 @@ func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client model.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
|
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
|
||||||
user, err := service.GetUserinfo(c, codeEntry.Sub)
|
user, err := service.GetUserinfo(c, codeEntry.Sub)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -532,7 +529,7 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client model.OID
|
|||||||
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
// Refresh token lives double the time of an access token but can't be used to access userinfo
|
// Refresh token lives double the time of an access token but can't be used to access userinfo
|
||||||
refreshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
||||||
|
|
||||||
tokenResponse := TokenResponse{
|
tokenResponse := TokenResponse{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
@@ -550,7 +547,7 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client model.OID
|
|||||||
ClientID: client.ClientID,
|
ClientID: client.ClientID,
|
||||||
Scope: codeEntry.Scope,
|
Scope: codeEntry.Scope,
|
||||||
TokenExpiresAt: tokenExpiresAt,
|
TokenExpiresAt: tokenExpiresAt,
|
||||||
RefreshTokenExpiresAt: refreshTokenExpiresAt,
|
RefreshTokenExpiresAt: refrshTokenExpiresAt,
|
||||||
Nonce: codeEntry.Nonce,
|
Nonce: codeEntry.Nonce,
|
||||||
CodeHash: codeEntry.CodeHash,
|
CodeHash: codeEntry.CodeHash,
|
||||||
})
|
})
|
||||||
@@ -566,7 +563,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if err == sql.ErrNoRows {
|
||||||
return TokenResponse{}, ErrTokenNotFound
|
return TokenResponse{}, ErrTokenNotFound
|
||||||
}
|
}
|
||||||
return TokenResponse{}, err
|
return TokenResponse{}, err
|
||||||
@@ -587,7 +584,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
return TokenResponse{}, err
|
return TokenResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, err := service.generateIDToken(model.OIDCClientConfig{
|
idToken, err := service.generateIDToken(config.OIDCClientConfig{
|
||||||
ClientID: entry.ClientID,
|
ClientID: entry.ClientID,
|
||||||
}, user, entry.Scope, entry.Nonce)
|
}, user, entry.Scope, entry.Nonce)
|
||||||
|
|
||||||
@@ -599,7 +596,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
newRefreshToken := utils.GenerateString(32)
|
newRefreshToken := utils.GenerateString(32)
|
||||||
|
|
||||||
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
refreshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
||||||
|
|
||||||
tokenResponse := TokenResponse{
|
tokenResponse := TokenResponse{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
@@ -614,7 +611,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
AccessTokenHash: service.Hash(accessToken),
|
AccessTokenHash: service.Hash(accessToken),
|
||||||
RefreshTokenHash: service.Hash(newRefreshToken),
|
RefreshTokenHash: service.Hash(newRefreshToken),
|
||||||
TokenExpiresAt: tokenExpiresAt,
|
TokenExpiresAt: tokenExpiresAt,
|
||||||
RefreshTokenExpiresAt: refreshTokenExpiresAt,
|
RefreshTokenExpiresAt: refrshTokenExpiresAt,
|
||||||
RefreshTokenHash_2: service.Hash(refreshToken), // that's the selector, it's not stored in the db
|
RefreshTokenHash_2: service.Hash(refreshToken), // that's the selector, it's not stored in the db
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -645,7 +642,7 @@ func (service *OIDCService) GetAccessToken(c *gin.Context, tokenHash string) (re
|
|||||||
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if err == sql.ErrNoRows {
|
||||||
return repository.OidcToken{}, ErrTokenNotFound
|
return repository.OidcToken{}, ErrTokenNotFound
|
||||||
}
|
}
|
||||||
return repository.OidcToken{}, err
|
return repository.OidcToken{}, err
|
||||||
@@ -716,7 +713,7 @@ func (service *OIDCService) CompileUserinfo(user repository.OidcUserinfo, scope
|
|||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "address") {
|
if slices.Contains(scopes, "address") {
|
||||||
var addr model.AddressClaim
|
var addr config.AddressClaim
|
||||||
if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
|
if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
|
||||||
userInfo.Address = &addr
|
userInfo.Address = &addr
|
||||||
}
|
}
|
||||||
@@ -786,7 +783,7 @@ func (service *OIDCService) Cleanup() {
|
|||||||
token, err := service.queries.GetOidcTokenBySub(ctx, expiredCode.Sub)
|
token, err := service.queries.GetOidcTokenBySub(ctx, expiredCode.Sub)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if err == sql.ErrNoRows {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to get OIDC token by sub")
|
tlog.App.Warn().Err(err).Msg("Failed to get OIDC token by sub")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,6 +73,22 @@ func Filter[T any](slice []T, test func(T) bool) (res []T) {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetContext(c *gin.Context) (config.UserContext, error) {
|
||||||
|
userContextValue, exists := c.Get("context")
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return config.UserContext{}, errors.New("no user context in request")
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext, ok := userContextValue.(*config.UserContext)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return config.UserContext{}, errors.New("invalid user context in request")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *userContext, nil
|
||||||
|
}
|
||||||
|
|
||||||
func IsRedirectSafe(redirectURL string, domain string) bool {
|
func IsRedirectSafe(redirectURL string, domain string) bool {
|
||||||
if redirectURL == "" {
|
if redirectURL == "" {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetRootDomain(t *testing.T) {
|
func TestGetRootDomain(t *testing.T) {
|
||||||
@@ -12,14 +15,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 +44,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 +98,57 @@ 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 TestGetContext(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
c, _ := gin.CreateTestContext(nil)
|
||||||
|
|
||||||
|
// Normal case
|
||||||
|
c.Set("context", &config.UserContext{Username: "testuser"})
|
||||||
|
result, err := utils.GetContext(c)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, "testuser", result.Username)
|
||||||
|
|
||||||
|
// Case with no context
|
||||||
|
c.Set("context", nil)
|
||||||
|
_, err = utils.GetContext(c)
|
||||||
|
assert.Error(t, err, "invalid user context in request")
|
||||||
|
|
||||||
|
// Case with invalid context type
|
||||||
|
c.Set("context", "invalid type")
|
||||||
|
_, err = utils.GetContext(c)
|
||||||
|
assert.Error(t, err, "invalid user context in request")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsRedirectSafe(t *testing.T) {
|
func TestIsRedirectSafe(t *testing.T) {
|
||||||
@@ -133,50 +158,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,24 +4,24 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
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")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
_, err = file.WriteString("file content\n")
|
_, err = file.WriteString("file content\n")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
assert.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) {
|
||||||
|
|||||||
@@ -4,20 +4,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
"github.com/tinyauthapp/paerser/env"
|
"github.com/tinyauthapp/paerser/env"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvLoader struct{}
|
type EnvLoader struct{}
|
||||||
|
|
||||||
func (e *EnvLoader) Load(_ []string, cmd *cli.Command) (bool, error) {
|
func (e *EnvLoader) Load(_ []string, cmd *cli.Command) (bool, error) {
|
||||||
vars := env.FindPrefixedEnvVars(os.Environ(), model.DefaultNamePrefix, cmd.Configuration)
|
vars := env.FindPrefixedEnvVars(os.Environ(), config.DefaultNamePrefix, cmd.Configuration)
|
||||||
if len(vars) == 0 {
|
if len(vars) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := env.Decode(vars, model.DefaultNamePrefix, cmd.Configuration); err != nil {
|
if err := env.Decode(vars, config.DefaultNamePrefix, cmd.Configuration); err != nil {
|
||||||
return false, fmt.Errorf("failed to decode configuration from environment variables: %w", err)
|
return false, fmt.Errorf("failed to decode configuration from environment variables: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,20 +4,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"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")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
_, err = file.WriteString(" secret \n")
|
_, err = file.WriteString(" secret \n")
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
assert.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
|
||||||
@@ -54,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
|
||||||
@@ -144,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) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
@@ -22,7 +22,7 @@ var (
|
|||||||
App zerolog.Logger
|
App zerolog.Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLogger(cfg model.LogConfig) *Logger {
|
func NewLogger(cfg config.LogConfig) *Logger {
|
||||||
baseLogger := log.With().
|
baseLogger := log.With().
|
||||||
Timestamp().
|
Timestamp().
|
||||||
Caller().
|
Caller().
|
||||||
@@ -44,24 +44,24 @@ func NewLogger(cfg model.LogConfig) *Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewSimpleLogger() *Logger {
|
func NewSimpleLogger() *Logger {
|
||||||
return NewLogger(model.LogConfig{
|
return NewLogger(config.LogConfig{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
Json: false,
|
Json: false,
|
||||||
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: false},
|
Audit: config.LogStreamConfig{Enabled: false},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestLogger() *Logger {
|
func NewTestLogger() *Logger {
|
||||||
return NewLogger(model.LogConfig{
|
return NewLogger(config.LogConfig{
|
||||||
Level: "trace",
|
Level: "trace",
|
||||||
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},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ func (l *Logger) Init() {
|
|||||||
App = l.App
|
App = l.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLogger(component string, streamCfg model.LogStreamConfig, baseLogger zerolog.Logger) zerolog.Logger {
|
func createLogger(component string, streamCfg config.LogStreamConfig, baseLogger zerolog.Logger) zerolog.Logger {
|
||||||
if !streamCfg.Enabled {
|
if !streamCfg.Enabled {
|
||||||
return zerolog.Nop()
|
return zerolog.Nop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) {
|
func ParseUsers(usersStr []string, userAttributes map[string]config.UserAttributes) ([]config.User, error) {
|
||||||
var users []model.LocalUser
|
var users []config.User
|
||||||
|
|
||||||
if len(usersStr) == 0 {
|
if len(usersStr) == 0 {
|
||||||
return &users, nil
|
return []config.User{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range usersStr {
|
for _, user := range usersStr {
|
||||||
@@ -22,22 +22,22 @@ func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttribute
|
|||||||
}
|
}
|
||||||
parsed, err := ParseUser(strings.TrimSpace(user))
|
parsed, err := ParseUser(strings.TrimSpace(user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return []config.User{}, err
|
||||||
}
|
}
|
||||||
if attrs, ok := userAttributes[parsed.Username]; ok {
|
if attrs, ok := userAttributes[parsed.Username]; ok {
|
||||||
parsed.Attributes = attrs
|
parsed.Attributes = attrs
|
||||||
}
|
}
|
||||||
users = append(users, *parsed)
|
users = append(users, parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) {
|
func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]config.UserAttributes) ([]config.User, error) {
|
||||||
var usersStr []string
|
var usersStr []string
|
||||||
|
|
||||||
if len(usersCfg) == 0 && usersPath == "" {
|
if len(usersCfg) == 0 && usersPath == "" {
|
||||||
return nil, nil
|
return []config.User{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(usersCfg) > 0 {
|
if len(usersCfg) > 0 {
|
||||||
@@ -48,7 +48,7 @@ func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]mod
|
|||||||
contents, err := ReadFile(usersPath)
|
contents, err := ReadFile(usersPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return []config.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.SplitSeq(contents, "\n")
|
lines := strings.SplitSeq(contents, "\n")
|
||||||
@@ -65,7 +65,7 @@ func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]mod
|
|||||||
return ParseUsers(usersStr, userAttributes)
|
return ParseUsers(usersStr, userAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseUser(userStr string) (*model.LocalUser, error) {
|
func ParseUser(userStr string) (config.User, error) {
|
||||||
if strings.Contains(userStr, "$$") {
|
if strings.Contains(userStr, "$$") {
|
||||||
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
||||||
}
|
}
|
||||||
@@ -73,27 +73,27 @@ func ParseUser(userStr string) (*model.LocalUser, error) {
|
|||||||
parts := strings.SplitN(userStr, ":", 4)
|
parts := strings.SplitN(userStr, ":", 4)
|
||||||
|
|
||||||
if len(parts) < 2 || len(parts) > 3 {
|
if len(parts) < 2 || len(parts) > 3 {
|
||||||
return nil, errors.New("invalid user format")
|
return config.User{}, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
trimmed := strings.TrimSpace(part)
|
trimmed := strings.TrimSpace(part)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
return nil, errors.New("invalid user format")
|
return config.User{}, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
parts[i] = trimmed
|
parts[i] = trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
user := model.LocalUser{
|
user := config.User{
|
||||||
Username: parts[0],
|
Username: parts[0],
|
||||||
Password: parts[1],
|
Password: parts[1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
user.TOTPSecret = parts[2]
|
user.TotpSecret = parts[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
return &user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompileUserEmail(username string, domain string) string {
|
func CompileUserEmail(username string, domain string) string {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"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"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetUsers(t *testing.T) {
|
func TestGetUsers(t *testing.T) {
|
||||||
@@ -14,63 +15,63 @@ func TestGetUsers(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
file, err := os.Create("/tmp/tinyauth_users_test.txt")
|
file, err := os.Create("/tmp/tinyauth_users_test.txt")
|
||||||
assert.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
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
assert.NoError(t, err)
|
assert.NilError(t, err)
|
||||||
defer os.Remove("/tmp/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{}, "/tmp/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}, "/tmp/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{}, "/tmp/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)
|
||||||
@@ -83,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{}, "/tmp/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) {
|
||||||
@@ -99,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