mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-09 11:40:15 +00:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8362a1ca6c | |||
| 68caf8272c | |||
| ff968ef08f | |||
| 464769c673 | |||
| b4c7a877ab | |||
| 67d0e9321f | |||
| f4faa422ae | |||
| 3d8288a7eb | |||
| 96d0a2a3ab | |||
| 0bd2821a9b | |||
| 364175adc0 | |||
| 73cc4808bc | |||
| 7abd9fe310 | |||
| 6ab9c0a0c5 | |||
| 4aa05aeb79 | |||
| 440a3a3ef5 | |||
| a3c4d6ac83 | |||
| c8b31c54a0 | |||
| 04b93fa107 | |||
| a6c716c4e2 | |||
| ffafb5bff5 | |||
| bb867ea5f4 | |||
| fdd516edf1 | |||
| 1b14b90ede | |||
| 6ba55b3d9c | |||
| 09ec40cb76 | |||
| 08af4557fd | |||
| 45a88ea041 | |||
| 89ffdf7e22 | |||
| c692dfe422 | |||
| ac819cc868 | |||
| 69f4206f65 | |||
| 2572376686 | |||
| ea1baaa9ac | |||
| 72d39a23a0 | |||
| efe373084f | |||
| 7f18b45e21 | |||
| 6ccc894570 | |||
| 53af1b99c0 | |||
| 654b5cc436 | |||
| f7d7f1c4f0 | |||
| e7d26f497d | |||
| a9face749d | |||
| 905f67292c | |||
| 6ed5c2d0a0 | |||
| 9dd4515464 | |||
| 40bcc7d9d8 | |||
| 556096cdb8 | |||
| c825d81b2d | |||
| f404c2ef16 |
+17
-15
@@ -32,8 +32,6 @@ TINYAUTH_SERVER_PORT=3000
|
|||||||
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
|
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
|
||||||
# The path to the Unix socket.
|
# The path to the Unix socket.
|
||||||
TINYAUTH_SERVER_SOCKETPATH=
|
TINYAUTH_SERVER_SOCKETPATH=
|
||||||
# Enable listening on both TCP and Unix socket at the same time.
|
|
||||||
TINYAUTH_SERVER_CONCURRENTLISTENERSENABLED=false
|
|
||||||
|
|
||||||
# auth config
|
# auth config
|
||||||
|
|
||||||
@@ -99,6 +97,8 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
|
|||||||
TINYAUTH_AUTH_LOGINTIMEOUT=300
|
TINYAUTH_AUTH_LOGINTIMEOUT=300
|
||||||
# Maximum login retries.
|
# Maximum login retries.
|
||||||
TINYAUTH_AUTH_LOGINMAXRETRIES=3
|
TINYAUTH_AUTH_LOGINMAXRETRIES=3
|
||||||
|
# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically.
|
||||||
|
TINYAUTH_AUTH_LOCKDOWNENABLED=true
|
||||||
# Comma-separated list of trusted proxy addresses.
|
# Comma-separated list of trusted proxy addresses.
|
||||||
TINYAUTH_AUTH_TRUSTEDPROXIES=
|
TINYAUTH_AUTH_TRUSTEDPROXIES=
|
||||||
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
|
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
|
||||||
@@ -206,6 +206,8 @@ TINYAUTH_LDAP_ADDRESS=
|
|||||||
TINYAUTH_LDAP_BINDDN=
|
TINYAUTH_LDAP_BINDDN=
|
||||||
# Bind password for LDAP authentication.
|
# Bind password for LDAP authentication.
|
||||||
TINYAUTH_LDAP_BINDPASSWORD=
|
TINYAUTH_LDAP_BINDPASSWORD=
|
||||||
|
# Path to the Bind password.
|
||||||
|
TINYAUTH_LDAP_BINDPASSWORDFILE=
|
||||||
# Base DN for LDAP searches.
|
# Base DN for LDAP searches.
|
||||||
TINYAUTH_LDAP_BASEDN=
|
TINYAUTH_LDAP_BASEDN=
|
||||||
# Allow insecure LDAP connections.
|
# Allow insecure LDAP connections.
|
||||||
@@ -218,6 +220,19 @@ TINYAUTH_LDAP_AUTHCERT=
|
|||||||
TINYAUTH_LDAP_AUTHKEY=
|
TINYAUTH_LDAP_AUTHKEY=
|
||||||
# Cache duration for LDAP group membership in seconds.
|
# Cache duration for LDAP group membership in seconds.
|
||||||
TINYAUTH_LDAP_GROUPCACHETTL=900
|
TINYAUTH_LDAP_GROUPCACHETTL=900
|
||||||
|
|
||||||
|
# tailscale config
|
||||||
|
|
||||||
|
# Enable Tailscale integration.
|
||||||
|
TINYAUTH_TAILSCALE_ENABLED=false
|
||||||
|
# Tailscale API token.
|
||||||
|
TINYAUTH_TAILSCALE_APITOKEN=
|
||||||
|
# Path to the file containing the Tailscale API token.
|
||||||
|
TINYAUTH_TAILSCALE_APITOKENFILE=
|
||||||
|
# Tailnet name.
|
||||||
|
TINYAUTH_TAILSCALE_TAILNET=
|
||||||
|
# Cache duration for Tailscale device and user lists in seconds.
|
||||||
|
TINYAUTH_TAILSCALE_CACHEDURATION=300
|
||||||
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
|
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
|
||||||
TINYAUTH_LABELPROVIDER="auto"
|
TINYAUTH_LABELPROVIDER="auto"
|
||||||
|
|
||||||
@@ -239,16 +254,3 @@ TINYAUTH_LOG_STREAMS_APP_LEVEL=
|
|||||||
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
|
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
|
||||||
# Log level for this stream. Use global if empty.
|
# Log level for this stream. Use global if empty.
|
||||||
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL=
|
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL=
|
||||||
|
|
||||||
# tailscale config
|
|
||||||
|
|
||||||
# Enable Tailscale integration.
|
|
||||||
TINYAUTH_TAILSCALE_ENABLED=false
|
|
||||||
# Tailscale state directory.
|
|
||||||
TINYAUTH_TAILSCALE_DIR="./tailscale_state"
|
|
||||||
# Tailscale hostname.
|
|
||||||
TINYAUTH_TAILSCALE_HOSTNAME=
|
|
||||||
# Tailscale auth key.
|
|
||||||
TINYAUTH_TAILSCALE_AUTHKEY=
|
|
||||||
# Use ephemeral Tailscale node.
|
|
||||||
TINYAUTH_TAILSCALE_EPHEMERAL=false
|
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
with:
|
with:
|
||||||
package_json_file: ./frontend/package.json
|
package_json_file: ./frontend/package.json
|
||||||
|
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.4"
|
go-version: "^1.26.4"
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ jobs:
|
|||||||
- name: Check codegen is up to date
|
- name: Check codegen is up to date
|
||||||
run: |
|
run: |
|
||||||
sqlc generate
|
sqlc generate
|
||||||
go generate ./internal/repository/...
|
go generate ./...
|
||||||
git diff --exit-code -- internal/repository/
|
git diff --exit-code
|
||||||
git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true
|
git status --porcelain | grep -q . && echo "untracked files code gen files" && exit 1 || true
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
@@ -62,6 +62,6 @@ jobs:
|
|||||||
run: go test -coverprofile=coverage.txt -v ./...
|
run: go test -coverprofile=coverage.txt -v ./...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Delete old release
|
- name: Delete old release
|
||||||
run: gh release delete --cleanup-tag --yes nightly || echo release not found
|
run: gh release delete --cleanup-tag --yes nightly || echo release not found
|
||||||
@@ -23,21 +23,22 @@ jobs:
|
|||||||
REPO: ${{ github.event.repository.name }}
|
REPO: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: nightly
|
tag_name: nightly
|
||||||
|
|
||||||
generate-metadata:
|
generate-metadata:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs: create-release
|
needs: create-release
|
||||||
outputs:
|
outputs:
|
||||||
VERSION: ${{ steps.metadata.outputs.VERSION }}
|
VERSION: ${{ steps.metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH: ${{ steps.metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH: ${{ steps.metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
|
BUILD_TAGS: ${{ steps.metadata.outputs.BUILD_TAGS }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
@@ -47,25 +48,27 @@ jobs:
|
|||||||
echo "VERSION=nightly" >> "$GITHUB_OUTPUT"
|
echo "VERSION=nightly" >> "$GITHUB_OUTPUT"
|
||||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
echo "BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')" >> "$GITHUB_OUTPUT"
|
echo "BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')" >> "$GITHUB_OUTPUT"
|
||||||
|
export GIN_BUILD_TAGS=nomsgpack
|
||||||
|
echo "BUILD_TAGS=$GIN_BUILD_TAGS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
binary-build:
|
binary-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
with:
|
with:
|
||||||
package_json_file: ./frontend/package.json
|
package_json_file: ./frontend/package.json
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.4"
|
go-version: "^1.26.4"
|
||||||
|
|
||||||
@@ -83,7 +86,11 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-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 -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||||
|
-ldflags "-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
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -94,23 +101,23 @@ jobs:
|
|||||||
path: tinyauth-amd64
|
path: tinyauth-amd64
|
||||||
|
|
||||||
binary-build-arm:
|
binary-build-arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
with:
|
with:
|
||||||
package_json_file: ./frontend/package.json
|
package_json_file: ./frontend/package.json
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.4"
|
go-version: "^1.26.4"
|
||||||
|
|
||||||
@@ -128,7 +135,11 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
go build -ldflags "-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 -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||||
|
-ldflags "-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
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -139,47 +150,46 @@ jobs:
|
|||||||
path: tinyauth-arm64
|
path: tinyauth-arm64
|
||||||
|
|
||||||
image-build:
|
image-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -190,41 +200,41 @@ jobs:
|
|||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-amd64
|
name: digests-alpine-linux-amd64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-build-distroless:
|
image-build-distroless:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
- image-build
|
- image-build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -232,13 +242,12 @@ jobs:
|
|||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
file: Dockerfile.distroless
|
file: Dockerfile.distroless
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -255,47 +264,46 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-build-arm:
|
image-build-arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -306,41 +314,41 @@ jobs:
|
|||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-arm64
|
name: digests-alpine-linux-arm64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-build-arm-distroless:
|
image-build-arm-distroless:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
needs:
|
needs:
|
||||||
- create-release
|
- create-release
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
- image-build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
@@ -348,13 +356,12 @@ jobs:
|
|||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
file: Dockerfile.distroless
|
file: Dockerfile.distroless
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -371,7 +378,7 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-merge:
|
image-merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- image-build
|
- image-build
|
||||||
- image-build-arm
|
- image-build-arm
|
||||||
@@ -380,22 +387,22 @@ jobs:
|
|||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-alpine-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -410,7 +417,7 @@ jobs:
|
|||||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
||||||
|
|
||||||
image-merge-distroless:
|
image-merge-distroless:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- image-build-distroless
|
- image-build-distroless
|
||||||
- image-build-arm-distroless
|
- image-build-arm-distroless
|
||||||
@@ -423,18 +430,18 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -449,7 +456,7 @@ jobs:
|
|||||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
||||||
|
|
||||||
update-release:
|
update-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- binary-build
|
- binary-build
|
||||||
- binary-build-arm
|
- binary-build-arm
|
||||||
@@ -461,7 +468,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||||
with:
|
with:
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
tag_name: nightly
|
tag_name: nightly
|
||||||
|
|||||||
@@ -11,37 +11,42 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-metadata:
|
generate-metadata:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
outputs:
|
outputs:
|
||||||
VERSION: ${{ steps.metadata.outputs.VERSION }}
|
VERSION: ${{ steps.metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH: ${{ steps.metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH: ${{ steps.metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
|
BUILD_TAGS: ${{ steps.metadata.outputs.BUILD_TAGS }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Generate metadata
|
- name: Generate metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
|
env:
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
echo "VERSION=$REF_NAME" >> "$GITHUB_OUTPUT"
|
||||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
echo "BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')" >> "$GITHUB_OUTPUT"
|
echo "BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')" >> "$GITHUB_OUTPUT"
|
||||||
|
export GIN_BUILD_TAGS=nomsgpack
|
||||||
|
echo "BUILD_TAGS=$GIN_BUILD_TAGS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
binary-build:
|
binary-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
with:
|
with:
|
||||||
package_json_file: ./frontend/package.json
|
package_json_file: ./frontend/package.json
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.4"
|
go-version: "^1.26.4"
|
||||||
|
|
||||||
@@ -59,7 +64,11 @@ 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 -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||||
|
-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
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -70,20 +79,20 @@ jobs:
|
|||||||
path: tinyauth-amd64
|
path: tinyauth-amd64
|
||||||
|
|
||||||
binary-build-arm:
|
binary-build-arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
needs:
|
needs:
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
with:
|
with:
|
||||||
package_json_file: ./frontend/package.json
|
package_json_file: ./frontend/package.json
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.4"
|
go-version: "^1.26.4"
|
||||||
|
|
||||||
@@ -101,7 +110,11 @@ 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 -tags ${{ needs.generate-metadata.outputs.BUILD_TAGS }} \
|
||||||
|
-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
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
@@ -112,45 +125,44 @@ jobs:
|
|||||||
path: tinyauth-arm64
|
path: tinyauth-arm64
|
||||||
|
|
||||||
image-build:
|
image-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
LDFLAGS="-s -w"
|
LDFLAGS=-s -w
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -161,38 +173,38 @@ jobs:
|
|||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-amd64
|
name: digests-alpine-linux-amd64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-build-distroless:
|
image-build-distroless:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
- image-build
|
- image-build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -200,14 +212,13 @@ jobs:
|
|||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
file: Dockerfile.distroless
|
file: Dockerfile.distroless
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
LDFLAGS="-s -w"
|
LDFLAGS=-s -w
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -224,45 +235,44 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-build-arm:
|
image-build-arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
needs:
|
needs:
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
LDFLAGS="-s -w"
|
LDFLAGS=-s -w
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -273,38 +283,38 @@ jobs:
|
|||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-arm64
|
name: digests-alpine-linux-arm64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-build-arm-distroless:
|
image-build-arm-distroless:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
needs:
|
needs:
|
||||||
- generate-metadata
|
- generate-metadata
|
||||||
- image-build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: useblacksmith/build-push-action@v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
@@ -312,14 +322,13 @@ jobs:
|
|||||||
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
tags: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
file: Dockerfile.distroless
|
file: Dockerfile.distroless
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
VERSION=${{ needs.generate-metadata.outputs.VERSION }}
|
||||||
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
|
||||||
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
LDFLAGS="-s -w"
|
LDFLAGS=-s -w
|
||||||
|
BUILD_TAGS=${{ needs.generate-metadata.outputs.BUILD_TAGS }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -336,7 +345,7 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
image-merge:
|
image-merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- image-build
|
- image-build
|
||||||
- image-build-arm
|
- image-build-arm
|
||||||
@@ -345,22 +354,22 @@ jobs:
|
|||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-alpine-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -377,7 +386,7 @@ jobs:
|
|||||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
||||||
|
|
||||||
image-merge-distroless:
|
image-merge-distroless:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- image-build-distroless
|
- image-build-distroless
|
||||||
- image-build-arm-distroless
|
- image-build-arm-distroless
|
||||||
@@ -390,18 +399,18 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Blacksmith Builder
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: useblacksmith/setup-docker-builder@v1
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -420,7 +429,7 @@ jobs:
|
|||||||
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
$(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *)
|
||||||
|
|
||||||
update-release:
|
update-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
needs:
|
needs:
|
||||||
- binary-build
|
- binary-build
|
||||||
- binary-build-arm
|
- binary-build-arm
|
||||||
@@ -432,6 +441,6 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||||
with:
|
with:
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ permissions: read-all
|
|||||||
jobs:
|
jobs:
|
||||||
analysis:
|
analysis:
|
||||||
name: Scorecard analysis
|
name: Scorecard analysis
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
|
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -38,6 +38,6 @@ jobs:
|
|||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
- name: Upload to code-scanning
|
- name: Upload to code-scanning
|
||||||
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
|
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-sponsors:
|
generate-sponsors:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Generate Sponsors
|
- name: Generate Sponsors
|
||||||
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
|
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
|
||||||
|
|||||||
@@ -51,3 +51,6 @@ config.certify.yml
|
|||||||
|
|
||||||
# deepsec
|
# deepsec
|
||||||
/.deepsec
|
/.deepsec
|
||||||
|
|
||||||
|
# jetbrains
|
||||||
|
/.idea/
|
||||||
|
|||||||
+13
-9
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM node:26.3-alpine3.23 AS frontend-builder
|
FROM node:26.4-alpine3.23 AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ ARG VERSION
|
|||||||
ARG COMMIT_HASH
|
ARG COMMIT_HASH
|
||||||
ARG BUILD_TIMESTAMP
|
ARG BUILD_TIMESTAMP
|
||||||
ARG LDFLAGS
|
ARG LDFLAGS
|
||||||
|
ARG BUILD_TAGS
|
||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
@@ -40,27 +41,30 @@ COPY ./cmd ./cmd
|
|||||||
COPY ./internal ./internal
|
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 "${LDFLAGS} \
|
RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
|
-o tinyauth ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM alpine:3.23 AS runner
|
FROM alpine:3.24 AS runner
|
||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
COPY --from=builder /tinyauth/tinyauth ./
|
COPY --from=builder /tinyauth/tinyauth ./
|
||||||
|
|
||||||
RUN mkdir -p /data
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Make the data directory with a non-root user
|
||||||
|
RUN addgroup tinyauth && adduser -DH tinyauth -G tinyauth
|
||||||
|
RUN mkdir -p /data/resources /data/oidc /data/tailscale
|
||||||
|
RUN chown -R tinyauth:tinyauth /data
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||||
|
ENV RUNTIME_ENV=docker
|
||||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM node:26.3-alpine3.23 AS frontend-builder
|
FROM node:26.4-alpine3.23 AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ ARG VERSION
|
|||||||
ARG COMMIT_HASH
|
ARG COMMIT_HASH
|
||||||
ARG BUILD_TIMESTAMP
|
ARG BUILD_TIMESTAMP
|
||||||
ARG LDFLAGS
|
ARG LDFLAGS
|
||||||
|
ARG BUILD_TAGS
|
||||||
|
|
||||||
WORKDIR /tinyauth
|
WORKDIR /tinyauth
|
||||||
|
|
||||||
@@ -36,16 +37,20 @@ COPY go.sum ./
|
|||||||
|
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY ./cmd ./cmd
|
COPY ./cmd ./cmd/
|
||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN mkdir -p data
|
RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "${LDFLAGS} \
|
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
|
-o tinyauth ./cmd/tinyauth
|
||||||
|
|
||||||
|
# Make the data directory with a non-root user
|
||||||
|
RUN addgroup tinyauth && adduser -DH tinyauth -G tinyauth
|
||||||
|
RUN mkdir -p /data/resources /data/oidc /data/tailscale
|
||||||
|
RUN chown -R tinyauth:tinyauth /data
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||||
@@ -55,15 +60,14 @@ WORKDIR /tinyauth
|
|||||||
COPY --from=builder /tinyauth/tinyauth ./
|
COPY --from=builder /tinyauth/tinyauth ./
|
||||||
|
|
||||||
# Since it's distroless, we need to copy the data directory from the builder stage
|
# Since it's distroless, we need to copy the data directory from the builder stage
|
||||||
COPY --from=builder /tinyauth/data /data
|
COPY --from=builder /data /data
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
|
# Tell tinyauth that it's running in a container and where to find the data directory
|
||||||
|
ENV RUNTIME_ENV=docker
|
||||||
ENV TINYAUTH_RESOURCES_PATH=/data/resources
|
|
||||||
|
|
||||||
ENV PATH=$PATH:/tinyauth
|
ENV PATH=$PATH:/tinyauth
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ COMMIT_HASH := $(shell git rev-parse HEAD)
|
|||||||
BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S')
|
BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S')
|
||||||
BIN_NAME := tinyauth-$(GOARCH)
|
BIN_NAME := tinyauth-$(GOARCH)
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
# Whatever 6MB serialization lib Gin is using
|
||||||
|
GIN_BUILD_TAGS := nomsgpack
|
||||||
|
BUILD_TAGS = $(GIN_BUILD_TAGS)
|
||||||
|
|
||||||
# Development vars
|
# Development vars
|
||||||
DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.dev.yml" )
|
DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.dev.yml" )
|
||||||
@@ -16,6 +19,8 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
|||||||
|
|
||||||
.DEFAULT_GOAL := binary
|
.DEFAULT_GOAL := binary
|
||||||
|
|
||||||
|
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless
|
||||||
|
|
||||||
# Deps
|
# Deps
|
||||||
deps:
|
deps:
|
||||||
cd frontend && pnpm ci
|
cd frontend && pnpm ci
|
||||||
@@ -37,7 +42,7 @@ webui: clean-webui
|
|||||||
|
|
||||||
# Build the binary
|
# Build the binary
|
||||||
binary: webui
|
binary: webui
|
||||||
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "${LDFLAGS} \
|
CGO_ENABLED=$(CGO_ENABLED) go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${TAG_NAME} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${TAG_NAME} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
@@ -58,12 +63,10 @@ binary-linux-arm64:
|
|||||||
$(MAKE) binary
|
$(MAKE) binary
|
||||||
|
|
||||||
# Go test
|
# Go test
|
||||||
.PHONY: test
|
|
||||||
test:
|
test:
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
# Go vet
|
# Go vet
|
||||||
.PHONY: vet
|
|
||||||
vet:
|
vet:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
|
||||||
@@ -88,11 +91,27 @@ prod-infisical:
|
|||||||
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
# SQL
|
# SQL
|
||||||
.PHONY: sql
|
|
||||||
sql:
|
sql:
|
||||||
sqlc generate
|
sqlc generate
|
||||||
|
|
||||||
# Go gen
|
# Go gen
|
||||||
generate:
|
generate:
|
||||||
go run ./gen
|
go generate ./...
|
||||||
go generate ./internal/repository/...
|
|
||||||
|
# Docker image
|
||||||
|
docker:
|
||||||
|
docker buildx build -t tinyauthapp/tinyauth:dev \
|
||||||
|
--build-arg=VERSION=$(TAG_NAME) \
|
||||||
|
--build-arg=COMMIT_HASH=$(COMMIT_HASH) \
|
||||||
|
--build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \
|
||||||
|
--build-arg=BUILD_TAGS=$(BUILD_TAGS) \
|
||||||
|
-f Dockerfile .
|
||||||
|
|
||||||
|
# Docker image distroless
|
||||||
|
docker-distroless:
|
||||||
|
docker buildx build -t tinyauthapp/tinyauth:dev-distroless \
|
||||||
|
--build-arg=VERSION=$(TAG_NAME) \
|
||||||
|
--build-arg=COMMIT_HASH=$(COMMIT_HASH) \
|
||||||
|
--build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \
|
||||||
|
--build-arg=BUILD_TAGS=$(BUILD_TAGS) \
|
||||||
|
-f Dockerfile.distroless .
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
||||||
<h1>Tinyauth</h1>
|
<h1>Tinyauth</h1>
|
||||||
<p>The tiniest authentication and authorization server you have ever seen.</p>
|
<p>The tiniest OpenID Certified™ authorization and authentication server you have ever seen.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -28,6 +28,10 @@ Tinyauth is the simplest and tiniest authentication and authorization server you
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
|
||||||
|
|
||||||
|
As of 2026-06-25, Tinyauth v5.1.0 is OpenID Certified™ for Basic OP. You can find the certification details [here](https://openid.net/certification-old/certified-openid-providers-profiles/), test suite available [here](https://www.certification.openid.net/plan-detail.html?public=true&plan=H0qhpsOcQkxUE).
|
||||||
|
|
||||||
|
<img alt="OpenID Certified" width="200" src="https://openid.net/wordpress-content/uploads/2016/05/oid-l-certification-mark-l-cmyk-150dpi-90mm.jpg" />
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
||||||
@@ -58,11 +62,20 @@ If you like, you can help translate Tinyauth into more languages by visiting the
|
|||||||
|
|
||||||
Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file.
|
Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file.
|
||||||
|
|
||||||
|
|
||||||
|
## Hosting Partners
|
||||||
|
|
||||||
|
If you use one of our partners, you can help support us while getting a great hosting deal.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a title="InstaPods" target="_blank" href="https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth"><img src="https://instapods.com/deploy-button.svg"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
A big thank you to the following people for providing me with more coffee:
|
A big thank you to the following people for providing me with more coffee:
|
||||||
|
|
||||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <a href="https://github.com/apearson"><img src="https://github.com/apearson.png" width="64px" alt="User avatar: apearson" /></a> <!-- sponsors -->
|
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/axjab"><img src="https://github.com/axjab.png" width="64px" alt="User avatar: axjab" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <a href="https://github.com/apearson"><img src="https://github.com/apearson.png" width="64px" alt="User avatar: apearson" /></a> <a href="https://github.com/Micky5991"><img src="https://github.com/Micky5991.png" width="64px" alt="User avatar: Micky5991" /></a> <!-- sponsors -->
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "config",
|
||||||
|
Description: "Dump the current configuration in YAML format, useful for debugging",
|
||||||
|
Configuration: tconfig,
|
||||||
|
Resources: loaders,
|
||||||
|
Run: func(_ []string) error {
|
||||||
|
buf := strings.Builder{}
|
||||||
|
|
||||||
|
fmt.Fprint(&buf, "Your current configuration in YAML is:\n\n")
|
||||||
|
|
||||||
|
err := renderYamlToBuf(&buf, tconfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(buf.String())
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createOidcClientCmd() *cli.Command {
|
func createOidcClientCmd() *cli.Command {
|
||||||
@@ -31,40 +32,84 @@ func createOidcClientCmd() *cli.Command {
|
|||||||
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
return errors.New("client name can only contain alphanumeric characters and hyphens")
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid := uuid.New()
|
u := uuid.New()
|
||||||
clientId := uuid.String()
|
clientId := u.String()
|
||||||
clientSecret := "ta-" + utils.GenerateString(61)
|
clientSecret := "ta-" + utils.GenerateString(61)
|
||||||
|
|
||||||
uclientName := strings.ToUpper(clientName)
|
uclientName := strings.ToUpper(clientName)
|
||||||
lclientName := strings.ToLower(clientName)
|
lclientName := strings.ToLower(clientName)
|
||||||
|
|
||||||
builder := strings.Builder{}
|
buf := strings.Builder{}
|
||||||
|
|
||||||
// header
|
// header
|
||||||
fmt.Fprintf(&builder, "Created credentials for client %s\n\n", clientName)
|
fmt.Fprintf(&buf, "Created '%s' OIDC client.\n\n", clientName)
|
||||||
|
|
||||||
// credentials
|
// credentials
|
||||||
fmt.Fprintf(&builder, "Client Name: %s\n", clientName)
|
fmt.Fprintf(&buf, "Credentials:\n\n")
|
||||||
fmt.Fprintf(&builder, "Client ID: %s\n", clientId)
|
fmt.Fprintf(&buf, "Client Name: %s\n", clientName)
|
||||||
fmt.Fprintf(&builder, "Client Secret: %s\n\n", clientSecret)
|
fmt.Fprintf(&buf, "Client ID: %s\n", clientId)
|
||||||
|
fmt.Fprintf(&buf, "Client Secret: %s\n\n", clientSecret)
|
||||||
|
|
||||||
// env variables
|
// end variables
|
||||||
fmt.Fprint(&builder, "Environment variables:\n\n")
|
fmt.Fprintf(&buf, "Environment variables:\n\n")
|
||||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTID=%s\n", uclientName, clientId)
|
renderToBuf(&buf, []kv{
|
||||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET=%s\n", uclientName, clientSecret)
|
{
|
||||||
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_NAME=%s\n\n", uclientName, utils.Capitalize(lclientName))
|
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTID", uclientName),
|
||||||
|
v: clientId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET", uclientName),
|
||||||
|
v: clientSecret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_NAME", uclientName),
|
||||||
|
v: utils.Capitalize(lclientName),
|
||||||
|
},
|
||||||
|
}, "=")
|
||||||
|
fmt.Fprintf(&buf, "\n")
|
||||||
|
|
||||||
// cli flags
|
// cli flags
|
||||||
fmt.Fprint(&builder, "CLI flags:\n\n")
|
fmt.Fprintf(&buf, "CLI flags:\n\n")
|
||||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientid=%s\n", lclientName, clientId)
|
renderToBuf(&buf, []kv{
|
||||||
fmt.Fprintf(&builder, "--oidc.clients.%s.clientsecret=%s\n", lclientName, clientSecret)
|
{
|
||||||
fmt.Fprintf(&builder, "--oidc.clients.%s.name=%s\n\n", lclientName, utils.Capitalize(lclientName))
|
k: fmt.Sprintf("--oidc.clients.%s.clientid", lclientName),
|
||||||
|
v: clientId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("--oidc.clients.%s.clientsecret", lclientName),
|
||||||
|
v: clientSecret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: fmt.Sprintf("--oidc.clients.%s.name", lclientName),
|
||||||
|
v: utils.Capitalize(lclientName),
|
||||||
|
},
|
||||||
|
}, "=")
|
||||||
|
fmt.Fprintf(&buf, "\n")
|
||||||
|
|
||||||
|
// yaml config
|
||||||
|
fmt.Fprintf(&buf, "YAML config:\n\n")
|
||||||
|
|
||||||
|
err = renderYamlToBuf(&buf, &model.OIDCConfig{
|
||||||
|
Clients: map[string]model.OIDCClientConfig{
|
||||||
|
lclientName: {
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
Name: utils.Capitalize(lclientName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
// footer
|
// footer
|
||||||
fmt.Fprintln(&builder, "You can use either option to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
fmt.Fprintln(&buf, "You can use any of the above options to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
|
||||||
|
|
||||||
// print
|
// print
|
||||||
out := builder.String()
|
out := buf.String()
|
||||||
fmt.Print(out)
|
fmt.Print(out)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
+61
-15
@@ -3,11 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,62 +35,107 @@ func createUserCmd() *cli.Command {
|
|||||||
&cli.FlagLoader{},
|
&cli.FlagLoader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: "create",
|
Name: "create",
|
||||||
Description: "Create a user",
|
Description: "Create a user",
|
||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
}
|
||||||
log := logger.NewLogger().WithSimpleConfig()
|
|
||||||
log.Init()
|
|
||||||
|
|
||||||
|
cmd.Run = func(_ []string) error {
|
||||||
if tCfg.Interactive {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
|
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate(func(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return errors.New("username cannot be empty")
|
return errors.New("username cannot be empty")
|
||||||
}
|
}
|
||||||
|
if strings.Contains(s, ":") {
|
||||||
|
return errors.New("username cannot contain ':'")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})),
|
}),
|
||||||
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
|
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate(func(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return errors.New("password cannot be empty")
|
return errors.New("password cannot be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})),
|
}),
|
||||||
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
|
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
theme := new(themeBase)
|
theme := new(themeBase)
|
||||||
err := form.WithTheme(theme).Run()
|
|
||||||
|
|
||||||
|
err := form.WithTheme(theme).Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tCfg.Username == "" || tCfg.Password == "" {
|
if tCfg.Username == "" || tCfg.Password == "" {
|
||||||
|
cmd.PrintHelp(os.Stdout)
|
||||||
return errors.New("username and password cannot be empty")
|
return errors.New("username and password cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.App.Info().Str("username", tCfg.Username).Msg("Creating user")
|
if strings.Contains(tCfg.Username, ":") {
|
||||||
|
return errors.New("username cannot contain ':'")
|
||||||
|
}
|
||||||
|
|
||||||
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to hash password: %w", err)
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If docker format is enabled, escape the dollar sign
|
// Only the docker compose output needs $ escaped, the raw hash is correct everywhere else
|
||||||
passwdStr := string(passwd)
|
passwdStr := string(passwd)
|
||||||
|
outputStr := passwdStr
|
||||||
|
|
||||||
if tCfg.Docker {
|
if tCfg.Docker {
|
||||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
outputStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
|
user := fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)
|
||||||
|
escapedUser := fmt.Sprintf("%s:%s", tCfg.Username, outputStr)
|
||||||
|
|
||||||
return nil
|
buf := strings.Builder{}
|
||||||
|
|
||||||
|
// header
|
||||||
|
fmt.Fprintf(&buf, "Created user '%s'.\n\n", tCfg.Username)
|
||||||
|
|
||||||
|
// environment variable
|
||||||
|
fmt.Fprint(&buf, "Environment variable:\n\n")
|
||||||
|
renderToBuf(&buf, []kv{
|
||||||
|
{"TINYAUTH_AUTH_USERS", escapedUser},
|
||||||
|
}, "=")
|
||||||
|
|
||||||
|
// cli flags
|
||||||
|
fmt.Fprint(&buf, "\nCLI flags:\n\n")
|
||||||
|
renderToBuf(&buf, []kv{
|
||||||
|
{"--auth.users", user},
|
||||||
|
}, "=")
|
||||||
|
|
||||||
|
// yaml config
|
||||||
|
fmt.Fprint(&buf, "\nYAML config:\n\n")
|
||||||
|
|
||||||
|
err = renderYamlToBuf(&buf, &model.Config{
|
||||||
|
Auth: model.AuthConfig{
|
||||||
|
Users: []string{user},
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to render yaml config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
|
// footer
|
||||||
|
fmt.Fprint(&buf, "Use your config option of choice to add the user to Tinyauth and then restart.")
|
||||||
|
|
||||||
|
fmt.Println(buf.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/mdp/qrterminal/v3"
|
"github.com/mdp/qrterminal/v3"
|
||||||
@@ -34,14 +33,15 @@ func generateTotpCmd() *cli.Command {
|
|||||||
&cli.FlagLoader{},
|
&cli.FlagLoader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: "generate",
|
Name: "generate",
|
||||||
Description: "Generate a TOTP secret",
|
Description: "Generate a TOTP secret",
|
||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
}
|
||||||
log := logger.NewLogger().WithSimpleConfig()
|
|
||||||
log.Init()
|
cmd.Run = func(_ []string) error {
|
||||||
|
colors := getColors()
|
||||||
|
|
||||||
if tCfg.Interactive {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -63,6 +63,11 @@ func generateTotpCmd() *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tCfg.User == "" {
|
||||||
|
cmd.PrintHelp(os.Stdout)
|
||||||
|
return fmt.Errorf("user is required")
|
||||||
|
}
|
||||||
|
|
||||||
user, err := utils.ParseUser(tCfg.User)
|
user, err := utils.ParseUser(tCfg.User)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,9 +94,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
|
|
||||||
secret := key.Secret()
|
secret := key.Secret()
|
||||||
|
|
||||||
log.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
|
fmt.Printf("Scan the following QR code with your authenticator app (e.g., Google Authenticator, 2fauth, Microsoft Authenticator):\n\n")
|
||||||
|
|
||||||
log.App.Info().Msg("Generated QR code")
|
|
||||||
|
|
||||||
config := qrterminal.Config{
|
config := qrterminal.Config{
|
||||||
Level: qrterminal.L,
|
Level: qrterminal.L,
|
||||||
@@ -110,9 +113,18 @@ func generateTotpCmd() *cli.Command {
|
|||||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.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.")
|
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
|
||||||
|
|
||||||
|
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
|
||||||
|
fmt.Print(colors.green.Render(secret))
|
||||||
|
fmt.Print("\n\n")
|
||||||
|
|
||||||
|
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
|
||||||
|
fmt.Print(colors.green.Render(userStr))
|
||||||
|
fmt.Print("\n")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-16
@@ -2,18 +2,23 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
tConfig := model.NewDefaultConfiguration()
|
env := model.DetectRuntimeEnv()
|
||||||
|
tConfig := model.NewDefaultConfiguration(env)
|
||||||
|
|
||||||
loaders := []cli.ResourceLoader{
|
loaders := []cli.ResourceLoader{
|
||||||
&loaders.FileLoader{},
|
&loaders.FileLoader{},
|
||||||
@@ -27,83 +32,114 @@ func main() {
|
|||||||
Configuration: tConfig,
|
Configuration: tConfig,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
|
if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
|
||||||
|
colors := getColors()
|
||||||
|
fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
|
||||||
|
}
|
||||||
return runCmd(*tConfig)
|
return runCmd(*tConfig)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdUser := &cli.Command{
|
cmdUser := &cli.Command{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Description: "Manage Tinyauth users",
|
Description: "Manage users",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdTotp := &cli.Command{
|
cmdTotp := &cli.Command{
|
||||||
Name: "totp",
|
Name: "totp",
|
||||||
Description: "Manage Tinyauth TOTP users",
|
Description: "Manage TOTP users",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdOidc := &cli.Command{
|
cmdOidc := &cli.Command{
|
||||||
Name: "oidc",
|
Name: "oidc",
|
||||||
Description: "Manage Tinyauth OIDC clients",
|
Description: "Manage OIDC clients",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cmdTinyauth.AddCommand(versionCmd())
|
helpCmd := &cli.Command{
|
||||||
|
Name: "help",
|
||||||
|
Description: "Show the help message",
|
||||||
|
Run: func(_ []string) error {
|
||||||
|
return cmdTinyauth.PrintHelp(os.Stdout)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmdTinyauth.AddCommand(helpCmd)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add version command")
|
fatalf(err, "Failed to add help command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(versionCmd())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fatalf(err, "Failed to add version command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdTinyauth.AddCommand(configCmd(tConfig, loaders))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fatalf(err, "Failed to add config command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdUser.AddCommand(verifyUserCmd())
|
err = cmdUser.AddCommand(verifyUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add verify command")
|
fatalf(err, "Failed to add user verify command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
err = cmdTinyauth.AddCommand(healthcheckCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add healthcheck command")
|
fatalf(err, "Failed to add healthcheck command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTotp.AddCommand(generateTotpCmd())
|
err = cmdTotp.AddCommand(generateTotpCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add generate command")
|
fatalf(err, "Failed to add totp generate command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdUser.AddCommand(createUserCmd())
|
err = cmdUser.AddCommand(createUserCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
fatalf(err, "Failed to add create user command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdOidc.AddCommand(createOidcClientCmd())
|
err = cmdOidc.AddCommand(createOidcClientCmd())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add create command")
|
fatalf(err, "Failed to add create oidc client command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(cmdUser)
|
err = cmdTinyauth.AddCommand(cmdUser)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add user command")
|
fatalf(err, "Failed to add user command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(cmdTotp)
|
err = cmdTinyauth.AddCommand(cmdTotp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add totp command")
|
fatalf(err, "Failed to add totp command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmdTinyauth.AddCommand(cmdOidc)
|
err = cmdTinyauth.AddCommand(cmdOidc)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to add oidc command")
|
fatalf(err, "Failed to add oidc command")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.Execute(cmdTinyauth)
|
err = cli.Execute(cmdTinyauth)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to execute command")
|
if strings.Contains(err.Error(), "command not found") {
|
||||||
|
fmt.Println("Command not found. Use 'tinyauth help' to see available commands.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "is not runnable") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fatalf(err, "Failed to execute command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,3 +160,98 @@ type themeBase struct{}
|
|||||||
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
||||||
return huh.ThemeBase(isDark)
|
return huh.ThemeBase(isDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type colors struct {
|
||||||
|
blue lipgloss.Style
|
||||||
|
gray lipgloss.Style
|
||||||
|
lightGray lipgloss.Style
|
||||||
|
green lipgloss.Style
|
||||||
|
yellow lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColors() colors {
|
||||||
|
noColor := os.Getenv("NO_COLOR")
|
||||||
|
forceColor := os.Getenv("FORCE_COLOR")
|
||||||
|
|
||||||
|
colorOut := colors{
|
||||||
|
green: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(34)),
|
||||||
|
gray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(245)),
|
||||||
|
yellow: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(214)),
|
||||||
|
blue: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(75)),
|
||||||
|
lightGray: lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(250)),
|
||||||
|
}
|
||||||
|
|
||||||
|
noColorOut := colors{
|
||||||
|
green: lipgloss.NewStyle(),
|
||||||
|
gray: lipgloss.NewStyle(),
|
||||||
|
yellow: lipgloss.NewStyle(),
|
||||||
|
blue: lipgloss.NewStyle(),
|
||||||
|
lightGray: lipgloss.NewStyle(),
|
||||||
|
}
|
||||||
|
|
||||||
|
useColors := true
|
||||||
|
|
||||||
|
if noColor == "true" || noColor == "1" {
|
||||||
|
useColors = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceColor == "true" || forceColor == "1" {
|
||||||
|
useColors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !useColors {
|
||||||
|
return noColorOut
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorOut
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalf(err error, msg string) {
|
||||||
|
fmt.Printf("%s: %v\n", msg, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type kv struct {
|
||||||
|
k string
|
||||||
|
v string
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderToBuf(buf *strings.Builder, kv []kv, sep string) {
|
||||||
|
colors := getColors()
|
||||||
|
for _, i := range kv {
|
||||||
|
buf.WriteString(colors.blue.Render(i.k))
|
||||||
|
buf.WriteString(colors.gray.Render(sep))
|
||||||
|
buf.WriteString(colors.lightGray.Render(i.v))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderYamlToBuf(buf *strings.Builder, i any) error {
|
||||||
|
colors := getColors()
|
||||||
|
|
||||||
|
yout, err := yaml.Marshal(i)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for l := range strings.SplitSeq(string(yout), "\n") {
|
||||||
|
if l == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.TrimLeft(l, " "), "- ") {
|
||||||
|
buf.WriteString(colors.lightGray.Render(l))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lp := strings.SplitN(l, ":", 2)
|
||||||
|
buf.WriteString(colors.blue.Render(lp[0]))
|
||||||
|
buf.WriteString(colors.gray.Render(":"))
|
||||||
|
if len(lp) == 2 {
|
||||||
|
buf.WriteString(colors.lightGray.Render(lp[1]))
|
||||||
|
}
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+16
-10
@@ -3,9 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -38,14 +38,15 @@ func verifyUserCmd() *cli.Command {
|
|||||||
&cli.FlagLoader{},
|
&cli.FlagLoader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: "verify",
|
Name: "verify",
|
||||||
Description: "Verify a user is set up correctly",
|
Description: "Verify a user is set up correctly",
|
||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
}
|
||||||
log := logger.NewLogger().WithSimpleConfig()
|
|
||||||
log.Init()
|
cmd.Run = func(_ []string) error {
|
||||||
|
colors := getColors()
|
||||||
|
|
||||||
if tCfg.Interactive {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -80,6 +81,11 @@ func verifyUserCmd() *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tCfg.User == "" || tCfg.Username == "" || tCfg.Password == "" {
|
||||||
|
cmd.PrintHelp(os.Stdout)
|
||||||
|
return fmt.Errorf("user, username, and password are required")
|
||||||
|
}
|
||||||
|
|
||||||
user, err := utils.ParseUser(tCfg.User)
|
user, err := utils.ParseUser(tCfg.User)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -98,21 +104,21 @@ func verifyUserCmd() *cli.Command {
|
|||||||
|
|
||||||
if user.TOTPSecret == "" {
|
if user.TOTPSecret == "" {
|
||||||
if tCfg.Totp != "" {
|
if tCfg.Totp != "" {
|
||||||
log.App.Warn().Msg("User does not have TOTP secret")
|
fmt.Println(colors.yellow.Render("⚠") + " TOTP code provided but user does not have TOTP enabled")
|
||||||
}
|
}
|
||||||
log.App.Info().Msg("User verified")
|
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.App.Info().Msg("User verified")
|
fmt.Println(colors.green.Render("✓") + " User verified")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ 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)
|
colors := getColors()
|
||||||
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
fmt.Printf("Version: %s\n", colors.blue.Render(model.Version))
|
||||||
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
fmt.Printf("Commit Hash: %s\n", colors.blue.Render(model.CommitHash))
|
||||||
|
fmt.Printf("Build Timestamp: %s\n", colors.blue.Render(model.BuildTimestamp))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-27
@@ -12,50 +12,49 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.1.2",
|
"packageManager": "pnpm@11.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.19",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.11",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.3.2",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.11",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.3.0",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tanstack/react-query": "^5.99.0",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.18.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^26.0.4",
|
"i18next": "^26.3.4",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.23.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.6.1",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.7",
|
||||||
"react-hook-form": "^7.72.1",
|
"react-hook-form": "^7.81.0",
|
||||||
"react-i18next": "^17.0.2",
|
"react-i18next": "^17.0.8",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.14.0",
|
"react-router": "^8.1.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tanstack/eslint-plugin-query": "^5.99.0",
|
"@tanstack/eslint-plugin-query": "^5.101.2",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
"eslint": "^10.2.0",
|
"eslint": "^10.6.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
"globals": "^17.5.0",
|
"globals": "^17.7.0",
|
||||||
"prettier": "3.8.2",
|
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.1",
|
"typescript-eslint": "^8.62.1",
|
||||||
"vite": "^8.0.8"
|
"vite": "^8.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1299
-1394
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
dangerouslyAllowAllBuilds: false
|
dangerouslyAllowAllBuilds: false
|
||||||
blockExoticSubdeps: true
|
blockExoticSubdeps: true
|
||||||
minimumReleaseAge: 1440 # 1 day
|
minimumReleaseAge: 1440 # 1 day
|
||||||
trustPolicy: no-downgrade
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function LocalAuthIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h5m3.5 3.5L15 22l-1.5-1.5m5.054-2.086a2 2 0 1 1 2.828-2.828a2 2 0 0 1-2.828 2.828M16 19l1 1"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Outlet } from "react-router";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { DomainWarning } from "../domain-warning/domain-warning";
|
import { DomainWarning } from "../domain-warning/domain-warning";
|
||||||
import { QuickActions } from "../quick-actions/quick-actions";
|
import { QuickActions } from "../quick-actions/quick-actions";
|
||||||
|
import { isTrustedDomain } from "@/lib/hooks/redirect-uri";
|
||||||
|
|
||||||
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { ui } = useAppContext();
|
const { ui } = useAppContext();
|
||||||
@@ -40,11 +41,18 @@ export const Layout = () => {
|
|||||||
setIgnoreDomainWarning(true);
|
setIgnoreDomainWarning(true);
|
||||||
}, [setIgnoreDomainWarning]);
|
}, [setIgnoreDomainWarning]);
|
||||||
|
|
||||||
if (
|
const isTrusted = (() => {
|
||||||
!ignoreDomainWarning &&
|
try {
|
||||||
ui.warningsEnabled &&
|
const appUrlObj = new URL(app.appUrl);
|
||||||
!app.trustedDomains.includes(currentUrl)
|
const currentUrlObj = new URL(currentUrl);
|
||||||
) {
|
|
||||||
|
return isTrustedDomain(currentUrlObj, appUrlObj, "", false);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!ignoreDomainWarning && ui.warningsEnabled && !isTrusted) {
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<DomainWarning
|
<DomainWarning
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
Palette,
|
Palette,
|
||||||
Settings,
|
Settings,
|
||||||
Sun,
|
Sun,
|
||||||
|
UserRoundKey,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
@@ -37,20 +39,26 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { GoogleIcon } from "../icons/google";
|
||||||
|
import { GithubIcon } from "../icons/github";
|
||||||
|
import { TailscaleIcon } from "../icons/tailscale";
|
||||||
|
import { MicrosoftIcon } from "../icons/microsoft";
|
||||||
|
import { PocketIDIcon } from "../icons/pocket-id";
|
||||||
|
import { OAuthIcon } from "../icons/oauth";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
|
||||||
function Avatar({ initial }: { initial: string }) {
|
const iconStyles = "size-4";
|
||||||
return (
|
|
||||||
<span className="group relative grid size-10 place-items-center rounded-full">
|
const iconMap: Record<string, React.ReactNode> = {
|
||||||
<span className="absolute inset-0 overflow-hidden rounded-full bg-linear-to-b from-neutral-50 to-neutral-100 dark:from-neutral-700 dark:to-neutral-950 shadow-lg"></span>
|
google: <GoogleIcon className={iconStyles} />,
|
||||||
<span className="relative text-sm font-semibold text-primary">
|
github: <GithubIcon className={iconStyles} />,
|
||||||
{initial}
|
tailscale: <TailscaleIcon className={iconStyles} />,
|
||||||
</span>
|
microsoft: <MicrosoftIcon className={iconStyles} />,
|
||||||
</span>
|
pocketid: <PocketIDIcon className={iconStyles} />,
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const QuickActions = () => {
|
export const QuickActions = () => {
|
||||||
const { auth } = useUserContext();
|
const { auth, oauth, tailscale } = useUserContext();
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
@@ -64,6 +72,49 @@ export const QuickActions = () => {
|
|||||||
const screenParams = useScreenParams(searchParams);
|
const screenParams = useScreenParams(searchParams);
|
||||||
const compiledParams = recompileScreenParams(screenParams);
|
const compiledParams = recompileScreenParams(screenParams);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const providerDetails = (():
|
||||||
|
| { name: string; icon: React.ReactNode }
|
||||||
|
| undefined => {
|
||||||
|
if (!auth.authenticated) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.providerId === "local" || auth.providerId === "ldap") {
|
||||||
|
return {
|
||||||
|
name: t(
|
||||||
|
auth.providerId === "ldap"
|
||||||
|
? "quickActionsProviderLDAP"
|
||||||
|
: "quickActionsProviderLocal",
|
||||||
|
),
|
||||||
|
icon: (
|
||||||
|
<UserRoundKey
|
||||||
|
strokeWidth={1.5}
|
||||||
|
size={16}
|
||||||
|
className="text-muted-foreground ml-0.5"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oauth.active) {
|
||||||
|
return {
|
||||||
|
name: t("quickActionsProviderOAuth", { provider: oauth.displayName }),
|
||||||
|
icon: iconMap[auth.providerId] || <OAuthIcon className={iconStyles} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.providerId === "tailscale") {
|
||||||
|
return {
|
||||||
|
name: `Tailscale (${tailscale.nodeName})`,
|
||||||
|
icon: <TailscaleIcon className={iconStyles} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
|
|
||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
mutationFn: () => axios.post("/api/user/logout"),
|
mutationFn: () => axios.post("/api/user/logout"),
|
||||||
mutationKey: ["logout"],
|
mutationKey: ["logout"],
|
||||||
@@ -107,17 +158,29 @@ export const QuickActions = () => {
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
aria-label={t("quickActionsTitle")}
|
aria-label={t("quickActionsTitle")}
|
||||||
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
|
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
|
||||||
>
|
>
|
||||||
{auth.authenticated ? (
|
{auth.authenticated ? (
|
||||||
<Avatar initial={initial!} />
|
<div className="size-10 flex justify-center items-center p-2 rounded-full bg-card border border-border">
|
||||||
|
{isOpen ? (
|
||||||
|
<X className="size-4 text-primary rotate-0 transition-transform duration-200 starting:rotate-45" />
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-primary rotate-0 transition-transform duration-200 starting:-rotate-45">
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
|
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
|
||||||
<Settings className="size-4" />
|
<Settings
|
||||||
|
className={`size-4 transition-transform duration-200 ${
|
||||||
|
isOpen ? "rotate-45" : "rotate-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -126,19 +189,22 @@ export const QuickActions = () => {
|
|||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
className="rounded-xl p-1"
|
className="rounded-xl p-1 w-3xs"
|
||||||
>
|
>
|
||||||
{auth.authenticated && (
|
{auth.authenticated && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="flex items-center gap-3 p-2">
|
<DropdownMenuLabel className="flex items-center gap-3 p-2">
|
||||||
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium">
|
<Tooltip>
|
||||||
{initial}
|
<TooltipTrigger className="size-9 rounded-full p-2 bg-muted border-border border flex items-center justify-center">
|
||||||
</div>
|
{providerDetails!.icon}
|
||||||
<div className="flex min-w-0 flex-col">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{providerDetails!.name}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<span className="truncate text-sm font-medium">
|
<span className="truncate text-sm font-medium">
|
||||||
{auth.name}
|
{auth.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground truncate text-xs font-normal">
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
{auth.email}
|
{auth.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +263,7 @@ export const QuickActions = () => {
|
|||||||
onSelect={() => logoutMutation.mutate()}
|
onSelect={() => logoutMutation.mutate()}
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
>
|
>
|
||||||
<DoorOpenIcon className="size-4" />
|
<DoorOpenIcon className="size-4 text-destructive" />
|
||||||
{t("quickActionsLogout")}
|
{t("quickActionsLogout")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -9,12 +9,27 @@ type IuseRedirectUri = {
|
|||||||
export const useRedirectUri = (
|
export const useRedirectUri = (
|
||||||
redirect_uri: string | undefined,
|
redirect_uri: string | undefined,
|
||||||
cookieDomain: string,
|
cookieDomain: string,
|
||||||
|
appUrl: string,
|
||||||
|
subdomainsEnabled: boolean,
|
||||||
): IuseRedirectUri => {
|
): IuseRedirectUri => {
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
let isTrusted = false;
|
let isTrusted = false;
|
||||||
let isAllowedProto = false;
|
let isAllowedProto = false;
|
||||||
let isHttpsDowngrade = false;
|
let isHttpsDowngrade = false;
|
||||||
|
|
||||||
|
let appUrlObj: URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
appUrlObj = new URL(appUrl);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
valid: isValid,
|
||||||
|
trusted: isTrusted,
|
||||||
|
allowedProto: isAllowedProto,
|
||||||
|
httpsDowngrade: isHttpsDowngrade,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!redirect_uri) {
|
if (!redirect_uri) {
|
||||||
return {
|
return {
|
||||||
valid: isValid,
|
valid: isValid,
|
||||||
@@ -39,10 +54,7 @@ export const useRedirectUri = (
|
|||||||
|
|
||||||
isValid = true;
|
isValid = true;
|
||||||
|
|
||||||
if (
|
if (isTrustedDomain(url, appUrlObj, cookieDomain, subdomainsEnabled)) {
|
||||||
url.hostname == cookieDomain ||
|
|
||||||
url.hostname.endsWith(`.${cookieDomain}`)
|
|
||||||
) {
|
|
||||||
isTrusted = true;
|
isTrusted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,3 +74,45 @@ export const useRedirectUri = (
|
|||||||
httpsDowngrade: isHttpsDowngrade,
|
httpsDowngrade: isHttpsDowngrade,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ported from internal/controller/oauth_controller.go
|
||||||
|
const getEffectivePort = (url: URL): string => {
|
||||||
|
if (url.port) {
|
||||||
|
return url.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol == "https:") {
|
||||||
|
return "443";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "80";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isTrustedDomain = (
|
||||||
|
url: URL,
|
||||||
|
appUrl: URL,
|
||||||
|
cookieDomain: string,
|
||||||
|
subdomainsEnabled: boolean,
|
||||||
|
): boolean => {
|
||||||
|
if (url.protocol != appUrl.protocol) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getEffectivePort(url) != getEffectivePort(appUrl)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.hostname == appUrl.hostname) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subdomainsEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.hostname.endsWith("." + cookieDomain.toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type ScreenParams = {
|
|||||||
oidc_ticket?: string;
|
oidc_ticket?: string;
|
||||||
oidc_scope?: string;
|
oidc_scope?: string;
|
||||||
oidc_name?: string;
|
oidc_name?: string;
|
||||||
|
oidc_prompt?: "none" | "login";
|
||||||
};
|
};
|
||||||
|
|
||||||
const zodScreenParams = z.object({
|
const zodScreenParams = z.object({
|
||||||
@@ -14,6 +15,7 @@ const zodScreenParams = z.object({
|
|||||||
oidc_ticket: z.string().optional(),
|
oidc_ticket: z.string().optional(),
|
||||||
oidc_scope: z.string().optional(),
|
oidc_scope: z.string().optional(),
|
||||||
oidc_name: z.string().optional(),
|
oidc_name: z.string().optional(),
|
||||||
|
oidc_prompt: z.enum(["none", "login"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useScreenParams(params: URLSearchParams): ScreenParams {
|
export function useScreenParams(params: URLSearchParams): ScreenParams {
|
||||||
|
|||||||
@@ -99,5 +99,8 @@
|
|||||||
"quickActionsThemeDark": "Dark",
|
"quickActionsThemeDark": "Dark",
|
||||||
"quickActionsThemeSystem": "System",
|
"quickActionsThemeSystem": "System",
|
||||||
"quickActionsLogout": "Logout",
|
"quickActionsLogout": "Logout",
|
||||||
"quickActionsTitle": "Quick Actions"
|
"quickActionsTitle": "Quick Actions",
|
||||||
|
"quickActionsProviderLocal": "Local",
|
||||||
|
"quickActionsProviderLDAP": "LDAP",
|
||||||
|
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,5 +99,8 @@
|
|||||||
"quickActionsThemeDark": "Dark",
|
"quickActionsThemeDark": "Dark",
|
||||||
"quickActionsThemeSystem": "System",
|
"quickActionsThemeSystem": "System",
|
||||||
"quickActionsLogout": "Logout",
|
"quickActionsLogout": "Logout",
|
||||||
"quickActionsTitle": "Quick Actions"
|
"quickActionsTitle": "Quick Actions",
|
||||||
|
"quickActionsProviderLocal": "Local",
|
||||||
|
"quickActionsProviderLDAP": "LDAP",
|
||||||
|
"quickActionsProviderOAuth": "{{provider}} OAuth"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
recompileScreenParams,
|
recompileScreenParams,
|
||||||
useScreenParams,
|
useScreenParams,
|
||||||
} from "@/lib/hooks/screen-params";
|
} from "@/lib/hooks/screen-params";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
type Scope = {
|
type Scope = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -90,7 +91,15 @@ export const AuthorizePage = () => {
|
|||||||
const isOidc = screenParams.login_for === "oidc";
|
const isOidc = screenParams.login_for === "oidc";
|
||||||
const compiledParams = recompileScreenParams(screenParams);
|
const compiledParams = recompileScreenParams(screenParams);
|
||||||
|
|
||||||
const authorizeMutation = useMutation({
|
// TODO: maybe a better way to do this
|
||||||
|
const shouldAutoAuthorize =
|
||||||
|
auth.authenticated &&
|
||||||
|
isOidc &&
|
||||||
|
screenParams.oidc_ticket !== undefined &&
|
||||||
|
screenParams.oidc_scope !== undefined &&
|
||||||
|
screenParams.oidc_prompt === "none";
|
||||||
|
|
||||||
|
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
return axios.post("/api/oidc/authorize-complete", {
|
return axios.post("/api/oidc/authorize-complete", {
|
||||||
ticket: screenParams.oidc_ticket,
|
ticket: screenParams.oidc_ticket,
|
||||||
@@ -110,6 +119,12 @@ export const AuthorizePage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldAutoAuthorize) {
|
||||||
|
authorizeMutate();
|
||||||
|
}
|
||||||
|
}, [shouldAutoAuthorize, authorizeMutate]);
|
||||||
|
|
||||||
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
|
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
@@ -119,7 +134,7 @@ export const AuthorizePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth.authenticated) {
|
if (!auth.authenticated || screenParams.oidc_prompt === "login") {
|
||||||
return <Navigate to={`/login${compiledParams}`} replace />;
|
return <Navigate to={`/login${compiledParams}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +183,15 @@ export const AuthorizePage = () => {
|
|||||||
)}
|
)}
|
||||||
<CardFooter className="flex flex-col items-stretch gap-3">
|
<CardFooter className="flex flex-col items-stretch gap-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => authorizeMutation.mutate()}
|
onClick={() => authorizeMutate()}
|
||||||
loading={authorizeMutation.isPending}
|
loading={authorizePending}
|
||||||
|
disabled={shouldAutoAuthorize}
|
||||||
>
|
>
|
||||||
{t("authorizeTitle")}
|
{t("authorizeTitle")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/logout${compiledParams}`)}
|
onClick={() => navigate(`/logout${compiledParams}`)}
|
||||||
disabled={authorizeMutation.isPending}
|
disabled={authorizePending || shouldAutoAuthorize}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{t("cancelTitle")}
|
{t("cancelTitle")}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const ContinuePage = () => {
|
|||||||
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
|
||||||
redirectUri,
|
redirectUri,
|
||||||
app.cookieDomain,
|
app.cookieDomain,
|
||||||
|
app.appUrl,
|
||||||
|
app.subdomainsEnabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
const urlHref = url?.href;
|
const urlHref = url?.href;
|
||||||
@@ -108,7 +110,11 @@ export const ContinuePage = () => {
|
|||||||
components={{
|
components={{
|
||||||
code: <code />,
|
code: <code />,
|
||||||
}}
|
}}
|
||||||
values={{ cookieDomain: app.cookieDomain }}
|
values={{
|
||||||
|
cookieDomain: app.subdomainsEnabled
|
||||||
|
? `.${app.cookieDomain}`
|
||||||
|
: app.cookieDomain,
|
||||||
|
}}
|
||||||
shouldUnescape={true}
|
shouldUnescape={true}
|
||||||
/>
|
/>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ export const LoginPage = () => {
|
|||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const screenParams = useScreenParams(searchParams);
|
const screenParams = useScreenParams(searchParams);
|
||||||
const compiledParams = recompileScreenParams(screenParams);
|
const compiledParams = recompileScreenParams({
|
||||||
|
...screenParams,
|
||||||
|
oidc_prompt: undefined,
|
||||||
|
});
|
||||||
const loginForUrl = useLoginFor({
|
const loginForUrl = useLoginFor({
|
||||||
login_for: screenParams.login_for,
|
login_for: screenParams.login_for,
|
||||||
compiledParams,
|
compiledParams,
|
||||||
@@ -196,7 +199,7 @@ export const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
}, [redirectTimer, redirectButtonTimer]);
|
}, [redirectTimer, redirectButtonTimer]);
|
||||||
|
|
||||||
if (auth.authenticated) {
|
if (auth.authenticated && screenParams.oidc_prompt !== "login") {
|
||||||
return <Navigate to={loginForUrl} replace />;
|
return <Navigate to={loginForUrl} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full text-destructive"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
loading={logoutMutation.isPending}
|
loading={logoutMutation.isPending}
|
||||||
onClick={() => logoutMutation.mutate()}
|
onClick={() => logoutMutation.mutate()}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const uiSchema = z.object({
|
|||||||
const appSchema = z.object({
|
const appSchema = z.object({
|
||||||
appUrl: z.string(),
|
appUrl: z.string(),
|
||||||
cookieDomain: z.string(),
|
cookieDomain: z.string(),
|
||||||
trustedDomains: z.array(z.string()),
|
subdomainsEnabled: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const appContextSchema = z.object({
|
export const appContextSchema = z.object({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type EnvEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateExampleEnv() {
|
func generateExampleEnv() {
|
||||||
cfg := model.NewDefaultConfiguration()
|
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||||
entries := make([]EnvEntry, 0)
|
entries := make([]EnvEntry, 0)
|
||||||
|
|
||||||
root := reflect.TypeOf(cfg).Elem()
|
root := reflect.TypeOf(cfg).Elem()
|
||||||
@@ -21,7 +21,7 @@ type MarkdownEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateMarkdown() {
|
func generateMarkdown() {
|
||||||
cfg := model.NewDefaultConfiguration()
|
cfg := model.NewDefaultConfiguration(model.RuntimeEnvUnknown)
|
||||||
entries := make([]MarkdownEntry, 0)
|
entries := make([]MarkdownEntry, 0)
|
||||||
|
|
||||||
root := reflect.TypeOf(cfg).Elem()
|
root := reflect.TypeOf(cfg).Elem()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under
|
// gen/sqlc_wrapper generates store.go wrapper files for each sqlc driver package under
|
||||||
// internal/repository/<driver>/. Run via:
|
// internal/repository/<driver>/. Run via:
|
||||||
//
|
//
|
||||||
// go generate ./internal/repository/...
|
// go generate ./internal/repository/...
|
||||||
@@ -32,7 +32,7 @@ import (
|
|||||||
var storeSrc string
|
var storeSrc string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("sqlc-wrapper: generating store.go files for sqlc driver packages...")
|
fmt.Println("sqlc_wrapper: generating store.go files for sqlc driver packages...")
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package tinyauth
|
||||||
|
|
||||||
|
//go:generate go run github.com/tinyauthapp/tinyauth/gen/docs
|
||||||
@@ -4,6 +4,7 @@ go 1.26.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/huh/v2 v2.0.3
|
charm.land/huh/v2 v2.0.3
|
||||||
|
charm.land/lipgloss/v2 v2.0.5
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
@@ -21,29 +22,26 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
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.52.0
|
go.uber.org/dig v1.19.0
|
||||||
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/tools v0.45.0
|
golang.org/x/tools v0.47.0
|
||||||
k8s.io/apimachinery v0.36.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/client-go v0.36.1
|
k8s.io/apimachinery v0.36.2
|
||||||
modernc.org/sqlite v1.51.0
|
k8s.io/client-go v0.36.2
|
||||||
tailscale.com v1.100.0
|
modernc.org/sqlite v1.53.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/boombuler/barcode v1.0.2 // indirect
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
@@ -51,9 +49,9 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/catppuccin/go v0.3.0 // indirect
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
@@ -62,12 +60,10 @@ require (
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/coder/websocket v1.8.12 // indirect
|
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/creachadair/msync v0.7.1 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
@@ -75,46 +71,35 @@ require (
|
|||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gaissmai/bart v0.26.1 // 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
|
||||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
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/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
|
||||||
github.com/google/btree v1.1.3 // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/huin/goupnp v1.3.0 // indirect
|
|
||||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
|
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.18.5 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
|
||||||
github.com/mdlayher/socket v0.5.0 // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
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.3-0.20250322232337-35a7c28c31ee // indirect
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
@@ -123,22 +108,16 @@ require (
|
|||||||
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.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/safchain/ethtool v0.3.0 // indirect
|
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
|
||||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // 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/x448/float16 v0.8.4 // indirect
|
||||||
@@ -153,28 +132,22 @@ 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
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // 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/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/mod v0.36.0 // indirect
|
golang.org/x/mod v0.37.0 // indirect
|
||||||
golang.org/x/net v0.55.0 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.45.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/term v0.43.0 // indirect
|
golang.org/x/term v0.44.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
|
||||||
k8s.io/klog/v2 v2.140.0 // indirect
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||||
modernc.org/libc v1.72.3 // indirect
|
modernc.org/libc v1.73.4 // 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
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
|
||||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
|
||||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
|
||||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
|
||||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
|
||||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||||
@@ -30,44 +24,10 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
|
|||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
|
||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
|
||||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
|
||||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
|
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
@@ -85,12 +45,12 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
|||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
@@ -109,48 +69,28 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2
|
|||||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
|
||||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
|
||||||
github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=
|
|
||||||
github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=
|
|
||||||
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
|
|
||||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
|
||||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
|
||||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
|
||||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
|
|
||||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
|
||||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
|
||||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
@@ -169,20 +109,14 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
|||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
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/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
|
||||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
|
||||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
|
|
||||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -190,8 +124,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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
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/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||||
@@ -206,24 +138,14 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
|
|
||||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
|
|
||||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
|
||||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
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/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
|
||||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
|
||||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
|
||||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
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=
|
||||||
@@ -231,11 +153,7 @@ 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/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
|
|
||||||
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
|
||||||
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/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
|
||||||
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=
|
||||||
@@ -244,19 +162,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
|
||||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
|
||||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
|
||||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
|
||||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
||||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -279,24 +188,12 @@ 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/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
|
||||||
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
|
||||||
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/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
|
||||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
|
||||||
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/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -305,34 +202,22 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
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.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
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=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
|
||||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
|
||||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
|
||||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
|
||||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
|
||||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
@@ -359,31 +244,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
|
||||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
|
||||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
|
||||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
|
||||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||||
@@ -396,8 +269,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
|
||||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
@@ -421,42 +292,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
|
||||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
|
||||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
|
||||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
|
||||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
|
|
||||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
|
||||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
|
|
||||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
|
||||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
|
||||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
|
||||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
|
||||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
|
|
||||||
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
|
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
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/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
|
||||||
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
@@ -485,51 +326,39 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
|
|||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||||
|
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
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=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
|
||||||
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
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/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
|
||||||
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.21.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-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
|
||||||
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=
|
||||||
@@ -551,38 +380,32 @@ 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=
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=
|
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
|
||||||
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
|
||||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
|
||||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
|
||||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
|
||||||
k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
|
|
||||||
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
|
|
||||||
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
|
|
||||||
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
|
|
||||||
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
|
|
||||||
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
|
|
||||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -591,8 +414,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
|||||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
|
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||||
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
@@ -607,7 +430,3 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80
|
|||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
|
||||||
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
|
|
||||||
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
@@ -31,10 +33,23 @@ import (
|
|||||||
// 2. HTTP server listeners - ding.RingNormal
|
// 2. HTTP server listeners - ding.RingNormal
|
||||||
// 3. Networking layers, user and label providers (e.g. ailscale service, kubernetes service) - ding.RingMajor
|
// 3. Networking layers, user and label providers (e.g. ailscale service, kubernetes service) - ding.RingMajor
|
||||||
// 4. Database connection - ding.RingCritical
|
// 4. Database connection - ding.RingCritical
|
||||||
|
|
||||||
|
type Services struct {
|
||||||
|
accessControlService *service.AccessControlsService
|
||||||
|
authService *service.AuthService
|
||||||
|
dockerService *service.DockerService
|
||||||
|
kubernetesService *service.KubernetesService
|
||||||
|
ldapService *service.LdapService
|
||||||
|
oauthBrokerService *service.OAuthBrokerService
|
||||||
|
oidcService *service.OIDCService
|
||||||
|
tailscaleService *service.TailscaleService
|
||||||
|
policyEngine *service.PolicyEngine
|
||||||
|
}
|
||||||
|
|
||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config model.Config
|
config model.Config
|
||||||
runtime model.RuntimeConfig
|
runtime model.RuntimeConfig
|
||||||
services service.Services
|
services Services
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -42,10 +57,7 @@ type BootstrapApp struct {
|
|||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
ding *ding.Ding
|
ding *ding.Ding
|
||||||
listeners []Listener
|
dig *dig.Container
|
||||||
deps struct {
|
|
||||||
service *service.ServiceDependencies
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
||||||
@@ -60,7 +72,11 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.ctx = ctx
|
app.ctx = ctx
|
||||||
app.cancel = cancel
|
app.cancel = cancel
|
||||||
|
|
||||||
// Create a ding instance
|
// create the dig container
|
||||||
|
c := dig.New()
|
||||||
|
app.dig = c
|
||||||
|
|
||||||
|
// create a ding instance
|
||||||
dg := ding.New(ctx)
|
dg := ding.New(ctx)
|
||||||
app.ding = dg
|
app.ding = dg
|
||||||
|
|
||||||
@@ -82,8 +98,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return fmt.Errorf("failed to parse app url: %w", err)
|
return fmt.Errorf("failed to parse app url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
|
app.runtime.AppURL = strings.ToLower(appUrl.Scheme + "://" + appUrl.Host)
|
||||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, app.runtime.AppURL)
|
|
||||||
|
|
||||||
// validate session config
|
// validate session config
|
||||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||||
@@ -117,6 +132,10 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.runtime.OAuthProviders = app.config.OAuth.Providers
|
app.runtime.OAuthProviders = app.config.OAuth.Providers
|
||||||
|
|
||||||
for id, provider := range app.runtime.OAuthProviders {
|
for id, provider := range app.runtime.OAuthProviders {
|
||||||
|
if slices.Contains(model.ReservedProviderNames, id) {
|
||||||
|
return fmt.Errorf("provider id %s is reserved and cannot be used", id)
|
||||||
|
}
|
||||||
|
|
||||||
providerWhitelist, err := utils.GetStringList(provider.Whitelist, provider.WhitelistFile)
|
providerWhitelist, err := utils.GetStringList(provider.Whitelist, provider.WhitelistFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err)
|
return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err)
|
||||||
@@ -128,15 +147,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
provider.ClientSecret = secret
|
provider.ClientSecret = secret
|
||||||
provider.ClientSecretFile = ""
|
provider.ClientSecretFile = ""
|
||||||
|
|
||||||
if provider.RedirectURL == "" {
|
|
||||||
provider.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + id
|
|
||||||
}
|
|
||||||
|
|
||||||
app.runtime.OAuthProviders[id] = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// set presets for built-in providers
|
|
||||||
for id, provider := range app.runtime.OAuthProviders {
|
|
||||||
if provider.Name == "" {
|
if provider.Name == "" {
|
||||||
if name, ok := model.OverrideProviders[id]; ok {
|
if name, ok := model.OverrideProviders[id]; ok {
|
||||||
provider.Name = name
|
provider.Name = name
|
||||||
@@ -144,24 +154,16 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
provider.Name = utils.Capitalize(id)
|
provider.Name = utils.Capitalize(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.runtime.OAuthProviders[id] = provider
|
app.runtime.OAuthProviders[id] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup oidc clients
|
|
||||||
for id, client := range app.config.OIDC.Clients {
|
|
||||||
client.ID = id
|
|
||||||
app.runtime.OIDCClients = append(app.runtime.OIDCClients, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cookie domain
|
// cookie domain
|
||||||
cookieDomainResolver := utils.GetCookieDomain
|
|
||||||
|
|
||||||
if !app.config.Auth.SubdomainsEnabled {
|
if !app.config.Auth.SubdomainsEnabled {
|
||||||
app.log.App.Warn().Msg("Subdomains are disabled, using standalone cookie domain resolver which will not work with subdomains")
|
app.log.App.Warn().Msg("Subdomains are disabled, cookies will be set for the current domain only")
|
||||||
cookieDomainResolver = utils.GetStandaloneCookieDomain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieDomain, err := cookieDomainResolver(app.runtime.AppURL)
|
cookieDomain, err := utils.GetCookieDomain(app.runtime.AppURL, app.config.Auth.SubdomainsEnabled)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get cookie domain: %w", err)
|
return fmt.Errorf("failed to get cookie domain: %w", err)
|
||||||
@@ -201,6 +203,33 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
// store
|
// store
|
||||||
app.queries = store
|
app.queries = store
|
||||||
|
|
||||||
|
// provide basic utilities to container
|
||||||
|
type utilityProvider struct {
|
||||||
|
dig.Out
|
||||||
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
Runtime *model.RuntimeConfig
|
||||||
|
Ding *ding.Ding
|
||||||
|
Ctx context.Context
|
||||||
|
Queries repository.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.dig.Provide(func() utilityProvider {
|
||||||
|
return utilityProvider{
|
||||||
|
Log: app.log,
|
||||||
|
Config: &app.config,
|
||||||
|
Runtime: &app.runtime,
|
||||||
|
Ding: app.ding,
|
||||||
|
Ctx: app.ctx,
|
||||||
|
Queries: app.queries,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to provide utilities to container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// services
|
// services
|
||||||
err = app.setupServices()
|
err = app.setupServices()
|
||||||
|
|
||||||
@@ -223,7 +252,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
return configuredProviders[i].Name < configuredProviders[j].Name
|
return configuredProviders[i].Name < configuredProviders[j].Name
|
||||||
})
|
})
|
||||||
|
|
||||||
if app.services.AuthService.LocalAuthConfigured() {
|
if app.services.authService.LocalAuthConfigured() {
|
||||||
configuredProviders = append(configuredProviders, model.Provider{
|
configuredProviders = append(configuredProviders, model.Provider{
|
||||||
Name: "Local",
|
Name: "Local",
|
||||||
ID: "local",
|
ID: "local",
|
||||||
@@ -231,7 +260,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.services.AuthService.LDAPAuthConfigured() {
|
if app.services.authService.LDAPAuthConfigured() {
|
||||||
configuredProviders = append(configuredProviders, model.Provider{
|
configuredProviders = append(configuredProviders, model.Provider{
|
||||||
Name: "LDAP",
|
Name: "LDAP",
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
@@ -249,9 +278,22 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
app.runtime.ConfiguredProviders = configuredProviders
|
app.runtime.ConfiguredProviders = configuredProviders
|
||||||
|
|
||||||
// throw in tailscale if it's configured just before setting up the controllers
|
// force an update of the redirect urls for all oauth providers, if they are empty
|
||||||
if app.services.TailscaleService != nil {
|
services := app.services.oauthBrokerService.GetConfiguredServices()
|
||||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.TailscaleService.GetHostname())
|
|
||||||
|
for _, service := range services {
|
||||||
|
oauthService, ok := app.services.oauthBrokerService.GetService(service)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("failed to get oauth service for provider %s", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
providerConfig := oauthService.GetConfig()
|
||||||
|
|
||||||
|
if providerConfig.RedirectURL == "" {
|
||||||
|
providerConfig.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + service
|
||||||
|
oauthService.UpdateConfig(providerConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup router
|
// setup router
|
||||||
@@ -271,20 +313,20 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.ding.Go(app.heartbeatRoutine, ding.RingMinor)
|
app.ding.Go(app.heartbeatRoutine, ding.RingMinor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup listeners
|
// get listener
|
||||||
app.listeners = app.calculateListenerPolicy()
|
listenerFunc, err := app.getListenerFunc()
|
||||||
|
|
||||||
if app.config.Server.ConcurrentListenersEnabled {
|
|
||||||
app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
|
|
||||||
}
|
|
||||||
|
|
||||||
// run listeners
|
|
||||||
lec, err := app.runListeners()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run listeners: %w", err)
|
return fmt.Errorf("failed to get listener function: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run listener
|
||||||
|
lec := make(chan error, 1)
|
||||||
|
|
||||||
|
app.ding.Go(func(ctx context.Context) {
|
||||||
|
lec <- listenerFunc(ctx)
|
||||||
|
}, ding.RingNormal)
|
||||||
|
|
||||||
// monitor cancellation and server errors
|
// monitor cancellation and server errors
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -9,22 +9,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/ding"
|
|
||||||
"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/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ListenerHTTP Listener = iota
|
|
||||||
ListenerUnix
|
|
||||||
ListenerTailscale
|
|
||||||
)
|
|
||||||
|
|
||||||
func (app *BootstrapApp) setupRouter() error {
|
func (app *BootstrapApp) setupRouter() error {
|
||||||
// we don't want gin debug mode
|
// we don't want gin debug mode
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -40,109 +32,114 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.AuthService, app.services.OAuthBrokerService, app.services.TailscaleService)
|
middlewareProvideFor := []any{
|
||||||
engine.Use(contextMiddleware.Middleware())
|
middleware.NewContextMiddleware,
|
||||||
|
middleware.NewUIMiddleware,
|
||||||
uiMiddleware, err := middleware.NewUIMiddleware()
|
middleware.NewZerologMiddleware,
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize UI middleware: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.Use(uiMiddleware.Middleware())
|
for _, provider := range middlewareProvideFor {
|
||||||
|
err := app.dig.Provide(provider)
|
||||||
|
|
||||||
zerologMiddleware := middleware.NewZerologMiddleware(app.log)
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to provide middleware: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
engine.Use(zerologMiddleware.Middleware())
|
type middlewareInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
apiRouter := engine.Group("/api")
|
ContextMiddleware *middleware.ContextMiddleware
|
||||||
|
UIMiddleware *middleware.UIMiddleware
|
||||||
|
ZerologMiddleware *middleware.ZerologMiddleware
|
||||||
|
}
|
||||||
|
|
||||||
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
|
err := app.dig.Invoke(func(mi middlewareInput) {
|
||||||
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.AuthService)
|
engine.Use(mi.ContextMiddleware.Middleware())
|
||||||
controller.NewOIDCController(app.log, app.services.OIDCService, app.runtime, apiRouter, &engine.RouterGroup)
|
engine.Use(mi.UIMiddleware.Middleware())
|
||||||
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.AccessControlService, app.services.AuthService, app.services.PolicyEngine)
|
engine.Use(mi.ZerologMiddleware.Middleware())
|
||||||
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.AuthService)
|
})
|
||||||
controller.NewResourcesController(app.config, &engine.RouterGroup)
|
|
||||||
controller.NewHealthController(apiRouter)
|
if err != nil {
|
||||||
controller.NewWellKnownController(app.services.OIDCService, &engine.RouterGroup)
|
return fmt.Errorf("failed to invoke middleware: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.dig.Provide(func() *gin.RouterGroup {
|
||||||
|
return &engine.RouterGroup
|
||||||
|
}, dig.Name("mainRouterGroup"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to provide main router group: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.dig.Provide(func() *gin.RouterGroup {
|
||||||
|
return engine.Group("/api")
|
||||||
|
}, dig.Name("apiRouterGroup"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to provide api router group: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerProvideFor := []any{
|
||||||
|
controller.NewContextController,
|
||||||
|
controller.NewOAuthController,
|
||||||
|
controller.NewOIDCController,
|
||||||
|
controller.NewProxyController,
|
||||||
|
controller.NewUserController,
|
||||||
|
controller.NewResourcesController,
|
||||||
|
controller.NewHealthController,
|
||||||
|
controller.NewWellKnownController,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, provider := range controllerProvideFor {
|
||||||
|
err := app.dig.Provide(provider)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to provide controller: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type controllerInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
|
ContextController *controller.ContextController
|
||||||
|
OAuthController *controller.OAuthController
|
||||||
|
OIDCController *controller.OIDCController
|
||||||
|
ProxyController *controller.ProxyController
|
||||||
|
UserController *controller.UserController
|
||||||
|
ResourcesController *controller.ResourcesController
|
||||||
|
HealthController *controller.HealthController
|
||||||
|
WellKnownController *controller.WellKnownController
|
||||||
|
}
|
||||||
|
|
||||||
|
// force dig to build all controllers and register their routes
|
||||||
|
err = app.dig.Invoke(func(ci controllerInput) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to invoke controllers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
app.router = engine
|
app.router = engine
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) runListeners() (chan error, error) {
|
// Top down
|
||||||
// lec -> listener error channel
|
// 1. Unix socket (if server.socketPath)
|
||||||
lec := make(chan error, len(app.listeners))
|
// 2. HTTP - default
|
||||||
|
func (app *BootstrapApp) getListenerFunc() (func(ctx context.Context) error, error) {
|
||||||
for _, listenerType := range app.listeners {
|
|
||||||
listenerFunc, err := app.listenerFromType(listenerType)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get listener function: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.ding.Go(func(ctx context.Context) {
|
|
||||||
lec <- listenerFunc(ctx)
|
|
||||||
}, ding.RingNormal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The way we calculate listeners is as follows:
|
|
||||||
// If concurrent listeners are disabled, we pick the first available listener, so:
|
|
||||||
// 1. If tailscale is enabled, we use tailscale
|
|
||||||
// 2. If socket path is configured, we use unix socket
|
|
||||||
// 3. Finally if none is configured we use http
|
|
||||||
// If concurrent listeners are enabled, we add all available listeners in the following order
|
|
||||||
func (app *BootstrapApp) calculateListenerPolicy() []Listener {
|
|
||||||
l := []Listener{}
|
|
||||||
|
|
||||||
if !app.config.Server.ConcurrentListenersEnabled {
|
|
||||||
if app.services.TailscaleService != nil {
|
|
||||||
l = append(l, ListenerTailscale)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.config.Server.SocketPath != "" {
|
if app.config.Server.SocketPath != "" {
|
||||||
l = append(l, ListenerUnix)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
l = append(l, ListenerHTTP)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.config.Server.SocketPath != "" {
|
|
||||||
l = append(l, ListenerUnix)
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.services.TailscaleService != nil {
|
|
||||||
l = append(l, ListenerTailscale)
|
|
||||||
}
|
|
||||||
|
|
||||||
l = append(l, ListenerHTTP)
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func(ctx context.Context) error, error) {
|
|
||||||
switch listenerType {
|
|
||||||
case ListenerHTTP:
|
|
||||||
return app.serveHTTP, nil
|
|
||||||
case ListenerUnix:
|
|
||||||
return app.serveUnix, nil
|
return app.serveUnix, nil
|
||||||
case ListenerTailscale:
|
|
||||||
return app.serveTailscale, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid listener type: %d", listenerType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return app.serveHTTP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
|
func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
|
||||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||||
|
|
||||||
app.log.App.Info().Msgf("Starting server on %s", address)
|
app.log.App.Info().Msgf("Starting server on http://%s", address)
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", address)
|
listener, err := net.Listen("tcp", address)
|
||||||
|
|
||||||
@@ -185,22 +182,6 @@ func (app *BootstrapApp) serveUnix(ctx context.Context) error {
|
|||||||
return app.serve(listener, server, ctx, "unix socket")
|
return app.serve(listener, server, ctx, "unix socket")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) serveTailscale(ctx context.Context) error {
|
|
||||||
app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.TailscaleService.GetHostname()))
|
|
||||||
|
|
||||||
listener, err := app.services.TailscaleService.CreateListener()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create tailscale listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &http.Server{
|
|
||||||
Handler: app.router.Handler(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.serve(listener, server, ctx, "tailscale")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx context.Context, name string) error {
|
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx context.Context, name string) error {
|
||||||
shutdown := func() {
|
shutdown := func() {
|
||||||
// we use a new context for the shutdown since the main one is cancelled
|
// we use a new context for the shutdown since the main one is cancelled
|
||||||
@@ -222,7 +203,6 @@ func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx c
|
|||||||
err := server.Serve(listener)
|
err := server.Serve(listener)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
shutdown()
|
|
||||||
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,66 +5,67 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *BootstrapApp) setupServices() error {
|
func (app *BootstrapApp) setupServices() error {
|
||||||
app.deps.service = &service.ServiceDependencies{
|
err := app.setupPolicyEngine()
|
||||||
Log: app.log,
|
|
||||||
StaticConfig: &app.config,
|
|
||||||
RuntimeConfig: &app.runtime,
|
|
||||||
Ctx: app.ctx,
|
|
||||||
Ding: app.ding,
|
|
||||||
Services: &app.services,
|
|
||||||
Queries: &app.queries,
|
|
||||||
}
|
|
||||||
|
|
||||||
ldap, err := service.NewLdapService(app.deps.service)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it")
|
return fmt.Errorf("failed to setup policy engine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.services.LDAPService = ldap
|
|
||||||
|
|
||||||
labelProvider, err := app.getLabelProvider()
|
labelProvider, err := app.getLabelProvider()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize label provider: %w", err)
|
return fmt.Errorf("failed to get label provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.deps.service.LabelProvider = labelProvider
|
serviceProvideFor := []any{
|
||||||
|
func() service.LabelProvider {
|
||||||
|
return labelProvider
|
||||||
|
},
|
||||||
|
service.NewLdapService,
|
||||||
|
service.NewTailscaleService,
|
||||||
|
service.NewAccessControlsService,
|
||||||
|
service.NewOAuthBrokerService,
|
||||||
|
service.NewAuthService,
|
||||||
|
service.NewOIDCService,
|
||||||
|
}
|
||||||
|
|
||||||
tailscaleService, err := service.NewTailscaleService(app.deps.service)
|
for _, provider := range serviceProvideFor {
|
||||||
|
err = app.dig.Provide(provider)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it")
|
return fmt.Errorf("failed to provide service: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.services.TailscaleService = tailscaleService
|
type svcInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(app.deps.service)
|
AccessControlService *service.AccessControlsService
|
||||||
app.services.AccessControlService = accessControlsService
|
AuthService *service.AuthService
|
||||||
|
LDAPService *service.LdapService
|
||||||
|
OAuthBrokerService *service.OAuthBrokerService
|
||||||
|
OIDCService *service.OIDCService
|
||||||
|
TailscaleService *service.TailscaleService
|
||||||
|
}
|
||||||
|
|
||||||
err = app.setupPolicyEngine()
|
err = app.dig.Invoke(func(i svcInput) error {
|
||||||
|
app.services.accessControlService = i.AccessControlService
|
||||||
|
app.services.authService = i.AuthService
|
||||||
|
app.services.ldapService = i.LDAPService
|
||||||
|
app.services.oauthBrokerService = i.OAuthBrokerService
|
||||||
|
app.services.oidcService = i.OIDCService
|
||||||
|
app.services.tailscaleService = i.TailscaleService
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
return fmt.Errorf("failed to invoke services: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthBrokerService := service.NewOAuthBrokerService(app.deps.service)
|
|
||||||
app.services.OAuthBrokerService = oauthBrokerService
|
|
||||||
|
|
||||||
authService := service.NewAuthService(app.deps.service)
|
|
||||||
app.services.AuthService = authService
|
|
||||||
|
|
||||||
oidcService, err := service.NewOIDCService(app.deps.service)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize oidc service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.services.OIDCService = oidcService
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,45 +82,71 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
|
|||||||
if useKubernetes {
|
if useKubernetes {
|
||||||
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
||||||
|
|
||||||
kubernetesService, err := service.NewKubernetesService(app.deps.service)
|
err := app.dig.Provide(service.NewKubernetesService)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
return nil, fmt.Errorf("failed to provide kubernetes service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.services.KubernetesService = kubernetesService
|
err = app.dig.Invoke(func(k *service.KubernetesService) error {
|
||||||
return kubernetesService, nil
|
app.services.kubernetesService = k
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to invoke kubernetes service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kubernetes will fail to initialize with an error if it cannot connect to the cluster
|
||||||
|
// but just to be safe, we check if the service is nil and log a warning if it is
|
||||||
|
if app.services.kubernetesService == nil {
|
||||||
|
if app.config.LabelProvider == "kubernetes" {
|
||||||
|
app.log.App.Warn().Msg("Kubernetes label provider selected but Kubernetes is not available, will continue without it")
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.services.kubernetesService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app.log.App.Debug().Msg("Using Docker label provider")
|
app.log.App.Debug().Msg("Using Docker label provider")
|
||||||
|
|
||||||
dockerService, err := service.NewDockerService(app.deps.service)
|
err := app.dig.Provide(service.NewDockerService)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
|
return nil, fmt.Errorf("failed to provide docker service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dockerService == nil {
|
err = app.dig.Invoke(func(d *service.DockerService) error {
|
||||||
|
app.services.dockerService = d
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to invoke docker service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.services.dockerService == nil {
|
||||||
if app.config.LabelProvider == "docker" {
|
if app.config.LabelProvider == "docker" {
|
||||||
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
|
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app.services.DockerService = dockerService
|
return app.services.dockerService, nil
|
||||||
return dockerService, nil
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
|
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) setupPolicyEngine() error {
|
func (app *BootstrapApp) setupPolicyEngine() error {
|
||||||
policyEngine, err := service.NewPolicyEngine(app.deps.service)
|
err := app.dig.Provide(service.NewPolicyEngine)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
return fmt.Errorf("failed to create policy engine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = app.dig.Invoke(func(policyEngine *service.PolicyEngine) error {
|
||||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
||||||
Log: app.log,
|
Log: app.log,
|
||||||
})
|
})
|
||||||
@@ -140,7 +167,8 @@ func (app *BootstrapApp) setupPolicyEngine() error {
|
|||||||
Log: app.log,
|
Log: app.log,
|
||||||
Config: app.config,
|
Config: app.config,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.services.PolicyEngine = policyEngine
|
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -59,7 +62,7 @@ type ACRUI struct {
|
|||||||
type ACRApp struct {
|
type ACRApp struct {
|
||||||
AppURL string `json:"appUrl"`
|
AppURL string `json:"appUrl"`
|
||||||
CookieDomain string `json:"cookieDomain"`
|
CookieDomain string `json:"cookieDomain"`
|
||||||
TrustedDomains []string `json:"trustedDomains"`
|
SubdomainsEnabled bool `json:"subdomainsEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppContextResponse struct {
|
type AppContextResponse struct {
|
||||||
@@ -71,29 +74,33 @@ type AppContextResponse struct {
|
|||||||
App ACRApp `json:"app"`
|
App ACRApp `json:"app"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextController struct {
|
type ContextControllerInput struct {
|
||||||
log *logger.Logger
|
dig.In
|
||||||
config model.Config
|
|
||||||
runtime model.RuntimeConfig
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
Runtime *model.RuntimeConfig
|
||||||
|
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContextController(
|
type ContextController struct {
|
||||||
log *logger.Logger,
|
log *logger.Logger
|
||||||
config model.Config,
|
config *model.Config
|
||||||
runtimeConfig model.RuntimeConfig,
|
runtime *model.RuntimeConfig
|
||||||
router *gin.RouterGroup,
|
}
|
||||||
) *ContextController {
|
|
||||||
|
func NewContextController(i ContextControllerInput) *ContextController {
|
||||||
controller := &ContextController{
|
controller := &ContextController{
|
||||||
log: log,
|
log: i.Log,
|
||||||
config: config,
|
config: i.Config,
|
||||||
runtime: runtimeConfig,
|
runtime: i.Runtime,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.UI.WarningsEnabled {
|
if !i.Config.UI.WarningsEnabled {
|
||||||
log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.")
|
i.Log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.")
|
||||||
}
|
}
|
||||||
|
|
||||||
contextGroup := router.Group("/context")
|
contextGroup := i.RouterGroup.Group("/context")
|
||||||
contextGroup.GET("/user", controller.userContextHandler)
|
contextGroup.GET("/user", controller.userContextHandler)
|
||||||
contextGroup.GET("/app", controller.appContextHandler)
|
contextGroup.GET("/app", controller.appContextHandler)
|
||||||
|
|
||||||
@@ -104,7 +111,9 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
||||||
|
}
|
||||||
c.JSON(200, UserContextResponse{
|
c.JSON(200, UserContextResponse{
|
||||||
Status: 401,
|
Status: 401,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
@@ -157,7 +166,7 @@ func (controller *ContextController) appContextHandler(c *gin.Context) {
|
|||||||
App: ACRApp{
|
App: ACRApp{
|
||||||
AppURL: controller.runtime.AppURL,
|
AppURL: controller.runtime.AppURL,
|
||||||
CookieDomain: controller.runtime.CookieDomain,
|
CookieDomain: controller.runtime.CookieDomain,
|
||||||
TrustedDomains: controller.runtime.TrustedDomains,
|
SubdomainsEnabled: controller.config.Auth.SubdomainsEnabled,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"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/controller"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
@@ -33,25 +32,25 @@ func TestContextController(t *testing.T) {
|
|||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
path: "/api/context/app",
|
path: "/api/context/app",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedAppContextResponse := controller.AppContextResponse{
|
expectedAppContextResponse := AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Auth: controller.ACRAuth{
|
Auth: ACRAuth{
|
||||||
Providers: runtime.ConfiguredProviders,
|
Providers: runtime.ConfiguredProviders,
|
||||||
},
|
},
|
||||||
OAuth: controller.ACROAuth{
|
OAuth: ACROAuth{
|
||||||
AutoRedirect: cfg.OAuth.AutoRedirect,
|
AutoRedirect: cfg.OAuth.AutoRedirect,
|
||||||
},
|
},
|
||||||
UI: controller.ACRUI{
|
UI: ACRUI{
|
||||||
Title: cfg.UI.Title,
|
Title: cfg.UI.Title,
|
||||||
ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
|
ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
|
||||||
BackgroundImage: cfg.UI.BackgroundImage,
|
BackgroundImage: cfg.UI.BackgroundImage,
|
||||||
WarningsEnabled: cfg.UI.WarningsEnabled,
|
WarningsEnabled: cfg.UI.WarningsEnabled,
|
||||||
},
|
},
|
||||||
App: controller.ACRApp{
|
App: ACRApp{
|
||||||
AppURL: runtime.AppURL,
|
AppURL: runtime.AppURL,
|
||||||
CookieDomain: runtime.CookieDomain,
|
CookieDomain: runtime.CookieDomain,
|
||||||
TrustedDomains: runtime.TrustedDomains,
|
SubdomainsEnabled: cfg.Auth.SubdomainsEnabled,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(expectedAppContextResponse)
|
bytes, err := json.Marshal(expectedAppContextResponse)
|
||||||
@@ -64,7 +63,7 @@ func TestContextController(t *testing.T) {
|
|||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
path: "/api/context/user",
|
path: "/api/context/user",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedUserContextResponse := controller.UserContextResponse{
|
expectedUserContextResponse := UserContextResponse{
|
||||||
Status: 401,
|
Status: 401,
|
||||||
Message: "Unauthorized",
|
Message: "Unauthorized",
|
||||||
}
|
}
|
||||||
@@ -92,10 +91,10 @@ func TestContextController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
path: "/api/context/user",
|
path: "/api/context/user",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedUserContextResponse := controller.UserContextResponse{
|
expectedUserContextResponse := UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Auth: controller.UCRAuth{
|
Auth: UCRAuth{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
Username: "johndoe",
|
Username: "johndoe",
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
@@ -121,7 +120,12 @@ func TestContextController(t *testing.T) {
|
|||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
controller.NewContextController(log, cfg, runtime, group)
|
NewContextController(ContextControllerInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
RouterGroup: group,
|
||||||
|
})
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
)
|
||||||
|
|
||||||
type HealthController struct {
|
type HealthController struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHealthController(router *gin.RouterGroup) *HealthController {
|
type HealthControllerInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
|
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHealthController(i HealthControllerInput) *HealthController {
|
||||||
controller := &HealthController{}
|
controller := &HealthController{}
|
||||||
|
|
||||||
router.GET("/healthz", controller.healthHandler)
|
i.RouterGroup.GET("/healthz", controller.healthHandler)
|
||||||
router.HEAD("/healthz", controller.healthHandler)
|
i.RouterGroup.HEAD("/healthz", controller.healthHandler)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"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/controller"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHealthController(t *testing.T) {
|
func TestHealthController(t *testing.T) {
|
||||||
@@ -55,7 +54,9 @@ func TestHealthController(t *testing.T) {
|
|||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
controller.NewHealthController(group)
|
NewHealthController(HealthControllerInput{
|
||||||
|
RouterGroup: group,
|
||||||
|
})
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"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/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
@@ -22,26 +24,30 @@ type OAuthRequest struct {
|
|||||||
|
|
||||||
type OAuthController struct {
|
type OAuthController struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
config model.Config
|
config *model.Config
|
||||||
runtime model.RuntimeConfig
|
runtime *model.RuntimeConfig
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOAuthController(
|
type OAuthControllerInput struct {
|
||||||
log *logger.Logger,
|
dig.In
|
||||||
config model.Config,
|
|
||||||
runtimeConfig model.RuntimeConfig,
|
Log *logger.Logger
|
||||||
router *gin.RouterGroup,
|
Config *model.Config
|
||||||
auth *service.AuthService,
|
RuntimeConfig *model.RuntimeConfig
|
||||||
) *OAuthController {
|
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||||
|
AuthService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOAuthController(i OAuthControllerInput) *OAuthController {
|
||||||
controller := &OAuthController{
|
controller := &OAuthController{
|
||||||
log: log,
|
log: i.Log,
|
||||||
config: config,
|
config: i.Config,
|
||||||
runtime: runtimeConfig,
|
runtime: i.RuntimeConfig,
|
||||||
auth: auth,
|
auth: i.AuthService,
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthGroup := router.Group("/oauth")
|
oauthGroup := i.RouterGroup.Group("/oauth")
|
||||||
oauthGroup.GET("/url/:provider", controller.oauthURLHandler)
|
oauthGroup.GET("/url/:provider", controller.oauthURLHandler)
|
||||||
oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler)
|
oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler)
|
||||||
|
|
||||||
@@ -75,9 +81,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !controller.isOidcRequest(reqParams) {
|
if !controller.isOidcRequest(reqParams) {
|
||||||
isRedirectSafe := utils.IsRedirectSafe(reqParams.RedirectURI, controller.runtime.CookieDomain)
|
if !controller.isRedirectSafe(reqParams.RedirectURI) {
|
||||||
|
|
||||||
if !isRedirectSafe {
|
|
||||||
controller.log.App.Warn().Str("redirectUri", reqParams.RedirectURI).Msg("Unsafe redirect URI, ignoring")
|
controller.log.App.Warn().Str("redirectUri", reqParams.RedirectURI).Msg("Unsafe redirect URI, ignoring")
|
||||||
reqParams.RedirectURI = ""
|
reqParams.RedirectURI = ""
|
||||||
}
|
}
|
||||||
@@ -300,8 +304,63 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackPar
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OAuthController) getCookieDomain() string {
|
func (controller *OAuthController) getCookieDomain() string {
|
||||||
if controller.config.Auth.SubdomainsEnabled {
|
if !controller.config.Auth.SubdomainsEnabled {
|
||||||
return "." + controller.runtime.CookieDomain
|
return ""
|
||||||
}
|
}
|
||||||
return controller.runtime.CookieDomain
|
return controller.runtime.CookieDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
|
||||||
|
u, err := url.Parse(redirectURI)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.log.App.Error().Err(err).Msg("Failed to parse redirect URI")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "" || u.Host == "" {
|
||||||
|
controller.log.App.Warn().Msg("Redirect URI has invalid scheme or host")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
au, err := url.Parse(controller.runtime.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
controller.log.App.Error().Err(err).Msg("Failed to parse app URL")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme != au.Scheme {
|
||||||
|
controller.log.App.Warn().Msg("Redirect URI scheme does not match app URL scheme")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getEffectivePort := func(u *url.URL) string {
|
||||||
|
if u.Port() != "" {
|
||||||
|
return u.Port()
|
||||||
|
}
|
||||||
|
if u.Scheme == "https" {
|
||||||
|
return "443"
|
||||||
|
}
|
||||||
|
return "80"
|
||||||
|
}
|
||||||
|
|
||||||
|
if getEffectivePort(u) != getEffectivePort(au) {
|
||||||
|
controller.log.App.Warn().Msg("Redirect URI port does not match app URL port")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(u.Hostname(), au.Hostname()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !controller.config.Auth.SubdomainsEnabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(strings.ToLower(u.Hostname()), "."+strings.ToLower(controller.runtime.CookieDomain)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOAuthControllerIsRedirectSafe(t *testing.T) {
|
||||||
|
log := logger.NewLogger().WithTestConfig()
|
||||||
|
log.Init()
|
||||||
|
|
||||||
|
cfg, runtime := test.CreateTestConfigs(t)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
appURL string
|
||||||
|
cookieDomain string
|
||||||
|
subdomainsEnabled bool
|
||||||
|
redirectURI string
|
||||||
|
expected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
description: "Exact host match returns true",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://tinyauth.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Exact host match is case insensitive",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://TinyAuth.Example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Exact host match with subdomains disabled returns true",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: false,
|
||||||
|
redirectURI: "https://tinyauth.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Subdomain of cookie domain returns true when subdomains enabled",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://sub.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Subdomain of cookie domain is case insensitive",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "Example.COM",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://SUB.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Subdomain not matching cookie domain returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://sub.evil.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Subdomain returns false when subdomains disabled",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: false,
|
||||||
|
redirectURI: "https://sub.example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Cookie domain itself is not a subdomain match",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Different scheme returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "http://tinyauth.example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Different port returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://tinyauth.example.com:8080",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Empty redirect URI returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Redirect URI without host returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https:/malicious",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Redirect URI without scheme returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "tinyauth.example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Relative redirect URI returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "/some/path",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Userinfo trick with malicious host returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://malicious.example.com@evil.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Unparseable redirect URI returns false",
|
||||||
|
appURL: "https://tinyauth.example.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://exa\x7fmple.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Unparseable app URL returns false",
|
||||||
|
appURL: "https://tinyauth.\x7fexample.com",
|
||||||
|
cookieDomain: "example.com",
|
||||||
|
subdomainsEnabled: true,
|
||||||
|
redirectURI: "https://tinyauth.example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
router := gin.Default()
|
||||||
|
group := router.Group("/api")
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
// Overwrite the app URL, cookie domain and subdomain setting for each test case
|
||||||
|
runtime.AppURL = tc.appURL
|
||||||
|
runtime.CookieDomain = tc.cookieDomain
|
||||||
|
cfg.Auth.SubdomainsEnabled = tc.subdomainsEnabled
|
||||||
|
|
||||||
|
ctrl := NewOAuthController(OAuthControllerInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
RuntimeConfig: &runtime,
|
||||||
|
RouterGroup: group,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, ctrl.isRedirectSafe(tc.redirectURI))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
@@ -30,7 +33,7 @@ type authorizeErrorParams struct {
|
|||||||
type OIDCController struct {
|
type OIDCController struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
oidc *service.OIDCService
|
oidc *service.OIDCService
|
||||||
runtime model.RuntimeConfig
|
runtime *model.RuntimeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeCallback struct {
|
type AuthorizeCallback struct {
|
||||||
@@ -72,28 +75,34 @@ type AuthorizeScreenParams struct {
|
|||||||
OIDCTicket string `url:"oidc_ticket"`
|
OIDCTicket string `url:"oidc_ticket"`
|
||||||
OIDCScope string `url:"oidc_scope"`
|
OIDCScope string `url:"oidc_scope"`
|
||||||
OIDCName string `url:"oidc_name"`
|
OIDCName string `url:"oidc_name"`
|
||||||
|
OIDCPrompt service.OIDCPrompt `url:"oidc_prompt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeCompleteRequest struct {
|
type AuthorizeCompleteRequest struct {
|
||||||
Ticket string `json:"ticket" binding:"required"`
|
Ticket string `json:"ticket" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOIDCController(
|
type OIDCControllerInput struct {
|
||||||
log *logger.Logger,
|
dig.In
|
||||||
oidcService *service.OIDCService,
|
|
||||||
runtimeConfig model.RuntimeConfig,
|
Log *logger.Logger
|
||||||
router *gin.RouterGroup,
|
OIDCService *service.OIDCService
|
||||||
mainRouter *gin.RouterGroup) *OIDCController {
|
RuntimeConfig *model.RuntimeConfig
|
||||||
|
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||||
|
MainRouter *gin.RouterGroup `name:"mainRouterGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
||||||
controller := &OIDCController{
|
controller := &OIDCController{
|
||||||
log: log,
|
log: i.Log,
|
||||||
oidc: oidcService,
|
oidc: i.OIDCService,
|
||||||
runtime: runtimeConfig,
|
runtime: i.RuntimeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
mainRouter.POST("/authorize", controller.authorize)
|
i.MainRouter.POST("/authorize", controller.authorize)
|
||||||
mainRouter.GET("/authorize", controller.authorize)
|
i.MainRouter.GET("/authorize", controller.authorize)
|
||||||
|
|
||||||
oidcGroup := router.Group("/oidc")
|
oidcGroup := i.RouterGroup.Group("/oidc")
|
||||||
oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
|
oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
|
||||||
oidcGroup.POST("/token", controller.Token)
|
oidcGroup.POST("/token", controller.Token)
|
||||||
oidcGroup.GET("/userinfo", controller.Userinfo)
|
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||||
@@ -161,20 +170,87 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompts := controller.oidc.GetPrompt(req.Prompt)
|
||||||
|
|
||||||
|
if slices.Contains(prompts, service.OIDCPromptNone) && len(prompts) > 1 {
|
||||||
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
|
err: errors.New("invalid prompt"),
|
||||||
|
reason: "Invalid prompt",
|
||||||
|
reasonPublic: "The prompt parameters are invalid",
|
||||||
|
callback: req.RedirectURI,
|
||||||
|
callbackError: "invalid_request",
|
||||||
|
state: req.State,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
|
controller.log.App.Warn().Err(err).Msg("Failed to get user context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err != nil || !userContext.Authenticated) && slices.Contains(prompts, service.OIDCPromptNone) {
|
||||||
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
|
err: errors.New("user not logged in"),
|
||||||
|
reason: "User not logged in",
|
||||||
|
reasonPublic: "The user is not logged in",
|
||||||
|
callback: req.RedirectURI,
|
||||||
|
callbackError: "login_required",
|
||||||
|
state: req.State,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ticket := controller.oidc.CreateAuthorizeRequestTicket(*req)
|
ticket := controller.oidc.CreateAuthorizeRequestTicket(*req)
|
||||||
|
|
||||||
queries, err := query.Values(AuthorizeScreenParams{
|
values := AuthorizeScreenParams{
|
||||||
LoginFor: FrontendLoginForOIDC,
|
LoginFor: FrontendLoginForOIDC,
|
||||||
OIDCTicket: ticket,
|
OIDCTicket: ticket,
|
||||||
OIDCScope: req.Scope,
|
OIDCScope: req.Scope,
|
||||||
OIDCName: client.Name,
|
OIDCName: client.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(prompts, service.OIDCPromptLogin) {
|
||||||
|
values.OIDCPrompt = service.OIDCPromptLogin
|
||||||
|
} else if slices.Contains(prompts, service.OIDCPromptNone) {
|
||||||
|
values.OIDCPrompt = service.OIDCPromptNone
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MaxAge != "" && userContext != nil {
|
||||||
|
maxAge, err := strconv.Atoi(req.MaxAge)
|
||||||
|
if err != nil {
|
||||||
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
|
err: err,
|
||||||
|
reason: "Invalid max_age",
|
||||||
|
reasonPublic: "The max_age parameter is invalid",
|
||||||
|
callback: req.RedirectURI,
|
||||||
|
callbackError: "invalid_request",
|
||||||
|
state: req.State,
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userContext.Authenticated {
|
||||||
|
authTime := time.Unix(userContext.AuthTime, 0)
|
||||||
|
if authTime.Add(time.Duration(maxAge) * time.Second).Before(time.Now()) {
|
||||||
|
values.OIDCPrompt = service.OIDCPromptLogin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queries, err := query.Values(values)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.authorizeError(c, authorizeErrorParams{
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
err: err,
|
err: err,
|
||||||
reason: "Failed to compile authorize queries",
|
reason: "Failed to compile authorize queries",
|
||||||
reasonPublic: "An internal error occured while processing your request",
|
reasonPublic: "An internal error occured while processing your request",
|
||||||
|
callback: req.RedirectURI,
|
||||||
|
callbackError: "server_error",
|
||||||
|
state: req.State,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,16 +278,12 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
|||||||
userContext, err := new(model.UserContext).NewFromGin(c)
|
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.authorizeError(c, authorizeErrorParams{
|
if !errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
err: err,
|
controller.log.App.Warn().Err(err).Msg("Failed to get user context")
|
||||||
reason: "Failed to get user context",
|
}
|
||||||
reasonPublic: "User is not logged in or the session is invalid",
|
|
||||||
json: true,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userContext.Authenticated {
|
if err != nil || !userContext.Authenticated {
|
||||||
controller.authorizeError(c, authorizeErrorParams{
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
err: errors.New("err user not logged in"),
|
err: errors.New("err user not logged in"),
|
||||||
reason: "User not logged in",
|
reason: "User not logged in",
|
||||||
@@ -419,7 +491,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, *entry)
|
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, *entry, entry.AuthTime)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
||||||
@@ -667,7 +739,7 @@ func (controller *OIDCController) resolveRequestObject(c *gin.Context) (*service
|
|||||||
func (controller *OIDCController) resolveNormalParams(c *gin.Context) (*service.AuthorizeRequest, error) {
|
func (controller *OIDCController) resolveNormalParams(c *gin.Context) (*service.AuthorizeRequest, error) {
|
||||||
var req service.AuthorizeRequest
|
var req service.AuthorizeRequest
|
||||||
|
|
||||||
bind := binding.Query
|
var bind binding.Binding = binding.Query
|
||||||
|
|
||||||
if c.Request.Method == http.MethodPost {
|
if c.Request.Method == http.MethodPost {
|
||||||
bind = binding.Form
|
bind = binding.Form
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
"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/controller"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
||||||
@@ -35,11 +34,17 @@ func TestOIDCController(t *testing.T) {
|
|||||||
|
|
||||||
store := memory.New()
|
store := memory.New()
|
||||||
|
|
||||||
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg)
|
oidcService, err := service.NewOIDCService(service.OIDCServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Queries: store,
|
||||||
|
Ding: dg,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Middleware that injects an authenticated local user into the gin context,
|
// Middleware that injects an authenticated local user into the gin context,
|
||||||
// mimicking the context middleware that runs before the OIDC controller.
|
// mimicking the context middleware that runs before the OIDC
|
||||||
authedUser := func(c *gin.Context) {
|
authedUser := func(c *gin.Context) {
|
||||||
c.Set("context", &model.UserContext{
|
c.Set("context", &model.UserContext{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
@@ -204,10 +209,30 @@ func TestOIDCController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// --- authorize-complete ---
|
// --- authorize-complete ---
|
||||||
|
{
|
||||||
|
description: "Should fail if oidc is disabled",
|
||||||
|
oidcDisabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: "some-ticket"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
var res map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||||
|
redirectURI, ok := res["redirect_uri"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Contains(t, redirectURI, oidcService.GetIssuer()+"/error")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Authorize complete returns a JSON error when the user context is missing",
|
description: "Authorize complete returns a JSON error when the user context is missing",
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
body, err := json.Marshal(controller.AuthorizeCompleteRequest{Ticket: "some-ticket"})
|
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: "some-ticket"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||||
@@ -237,7 +262,7 @@ func TestOIDCController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
body, err := json.Marshal(controller.AuthorizeCompleteRequest{Ticket: "some-ticket"})
|
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: "some-ticket"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||||
@@ -257,7 +282,7 @@ func TestOIDCController(t *testing.T) {
|
|||||||
description: "Authorize complete returns a JSON error when the ticket is invalid",
|
description: "Authorize complete returns a JSON error when the ticket is invalid",
|
||||||
middlewares: []gin.HandlerFunc{authedUser},
|
middlewares: []gin.HandlerFunc{authedUser},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
body, err := json.Marshal(controller.AuthorizeCompleteRequest{Ticket: "nonexistent-ticket"})
|
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: "nonexistent-ticket"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||||
@@ -285,7 +310,7 @@ func TestOIDCController(t *testing.T) {
|
|||||||
State: "state-123",
|
State: "state-123",
|
||||||
})
|
})
|
||||||
|
|
||||||
body, err := json.Marshal(controller.AuthorizeCompleteRequest{Ticket: ticket})
|
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: ticket})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
|
||||||
@@ -831,7 +856,13 @@ func TestOIDCController(t *testing.T) {
|
|||||||
svc = nil
|
svc = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.NewOIDCController(log, svc, runtime, group, &router.RouterGroup)
|
NewOIDCController(OIDCControllerInput{
|
||||||
|
Log: log,
|
||||||
|
OIDCService: svc,
|
||||||
|
RuntimeConfig: &runtime,
|
||||||
|
RouterGroup: group,
|
||||||
|
MainRouter: &router.RouterGroup,
|
||||||
|
})
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"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/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
@@ -53,29 +54,33 @@ type ProxyContext struct {
|
|||||||
|
|
||||||
type ProxyController struct {
|
type ProxyController struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
runtime model.RuntimeConfig
|
runtime *model.RuntimeConfig
|
||||||
acls *service.AccessControlsService
|
acls *service.AccessControlsService
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
policyEngine *service.PolicyEngine
|
policyEngine *service.PolicyEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyController(
|
type ProxyControllerInput struct {
|
||||||
log *logger.Logger,
|
dig.In
|
||||||
runtime model.RuntimeConfig,
|
|
||||||
router *gin.RouterGroup,
|
Log *logger.Logger
|
||||||
acls *service.AccessControlsService,
|
RuntimeConfig *model.RuntimeConfig
|
||||||
auth *service.AuthService,
|
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||||
policyEngine *service.PolicyEngine,
|
ACLsService *service.AccessControlsService
|
||||||
) *ProxyController {
|
AuthService *service.AuthService
|
||||||
|
PolicyEngine *service.PolicyEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyController(i ProxyControllerInput) *ProxyController {
|
||||||
controller := &ProxyController{
|
controller := &ProxyController{
|
||||||
log: log,
|
log: i.Log,
|
||||||
runtime: runtime,
|
runtime: i.RuntimeConfig,
|
||||||
acls: acls,
|
acls: i.ACLsService,
|
||||||
auth: auth,
|
auth: i.AuthService,
|
||||||
policyEngine: policyEngine,
|
policyEngine: i.PolicyEngine,
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyGroup := router.Group("/auth")
|
proxyGroup := i.RouterGroup.Group("/auth")
|
||||||
proxyGroup.Any("/:proxy", controller.proxyHandler)
|
proxyGroup.Any("/:proxy", controller.proxyHandler)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
@@ -153,7 +158,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusFound, redirectURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +207,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusFound, redirectURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +251,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusFound, redirectURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +300,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusFound, redirectURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ProxyController) setHeaders(c *gin.Context, acls *model.App) {
|
func (controller *ProxyController) setHeaders(c *gin.Context, acls *model.App) {
|
||||||
@@ -331,7 +336,7 @@ func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyCon
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusFound, redirectURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ProxyController) getHeader(c *gin.Context, header string) (string, bool) {
|
func (controller *ProxyController) getHeader(c *gin.Context, header string) (string, bool) {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -10,7 +13,6 @@ import (
|
|||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
"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/controller"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
@@ -64,6 +66,17 @@ func TestProxyController(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
description: "Should get bad request on invalid proxy",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/invalid", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||||
|
assert.Contains(t, recorder.Body.String(), "Bad request")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Default forward auth should be detected and used for traefik",
|
description: "Default forward auth should be detected and used for traefik",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
@@ -75,7 +88,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("user-agent", browserUserAgent)
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 307, recorder.Code)
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
location := recorder.Header().Get("Location")
|
location := recorder.Header().Get("Location")
|
||||||
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
||||||
assert.Contains(t, location, "login_for=app")
|
assert.Contains(t, location, "login_for=app")
|
||||||
@@ -90,7 +103,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-original-url", "https://test.example.com/")
|
req.Header.Set("x-original-url", "https://test.example.com/")
|
||||||
req.Header.Set("user-agent", browserUserAgent)
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 401, recorder.Code)
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
location := recorder.Header().Get("x-tinyauth-location")
|
location := recorder.Header().Get("x-tinyauth-location")
|
||||||
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
||||||
assert.Contains(t, location, "login_for=app")
|
assert.Contains(t, location, "login_for=app")
|
||||||
@@ -106,7 +119,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
req.Header.Set("user-agent", browserUserAgent)
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 307, recorder.Code)
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
location := recorder.Header().Get("Location")
|
location := recorder.Header().Get("Location")
|
||||||
assert.Contains(t, location, url.QueryEscape("https://test.example.com/hello"))
|
assert.Contains(t, location, url.QueryEscape("https://test.example.com/hello"))
|
||||||
assert.Contains(t, location, "login_for=app")
|
assert.Contains(t, location, "login_for=app")
|
||||||
@@ -124,7 +137,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("user-agent", browserUserAgent)
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 307, recorder.Code)
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
location := recorder.Header().Get("Location")
|
location := recorder.Header().Get("Location")
|
||||||
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
||||||
assert.Contains(t, location, "login_for=app")
|
assert.Contains(t, location, "login_for=app")
|
||||||
@@ -141,7 +154,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
req.Header.Set("user-agent", browserUserAgent)
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 401, recorder.Code)
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
location := recorder.Header().Get("x-tinyauth-location")
|
location := recorder.Header().Get("x-tinyauth-location")
|
||||||
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
||||||
assert.Contains(t, location, "login_for=app")
|
assert.Contains(t, location, "login_for=app")
|
||||||
@@ -159,7 +172,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/hello")
|
req.Header.Set("x-forwarded-uri", "/hello")
|
||||||
req.Header.Set("user-agent", browserUserAgent)
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 307, recorder.Code)
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
location := recorder.Header().Get("Location")
|
location := recorder.Header().Get("Location")
|
||||||
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
assert.Contains(t, location, url.QueryEscape("https://test.example.com/"))
|
||||||
assert.Contains(t, location, "login_for=app")
|
assert.Contains(t, location, "login_for=app")
|
||||||
@@ -176,7 +189,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 401, recorder.Code)
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
||||||
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
||||||
},
|
},
|
||||||
@@ -191,7 +204,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 401, recorder.Code)
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
||||||
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
||||||
},
|
},
|
||||||
@@ -206,7 +219,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/hello")
|
req.Header.Set("x-forwarded-uri", "/hello")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 401, recorder.Code)
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
assert.Contains(t, recorder.Body.String(), `"status":401`)
|
||||||
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
|
||||||
},
|
},
|
||||||
@@ -223,7 +236,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||||
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||||
@@ -239,7 +252,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-original-url", "https://test.example.com/")
|
req.Header.Set("x-original-url", "https://test.example.com/")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||||
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||||
@@ -256,7 +269,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||||
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||||
@@ -271,7 +284,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
req.Header.Set("x-forwarded-uri", "/allowed")
|
req.Header.Set("x-forwarded-uri", "/allowed")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -281,7 +294,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||||
req.Header.Set("x-original-url", "https://path-allow.example.com/allowed")
|
req.Header.Set("x-original-url", "https://path-allow.example.com/allowed")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -292,7 +305,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Host = "path-allow.example.com"
|
req.Host = "path-allow.example.com"
|
||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -305,7 +318,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -316,7 +329,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-original-url", "https://ip-bypass.example.com/")
|
req.Header.Set("x-original-url", "https://ip-bypass.example.com/")
|
||||||
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -328,7 +341,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -342,7 +355,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -356,12 +369,301 @@ func TestProxyController(t *testing.T) {
|
|||||||
req.Header.Set("x-forwarded-proto", "https")
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
req.Header.Set("x-forwarded-uri", "/")
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 403, recorder.Code)
|
assert.Equal(t, http.StatusForbidden, recorder.Code)
|
||||||
assert.Equal(t, "", recorder.Header().Get("remote-user"))
|
assert.Equal(t, "", recorder.Header().Get("remote-user"))
|
||||||
assert.Equal(t, "", recorder.Header().Get("remote-name"))
|
assert.Equal(t, "", recorder.Header().Get("remote-name"))
|
||||||
assert.Equal(t, "", recorder.Header().Get("remote-email"))
|
assert.Equal(t, "", recorder.Header().Get("remote-email"))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Test IP block rule, with non browser user agent",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "ip-block.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, recorder.Code)
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), runtime.AppURL)
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ip=10.10.10.10")
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ip-block")
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Test IP block rule, with browser user agent",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "ip-block.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||||
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
|
location := recorder.Header().Get("Location")
|
||||||
|
assert.Contains(t, location, url.QueryEscape("10.10.10.10"))
|
||||||
|
assert.Contains(t, location, url.QueryEscape("ip-block"))
|
||||||
|
assert.Contains(t, location, runtime.AppURL)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "OAuth allowed group",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: true,
|
||||||
|
Provider: model.ProviderOAuth,
|
||||||
|
OAuth: &model.OAuthContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "testuser",
|
||||||
|
Name: "Testuser",
|
||||||
|
Email: "testuser@example.com",
|
||||||
|
},
|
||||||
|
Groups: []string{"group1"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "oauth-group.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||||
|
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||||
|
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||||
|
assert.Equal(t, "group1", recorder.Header().Get("remote-groups"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "OAuth not in required groups and non browser",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: true,
|
||||||
|
Provider: model.ProviderOAuth,
|
||||||
|
OAuth: &model.OAuthContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "testuser",
|
||||||
|
Name: "Testuser",
|
||||||
|
Email: "testuser@example.com",
|
||||||
|
},
|
||||||
|
Groups: []string{"group3"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "oauth-group.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, recorder.Code)
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-user"))
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-name"))
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-email"))
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-groups"))
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), runtime.AppURL)
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "groupErr=true")
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "oauth-group")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "OAuth not in required groups and browser",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: true,
|
||||||
|
Provider: model.ProviderOAuth,
|
||||||
|
OAuth: &model.OAuthContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "testuser",
|
||||||
|
Name: "Testuser",
|
||||||
|
Email: "testuser@example.com",
|
||||||
|
},
|
||||||
|
Groups: []string{"group3"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "oauth-group.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
|
location := recorder.Header().Get("Location")
|
||||||
|
assert.Contains(t, location, "groupErr=true")
|
||||||
|
assert.Contains(t, location, "oauth-group")
|
||||||
|
assert.Contains(t, location, runtime.AppURL)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "LDAP allowed group",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: true,
|
||||||
|
Provider: model.ProviderLDAP,
|
||||||
|
LDAP: &model.LDAPContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "testuser",
|
||||||
|
Name: "Testuser",
|
||||||
|
Email: "testuser@example.com",
|
||||||
|
},
|
||||||
|
Groups: []string{"group1"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "ldap-group.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||||
|
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||||
|
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||||
|
assert.Equal(t, "group1", recorder.Header().Get("remote-groups"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "LDAP not in required groups and non browser",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: true,
|
||||||
|
Provider: model.ProviderLDAP,
|
||||||
|
LDAP: &model.LDAPContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "testuser",
|
||||||
|
Name: "Testuser",
|
||||||
|
Email: "testuser@example.com",
|
||||||
|
},
|
||||||
|
Groups: []string{"group3"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "ldap-group.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, recorder.Code)
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-user"))
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-name"))
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-email"))
|
||||||
|
assert.Equal(t, "", recorder.Header().Get("remote-groups"))
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), runtime.AppURL)
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "groupErr=true")
|
||||||
|
assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ldap-group")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "LDAP not in required groups and browser",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: true,
|
||||||
|
Provider: model.ProviderLDAP,
|
||||||
|
LDAP: &model.LDAPContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "testuser",
|
||||||
|
Name: "Testuser",
|
||||||
|
Email: "testuser@example.com",
|
||||||
|
},
|
||||||
|
Groups: []string{"group3"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "ldap-group.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
req.Header.Set("user-agent", browserUserAgent)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||||
|
location := recorder.Header().Get("Location")
|
||||||
|
assert.Contains(t, location, "groupErr=true")
|
||||||
|
assert.Contains(t, location, "ldap-group")
|
||||||
|
assert.Contains(t, location, runtime.AppURL)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should add basic auth if it's in ACLs",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
simpleCtx,
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "basic-auth.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
req.Header.Set("authorization", "foo") // should be overridden by basic auth
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
authorizationHeader := recorder.Header().Get("Authorization")
|
||||||
|
assert.NotEmpty(t, authorizationHeader)
|
||||||
|
assert.Equal(t, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("test:password"))), authorizationHeader)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Authorization header should be preserved when not basic auth acls",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
simpleCtx,
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
req.Header.Set("authorization", "Bearer mytoken")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
authorizationHeader := recorder.Header().Get("Authorization")
|
||||||
|
assert.NotEmpty(t, authorizationHeader)
|
||||||
|
assert.Equal(t, "Bearer mytoken", authorizationHeader)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should add response headers if present",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
simpleCtx,
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "response-headers.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
assert.Equal(t, "bar", recorder.Header().Get("x-foo"))
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
store := memory.New()
|
store := memory.New()
|
||||||
@@ -369,10 +671,21 @@ func TestProxyController(t *testing.T) {
|
|||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
dg := ding.New(ctx)
|
dg := ding.New(ctx)
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{
|
||||||
aclsService := service.NewAccessControlsService(log, cfg, nil)
|
Log: log,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Ctx: ctx,
|
||||||
|
})
|
||||||
|
aclsService := service.NewAccessControlsService(service.AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
LabelProvider: nil,
|
||||||
|
})
|
||||||
|
|
||||||
policyEngine, err := service.NewPolicyEngine(cfg, log)
|
policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
||||||
@@ -395,7 +708,18 @@ func TestProxyController(t *testing.T) {
|
|||||||
Log: log,
|
Log: log,
|
||||||
})
|
})
|
||||||
|
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
|
authService := service.NewAuthService(service.AuthServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Ctx: ctx,
|
||||||
|
Ding: dg,
|
||||||
|
LDAP: nil,
|
||||||
|
Queries: store,
|
||||||
|
OAuthBroker: broker,
|
||||||
|
Tailscale: nil,
|
||||||
|
PolicyEngine: policyEngine,
|
||||||
|
})
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
@@ -410,7 +734,14 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
controller.NewProxyController(log, runtime, group, aclsService, authService, policyEngine)
|
NewProxyController(ProxyControllerInput{
|
||||||
|
Log: log,
|
||||||
|
RuntimeConfig: &runtime,
|
||||||
|
RouterGroup: group,
|
||||||
|
ACLsService: aclsService,
|
||||||
|
AuthService: authService,
|
||||||
|
PolicyEngine: policyEngine,
|
||||||
|
})
|
||||||
|
|
||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,25 +5,30 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourcesController struct {
|
type ResourcesController struct {
|
||||||
config model.Config
|
config *model.Config
|
||||||
fileServer http.Handler
|
fileServer http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResourcesController(
|
type ResourcesControllerInput struct {
|
||||||
config model.Config,
|
dig.In
|
||||||
router *gin.RouterGroup,
|
|
||||||
) *ResourcesController {
|
RouterGroup *gin.RouterGroup `name:"mainRouterGroup"`
|
||||||
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.Resources.Path)))
|
Config *model.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResourcesController(i ResourcesControllerInput) *ResourcesController {
|
||||||
|
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(i.Config.Resources.Path)))
|
||||||
|
|
||||||
controller := &ResourcesController{
|
controller := &ResourcesController{
|
||||||
config: config,
|
config: i.Config,
|
||||||
fileServer: fileServer,
|
fileServer: fileServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.GET("/resources/*resource", controller.resourcesHandler)
|
i.RouterGroup.GET("/resources/*resource", controller.resourcesHandler)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"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/controller"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,8 +19,12 @@ func TestResourcesController(t *testing.T) {
|
|||||||
err := os.MkdirAll(cfg.Resources.Path, 0777)
|
err := os.MkdirAll(cfg.Resources.Path, 0777)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create a "backup" of the original configuration to restore after each test
|
||||||
|
originalCfg := cfg.Resources
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
|
customCfg *model.ResourcesConfig
|
||||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +57,32 @@ func TestResourcesController(t *testing.T) {
|
|||||||
assert.Equal(t, 404, recorder.Code)
|
assert.Equal(t, 404, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure resources controller returns 404 when resources path is empty",
|
||||||
|
customCfg: &model.ResourcesConfig{
|
||||||
|
Path: "",
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/resources/testfile.txt", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 404, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure resources controller returns 403 when resources are disabled",
|
||||||
|
customCfg: &model.ResourcesConfig{
|
||||||
|
Path: cfg.Resources.Path,
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/resources/testfile.txt", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 403, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testFilePath := cfg.Resources.Path + "/testfile.txt"
|
testFilePath := cfg.Resources.Path + "/testfile.txt"
|
||||||
@@ -69,7 +99,18 @@ func TestResourcesController(t *testing.T) {
|
|||||||
group := router.Group("/")
|
group := router.Group("/")
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
controller.NewResourcesController(cfg, group)
|
// if custom configuration is provided, override the default config
|
||||||
|
if test.customCfg != nil {
|
||||||
|
cfg.Resources = *test.customCfg
|
||||||
|
} else {
|
||||||
|
// Reset to default configuration for each test
|
||||||
|
cfg.Resources = originalCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
NewResourcesController(ResourcesControllerInput{
|
||||||
|
RouterGroup: group,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"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/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -27,23 +28,27 @@ type TotpRequest struct {
|
|||||||
|
|
||||||
type UserController struct {
|
type UserController struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
runtime model.RuntimeConfig
|
runtime *model.RuntimeConfig
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserController(
|
type UserControllerInput struct {
|
||||||
log *logger.Logger,
|
dig.In
|
||||||
runtimeConfig model.RuntimeConfig,
|
|
||||||
router *gin.RouterGroup,
|
Log *logger.Logger
|
||||||
auth *service.AuthService,
|
RuntimeConfig *model.RuntimeConfig
|
||||||
) *UserController {
|
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
|
||||||
|
AuthService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserController(i UserControllerInput) *UserController {
|
||||||
controller := &UserController{
|
controller := &UserController{
|
||||||
log: log,
|
log: i.Log,
|
||||||
runtime: runtimeConfig,
|
runtime: i.RuntimeConfig,
|
||||||
auth: auth,
|
auth: i.AuthService,
|
||||||
}
|
}
|
||||||
|
|
||||||
userGroup := router.Group("/user")
|
userGroup := i.RouterGroup.Group("/user")
|
||||||
userGroup.POST("/login", controller.loginHandler)
|
userGroup.POST("/login", controller.loginHandler)
|
||||||
userGroup.POST("/logout", controller.logoutHandler)
|
userGroup.POST("/logout", controller.logoutHandler)
|
||||||
userGroup.POST("/totp", controller.totpHandler)
|
userGroup.POST("/totp", controller.totpHandler)
|
||||||
@@ -290,6 +295,14 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
|
controller.log.App.Warn().Msg("TOTP verification attempt without user context")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification")
|
controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
@@ -400,6 +413,14 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) {
|
|||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
|
controller.log.App.Warn().Msg("Tailscale login attempt without user context")
|
||||||
|
c.JSON(401, gin.H{
|
||||||
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
"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/controller"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
||||||
@@ -42,6 +41,7 @@ func TestUserController(t *testing.T) {
|
|||||||
TOTPPending: true,
|
TOTPPending: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
totpAttrCtx := func(c *gin.Context) {
|
totpAttrCtx := func(c *gin.Context) {
|
||||||
@@ -57,6 +57,7 @@ func TestUserController(t *testing.T) {
|
|||||||
TOTPPending: true,
|
TOTPPending: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
simpleCtx := func(c *gin.Context) {
|
simpleCtx := func(c *gin.Context) {
|
||||||
@@ -71,6 +72,7 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
store := memory.New()
|
store := memory.New()
|
||||||
@@ -82,11 +84,45 @@ func TestUserController(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
description: "Login should fail gracefully on invalid json",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(`{"username": "testuser", "password":`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 400, recorder.Code)
|
||||||
|
assert.Contains(t, recorder.Body.String(), "Bad Request")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Should fail on missing user",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
loginReq := LoginRequest{
|
||||||
|
Username: "nonexistentuser",
|
||||||
|
Password: "password",
|
||||||
|
}
|
||||||
|
loginReqBody, err := json.Marshal(loginReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 401, recorder.Code)
|
||||||
|
assert.Len(t, recorder.Result().Cookies(), 0)
|
||||||
|
assert.Contains(t, recorder.Body.String(), "Unauthorized")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Should be able to login with valid credentials",
|
description: "Should be able to login with valid credentials",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{
|
loginReq := LoginRequest{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
}
|
}
|
||||||
@@ -114,7 +150,7 @@ func TestUserController(t *testing.T) {
|
|||||||
description: "Should reject login with invalid credentials",
|
description: "Should reject login with invalid credentials",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{
|
loginReq := LoginRequest{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "wrongpassword",
|
Password: "wrongpassword",
|
||||||
}
|
}
|
||||||
@@ -135,7 +171,7 @@ func TestUserController(t *testing.T) {
|
|||||||
description: "Should rate limit on 3 invalid attempts",
|
description: "Should rate limit on 3 invalid attempts",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{
|
loginReq := LoginRequest{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "wrongpassword",
|
Password: "wrongpassword",
|
||||||
}
|
}
|
||||||
@@ -170,7 +206,7 @@ func TestUserController(t *testing.T) {
|
|||||||
description: "Should not allow full login with totp",
|
description: "Should not allow full login with totp",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{
|
loginReq := LoginRequest{
|
||||||
Username: "totpuser",
|
Username: "totpuser",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
}
|
}
|
||||||
@@ -207,7 +243,7 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
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 := LoginRequest{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
}
|
}
|
||||||
@@ -243,6 +279,87 @@ func TestUserController(t *testing.T) {
|
|||||||
assert.Equal(t, -1, cookie.MaxAge) // MaxAge -1 means delete cookie
|
assert.Equal(t, -1, cookie.MaxAge) // MaxAge -1 means delete cookie
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Logout should be treated as valid without a session cookie",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/logout", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "TOTP should gracefully reject invalid json",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(`{"code":`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 400, recorder.Code)
|
||||||
|
assert.Contains(t, recorder.Body.String(), "Bad Request")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "TOTP should fail on non-totp context",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
simpleCtx,
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
totpReq := TotpRequest{
|
||||||
|
Code: "123456",
|
||||||
|
}
|
||||||
|
|
||||||
|
totpReqBody, err := json.Marshal(totpReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 401, recorder.Code)
|
||||||
|
assert.Contains(t, recorder.Body.String(), "Unauthorized")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "TOTP should fail when user in context doesn't exist",
|
||||||
|
middlewares: []gin.HandlerFunc{
|
||||||
|
func(ctx *gin.Context) {
|
||||||
|
ctx.Set("context", &model.UserContext{
|
||||||
|
Authenticated: false,
|
||||||
|
Provider: model.ProviderLocal,
|
||||||
|
Local: &model.LocalContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: "idontexist",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
},
|
||||||
|
TOTPPending: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Next()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
totpReq := TotpRequest{
|
||||||
|
Code: "123456",
|
||||||
|
}
|
||||||
|
|
||||||
|
totpReqBody, err := json.Marshal(totpReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 401, recorder.Code)
|
||||||
|
assert.Contains(t, recorder.Body.String(), "Unauthorized")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Should be able to login with totp",
|
description: "Should be able to login with totp",
|
||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{
|
||||||
@@ -264,7 +381,7 @@ func TestUserController(t *testing.T) {
|
|||||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
totpReq := controller.TotpRequest{
|
totpReq := TotpRequest{
|
||||||
Code: code,
|
Code: code,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +419,7 @@ func TestUserController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
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 := TotpRequest{
|
||||||
Code: "000000", // invalid code
|
Code: "000000", // invalid code
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +451,7 @@ func TestUserController(t *testing.T) {
|
|||||||
description: "Login uses name and email from user attributes",
|
description: "Login uses name and email from user attributes",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{Username: "attruser", Password: "password"}
|
loginReq := LoginRequest{Username: "attruser", Password: "password"}
|
||||||
body, err := json.Marshal(loginReq)
|
body, err := json.Marshal(loginReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -352,7 +469,7 @@ func TestUserController(t *testing.T) {
|
|||||||
description: "Login with TOTP uses name and email from user attributes in pending session",
|
description: "Login with TOTP uses name and email from user attributes in pending session",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{Username: "attrtotpuser", Password: "password"}
|
loginReq := LoginRequest{Username: "attrtotpuser", Password: "password"}
|
||||||
body, err := json.Marshal(loginReq)
|
body, err := json.Marshal(loginReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -388,7 +505,7 @@ func TestUserController(t *testing.T) {
|
|||||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
totpReq := controller.TotpRequest{Code: code}
|
totpReq := TotpRequest{Code: code}
|
||||||
body, err := json.Marshal(totpReq)
|
body, err := json.Marshal(totpReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -414,11 +531,29 @@ func TestUserController(t *testing.T) {
|
|||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
dg := ding.New(ctx)
|
dg := ding.New(ctx)
|
||||||
|
|
||||||
policyEngine, err := service.NewPolicyEngine(cfg, log)
|
policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
|
Log: log,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Ctx: ctx,
|
||||||
|
})
|
||||||
|
authService := service.NewAuthService(service.AuthServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Ctx: ctx,
|
||||||
|
Ding: dg,
|
||||||
|
LDAP: nil,
|
||||||
|
Queries: store,
|
||||||
|
OAuthBroker: broker,
|
||||||
|
Tailscale: nil,
|
||||||
|
PolicyEngine: policyEngine,
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach := func() {
|
beforeEach := func() {
|
||||||
// Clear failed login attempts before each test
|
// Clear failed login attempts before each test
|
||||||
@@ -437,7 +572,12 @@ func TestUserController(t *testing.T) {
|
|||||||
group := router.Group("/api")
|
group := router.Group("/api")
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
controller.NewUserController(log, runtime, group, authService)
|
NewUserController(UserControllerInput{
|
||||||
|
Log: log,
|
||||||
|
RuntimeConfig: &runtime,
|
||||||
|
RouterGroup: group,
|
||||||
|
AuthService: authService,
|
||||||
|
})
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,27 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer"
|
||||||
|
|
||||||
|
type WebfingerResponseLink struct {
|
||||||
|
Rel string `json:"rel,omitempty"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebfingerResponse struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Links []WebfingerResponseLink `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
type OpenIDConnectConfiguration struct {
|
type OpenIDConnectConfiguration struct {
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
@@ -30,13 +46,21 @@ type WellKnownController struct {
|
|||||||
oidc *service.OIDCService
|
oidc *service.OIDCService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWellKnownController(oidc *service.OIDCService, router *gin.RouterGroup) *WellKnownController {
|
type WellKnownControllerInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
|
OIDCService *service.OIDCService
|
||||||
|
RouterGroup *gin.RouterGroup `name:"mainRouterGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWellKnownController(i WellKnownControllerInput) *WellKnownController {
|
||||||
controller := &WellKnownController{
|
controller := &WellKnownController{
|
||||||
oidc: oidc,
|
oidc: i.OIDCService,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
i.RouterGroup.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
||||||
router.GET("/.well-known/jwks.json", controller.JWKS)
|
i.RouterGroup.GET("/.well-known/jwks.json", controller.JWKS)
|
||||||
|
i.RouterGroup.GET("/.well-known/webfinger", controller.WebFinger)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
@@ -97,3 +121,62 @@ func (controller *WellKnownController) JWKS(c *gin.Context) {
|
|||||||
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) WebFinger(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/jrd+json")
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
resource := c.Query("resource")
|
||||||
|
|
||||||
|
if !controller.validateWebFingerResource(resource) {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "invalid resource",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := WebfingerResponse{
|
||||||
|
Subject: resource,
|
||||||
|
Links: []WebfingerResponseLink{},
|
||||||
|
}
|
||||||
|
|
||||||
|
rel := c.Request.URL.Query()["rel"]
|
||||||
|
|
||||||
|
if controller.oidc != nil && (len(rel) == 0 || slices.Contains(rel, OpenIDConnectRel)) {
|
||||||
|
res.Links = append(res.Links, WebfingerResponseLink{Rel: OpenIDConnectRel, Href: controller.oidc.GetIssuer()})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) validateWebFingerResource(resource string) bool {
|
||||||
|
prefix, suffix, found := strings.Cut(resource, ":")
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch prefix {
|
||||||
|
case "acct":
|
||||||
|
if strings.Count(suffix, "@") != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
username, domain, found := strings.Cut(suffix, "@")
|
||||||
|
if !found || username == "" || domain == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case "https", "http":
|
||||||
|
u, err := url.Parse(resource)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.Host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package controller_test
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
"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/controller"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
@@ -26,23 +26,25 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
|
oidcEnabled bool
|
||||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
{
|
{
|
||||||
description: "Ensure well-known endpoint returns correct OIDC configuration",
|
description: "Ensure well-known endpoint returns correct OIDC configuration",
|
||||||
|
oidcEnabled: true,
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
|
req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
|
||||||
res := controller.OpenIDConnectConfiguration{}
|
res := OpenIDConnectConfiguration{}
|
||||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected := controller.OpenIDConnectConfiguration{
|
expected := OpenIDConnectConfiguration{
|
||||||
Issuer: runtime.AppURL,
|
Issuer: runtime.AppURL,
|
||||||
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", runtime.AppURL),
|
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", runtime.AppURL),
|
||||||
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", runtime.AppURL),
|
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", runtime.AppURL),
|
||||||
@@ -56,8 +58,8 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
||||||
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups", "phone_number", "phone_number_verified", "address", "given_name", "family_name", "middle_name", "nickname", "profile", "picture", "website", "gender", "birthdate", "zoneinfo", "locale"},
|
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups", "phone_number", "phone_number_verified", "address", "given_name", "family_name", "middle_name", "nickname", "profile", "picture", "website", "gender", "birthdate", "zoneinfo", "locale"},
|
||||||
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
|
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
|
||||||
RequestParameterSupported: true,
|
|
||||||
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
||||||
|
RequestParameterSupported: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, expected, res)
|
assert.Equal(t, expected, res)
|
||||||
@@ -65,6 +67,7 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Ensure well-known endpoint returns correct JWKS",
|
description: "Ensure well-known endpoint returns correct JWKS",
|
||||||
|
oidcEnabled: true,
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
req := httptest.NewRequest("GET", "/.well-known/jwks.json", nil)
|
req := httptest.NewRequest("GET", "/.well-known/jwks.json", nil)
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
@@ -73,19 +76,204 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
|
|
||||||
decodedBody := make(map[string]any)
|
decodedBody := make(map[string]any)
|
||||||
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys, ok := decodedBody["keys"].([]any)
|
keys, ok := decodedBody["keys"].([]any)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Len(t, keys, 1)
|
assert.Len(t, keys, 1)
|
||||||
|
|
||||||
keyData, ok := keys[0].(map[string]any)
|
keyData, ok := keys[0].(map[string]any)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "RSA", keyData["kty"])
|
assert.Equal(t, "RSA", keyData["kty"])
|
||||||
assert.Equal(t, "sig", keyData["use"])
|
assert.Equal(t, "sig", keyData["use"])
|
||||||
assert.Equal(t, "RS256", keyData["alg"])
|
assert.Equal(t, "RS256", keyData["alg"])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure openid configuration returns 500 on nil oidc service",
|
||||||
|
oidcEnabled: false,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 500, recorder.Code)
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "OIDC service not configured", decodedBody["message"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure jwks endpoint returns 500 on nil oidc service",
|
||||||
|
oidcEnabled: false,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/.well-known/jwks.json", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 500, recorder.Code)
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "OIDC service not configured", decodedBody["message"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure webfinger returns 400 on invalid resource",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/.well-known/webfinger?resource=invalid-resource", nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 400, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "invalid resource", decodedBody["message"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure webfinger resource validator allows acct",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := "acct:testuser@example.com"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s", url.QueryEscape(resource)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure webfinger resource validator allows https",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := "https://example.com/testuser"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s", url.QueryEscape(resource)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure webfinger resource validator allows http",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := "http://example.com/testuser"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s", url.QueryEscape(resource)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Webfinger should return no links when oidc is nil",
|
||||||
|
oidcEnabled: false,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := "acct:testuser@example.com"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s", url.QueryEscape(resource)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
links, ok := decodedBody["links"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Len(t, links, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Webfinger should return links when oidc is configured and no rel is provided",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := "acct:testuser@example.com"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s", url.QueryEscape(resource)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
links, ok := decodedBody["links"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Len(t, links, 1)
|
||||||
|
|
||||||
|
linkData, ok := links[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "http://openid.net/specs/connect/1.0/issuer", linkData["rel"])
|
||||||
|
assert.Equal(t, runtime.AppURL, linkData["href"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Webfinger should return links when oidc is configured and rel is provided",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := fmt.Sprintf("acct:%s@%s", "testuser", runtime.AppURL)
|
||||||
|
rel := "http://openid.net/specs/connect/1.0/issuer"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s&rel=%s", url.QueryEscape(resource), url.QueryEscape(rel)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
links, ok := decodedBody["links"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Len(t, links, 1)
|
||||||
|
|
||||||
|
linkData, ok := links[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, rel, linkData["rel"])
|
||||||
|
assert.Equal(t, runtime.AppURL, linkData["href"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Webfinger should return no links when oidc is configured and rel is provided but does not match",
|
||||||
|
oidcEnabled: true,
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
resource := "acct:testuser@example.com"
|
||||||
|
rel := "http://example.com/does-not-exist"
|
||||||
|
req := httptest.NewRequest("GET", fmt.Sprintf("/.well-known/webfinger?resource=%s&rel=%s", url.QueryEscape(resource), url.QueryEscape(rel)), nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
assert.Equal(t, "application/jrd+json", recorder.Header().Get("content-type"))
|
||||||
|
assert.Equal(t, "*", recorder.Header().Get("access-control-allow-origin"))
|
||||||
|
|
||||||
|
decodedBody := make(map[string]any)
|
||||||
|
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
links, ok := decodedBody["links"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Len(t, links, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
@@ -93,7 +281,13 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
|
|
||||||
store := memory.New()
|
store := memory.New()
|
||||||
|
|
||||||
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg)
|
oidcService, err := service.NewOIDCService(service.OIDCServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Queries: store,
|
||||||
|
Ding: dg,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -103,7 +297,15 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
controller.NewWellKnownController(oidcService, &router.RouterGroup)
|
wellKnownControllerInput := WellKnownControllerInput{
|
||||||
|
RouterGroup: &router.RouterGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.oidcEnabled {
|
||||||
|
wellKnownControllerInput.OIDCService = oidcService
|
||||||
|
}
|
||||||
|
|
||||||
|
NewWellKnownController(wellKnownControllerInput)
|
||||||
|
|
||||||
test.run(t, router, recorder)
|
test.run(t, router, recorder)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"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/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -37,25 +38,29 @@ var (
|
|||||||
|
|
||||||
type ContextMiddleware struct {
|
type ContextMiddleware struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
runtime model.RuntimeConfig
|
runtime *model.RuntimeConfig
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
broker *service.OAuthBrokerService
|
broker *service.OAuthBrokerService
|
||||||
tailscale *service.TailscaleService
|
tailscale *service.TailscaleService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContextMiddleware(
|
type ContextMiddlewareInput struct {
|
||||||
log *logger.Logger,
|
dig.In
|
||||||
runtime model.RuntimeConfig,
|
|
||||||
auth *service.AuthService,
|
Log *logger.Logger
|
||||||
broker *service.OAuthBrokerService,
|
RuntimeConfig *model.RuntimeConfig
|
||||||
tailscale *service.TailscaleService,
|
AuthService *service.AuthService
|
||||||
) *ContextMiddleware {
|
BrokerService *service.OAuthBrokerService
|
||||||
|
TailscaleService *service.TailscaleService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware {
|
||||||
return &ContextMiddleware{
|
return &ContextMiddleware{
|
||||||
log: log,
|
log: i.Log,
|
||||||
runtime: runtime,
|
runtime: i.RuntimeConfig,
|
||||||
auth: auth,
|
auth: i.AuthService,
|
||||||
broker: broker,
|
broker: i.BrokerService,
|
||||||
tailscale: tailscale,
|
tailscale: i.TailscaleService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
uuid, err := c.Cookie(m.runtime.SessionCookieName)
|
uuid, err := c.Cookie(m.runtime.SessionCookieName)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid, c.RemoteIP())
|
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid, c.ClientIP())
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if cookie != nil {
|
if cookie != nil {
|
||||||
@@ -107,10 +112,10 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
// Lastly check if we have a tailscale session to add
|
// Lastly check if we have a tailscale session to add
|
||||||
if m.tailscale != nil {
|
if m.tailscale != nil {
|
||||||
tailscaleContext, err := m.tailscaleWhois(c.Request.Context(), c.RemoteIP())
|
tailscaleContext, err := m.tailscaleWhois(c.ClientIP())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.App.Error().Err(err).Msgf("Error performing tailscale whois for IP %s: %v", c.RemoteIP(), err)
|
m.log.App.Error().Err(err).Msgf("Error performing tailscale whois for IP %s: %v", c.ClientIP(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tailscaleContext != nil {
|
if tailscaleContext != nil {
|
||||||
@@ -162,7 +167,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip stri
|
|||||||
userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.runtime.CookieDomain)
|
userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.runtime.CookieDomain)
|
||||||
}
|
}
|
||||||
case model.ProviderTailscale:
|
case model.ProviderTailscale:
|
||||||
tailscaleContext, err := m.tailscaleWhois(ctx, ip)
|
tailscaleContext, err := m.tailscaleWhois(ip)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error performing tailscale whois: %w", err)
|
return nil, nil, fmt.Errorf("error performing tailscale whois: %w", err)
|
||||||
@@ -172,6 +177,10 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip stri
|
|||||||
return nil, nil, fmt.Errorf("tailscale whois returned no result for IP: %s", ip)
|
return nil, nil, fmt.Errorf("tailscale whois returned no result for IP: %s", ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tailscaleContext.Email != userContext.Tailscale.Email {
|
||||||
|
return nil, nil, fmt.Errorf("device owner mismatch, login again")
|
||||||
|
}
|
||||||
|
|
||||||
userContext.Tailscale = tailscaleContext
|
userContext.Tailscale = tailscaleContext
|
||||||
case model.ProviderLDAP:
|
case model.ProviderLDAP:
|
||||||
search, err := m.auth.SearchUser(userContext.LDAP.Username)
|
search, err := m.auth.SearchUser(userContext.LDAP.Username)
|
||||||
@@ -303,12 +312,12 @@ func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextMiddleware) tailscaleWhois(ctx context.Context, ip string) (*model.TailscaleContext, error) {
|
func (m *ContextMiddleware) tailscaleWhois(ip string) (*model.TailscaleContext, error) {
|
||||||
if m.tailscale == nil {
|
if m.tailscale == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
whois, err := m.tailscale.Whois(ctx, ip)
|
whois, err := m.tailscale.Whois(ip)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.App.Error().Err(err).Msgf("Error performing Tailscale whois for IP %s: %v", ip, err)
|
m.log.App.Error().Err(err).Msgf("Error performing Tailscale whois for IP %s: %v", ip, err)
|
||||||
@@ -319,13 +328,15 @@ func (m *ContextMiddleware) tailscaleWhois(ctx context.Context, ip string) (*mod
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
username := strings.Replace(whois.LoginName, "@", "_", 1)
|
||||||
|
|
||||||
uctx := model.TailscaleContext{
|
uctx := model.TailscaleContext{
|
||||||
BaseContext: model.BaseContext{
|
BaseContext: model.BaseContext{
|
||||||
Username: whois.NodeName,
|
Username: username,
|
||||||
Email: whois.LoginName,
|
Email: whois.LoginName,
|
||||||
Name: whois.DisplayName,
|
Name: whois.DisplayName,
|
||||||
},
|
},
|
||||||
UserID: whois.UserID,
|
NodeName: whois.NodeName,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &uctx, nil
|
return &uctx, nil
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package middleware_test
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
"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/middleware"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
||||||
@@ -254,13 +253,37 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
|
|
||||||
store := memory.New()
|
store := memory.New()
|
||||||
|
|
||||||
policyEngine, err := service.NewPolicyEngine(cfg, log)
|
policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
|
Log: log,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Ctx: ctx,
|
||||||
|
})
|
||||||
|
authService := service.NewAuthService(service.AuthServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Ctx: ctx,
|
||||||
|
Ding: dg,
|
||||||
|
LDAP: nil,
|
||||||
|
Queries: store,
|
||||||
|
OAuthBroker: broker,
|
||||||
|
Tailscale: nil,
|
||||||
|
PolicyEngine: policyEngine,
|
||||||
|
})
|
||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil)
|
contextMiddleware := NewContextMiddleware(ContextMiddlewareInput{
|
||||||
|
Log: log,
|
||||||
|
RuntimeConfig: &runtime,
|
||||||
|
AuthService: authService,
|
||||||
|
BrokerService: broker,
|
||||||
|
TailscaleService: nil,
|
||||||
|
})
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
authService.ClearLoginAttempts()
|
authService.ClearLoginAttempts()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/assets"
|
"github.com/tinyauthapp/tinyauth/internal/assets"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -18,7 +19,12 @@ type UIMiddleware struct {
|
|||||||
uiFileServer http.Handler
|
uiFileServer http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUIMiddleware() (*UIMiddleware, error) {
|
// for future use if we need to inject dependencies into the middleware
|
||||||
|
type UIMiddlewareInput struct {
|
||||||
|
dig.In
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUIMiddleware(_ UIMiddlewareInput) (*UIMiddleware, error) {
|
||||||
m := &UIMiddleware{}
|
m := &UIMiddleware{}
|
||||||
|
|
||||||
ui, err := fs.Sub(assets.FrontendAssets, "dist")
|
ui, err := fs.Sub(assets.FrontendAssets, "dist")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// See context middleware for explanation of why we have to do this
|
// See context middleware for explanation of why we have to do this
|
||||||
@@ -21,9 +22,15 @@ type ZerologMiddleware struct {
|
|||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewZerologMiddleware(log *logger.Logger) *ZerologMiddleware {
|
type ZerologMiddlewareInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
|
Log *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZerologMiddleware(i ZerologMiddlewareInput) *ZerologMiddleware {
|
||||||
return &ZerologMiddleware{
|
return &ZerologMiddleware{
|
||||||
log: log,
|
log: i.Log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+172
-141
@@ -1,8 +1,30 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RuntimeEnv int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RuntimeEnvUnknown RuntimeEnv = iota
|
||||||
|
RuntimeEnvDocker
|
||||||
|
)
|
||||||
|
|
||||||
|
func DetectRuntimeEnv() RuntimeEnv {
|
||||||
|
env := os.Getenv("RUNTIME_ENV")
|
||||||
|
switch env {
|
||||||
|
case "docker":
|
||||||
|
return RuntimeEnvDocker
|
||||||
|
default:
|
||||||
|
return RuntimeEnvUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
func NewDefaultConfiguration() *Config {
|
func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
|
||||||
return &Config{
|
cfg := &Config{
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Driver: "sqlite",
|
Driver: "sqlite",
|
||||||
Path: "./tinyauth.db",
|
Path: "./tinyauth.db",
|
||||||
@@ -17,7 +39,6 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: 3000,
|
Port: 3000,
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
ConcurrentListenersEnabled: false,
|
|
||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
SubdomainsEnabled: true,
|
SubdomainsEnabled: true,
|
||||||
@@ -28,6 +49,7 @@ func NewDefaultConfiguration() *Config {
|
|||||||
ACLs: ACLsConfig{
|
ACLs: ACLsConfig{
|
||||||
Policy: "allow",
|
Policy: "allow",
|
||||||
},
|
},
|
||||||
|
LockdownEnabled: true,
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
@@ -63,243 +85,252 @@ func NewDefaultConfiguration() *Config {
|
|||||||
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
PublicKeyPath: "./tinyauth_oidc_key.pub",
|
||||||
},
|
},
|
||||||
Tailscale: TailscaleConfig{
|
Tailscale: TailscaleConfig{
|
||||||
Dir: "./tailscale_state",
|
CacheDuration: int(time.Duration(5 * time.Minute).Seconds()),
|
||||||
},
|
},
|
||||||
LabelProvider: "auto",
|
LabelProvider: "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply path overrides for docker runtime
|
||||||
|
if runtimeEnv == RuntimeEnvDocker {
|
||||||
|
cfg.Database.Path = "/data/tinyauth.db"
|
||||||
|
cfg.Resources.Path = "/data/resources"
|
||||||
|
cfg.OIDC.PrivateKeyPath = "/data/oidc/key.pem"
|
||||||
|
cfg.OIDC.PublicKeyPath = "/data/oidc/key.pub"
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
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,omitempty"`
|
||||||
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
|
||||||
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"`
|
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
|
||||||
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"`
|
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
|
||||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
|
||||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
|
||||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
|
||||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
|
||||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
|
||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
|
||||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
|
||||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
|
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
|
||||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
|
||||||
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
|
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
|
||||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver"`
|
Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver,omitempty"`
|
||||||
Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path"`
|
Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsConfig struct {
|
type AnalyticsConfig struct {
|
||||||
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled"`
|
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourcesConfig struct {
|
type ResourcesConfig struct {
|
||||||
Enabled bool `description:"Enable the resources server." yaml:"enabled"`
|
Enabled bool `description:"Enable the resources server." yaml:"enabled,omitempty"`
|
||||||
Path string `description:"The directory where resources are stored." yaml:"path"`
|
Path string `description:"The directory where resources are stored." yaml:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `description:"The port on which the server listens." yaml:"port"`
|
Port int `description:"The port on which the server listens." yaml:"port,omitempty"`
|
||||||
Address string `description:"The address on which the server listens." yaml:"address"`
|
Address string `description:"The address on which the server listens." yaml:"address,omitempty"`
|
||||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
|
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
|
||||||
ConcurrentListenersEnabled bool `description:"Enable listening on both TCP and Unix socket at the same time." yaml:"concurrentListenersEnabled"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
IP IPConfig `description:"IP whitelisting config options." yaml:"ip,omitempty"`
|
||||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users,omitempty"`
|
||||||
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
|
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled,omitempty"`
|
||||||
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes,omitempty"`
|
||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile,omitempty"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie,omitempty"`
|
||||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry,omitempty"`
|
||||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
|
||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"`
|
||||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"`
|
||||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
|
||||||
|
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAttributes struct {
|
type UserAttributes struct {
|
||||||
Name string `description:"Full name of the user." yaml:"name"`
|
Name string `description:"Full name of the user." yaml:"name,omitempty"`
|
||||||
GivenName string `description:"Given (first) name of the user." yaml:"givenName"`
|
GivenName string `description:"Given (first) name of the user." yaml:"givenName,omitempty"`
|
||||||
FamilyName string `description:"Family (last) name of the user." yaml:"familyName"`
|
FamilyName string `description:"Family (last) name of the user." yaml:"familyName,omitempty"`
|
||||||
MiddleName string `description:"Middle name of the user." yaml:"middleName"`
|
MiddleName string `description:"Middle name of the user." yaml:"middleName,omitempty"`
|
||||||
Nickname string `description:"Nickname of the user." yaml:"nickname"`
|
Nickname string `description:"Nickname of the user." yaml:"nickname,omitempty"`
|
||||||
Profile string `description:"URL of the user's profile page." yaml:"profile"`
|
Profile string `description:"URL of the user's profile page." yaml:"profile,omitempty"`
|
||||||
Picture string `description:"URL of the user's profile picture." yaml:"picture"`
|
Picture string `description:"URL of the user's profile picture." yaml:"picture,omitempty"`
|
||||||
Website string `description:"URL of the user's website." yaml:"website"`
|
Website string `description:"URL of the user's website." yaml:"website,omitempty"`
|
||||||
Email string `description:"Email address of the user." yaml:"email"`
|
Email string `description:"Email address of the user." yaml:"email,omitempty"`
|
||||||
Gender string `description:"Gender of the user." yaml:"gender"`
|
Gender string `description:"Gender of the user." yaml:"gender,omitempty"`
|
||||||
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"`
|
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate,omitempty"`
|
||||||
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"`
|
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo,omitempty"`
|
||||||
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"`
|
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale,omitempty"`
|
||||||
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"`
|
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber,omitempty"`
|
||||||
Address AddressClaim `description:"Address of the user." yaml:"address"`
|
Address AddressClaim `description:"Address of the user." yaml:"address,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddressClaim struct {
|
type AddressClaim struct {
|
||||||
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted" json:"formatted,omitempty"`
|
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted,omitempty" json:"formatted,omitempty"`
|
||||||
StreetAddress string `description:"Street address." yaml:"streetAddress" json:"street_address,omitempty"`
|
StreetAddress string `description:"Street address." yaml:"streetAddress,omitempty" json:"street_address,omitempty"`
|
||||||
Locality string `description:"City or locality." yaml:"locality" json:"locality,omitempty"`
|
Locality string `description:"City or locality." yaml:"locality,omitempty" json:"locality,omitempty"`
|
||||||
Region string `description:"State, province, or region." yaml:"region" json:"region,omitempty"`
|
Region string `description:"State, province, or region." yaml:"region,omitempty" json:"region,omitempty"`
|
||||||
PostalCode string `description:"Zip or postal code." yaml:"postalCode" json:"postal_code,omitempty"`
|
PostalCode string `description:"Zip or postal code." yaml:"postalCode,omitempty" json:"postal_code,omitempty"`
|
||||||
Country string `description:"Country." yaml:"country" json:"country,omitempty"`
|
Country string `description:"Country." yaml:"country,omitempty" json:"country,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IPConfig struct {
|
type IPConfig struct {
|
||||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow,omitempty"`
|
||||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block,omitempty"`
|
||||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass"`
|
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthConfig struct {
|
type OAuthConfig struct {
|
||||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist,omitempty"`
|
||||||
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"`
|
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile,omitempty"`
|
||||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect,omitempty"`
|
||||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCConfig struct {
|
type OIDCConfig struct {
|
||||||
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath"`
|
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath,omitempty"`
|
||||||
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath"`
|
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath,omitempty"`
|
||||||
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
|
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
Title string `description:"The title of the UI." yaml:"title"`
|
Title string `description:"The title of the UI." yaml:"title,omitempty"`
|
||||||
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
|
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage,omitempty"`
|
||||||
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"`
|
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage,omitempty"`
|
||||||
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPConfig struct {
|
type LDAPConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address,omitempty"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn,omitempty"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword,omitempty"`
|
||||||
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile"`
|
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile,omitempty"`
|
||||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn,omitempty"`
|
||||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure,omitempty"`
|
||||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter,omitempty"`
|
||||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert,omitempty"`
|
||||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey,omitempty"`
|
||||||
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
|
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"`
|
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level,omitempty"`
|
||||||
Json bool `description:"Enable JSON formatted logs." yaml:"json"`
|
Json bool `description:"Enable JSON formatted logs." yaml:"json,omitempty"`
|
||||||
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
|
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStreams struct {
|
type LogStreams struct {
|
||||||
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
|
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http,omitempty"`
|
||||||
App LogStreamConfig `description:"Application logging." yaml:"app"`
|
App LogStreamConfig `description:"Application logging." yaml:"app,omitempty"`
|
||||||
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"`
|
Audit LogStreamConfig `description:"Audit logging." yaml:"audit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStreamConfig struct {
|
type LogStreamConfig struct {
|
||||||
Enabled bool `description:"Enable this log stream." yaml:"enabled"`
|
Enabled bool `description:"Enable this log stream." yaml:"enabled,omitempty"`
|
||||||
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
|
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// no experimental features
|
|
||||||
type ExperimentalConfig struct{}
|
type ExperimentalConfig struct{}
|
||||||
|
|
||||||
type TailscaleConfig struct {
|
type TailscaleConfig struct {
|
||||||
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"`
|
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
|
||||||
Dir string `description:"Tailscale state directory." yaml:"dir"`
|
APIToken string `description:"Tailscale API token." yaml:"apiToken,omitempty"`
|
||||||
Hostname string `description:"Tailscale hostname." yaml:"hostname"`
|
APITokenFile string `description:"Path to the file containing the Tailscale API token." yaml:"apiTokenFile,omitempty"`
|
||||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
|
Tailnet string `description:"Tailnet name." yaml:"tailnet,omitempty"`
|
||||||
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"`
|
CacheDuration int `description:"Cache duration for Tailscale device and user lists in seconds." yaml:"cacheDuration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth/OIDC config
|
// OAuth/OIDC config
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
|
||||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
|
||||||
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"`
|
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
|
||||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist"`
|
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
|
||||||
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile"`
|
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
|
||||||
Scopes []string `description:"OAuth scopes." yaml:"scopes"`
|
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
|
||||||
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"`
|
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
|
||||||
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"`
|
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
|
||||||
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"`
|
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
|
||||||
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"`
|
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
|
||||||
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
|
||||||
Name string `description:"Provider name in UI." yaml:"name"`
|
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCClientConfig struct {
|
type OIDCClientConfig struct {
|
||||||
ID string `description:"OIDC client ID." yaml:"-"`
|
ID string `description:"OIDC client ID." yaml:"-"`
|
||||||
ClientID string `description:"OIDC client ID." yaml:"clientId"`
|
ClientID string `description:"OIDC client ID." yaml:"clientId,omitempty"`
|
||||||
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"`
|
||||||
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
|
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile,omitempty"`
|
||||||
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"`
|
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"`
|
||||||
Name string `description:"Client name in UI." yaml:"name"`
|
Name string `description:"Client name in UI." yaml:"name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ACLsConfig struct {
|
type ACLsConfig struct {
|
||||||
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
|
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
Apps map[string]App `description:"App ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"App ACLs configuration." yaml:"apps,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Config AppConfig `description:"App configuration." yaml:"config"`
|
Config AppConfig `description:"App configuration." yaml:"config,omitempty"`
|
||||||
Users AppUsers `description:"User access configuration." yaml:"users"`
|
Users AppUsers `description:"User access configuration." yaml:"users,omitempty"`
|
||||||
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth"`
|
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth,omitempty"`
|
||||||
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
IP AppIP `description:"IP access configuration." yaml:"ip,omitempty"`
|
||||||
Response AppResponse `description:"Response customization." yaml:"response"`
|
Response AppResponse `description:"Response customization." yaml:"response,omitempty"`
|
||||||
Path AppPath `description:"Path access configuration." yaml:"path"`
|
Path AppPath `description:"Path access configuration." yaml:"path,omitempty"`
|
||||||
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"`
|
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Domain string `description:"The domain of the app." yaml:"domain"`
|
Domain string `description:"The domain of the app." yaml:"domain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppUsers struct {
|
type AppUsers struct {
|
||||||
Allow string `description:"Comma-separated list of allowed users." yaml:"allow"`
|
Allow string `description:"Comma-separated list of allowed users." yaml:"allow,omitempty"`
|
||||||
Block string `description:"Comma-separated list of blocked users." yaml:"block"`
|
Block string `description:"Comma-separated list of blocked users." yaml:"block,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppOAuth struct {
|
type AppOAuth struct {
|
||||||
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist"`
|
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist,omitempty"`
|
||||||
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppLDAP struct {
|
type AppLDAP struct {
|
||||||
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"`
|
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppIP struct {
|
type AppIP struct {
|
||||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow,omitempty"`
|
||||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block,omitempty"`
|
||||||
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass"`
|
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppResponse struct {
|
type AppResponse struct {
|
||||||
Headers []string `description:"Custom headers to add to the response." yaml:"headers"`
|
Headers []string `description:"Custom headers to add to the response." yaml:"headers,omitempty"`
|
||||||
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth"`
|
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppBasicAuth struct {
|
type AppBasicAuth struct {
|
||||||
Username string `description:"Basic auth username." yaml:"username"`
|
Username string `description:"Basic auth username." yaml:"username,omitempty"`
|
||||||
Password string `description:"Basic auth password." yaml:"password"`
|
Password string `description:"Basic auth password." yaml:"password,omitempty"`
|
||||||
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile"`
|
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppPath struct {
|
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,omitempty"`
|
||||||
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
Block string `description:"Comma-separated list of blocked paths." yaml:"block,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ var OverrideProviders = map[string]string{
|
|||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ReservedProviderNames = []string{"local", "ldap", "tailscale"}
|
||||||
|
|
||||||
const SessionCookieName = "tinyauth-session"
|
const SessionCookieName = "tinyauth-session"
|
||||||
const CSRFCookieName = "tinyauth-csrf"
|
const CSRFCookieName = "tinyauth-csrf"
|
||||||
const RedirectCookieName = "tinyauth-redirect"
|
const RedirectCookieName = "tinyauth-redirect"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const (
|
|||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
Authenticated bool
|
Authenticated bool
|
||||||
Provider ProviderType
|
Provider ProviderType
|
||||||
|
AuthTime int64
|
||||||
Local *LocalContext
|
Local *LocalContext
|
||||||
OAuth *OAuthContext
|
OAuth *OAuthContext
|
||||||
LDAP *LDAPContext
|
LDAP *LDAPContext
|
||||||
@@ -58,7 +59,7 @@ type LDAPContext struct {
|
|||||||
|
|
||||||
type TailscaleContext struct {
|
type TailscaleContext struct {
|
||||||
BaseContext
|
BaseContext
|
||||||
UserID string
|
NodeName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) IsAuthenticated() bool {
|
func (c *UserContext) IsAuthenticated() bool {
|
||||||
@@ -110,6 +111,7 @@ func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
|||||||
func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) {
|
func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) {
|
||||||
*c = UserContext{
|
*c = UserContext{
|
||||||
Authenticated: !session.TotpPending,
|
Authenticated: !session.TotpPending,
|
||||||
|
AuthTime: session.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch session.Provider {
|
switch session.Provider {
|
||||||
@@ -247,7 +249,7 @@ func (c *UserContext) OAuthName() string {
|
|||||||
|
|
||||||
func (c *UserContext) TailscaleNodeName() string {
|
func (c *UserContext) TailscaleNodeName() string {
|
||||||
if c.Tailscale != nil {
|
if c.Tailscale != nil {
|
||||||
return c.Tailscale.Username
|
return c.Tailscale.NodeName
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package model_test
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"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/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,44 +21,44 @@ func TestContext(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
description string
|
description string
|
||||||
context *model.UserContext
|
context *UserContext
|
||||||
run func(*testing.T, *model.UserContext) any
|
run func(*testing.T, *UserContext) any
|
||||||
expected any
|
expected any
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
description: "IsAuthenticated reflects Authenticated field",
|
description: "IsAuthenticated reflects Authenticated field",
|
||||||
context: &model.UserContext{Authenticated: true},
|
context: &UserContext{Authenticated: true},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsAuthenticated() },
|
run: func(t *testing.T, c *UserContext) any { return c.IsAuthenticated() },
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "IsLocal returns true for ProviderLocal",
|
description: "IsLocal returns true for ProviderLocal",
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal, Local: &model.LocalContext{}},
|
context: &UserContext{Provider: ProviderLocal, Local: &LocalContext{}},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsLocal() },
|
run: func(t *testing.T, c *UserContext) any { return c.IsLocal() },
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "IsOAuth returns true for ProviderOAuth",
|
description: "IsOAuth returns true for ProviderOAuth",
|
||||||
context: &model.UserContext{Provider: model.ProviderOAuth, OAuth: &model.OAuthContext{}},
|
context: &UserContext{Provider: ProviderOAuth, OAuth: &OAuthContext{}},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsOAuth() },
|
run: func(t *testing.T, c *UserContext) any { return c.IsOAuth() },
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "IsLDAP returns true for ProviderLDAP",
|
description: "IsLDAP returns true for ProviderLDAP",
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP, LDAP: &model.LDAPContext{}},
|
context: &UserContext{Provider: ProviderLDAP, LDAP: &LDAPContext{}},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsLDAP() },
|
run: func(t *testing.T, c *UserContext) any { return c.IsLDAP() },
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "IsBasicAuth returns true for ProviderBasicAuth",
|
description: "IsBasicAuth returns true for ProviderBasicAuth",
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth, Local: &model.LocalContext{}},
|
context: &UserContext{Provider: ProviderBasicAuth, Local: &LocalContext{}},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.IsBasicAuth() },
|
run: func(t *testing.T, c *UserContext) any { return c.IsBasicAuth() },
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromSession local session is authenticated and ProviderLocal",
|
description: "NewFromSession local session is authenticated and ProviderLocal",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
got, err := c.NewFromSession(&repository.Session{
|
||||||
Username: "alice", Email: "alice@example.com", Name: "Alice",
|
Username: "alice", Email: "alice@example.com", Name: "Alice",
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
@@ -67,12 +66,12 @@ func TestContext(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return [2]any{got.Provider, got.Authenticated}
|
return [2]any{got.Provider, got.Authenticated}
|
||||||
},
|
},
|
||||||
expected: [2]any{model.ProviderLocal, true},
|
expected: [2]any{ProviderLocal, true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromSession local session with TotpPending is not authenticated",
|
description: "NewFromSession local session with TotpPending is not authenticated",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
got, err := c.NewFromSession(&repository.Session{
|
||||||
Username: "bob", Provider: "local", TotpPending: true,
|
Username: "bob", Provider: "local", TotpPending: true,
|
||||||
})
|
})
|
||||||
@@ -83,20 +82,20 @@ func TestContext(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromSession ldap session is ProviderLDAP",
|
description: "NewFromSession ldap session is ProviderLDAP",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
got, err := c.NewFromSession(&repository.Session{
|
||||||
Username: "carol", Provider: "ldap",
|
Username: "carol", Provider: "ldap",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return got.Provider
|
return got.Provider
|
||||||
},
|
},
|
||||||
expected: model.ProviderLDAP,
|
expected: ProviderLDAP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromSession unknown provider defaults to OAuth and populates oauth fields",
|
description: "NewFromSession unknown provider defaults to OAuth and populates oauth fields",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
got, err := c.NewFromSession(&repository.Session{
|
got, err := c.NewFromSession(&repository.Session{
|
||||||
Username: "dave", Provider: "github",
|
Username: "dave", Provider: "github",
|
||||||
OAuthGroups: "devs,admins", OAuthSub: "sub-123", OAuthName: "GitHub",
|
OAuthGroups: "devs,admins", OAuthSub: "sub-123", OAuthName: "GitHub",
|
||||||
@@ -104,126 +103,126 @@ func TestContext(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return [5]any{got.Provider, got.OAuth.ID, got.OAuth.Sub, got.OAuth.DisplayName, got.OAuth.Groups}
|
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"}},
|
expected: [5]any{ProviderOAuth, "github", "sub-123", "GitHub", []string{"devs", "admins"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Local getters return BaseContext fields",
|
description: "Local getters return BaseContext fields",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderLocal,
|
Provider: ProviderLocal,
|
||||||
Local: &model.LocalContext{BaseContext: model.BaseContext{Username: "alice", Email: "alice@example.com", Name: "Alice"}},
|
Local: &LocalContext{BaseContext: BaseContext{Username: "alice", Email: "alice@example.com", Name: "Alice"}},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
||||||
},
|
},
|
||||||
expected: [3]string{"alice", "alice@example.com", "Alice"},
|
expected: [3]string{"alice", "alice@example.com", "Alice"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "BasicAuth getters fall back to local fields",
|
description: "BasicAuth getters fall back to local fields",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderBasicAuth,
|
Provider: ProviderBasicAuth,
|
||||||
Local: &model.LocalContext{BaseContext: model.BaseContext{Username: "bob", Email: "bob@example.com", Name: "Bob"}},
|
Local: &LocalContext{BaseContext: BaseContext{Username: "bob", Email: "bob@example.com", Name: "Bob"}},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
||||||
},
|
},
|
||||||
expected: [3]string{"bob", "bob@example.com", "Bob"},
|
expected: [3]string{"bob", "bob@example.com", "Bob"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "LDAP getters return LDAP fields",
|
description: "LDAP getters return LDAP fields",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderLDAP,
|
Provider: ProviderLDAP,
|
||||||
LDAP: &model.LDAPContext{BaseContext: model.BaseContext{Username: "carol", Email: "carol@example.com", Name: "Carol"}},
|
LDAP: &LDAPContext{BaseContext: BaseContext{Username: "carol", Email: "carol@example.com", Name: "Carol"}},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
||||||
},
|
},
|
||||||
expected: [3]string{"carol", "carol@example.com", "Carol"},
|
expected: [3]string{"carol", "carol@example.com", "Carol"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "OAuth getters return OAuth fields",
|
description: "OAuth getters return OAuth fields",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderOAuth,
|
Provider: ProviderOAuth,
|
||||||
OAuth: &model.OAuthContext{BaseContext: model.BaseContext{Username: "dave", Email: "dave@example.com", Name: "Dave"}},
|
OAuth: &OAuthContext{BaseContext: BaseContext{Username: "dave", Email: "dave@example.com", Name: "Dave"}},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
||||||
},
|
},
|
||||||
expected: [3]string{"dave", "dave@example.com", "Dave"},
|
expected: [3]string{"dave", "dave@example.com", "Dave"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns 'local' for ProviderLocal",
|
description: "ProviderName returns 'local' for ProviderLocal",
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal},
|
context: &UserContext{Provider: ProviderLocal},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
run: func(t *testing.T, c *UserContext) any { return c.GetProviderID() },
|
||||||
expected: "local",
|
expected: "local",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns 'local' for ProviderBasicAuth",
|
description: "ProviderName returns 'local' for ProviderBasicAuth",
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth},
|
context: &UserContext{Provider: ProviderBasicAuth},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
run: func(t *testing.T, c *UserContext) any { return c.GetProviderID() },
|
||||||
expected: "local",
|
expected: "local",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns 'ldap' for ProviderLDAP",
|
description: "ProviderName returns 'ldap' for ProviderLDAP",
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP},
|
context: &UserContext{Provider: ProviderLDAP},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
run: func(t *testing.T, c *UserContext) any { return c.GetProviderID() },
|
||||||
expected: "ldap",
|
expected: "ldap",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns OAuth provider ID for ProviderOAuth",
|
description: "ProviderName returns OAuth provider ID for ProviderOAuth",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderOAuth,
|
Provider: ProviderOAuth,
|
||||||
OAuth: &model.OAuthContext{ID: "github"},
|
OAuth: &OAuthContext{ID: "github"},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
run: func(t *testing.T, c *UserContext) any { return c.GetProviderID() },
|
||||||
expected: "github",
|
expected: "github",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "TOTPPending returns true when local context is pending",
|
description: "TOTPPending returns true when local context is pending",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderLocal,
|
Provider: ProviderLocal,
|
||||||
Local: &model.LocalContext{TOTPPending: true},
|
Local: &LocalContext{TOTPPending: true},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.TOTPPending() },
|
run: func(t *testing.T, c *UserContext) any { return c.TOTPPending() },
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "TOTPPending returns false when local context is not pending",
|
description: "TOTPPending returns false when local context is not pending",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderLocal,
|
Provider: ProviderLocal,
|
||||||
Local: &model.LocalContext{TOTPPending: false},
|
Local: &LocalContext{TOTPPending: false},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.TOTPPending() },
|
run: func(t *testing.T, c *UserContext) any { return c.TOTPPending() },
|
||||||
expected: false,
|
expected: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "TOTPPending returns false for non-local providers",
|
description: "TOTPPending returns false for non-local providers",
|
||||||
context: &model.UserContext{Provider: model.ProviderOAuth, OAuth: &model.OAuthContext{}},
|
context: &UserContext{Provider: ProviderOAuth, OAuth: &OAuthContext{}},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.TOTPPending() },
|
run: func(t *testing.T, c *UserContext) any { return c.TOTPPending() },
|
||||||
expected: false,
|
expected: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "OAuthName returns DisplayName for ProviderOAuth",
|
description: "OAuthName returns DisplayName for ProviderOAuth",
|
||||||
context: &model.UserContext{
|
context: &UserContext{
|
||||||
Provider: model.ProviderOAuth,
|
Provider: ProviderOAuth,
|
||||||
OAuth: &model.OAuthContext{DisplayName: "Google"},
|
OAuth: &OAuthContext{DisplayName: "Google"},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.OAuthName() },
|
run: func(t *testing.T, c *UserContext) any { return c.OAuthName() },
|
||||||
expected: "Google",
|
expected: "Google",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "OAuthName returns empty string for non-oauth providers",
|
description: "OAuthName returns empty string for non-oauth providers",
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal, Local: &model.LocalContext{}},
|
context: &UserContext{Provider: ProviderLocal, Local: &LocalContext{}},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.OAuthName() },
|
run: func(t *testing.T, c *UserContext) any { return c.OAuthName() },
|
||||||
expected: "",
|
expected: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromGin populates context from gin value",
|
description: "NewFromGin populates context from gin value",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
stored := &model.UserContext{
|
stored := &UserContext{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
Provider: model.ProviderLocal,
|
Provider: ProviderLocal,
|
||||||
Local: &model.LocalContext{BaseContext: model.BaseContext{Username: "alice"}},
|
Local: &LocalContext{BaseContext: BaseContext{Username: "alice"}},
|
||||||
}
|
}
|
||||||
got, err := c.NewFromGin(newGinCtx(stored, true))
|
got, err := c.NewFromGin(newGinCtx(stored, true))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -233,17 +232,17 @@ func TestContext(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromGin returns error when context value is missing",
|
description: "NewFromGin returns error when context value is missing",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
_, err := c.NewFromGin(newGinCtx(nil, false))
|
_, err := c.NewFromGin(newGinCtx(nil, false))
|
||||||
return err.Error()
|
return err.Error()
|
||||||
},
|
},
|
||||||
expected: model.ErrUserContextNotFound.Error(),
|
expected: ErrUserContextNotFound.Error(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromGin returns error when context value has wrong type",
|
description: "NewFromGin returns error when context value has wrong type",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
_, err := c.NewFromGin(newGinCtx("not a user context", true))
|
_, err := c.NewFromGin(newGinCtx("not a user context", true))
|
||||||
return err.Error()
|
return err.Error()
|
||||||
},
|
},
|
||||||
@@ -251,17 +250,17 @@ func TestContext(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NewFromGin returns an error when context doesn't include user information",
|
description: "NewFromGin returns an error when context doesn't include user information",
|
||||||
context: &model.UserContext{},
|
context: &UserContext{},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
_, err := c.NewFromGin(newGinCtx(&model.UserContext{Provider: model.ProviderLocal}, true))
|
_, err := c.NewFromGin(newGinCtx(&UserContext{Provider: ProviderLocal}, true))
|
||||||
return err.Error()
|
return err.Error()
|
||||||
},
|
},
|
||||||
expected: "incomplete user context",
|
expected: "incomplete user context",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Getters should not panic if provider context is empty",
|
description: "Getters should not panic if provider context is empty",
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal},
|
context: &UserContext{Provider: ProviderLocal},
|
||||||
run: func(t *testing.T, c *model.UserContext) any {
|
run: func(t *testing.T, c *UserContext) any {
|
||||||
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
return [3]string{c.GetUsername(), c.GetEmail(), c.GetName()}
|
||||||
},
|
},
|
||||||
expected: [3]string{"", "", ""},
|
expected: [3]string{"", "", ""},
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ type RuntimeConfig struct {
|
|||||||
OAuthProviders map[string]OAuthServiceConfig
|
OAuthProviders map[string]OAuthServiceConfig
|
||||||
OAuthWhitelist []string
|
OAuthWhitelist []string
|
||||||
ConfiguredProviders []Provider
|
ConfiguredProviders []Provider
|
||||||
OIDCClients []OIDCClientConfig
|
|
||||||
TrustedDomains []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package postgres
|
package postgres
|
||||||
|
|
||||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
|
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package sqlite
|
package sqlite
|
||||||
|
|
||||||
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
|
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LabelProvider interface {
|
type LabelProvider interface {
|
||||||
@@ -14,17 +15,23 @@ type LabelProvider interface {
|
|||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
config *model.Config
|
config *model.Config
|
||||||
labelProvider *LabelProvider
|
labelProvider LabelProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(
|
type AccessControlServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) *AccessControlsService {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
LabelProvider LabelProvider `optional:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsService {
|
||||||
|
|
||||||
return &AccessControlsService{
|
return &AccessControlsService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
config: deps.StaticConfig,
|
config: i.Config,
|
||||||
labelProvider: &deps.LabelProvider,
|
labelProvider: i.LabelProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +63,8 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we have a label provider configured, try to get ACLs from it
|
// If we have a label provider configured, try to get ACLs from it
|
||||||
if service.labelProvider != nil && *service.labelProvider != nil {
|
if service.labelProvider != nil {
|
||||||
return (*service.labelProvider).GetLabels(domain)
|
return service.labelProvider.GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// no labels
|
// no labels
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ func TestLookupStaticACLs(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
svc := NewAccessControlsService(log, model.Config{Apps: tt.apps}, nil)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &model.Config{Apps: tt.apps},
|
||||||
|
LabelProvider: nil,
|
||||||
|
})
|
||||||
got := svc.lookupStaticACLs(tt.domain)
|
got := svc.lookupStaticACLs(tt.domain)
|
||||||
if tt.expectNil {
|
if tt.expectNil {
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
@@ -112,7 +116,11 @@ func TestGetAccessControls(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := NewAccessControlsService(log, config, nil)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &config,
|
||||||
|
LabelProvider: nil,
|
||||||
|
})
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("foo.example.com")
|
got, err := svc.GetAccessControls("foo.example.com")
|
||||||
|
|
||||||
@@ -123,7 +131,11 @@ func TestGetAccessControls(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns nil when no static match and no label provider", func(t *testing.T) {
|
t.Run("returns nil when no static match and no label provider", func(t *testing.T) {
|
||||||
svc := NewAccessControlsService(log, model.Config{}, nil)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &model.Config{},
|
||||||
|
LabelProvider: nil,
|
||||||
|
})
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("unknown.example.com")
|
got, err := svc.GetAccessControls("unknown.example.com")
|
||||||
|
|
||||||
@@ -133,7 +145,11 @@ func TestGetAccessControls(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("returns nil when label provider pointer wraps a nil interface", func(t *testing.T) {
|
t.Run("returns nil when label provider pointer wraps a nil interface", func(t *testing.T) {
|
||||||
var provider LabelProvider
|
var provider LabelProvider
|
||||||
svc := NewAccessControlsService(log, model.Config{}, &provider)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &model.Config{},
|
||||||
|
LabelProvider: provider, // nil provider
|
||||||
|
})
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("unknown.example.com")
|
got, err := svc.GetAccessControls("unknown.example.com")
|
||||||
|
|
||||||
@@ -152,7 +168,11 @@ func TestGetAccessControls(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
var provider LabelProvider = mock
|
var provider LabelProvider = mock
|
||||||
svc := NewAccessControlsService(log, model.Config{}, &provider)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &model.Config{},
|
||||||
|
LabelProvider: provider,
|
||||||
|
})
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("dynamic.example.com")
|
got, err := svc.GetAccessControls("dynamic.example.com")
|
||||||
|
|
||||||
@@ -170,7 +190,11 @@ func TestGetAccessControls(t *testing.T) {
|
|||||||
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := NewAccessControlsService(log, config, &provider)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &config,
|
||||||
|
LabelProvider: provider,
|
||||||
|
})
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("foo.example.com")
|
got, err := svc.GetAccessControls("foo.example.com")
|
||||||
|
|
||||||
@@ -188,7 +212,11 @@ func TestGetAccessControls(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
var provider LabelProvider = mock
|
var provider LabelProvider = mock
|
||||||
svc := NewAccessControlsService(log, model.Config{}, &provider)
|
svc := NewAccessControlsService(AccessControlServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &model.Config{},
|
||||||
|
LabelProvider: provider,
|
||||||
|
})
|
||||||
|
|
||||||
got, err := svc.GetAccessControls("dynamic.example.com")
|
got, err := svc.GetAccessControls("dynamic.example.com")
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
"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/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -24,7 +27,6 @@ import (
|
|||||||
// but for now these are just safety limits to prevent unbounded memory usage
|
// but for now these are just safety limits to prevent unbounded memory usage
|
||||||
const MaxOAuthPendingSessions = 256
|
const MaxOAuthPendingSessions = 256
|
||||||
const OAuthCleanupCount = 16
|
const OAuthCleanupCount = 16
|
||||||
const MaxLoginAttemptRecords = 256
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
@@ -44,7 +46,7 @@ type OAuthPendingSession struct {
|
|||||||
State string
|
State string
|
||||||
Verifier string
|
Verifier string
|
||||||
Token *oauth2.Token
|
Token *oauth2.Token
|
||||||
Service *OAuthServiceImpl
|
Service IOAuthService
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
CallbackParams OAuthCallbackParams
|
CallbackParams OAuthCallbackParams
|
||||||
}
|
}
|
||||||
@@ -80,33 +82,57 @@ type AuthService struct {
|
|||||||
oauth *CacheStore[OAuthPendingSession]
|
oauth *CacheStore[OAuthPendingSession]
|
||||||
ldap *CacheStore[[]string]
|
ldap *CacheStore[[]string]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maxLoginLimits int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(
|
type AuthServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) *AuthService {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
Runtime *model.RuntimeConfig
|
||||||
|
Ctx context.Context
|
||||||
|
Ding *ding.Ding
|
||||||
|
LDAP *LdapService `optional:"true"`
|
||||||
|
Queries repository.Store
|
||||||
|
OAuthBroker *OAuthBrokerService
|
||||||
|
Tailscale *TailscaleService `optional:"true"`
|
||||||
|
PolicyEngine *PolicyEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(i AuthServiceInput) *AuthService {
|
||||||
service := &AuthService{
|
service := &AuthService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
runtime: deps.RuntimeConfig,
|
runtime: i.Runtime,
|
||||||
ctx: deps.Ctx,
|
ctx: i.Ctx,
|
||||||
config: deps.StaticConfig,
|
config: i.Config,
|
||||||
ldap: deps.Services.LDAPService,
|
ldap: i.LDAP,
|
||||||
queries: *deps.Queries,
|
queries: i.Queries,
|
||||||
oauthBroker: deps.Services.OAuthBrokerService,
|
oauthBroker: i.OAuthBroker,
|
||||||
tailscale: deps.Services.TailscaleService,
|
tailscale: i.Tailscale,
|
||||||
policyEngine: deps.Services.PolicyEngine,
|
policyEngine: i.PolicyEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the max login limits based on the number of users and the configured max retries
|
||||||
|
service.maxLoginLimits = service.calculateLockdownLimit()
|
||||||
|
|
||||||
|
loginCacheSize := 0
|
||||||
|
|
||||||
|
if !service.config.Auth.LockdownEnabled {
|
||||||
|
loginCacheSize = service.maxLoginLimits
|
||||||
}
|
}
|
||||||
|
|
||||||
// caches setup
|
// caches setup
|
||||||
oauthCache := NewCacheStore[OAuthPendingSession](256)
|
oauthCache := NewCacheStore[OAuthPendingSession](256)
|
||||||
loginCache := NewCacheStore[LoginAttempt](1024)
|
loginCache := NewCacheStore[LoginAttempt](loginCacheSize)
|
||||||
ldapCache := NewCacheStore[[]string](1024)
|
ldapCache := NewCacheStore[[]string](1024)
|
||||||
|
|
||||||
service.caches.oauth = oauthCache
|
service.caches.oauth = oauthCache
|
||||||
service.caches.login = loginCache
|
service.caches.login = loginCache
|
||||||
service.caches.ldap = ldapCache
|
service.caches.ldap = ldapCache
|
||||||
|
|
||||||
deps.Ding.Go(func(ctx context.Context) {
|
i.Ding.Go(func(ctx context.Context) {
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -245,7 +271,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.caches.login.Size() >= MaxLoginAttemptRecords {
|
if !success && auth.config.Auth.LockdownEnabled && auth.caches.login.Size() >= auth.maxLoginLimits {
|
||||||
if locked, _ := auth.IsInLockdown(); locked {
|
if locked, _ := auth.IsInLockdown(); locked {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -354,33 +380,11 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
|||||||
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Provider == "tailscale" {
|
|
||||||
auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname")
|
|
||||||
|
|
||||||
tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname()))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: auth.runtime.SessionCookieName,
|
Name: auth.runtime.SessionCookieName,
|
||||||
Value: session.UUID,
|
Value: session.UUID,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: fmt.Sprintf(".%s", tsCookieDomain),
|
Domain: auth.getCookieDomain(),
|
||||||
Expires: expiresAt,
|
|
||||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
|
||||||
Secure: auth.config.Auth.SecureCookie,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Cookie{
|
|
||||||
Name: auth.runtime.SessionCookieName,
|
|
||||||
Value: session.UUID,
|
|
||||||
Path: "/",
|
|
||||||
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain),
|
|
||||||
Expires: expiresAt,
|
Expires: expiresAt,
|
||||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
MaxAge: int(time.Until(expiresAt).Seconds()),
|
||||||
Secure: auth.config.Auth.SecureCookie,
|
Secure: auth.config.Auth.SecureCookie,
|
||||||
@@ -433,7 +437,7 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http
|
|||||||
Name: auth.runtime.SessionCookieName,
|
Name: auth.runtime.SessionCookieName,
|
||||||
Value: session.UUID,
|
Value: session.UUID,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain),
|
Domain: auth.getCookieDomain(),
|
||||||
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
|
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
|
||||||
MaxAge: int(newExpiry - currentTime),
|
MaxAge: int(newExpiry - currentTime),
|
||||||
Secure: auth.config.Auth.SecureCookie,
|
Secure: auth.config.Auth.SecureCookie,
|
||||||
@@ -454,7 +458,7 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
|
|||||||
Name: auth.runtime.SessionCookieName,
|
Name: auth.runtime.SessionCookieName,
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain),
|
Domain: auth.getCookieDomain(),
|
||||||
Expires: time.Now(),
|
Expires: time.Now(),
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Secure: auth.config.Auth.SecureCookie,
|
Secure: auth.config.Auth.SecureCookie,
|
||||||
@@ -523,7 +527,7 @@ func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthCallbac
|
|||||||
session := OAuthPendingSession{
|
session := OAuthPendingSession{
|
||||||
State: state,
|
State: state,
|
||||||
Verifier: verifier,
|
Verifier: verifier,
|
||||||
Service: &service,
|
Service: service,
|
||||||
ExpiresAt: time.Now().Add(1 * time.Hour),
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||||
CallbackParams: params,
|
CallbackParams: params,
|
||||||
}
|
}
|
||||||
@@ -540,7 +544,7 @@ func (auth *AuthService) GetOAuthURL(sessionId string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return (*session.Service).GetAuthURL(session.State, session.Verifier), nil
|
return session.Service.GetAuthURL(session.State, session.Verifier), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) {
|
func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.Token, error) {
|
||||||
@@ -550,7 +554,7 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
|
|||||||
return nil, fmt.Errorf("oauth session not found: %s", sessionId)
|
return nil, fmt.Errorf("oauth session not found: %s", sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := (*session.Service).GetToken(code, session.Verifier)
|
token, err := session.Service.GetToken(code, session.Verifier)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
|
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
|
||||||
@@ -579,7 +583,7 @@ func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, erro
|
|||||||
return nil, fmt.Errorf("oauth token not found for session: %s", sessionId)
|
return nil, 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 nil, fmt.Errorf("failed to get userinfo: %w", err)
|
||||||
@@ -588,14 +592,14 @@ func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, erro
|
|||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetOAuthService(sessionId string) (OAuthServiceImpl, error) {
|
func (auth *AuthService) GetOAuthService(sessionId string) (IOAuthService, error) {
|
||||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return *session.Service, nil
|
return session.Service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) EndOAuthSession(sessionId string) {
|
func (auth *AuthService) EndOAuthSession(sessionId string) {
|
||||||
@@ -620,16 +624,17 @@ func (auth *AuthService) lockdownMode() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(auth.ctx)
|
||||||
|
|
||||||
auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode")
|
auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode")
|
||||||
|
|
||||||
auth.lockdown.active = true
|
auth.lockdown.active = true
|
||||||
auth.lockdown.ctx = ctx
|
auth.lockdown.ctx = ctx
|
||||||
auth.lockdown.cancelFunc = cancel
|
auth.lockdown.cancelFunc = cancel
|
||||||
auth.lockdown.until = time.Now().Add(time.Duration(auth.config.Auth.LoginTimeout) * time.Second)
|
|
||||||
|
|
||||||
timer := time.NewTimer(time.Until(auth.lockdown.until))
|
d := time.Duration(auth.config.Auth.LoginTimeout) * time.Second
|
||||||
|
auth.lockdown.until = time.Now().Add(d)
|
||||||
|
timer := time.NewTimer(d)
|
||||||
|
|
||||||
auth.lockdown.mu.Unlock()
|
auth.lockdown.mu.Unlock()
|
||||||
|
|
||||||
@@ -641,14 +646,13 @@ func (auth *AuthService) lockdownMode() {
|
|||||||
// Timer expired, end lockdown
|
// Timer expired, end lockdown
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// Context cancelled, end lockdown
|
// Context cancelled, end lockdown
|
||||||
case <-auth.ctx.Done():
|
|
||||||
// Service is shutting down, end lockdown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.lockdown.mu.Lock()
|
auth.lockdown.mu.Lock()
|
||||||
|
|
||||||
auth.log.App.Info().Msg("Exiting lockdown mode")
|
auth.log.App.Info().Msg("Exiting lockdown mode")
|
||||||
|
|
||||||
|
auth.caches.login.Clear()
|
||||||
auth.lockdown.active = false
|
auth.lockdown.active = false
|
||||||
auth.lockdown.until = time.Time{}
|
auth.lockdown.until = time.Time{}
|
||||||
auth.lockdown.ctx = nil
|
auth.lockdown.ctx = nil
|
||||||
@@ -671,3 +675,39 @@ func (auth *AuthService) IsInLockdown() (bool, int) {
|
|||||||
func (auth *AuthService) ClearLoginAttempts() {
|
func (auth *AuthService) ClearLoginAttempts() {
|
||||||
auth.caches.login.Clear()
|
auth.caches.login.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) calculateLockdownLimit() int {
|
||||||
|
userCount := len(auth.runtime.LocalUsers)
|
||||||
|
|
||||||
|
if auth.ldap != nil {
|
||||||
|
ldapUsers, err := auth.ldap.GetUserCount()
|
||||||
|
if err != nil {
|
||||||
|
auth.log.App.Warn().Err(err).Msg("Failed to get LDAP user count")
|
||||||
|
} else {
|
||||||
|
userCount += ldapUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := userCount * auth.config.Auth.LoginMaxRetries
|
||||||
|
|
||||||
|
jitter, err := rand.Int(rand.Reader, big.NewInt(64))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
auth.log.App.Warn().Err(err).Msg("Failed to generate jitter for lockdown limit")
|
||||||
|
} else {
|
||||||
|
limit += int(jitter.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit < 256 {
|
||||||
|
limit = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) getCookieDomain() string {
|
||||||
|
if !auth.config.Auth.SubdomainsEnabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return auth.runtime.CookieDomain
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
)
|
)
|
||||||
@@ -12,9 +13,22 @@ func TestIsEmailWhitelistedUsesProviderSpecificList(t *testing.T) {
|
|||||||
log := logger.NewLogger().WithTestConfig()
|
log := logger.NewLogger().WithTestConfig()
|
||||||
log.Init()
|
log.Init()
|
||||||
|
|
||||||
|
policyEngine, err := NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &model.Config{
|
||||||
|
Auth: model.AuthConfig{
|
||||||
|
ACLs: model.ACLsConfig{
|
||||||
|
Policy: string(PolicyAllow),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
auth := &AuthService{
|
auth := &AuthService{
|
||||||
log: log,
|
log: log,
|
||||||
runtime: model.RuntimeConfig{
|
runtime: &model.RuntimeConfig{
|
||||||
OAuthWhitelist: []string{"global@example.com"},
|
OAuthWhitelist: []string{"global@example.com"},
|
||||||
OAuthProviders: map[string]model.OAuthServiceConfig{
|
OAuthProviders: map[string]model.OAuthServiceConfig{
|
||||||
"github": {
|
"github": {
|
||||||
@@ -28,6 +42,7 @@ func TestIsEmailWhitelistedUsesProviderSpecificList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
policyEngine: policyEngine,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.True(t, auth.IsEmailWhitelisted("github", "github@example.com"))
|
assert.True(t, auth.IsEmailWhitelisted("github", "github@example.com"))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
container "github.com/docker/docker/api/types/container"
|
container "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
@@ -21,34 +22,40 @@ type DockerService struct {
|
|||||||
isConnected bool
|
isConnected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerService(
|
type DockerServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) (*DockerService, error) {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Ctx context.Context
|
||||||
|
Ding *ding.Ding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerService(i DockerServiceInput) (*DockerService, error) {
|
||||||
|
|
||||||
client, err := client.NewClientWithOpts(client.FromEnv)
|
client, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client.NegotiateAPIVersion(deps.Ctx)
|
client.NegotiateAPIVersion(i.Ctx)
|
||||||
|
|
||||||
_, err = client.Ping(deps.Ctx)
|
_, err = client.Ping(i.Ctx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deps.Log.App.Debug().Err(err).Msg("Docker not connected")
|
i.Log.App.Debug().Err(err).Msg("Docker not connected")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
service := &DockerService{
|
service := &DockerService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
client: client,
|
client: client,
|
||||||
context: deps.Ctx,
|
context: i.Ctx,
|
||||||
}
|
}
|
||||||
|
|
||||||
service.isConnected = true
|
service.isConnected = true
|
||||||
service.log.App.Debug().Msg("Docker connected successfully")
|
service.log.App.Debug().Msg("Docker connected successfully")
|
||||||
|
|
||||||
deps.Ding.Go(service.watchAndClose, ding.RingMajor)
|
i.Ding.Go(service.watchAndClose, ding.RingMajor)
|
||||||
|
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
@@ -48,9 +49,15 @@ type KubernetesService struct {
|
|||||||
appNameIndex map[string]ingressAppKey
|
appNameIndex map[string]ingressAppKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKubernetesService(
|
type KubernetesServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) (*KubernetesService, error) {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Ctx context.Context
|
||||||
|
Ding *ding.Ding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error) {
|
||||||
cfg, err := rest.InClusterConfig()
|
cfg, err := rest.InClusterConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get in-cluster kubernetes config: %w", err)
|
return nil, fmt.Errorf("failed to get in-cluster kubernetes config: %w", err)
|
||||||
@@ -67,31 +74,31 @@ func NewKubernetesService(
|
|||||||
Resource: "ingresses",
|
Resource: "ingresses",
|
||||||
}
|
}
|
||||||
|
|
||||||
accessCtx, accessCancel := context.WithTimeout(deps.Ctx, 5*time.Second)
|
accessCtx, accessCancel := context.WithTimeout(i.Ctx, 5*time.Second)
|
||||||
defer accessCancel()
|
defer accessCancel()
|
||||||
|
|
||||||
_, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
|
_, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deps.Log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled")
|
i.Log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled")
|
||||||
return nil, fmt.Errorf("failed to access ingress api: %w", err)
|
return nil, fmt.Errorf("failed to access ingress api: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.Log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher")
|
i.Log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher")
|
||||||
|
|
||||||
service := &KubernetesService{
|
service := &KubernetesService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
client: client,
|
client: client,
|
||||||
ingressApps: make(map[ingressKey][]ingressApp),
|
ingressApps: make(map[ingressKey][]ingressApp),
|
||||||
domainIndex: make(map[string]ingressAppKey),
|
domainIndex: make(map[string]ingressAppKey),
|
||||||
appNameIndex: make(map[string]ingressAppKey),
|
appNameIndex: make(map[string]ingressAppKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.Ding.Go(func(ctx context.Context) {
|
i.Ding.Go(func(ctx context.Context) {
|
||||||
service.watchGVR(gvr, ctx)
|
service.watchGVR(gvr, ctx)
|
||||||
}, ding.RingMajor)
|
}, ding.RingMajor)
|
||||||
|
|
||||||
service.started = true
|
service.started = true
|
||||||
deps.Log.App.Debug().Msg("Kubernetes label provider started successfully")
|
i.Log.App.Debug().Msg("Kubernetes label provider started successfully")
|
||||||
|
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,42 +13,51 @@ import (
|
|||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LdapService struct {
|
type LdapService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
ctx context.Context
|
||||||
config *model.Config
|
config *model.Config
|
||||||
|
|
||||||
conn *ldapgo.Conn
|
conn *ldapgo.Conn
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
cert *tls.Certificate
|
cert *tls.Certificate
|
||||||
ldapBindPw string
|
bindPw string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLdapService(
|
type LdapServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) (*LdapService, error) {
|
|
||||||
if deps.StaticConfig.LDAP.Address == "" {
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
Ding *ding.Ding
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLdapService(i LdapServiceInput) (*LdapService, error) {
|
||||||
|
if i.Config.LDAP.Address == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ldapBindPw := utils.GetSecret(deps.StaticConfig.LDAP.BindPassword, deps.StaticConfig.LDAP.BindPasswordFile)
|
|
||||||
|
|
||||||
ldap := &LdapService{
|
ldap := &LdapService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
config: deps.StaticConfig,
|
config: i.Config,
|
||||||
ldapBindPw: ldapBindPw,
|
ctx: i.Ctx,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ldap.bindPw = utils.GetSecret(i.Config.LDAP.BindPassword, i.Config.LDAP.BindPasswordFile)
|
||||||
|
|
||||||
// Check whether authentication with client certificate is possible
|
// Check whether authentication with client certificate is possible
|
||||||
if deps.StaticConfig.LDAP.AuthCert != "" && deps.StaticConfig.LDAP.AuthKey != "" {
|
if i.Config.LDAP.AuthCert != "" && i.Config.LDAP.AuthKey != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(deps.StaticConfig.LDAP.AuthCert, deps.StaticConfig.LDAP.AuthKey)
|
cert, err := tls.LoadX509KeyPair(i.Config.LDAP.AuthCert, i.Config.LDAP.AuthKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
|
return nil, fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ldap.log.App.Info().Msg("LDAP mTLS authentication configured successfully")
|
i.Log.App.Info().Msg("LDAP mTLS authentication configured successfully")
|
||||||
|
|
||||||
ldap.cert = &cert
|
ldap.cert = &cert
|
||||||
|
|
||||||
@@ -67,10 +76,12 @@ func NewLdapService(
|
|||||||
_, err := ldap.connect()
|
_, err := ldap.connect()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 3s + 4.5s (3x1.5) = ~6.75-8.25s total wait time before giving up
|
||||||
|
err = ldap.reconnect(3 * time.Second)
|
||||||
return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
|
return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.Ding.Go(func(ctx context.Context) {
|
i.Ding.Go(func(ctx context.Context) {
|
||||||
ldap.log.App.Debug().Msg("Starting LDAP connection heartbeat routine")
|
ldap.log.App.Debug().Msg("Starting LDAP connection heartbeat routine")
|
||||||
|
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
@@ -82,7 +93,7 @@ func NewLdapService(
|
|||||||
err := ldap.heartbeat()
|
err := ldap.heartbeat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ldap.log.App.Warn().Err(err).Msg("LDAP connection heartbeat failed, attempting to reconnect")
|
ldap.log.App.Warn().Err(err).Msg("LDAP connection heartbeat failed, attempting to reconnect")
|
||||||
if reconnectErr := ldap.reconnect(); reconnectErr != nil {
|
if reconnectErr := ldap.reconnect(1 * time.Second); reconnectErr != nil {
|
||||||
ldap.log.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
|
ldap.log.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -163,6 +174,26 @@ func (ldap *LdapService) GetUserInfo(username string) (dn string, email string,
|
|||||||
return entry.DN, entry.GetAttributeValue("mail"), nil
|
return entry.DN, entry.GetAttributeValue("mail"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) GetUserCount() (int, error) {
|
||||||
|
searchRequest := ldapgo.NewSearchRequest(
|
||||||
|
ldap.config.LDAP.BaseDN,
|
||||||
|
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||||
|
"(objectClass=person)",
|
||||||
|
[]string{"dn"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
ldap.mutex.Lock()
|
||||||
|
defer ldap.mutex.Unlock()
|
||||||
|
|
||||||
|
searchResult, err := ldap.conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(searchResult.Entries), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
||||||
escapedUserDN := ldapgo.EscapeFilter(userDN)
|
escapedUserDN := ldapgo.EscapeFilter(userDN)
|
||||||
|
|
||||||
@@ -215,7 +246,13 @@ func (ldap *LdapService) BindService(rebind bool) error {
|
|||||||
if ldap.cert != nil {
|
if ldap.cert != nil {
|
||||||
return ldap.conn.ExternalBind()
|
return ldap.conn.ExternalBind()
|
||||||
}
|
}
|
||||||
return ldap.conn.Bind(ldap.config.LDAP.BindDN, ldap.config.LDAP.BindPassword)
|
|
||||||
|
// attempt unauthenticated/anonymous bind if both BindDN and bindpw are unset
|
||||||
|
if ldap.config.LDAP.BindDN == "" && ldap.bindPw == "" {
|
||||||
|
return ldap.conn.UnauthenticatedBind("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ldap.conn.Bind(ldap.config.LDAP.BindDN, ldap.bindPw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Bind(userDN string, password string) error {
|
func (ldap *LdapService) Bind(userDN string, password string) error {
|
||||||
@@ -250,17 +287,19 @@ func (ldap *LdapService) heartbeat() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) reconnect() error {
|
func (ldap *LdapService) reconnect(interval time.Duration) error {
|
||||||
ldap.log.App.Info().Msg("Attempting to reconnect to LDAP server")
|
ldap.log.App.Info().Msg("Attempting to reconnect to LDAP server")
|
||||||
|
|
||||||
exp := backoff.NewExponentialBackOff()
|
exp := backoff.NewExponentialBackOff()
|
||||||
exp.InitialInterval = 500 * time.Millisecond
|
exp.InitialInterval = interval
|
||||||
exp.RandomizationFactor = 0.1
|
exp.RandomizationFactor = 0.1
|
||||||
exp.Multiplier = 1.5
|
exp.Multiplier = 1.5
|
||||||
exp.Reset()
|
exp.Reset()
|
||||||
|
|
||||||
operation := func() (*ldapgo.Conn, error) {
|
operation := func() (*ldapgo.Conn, error) {
|
||||||
|
if ldap.conn != nil {
|
||||||
ldap.conn.Close()
|
ldap.conn.Close()
|
||||||
|
}
|
||||||
conn, err := ldap.connect()
|
conn, err := ldap.connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -268,7 +307,7 @@ func (ldap *LdapService) reconnect() error {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := backoff.Retry(context.TODO(), operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
|
_, err := backoff.Retry(ldap.ctx, operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,25 +5,28 @@ import (
|
|||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuthServiceImpl interface {
|
type IOAuthService interface {
|
||||||
Name() string
|
Name() string
|
||||||
ID() string
|
ID() string
|
||||||
NewRandom() string
|
NewRandom() string
|
||||||
GetAuthURL(state string, verifier string) string
|
GetAuthURL(state, verifier string) string
|
||||||
GetToken(code string, verifier string) (*oauth2.Token, error)
|
GetToken(code, verifier string) (*oauth2.Token, error)
|
||||||
GetUserinfo(token *oauth2.Token) (*model.Claims, error)
|
GetUserinfo(token *oauth2.Token) (*model.Claims, error)
|
||||||
|
GetConfig() model.OAuthServiceConfig
|
||||||
|
UpdateConfig(config model.OAuthServiceConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthBrokerService struct {
|
type OAuthBrokerService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
|
||||||
services map[string]OAuthServiceImpl
|
services map[string]IOAuthService
|
||||||
configs map[string]model.OAuthServiceConfig
|
configs map[string]model.OAuthServiceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,21 +35,27 @@ var presets = map[string]func(config model.OAuthServiceConfig, ctx context.Conte
|
|||||||
"google": newGoogleOAuthService,
|
"google": newGoogleOAuthService,
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOAuthBrokerService(
|
type OAuthBrokerServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) *OAuthBrokerService {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Runtime *model.RuntimeConfig
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOAuthBrokerService(i OAuthBrokerServiceInput) *OAuthBrokerService {
|
||||||
service := &OAuthBrokerService{
|
service := &OAuthBrokerService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
services: make(map[string]OAuthServiceImpl),
|
services: make(map[string]IOAuthService),
|
||||||
configs: deps.RuntimeConfig.OAuthProviders,
|
configs: i.Runtime.OAuthProviders,
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, cfg := range service.configs {
|
for name, cfg := range service.configs {
|
||||||
if presetFunc, exists := presets[name]; exists {
|
if presetFunc, exists := presets[name]; exists {
|
||||||
service.services[name] = presetFunc(cfg, deps.Ctx)
|
service.services[name] = presetFunc(cfg, i.Ctx)
|
||||||
service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset")
|
service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset")
|
||||||
} else {
|
} else {
|
||||||
service.services[name] = NewOAuthService(cfg, name, deps.Ctx)
|
service.services[name] = NewOAuthService(cfg, name, i.Ctx)
|
||||||
service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from custom config")
|
service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from custom config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +72,7 @@ func (broker *OAuthBrokerService) GetConfiguredServices() []string {
|
|||||||
return services
|
return services
|
||||||
}
|
}
|
||||||
|
|
||||||
func (broker *OAuthBrokerService) GetService(name string) (OAuthServiceImpl, bool) {
|
func (broker *OAuthBrokerService) GetService(name string) (IOAuthService, bool) {
|
||||||
service, exists := broker.services[name]
|
service, exists := broker.services[name]
|
||||||
return service, exists
|
return service, exists
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -23,21 +21,21 @@ 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, ctx context.Context, url string) (*model.Claims, error) {
|
||||||
return simpleReq[model.Claims](client, url, nil)
|
return simpleReq[model.Claims](client, ctx, url, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubExtractor(client *http.Client, _ string) (*model.Claims, error) {
|
func githubExtractor(client *http.Client, ctx context.Context, _ string) (*model.Claims, error) {
|
||||||
var user model.Claims
|
var user model.Claims
|
||||||
|
|
||||||
userInfo, err := simpleReq[GithubUserinfoResponse](client, "https://api.github.com/user", map[string]string{
|
userInfo, err := simpleReq[GithubUserinfoResponse](client, ctx, "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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userEmails, err := simpleReq[GithubEmailResponse](client, "https://api.github.com/user/emails", map[string]string{
|
userEmails, err := simpleReq[GithubEmailResponse](client, ctx, "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 {
|
||||||
@@ -75,38 +73,3 @@ func githubExtractor(client *http.Client, _ string) (*model.Claims, error) {
|
|||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (*T, error) {
|
|
||||||
var decodedRes T
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range headers {
|
|
||||||
req.Header.Add(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
||||||
return nil, fmt.Errorf("request failed with status: %s", res.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &decodedRes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &decodedRes, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuthUserinfoExtractor func(client *http.Client, url string) (*model.Claims, error)
|
type OAuthUserinfoExtractor func(client *http.Client, ctx context.Context, url string) (*model.Claims, error)
|
||||||
|
|
||||||
type OAuthService struct {
|
type OAuthService struct {
|
||||||
serviceCfg model.OAuthServiceConfig
|
serviceCfg model.OAuthServiceConfig
|
||||||
@@ -70,7 +70,7 @@ func (s *OAuthService) NewRandom() string {
|
|||||||
return random
|
return random
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthService) GetAuthURL(state string, verifier string) string {
|
func (s *OAuthService) GetAuthURL(state, verifier string) string {
|
||||||
return s.config.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
|
return s.config.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,5 +80,19 @@ func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, er
|
|||||||
|
|
||||||
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
|
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.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.ctx, s.serviceCfg.UserinfoURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) GetConfig() model.OAuthServiceConfig {
|
||||||
|
return s.serviceCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) UpdateConfig(config model.OAuthServiceConfig) {
|
||||||
|
s.serviceCfg = config
|
||||||
|
s.config.ClientID = config.ClientID
|
||||||
|
s.config.ClientSecret = config.ClientSecret
|
||||||
|
s.config.Scopes = config.Scopes
|
||||||
|
s.config.Endpoint.AuthURL = config.AuthURL
|
||||||
|
s.config.Endpoint.TokenURL = config.TokenURL
|
||||||
|
s.config.RedirectURL = config.RedirectURL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ import (
|
|||||||
"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/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -42,6 +44,15 @@ var (
|
|||||||
ErrInvalidClient = errors.New("invalid_client")
|
ErrInvalidClient = errors.New("invalid_client")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type OIDCPrompt string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OIDCPromptLogin OIDCPrompt = "login"
|
||||||
|
OIDCPromptNone OIDCPrompt = "none"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SupportedPrompts = []string{string(OIDCPromptLogin), string(OIDCPromptNone)}
|
||||||
|
|
||||||
// This is not spec-compliant, the ID token SHOULD NOT contain user info claims but,
|
// This is not spec-compliant, the ID token SHOULD NOT contain user info claims but,
|
||||||
// it has became a "standard" and apps are looking for the claims in the ID tokens
|
// it has became a "standard" and apps are looking for the claims in the ID tokens
|
||||||
// instead of calling the userinfo endpoint, so we include them in the ID token as well
|
// instead of calling the userinfo endpoint, so we include them in the ID token as well
|
||||||
@@ -52,6 +63,7 @@ type ClaimSet struct {
|
|||||||
Sub string `json:"sub"`
|
Sub string `json:"sub"`
|
||||||
Iat int64 `json:"iat"`
|
Iat int64 `json:"iat"`
|
||||||
Exp int64 `json:"exp"`
|
Exp int64 `json:"exp"`
|
||||||
|
AuthTime int64 `json:"auth_time,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
GivenName string `json:"given_name,omitempty"`
|
GivenName string `json:"given_name,omitempty"`
|
||||||
FamilyName string `json:"family_name,omitempty"`
|
FamilyName string `json:"family_name,omitempty"`
|
||||||
@@ -115,6 +127,8 @@ type AuthorizeRequest struct {
|
|||||||
Nonce string `form:"nonce" json:"nonce" url:"nonce"`
|
Nonce string `form:"nonce" json:"nonce" url:"nonce"`
|
||||||
CodeChallenge string `form:"code_challenge" json:"code_challenge" url:"code_challenge"`
|
CodeChallenge string `form:"code_challenge" json:"code_challenge" url:"code_challenge"`
|
||||||
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" url:"code_challenge_method"`
|
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" url:"code_challenge_method"`
|
||||||
|
Prompt string `form:"prompt" json:"prompt" url:"prompt"`
|
||||||
|
MaxAge string `form:"max_age" json:"max_age" url:"max_age"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeCodeEntry struct {
|
type AuthorizeCodeEntry struct {
|
||||||
@@ -125,6 +139,7 @@ type AuthorizeCodeEntry struct {
|
|||||||
Nonce string
|
Nonce string
|
||||||
CodeChallenge string
|
CodeChallenge string
|
||||||
Userinfo UserinfoResponse
|
Userinfo UserinfoResponse
|
||||||
|
AuthTime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type UsedCodeEntry struct {
|
type UsedCodeEntry struct {
|
||||||
@@ -149,16 +164,24 @@ type OIDCService struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOIDCService(
|
type OIDCServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) (*OIDCService, error) {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
Runtime *model.RuntimeConfig
|
||||||
|
Queries repository.Store
|
||||||
|
Ding *ding.Ding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
|
||||||
// If not configured, skip init
|
// If not configured, skip init
|
||||||
if len(deps.RuntimeConfig.OIDCClients) == 0 {
|
if len(i.Config.OIDC.Clients) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure issuer is https
|
// Ensure issuer is https
|
||||||
uissuer, err := url.Parse(deps.RuntimeConfig.AppURL)
|
uissuer, err := url.Parse(i.Runtime.AppURL)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse app url: %w", err)
|
return nil, fmt.Errorf("failed to parse app url: %w", err)
|
||||||
@@ -171,14 +194,14 @@ func NewOIDCService(
|
|||||||
issuer := fmt.Sprintf("%s://%s", uissuer.Scheme, uissuer.Host)
|
issuer := fmt.Sprintf("%s://%s", uissuer.Scheme, uissuer.Host)
|
||||||
|
|
||||||
// Create/load private and public keys
|
// Create/load private and public keys
|
||||||
if strings.TrimSpace(deps.StaticConfig.OIDC.PrivateKeyPath) == "" ||
|
if strings.TrimSpace(i.Config.OIDC.PrivateKeyPath) == "" ||
|
||||||
strings.TrimSpace(deps.StaticConfig.OIDC.PublicKeyPath) == "" {
|
strings.TrimSpace(i.Config.OIDC.PublicKeyPath) == "" {
|
||||||
return nil, errors.New("private key path and public key path are required")
|
return nil, errors.New("private key path and public key path are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
var privateKey *rsa.PrivateKey
|
var privateKey *rsa.PrivateKey
|
||||||
|
|
||||||
fprivateKey, err := os.ReadFile(deps.StaticConfig.OIDC.PrivateKeyPath)
|
fprivateKey, err := os.ReadFile(i.Config.OIDC.PrivateKeyPath)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -197,8 +220,12 @@ func NewOIDCService(
|
|||||||
Type: "RSA PRIVATE KEY",
|
Type: "RSA PRIVATE KEY",
|
||||||
Bytes: der,
|
Bytes: der,
|
||||||
})
|
})
|
||||||
deps.Log.App.Trace().Str("type", "RSA PRIVATE KEY").Msg("Generated private RSA key")
|
i.Log.App.Trace().Str("type", "RSA PRIVATE KEY").Msg("Generated private RSA key")
|
||||||
err = os.WriteFile(deps.StaticConfig.OIDC.PrivateKeyPath, encoded, 0600)
|
err := os.MkdirAll(filepath.Dir(i.Config.OIDC.PrivateKeyPath), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create directory for private key: %w", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(i.Config.OIDC.PrivateKeyPath, encoded, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write private key to file: %w", err)
|
return nil, fmt.Errorf("failed to write private key to file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -207,7 +234,7 @@ func NewOIDCService(
|
|||||||
if block == nil {
|
if block == nil {
|
||||||
return nil, errors.New("failed to decode private key")
|
return nil, errors.New("failed to decode private key")
|
||||||
}
|
}
|
||||||
deps.Log.App.Trace().Str("type", block.Type).Msg("Loaded private key")
|
i.Log.App.Trace().Str("type", block.Type).Msg("Loaded private key")
|
||||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||||
@@ -216,7 +243,7 @@ func NewOIDCService(
|
|||||||
|
|
||||||
var publicKey crypto.PublicKey
|
var publicKey crypto.PublicKey
|
||||||
|
|
||||||
fpublicKey, err := os.ReadFile(deps.StaticConfig.OIDC.PublicKeyPath)
|
fpublicKey, err := os.ReadFile(i.Config.OIDC.PublicKeyPath)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("failed to read public key: %w", err)
|
return nil, fmt.Errorf("failed to read public key: %w", err)
|
||||||
@@ -232,8 +259,12 @@ func NewOIDCService(
|
|||||||
Type: "RSA PUBLIC KEY",
|
Type: "RSA PUBLIC KEY",
|
||||||
Bytes: der,
|
Bytes: der,
|
||||||
})
|
})
|
||||||
deps.Log.App.Trace().Str("type", "RSA PUBLIC KEY").Msg("Generated public RSA key")
|
i.Log.App.Trace().Str("type", "RSA PUBLIC KEY").Msg("Generated public RSA key")
|
||||||
err = os.WriteFile(deps.StaticConfig.OIDC.PublicKeyPath, encoded, 0644)
|
err := os.MkdirAll(filepath.Dir(i.Config.OIDC.PublicKeyPath), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create directory for public key: %w", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(i.Config.OIDC.PublicKeyPath, encoded, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -242,7 +273,7 @@ func NewOIDCService(
|
|||||||
if block == nil {
|
if block == nil {
|
||||||
return nil, errors.New("failed to decode public key")
|
return nil, errors.New("failed to decode public key")
|
||||||
}
|
}
|
||||||
deps.Log.App.Trace().Str("type", block.Type).Msg("Loaded public key")
|
i.Log.App.Trace().Str("type", block.Type).Msg("Loaded public key")
|
||||||
switch block.Type {
|
switch block.Type {
|
||||||
case "RSA PUBLIC KEY":
|
case "RSA PUBLIC KEY":
|
||||||
publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
|
publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
|
||||||
@@ -272,7 +303,7 @@ func NewOIDCService(
|
|||||||
// We will reorganize the client into a map with the client ID as the key
|
// We will reorganize the client into a map with the client ID as the key
|
||||||
clients := make(map[string]model.OIDCClientConfig)
|
clients := make(map[string]model.OIDCClientConfig)
|
||||||
|
|
||||||
for id, client := range deps.StaticConfig.OIDC.Clients {
|
for id, client := range i.Config.OIDC.Clients {
|
||||||
client.ID = id
|
client.ID = id
|
||||||
if client.Name == "" {
|
if client.Name == "" {
|
||||||
client.Name = utils.Capitalize(client.ID)
|
client.Name = utils.Capitalize(client.ID)
|
||||||
@@ -288,15 +319,15 @@ func NewOIDCService(
|
|||||||
}
|
}
|
||||||
client.ClientSecretFile = ""
|
client.ClientSecretFile = ""
|
||||||
clients[id] = client
|
clients[id] = client
|
||||||
deps.Log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration")
|
i.Log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the service
|
// Initialize the service
|
||||||
service := &OIDCService{
|
service := &OIDCService{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
config: deps.StaticConfig,
|
config: i.Config,
|
||||||
runtime: deps.RuntimeConfig,
|
runtime: i.Runtime,
|
||||||
queries: *deps.Queries,
|
queries: i.Queries,
|
||||||
|
|
||||||
clients: clients,
|
clients: clients,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
@@ -305,7 +336,7 @@ func NewOIDCService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start cleanup routine
|
// Start cleanup routine
|
||||||
deps.Ding.Go(service.cleanupRoutine, ding.RingMinor)
|
i.Ding.Go(service.cleanupRoutine, ding.RingMinor)
|
||||||
|
|
||||||
// Create caches
|
// Create caches
|
||||||
codeCash := NewCacheStore[AuthorizeCodeEntry](256)
|
codeCash := NewCacheStore[AuthorizeCodeEntry](256)
|
||||||
@@ -317,7 +348,7 @@ func NewOIDCService(
|
|||||||
service.caches.authorize = authorize
|
service.caches.authorize = authorize
|
||||||
|
|
||||||
// Start cache cleanup routine
|
// Start cache cleanup routine
|
||||||
deps.Ding.Go(func(ctx context.Context) {
|
i.Ding.Go(func(ctx context.Context) {
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -405,6 +436,7 @@ func (service *OIDCService) CreateCode(req AuthorizeRequest, userContext model.U
|
|||||||
ClientID: req.ClientID,
|
ClientID: req.ClientID,
|
||||||
Nonce: req.Nonce,
|
Nonce: req.Nonce,
|
||||||
Userinfo: service.userinfoFromContext(userContext, sub),
|
Userinfo: service.userinfoFromContext(userContext, sub),
|
||||||
|
AuthTime: userContext.AuthTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.CodeChallenge != "" {
|
if req.CodeChallenge != "" {
|
||||||
@@ -494,7 +526,7 @@ func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*Aut
|
|||||||
return &entry, true
|
return &entry, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user UserinfoResponse, scope string, nonce string) (string, error) {
|
func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user UserinfoResponse, scope string, nonce string, authTime *int64) (string, error) {
|
||||||
createdAt := time.Now().Unix()
|
createdAt := time.Now().Unix()
|
||||||
expiresAt := time.Now().Add(time.Duration(service.config.Auth.SessionExpiry) * time.Second).Unix()
|
expiresAt := time.Now().Add(time.Duration(service.config.Auth.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
@@ -539,6 +571,10 @@ func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user
|
|||||||
Nonce: nonce,
|
Nonce: nonce,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authTime != nil {
|
||||||
|
claims.AuthTime = *authTime
|
||||||
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(claims)
|
payload, err := json.Marshal(claims)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -560,8 +596,8 @@ func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) GenerateAccessToken(ctx context.Context, client model.OIDCClientConfig, codeEntry AuthorizeCodeEntry) (*TokenResponse, error) {
|
func (service *OIDCService) GenerateAccessToken(ctx context.Context, client model.OIDCClientConfig, codeEntry AuthorizeCodeEntry, authTime int64) (*TokenResponse, error) {
|
||||||
idToken, err := service.generateIDToken(client, codeEntry.Userinfo, codeEntry.Scope, codeEntry.Nonce)
|
idToken, err := service.generateIDToken(client, codeEntry.Userinfo, codeEntry.Scope, codeEntry.Nonce, &authTime)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -640,9 +676,10 @@ func (service *OIDCService) RefreshAccessToken(ctx context.Context, refreshToken
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: store auth time in the database so we can include it in the new ID token, for now we omit it
|
||||||
idToken, err := service.generateIDToken(model.OIDCClientConfig{
|
idToken, err := service.generateIDToken(model.OIDCClientConfig{
|
||||||
ClientID: entry.ClientID,
|
ClientID: entry.ClientID,
|
||||||
}, userInfo, entry.Scope, entry.Nonce)
|
}, userInfo, entry.Scope, entry.Nonce, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -911,5 +948,24 @@ func (service *OIDCService) DecodeAuthorizeJWT(tokenString string) (*AuthorizeRe
|
|||||||
Nonce: get("nonce"),
|
Nonce: get("nonce"),
|
||||||
CodeChallenge: get("code_challenge"),
|
CodeChallenge: get("code_challenge"),
|
||||||
CodeChallengeMethod: get("code_challenge_method"),
|
CodeChallengeMethod: get("code_challenge_method"),
|
||||||
|
Prompt: get("prompt"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetPrompt(prompt string) []OIDCPrompt {
|
||||||
|
if prompt == "" {
|
||||||
|
return []OIDCPrompt{}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPromps := make([]OIDCPrompt, 0)
|
||||||
|
prompts := strings.SplitSeq(prompt, " ")
|
||||||
|
|
||||||
|
for p := range prompts {
|
||||||
|
if !slices.Contains(SupportedPrompts, p) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsedPromps = append(parsedPromps, OIDCPrompt(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPromps
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package service_test
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestUser() service.UserinfoResponse {
|
func newTestUser() UserinfoResponse {
|
||||||
return service.UserinfoResponse{
|
return UserinfoResponse{
|
||||||
Sub: "test-sub",
|
Sub: "test-sub",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
PreferredUsername: "testuser",
|
PreferredUsername: "testuser",
|
||||||
@@ -67,21 +67,29 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
dg := ding.New(ctx)
|
dg := ding.New(ctx)
|
||||||
|
|
||||||
svc, err := service.NewOIDCService(log, cfg, runtime, nil, dg)
|
store := memory.New()
|
||||||
|
|
||||||
|
svc, err := NewOIDCService(OIDCServiceInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
Runtime: &runtime,
|
||||||
|
Queries: store,
|
||||||
|
Ding: dg,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
mutate func(u *service.UserinfoResponse)
|
mutate func(u *UserinfoResponse)
|
||||||
scope string
|
scope string
|
||||||
run func(t *testing.T, info service.UserinfoResponse)
|
run func(t *testing.T, info UserinfoResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
{
|
{
|
||||||
description: "openid scope only returns sub and updated_at",
|
description: "openid scope only returns sub and updated_at",
|
||||||
scope: "openid",
|
scope: "openid",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Equal(t, "test-sub", info.Sub)
|
assert.Equal(t, "test-sub", info.Sub)
|
||||||
assert.Equal(t, int64(1234567890), info.UpdatedAt)
|
assert.Equal(t, int64(1234567890), info.UpdatedAt)
|
||||||
assert.Empty(t, info.Name)
|
assert.Empty(t, info.Name)
|
||||||
@@ -94,7 +102,7 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "profile scope returns all profile fields",
|
description: "profile scope returns all profile fields",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Equal(t, "Test User", info.Name)
|
assert.Equal(t, "Test User", info.Name)
|
||||||
assert.Equal(t, "testuser", info.PreferredUsername)
|
assert.Equal(t, "testuser", info.PreferredUsername)
|
||||||
assert.Equal(t, "Test", info.GivenName)
|
assert.Equal(t, "Test", info.GivenName)
|
||||||
@@ -114,7 +122,7 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "email scope sets email and email_verified true when email present",
|
description: "email scope sets email and email_verified true when email present",
|
||||||
scope: "openid email",
|
scope: "openid email",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Equal(t, "test@example.com", info.Email)
|
assert.Equal(t, "test@example.com", info.Email)
|
||||||
assert.True(t, info.EmailVerified)
|
assert.True(t, info.EmailVerified)
|
||||||
assert.Empty(t, info.Name)
|
assert.Empty(t, info.Name)
|
||||||
@@ -123,8 +131,8 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "email scope sets email_verified false when email absent",
|
description: "email scope sets email_verified false when email absent",
|
||||||
scope: "openid email",
|
scope: "openid email",
|
||||||
mutate: func(u *service.UserinfoResponse) { u.Email = "" },
|
mutate: func(u *UserinfoResponse) { u.Email = "" },
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Empty(t, info.Email)
|
assert.Empty(t, info.Email)
|
||||||
assert.False(t, info.EmailVerified)
|
assert.False(t, info.EmailVerified)
|
||||||
},
|
},
|
||||||
@@ -132,7 +140,7 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "phone scope sets phone_number_verified true when phone present",
|
description: "phone scope sets phone_number_verified true when phone present",
|
||||||
scope: "openid phone",
|
scope: "openid phone",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Equal(t, "+15555550100", info.PhoneNumber)
|
assert.Equal(t, "+15555550100", info.PhoneNumber)
|
||||||
require.NotNil(t, info.PhoneNumberVerified)
|
require.NotNil(t, info.PhoneNumberVerified)
|
||||||
assert.True(t, *info.PhoneNumberVerified)
|
assert.True(t, *info.PhoneNumberVerified)
|
||||||
@@ -141,8 +149,8 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "phone scope sets phone_number_verified false when phone absent",
|
description: "phone scope sets phone_number_verified false when phone absent",
|
||||||
scope: "openid phone",
|
scope: "openid phone",
|
||||||
mutate: func(u *service.UserinfoResponse) { u.PhoneNumber = "" },
|
mutate: func(u *UserinfoResponse) { u.PhoneNumber = "" },
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
require.NotNil(t, info.PhoneNumberVerified)
|
require.NotNil(t, info.PhoneNumberVerified)
|
||||||
assert.False(t, *info.PhoneNumberVerified)
|
assert.False(t, *info.PhoneNumberVerified)
|
||||||
},
|
},
|
||||||
@@ -150,7 +158,7 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "address scope returns parsed address",
|
description: "address scope returns parsed address",
|
||||||
scope: "openid address",
|
scope: "openid address",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
require.NotNil(t, info.Address)
|
require.NotNil(t, info.Address)
|
||||||
assert.Equal(t, "123 Main St", info.Address.Formatted)
|
assert.Equal(t, "123 Main St", info.Address.Formatted)
|
||||||
assert.Equal(t, "123 Main St", info.Address.StreetAddress)
|
assert.Equal(t, "123 Main St", info.Address.StreetAddress)
|
||||||
@@ -163,14 +171,14 @@ func TestCompileUserinfo(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "groups scope returns split groups",
|
description: "groups scope returns split groups",
|
||||||
scope: "openid groups",
|
scope: "openid groups",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Equal(t, []string{"admins", "users"}, info.Groups)
|
assert.Equal(t, []string{"admins", "users"}, info.Groups)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "all scopes return all fields",
|
description: "all scopes return all fields",
|
||||||
scope: "openid profile email phone address groups",
|
scope: "openid profile email phone address groups",
|
||||||
run: func(t *testing.T, info service.UserinfoResponse) {
|
run: func(t *testing.T, info UserinfoResponse) {
|
||||||
assert.Equal(t, "Test User", info.Name)
|
assert.Equal(t, "Test User", info.Name)
|
||||||
assert.Equal(t, "test@example.com", info.Email)
|
assert.Equal(t, "test@example.com", info.Email)
|
||||||
assert.Equal(t, "+15555550100", info.PhoneNumber)
|
assert.Equal(t, "+15555550100", info.PhoneNumber)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
|
"go.uber.org/dig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Policy string
|
type Policy string
|
||||||
@@ -40,23 +41,28 @@ type PolicyEngine struct {
|
|||||||
policy Policy
|
policy Policy
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPolicyEngine(
|
type PolicyEngineInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) (*PolicyEngine, error) {
|
|
||||||
|
Log *logger.Logger
|
||||||
|
Config *model.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPolicyEngine(i PolicyEngineInput) (*PolicyEngine, error) {
|
||||||
engine := PolicyEngine{
|
engine := PolicyEngine{
|
||||||
log: deps.Log,
|
log: i.Log,
|
||||||
rules: make(map[RuleName]Rule),
|
rules: make(map[RuleName]Rule),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch deps.StaticConfig.Auth.ACLs.Policy {
|
switch i.Config.Auth.ACLs.Policy {
|
||||||
case string(PolicyAllow):
|
case string(PolicyAllow):
|
||||||
deps.Log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
|
i.Log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
|
||||||
engine.policy = PolicyAllow
|
engine.policy = PolicyAllow
|
||||||
case string(PolicyDeny):
|
case string(PolicyDeny):
|
||||||
deps.Log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
|
i.Log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
|
||||||
engine.policy = PolicyDeny
|
engine.policy = PolicyDeny
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid acl policy: %s", deps.StaticConfig.Auth.ACLs.Policy)
|
return nil, fmt.Errorf("invalid acl policy: %s", i.Config.Auth.ACLs.Policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &engine, nil
|
return &engine, nil
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package service_test
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/test"
|
"github.com/tinyauthapp/tinyauth/internal/test"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
)
|
)
|
||||||
@@ -12,14 +11,14 @@ import (
|
|||||||
// Create test rule
|
// Create test rule
|
||||||
type TestRule struct{}
|
type TestRule struct{}
|
||||||
|
|
||||||
func (rule *TestRule) Evaluate(ctx *service.ACLContext) service.Effect {
|
func (rule *TestRule) Evaluate(ctx *ACLContext) Effect {
|
||||||
switch ctx.Path {
|
switch ctx.Path {
|
||||||
case "/allowed":
|
case "/allowed":
|
||||||
return service.EffectAllow
|
return EffectAllow
|
||||||
case "/denied":
|
case "/denied":
|
||||||
return service.EffectDeny
|
return EffectDeny
|
||||||
default:
|
default:
|
||||||
return service.EffectAbstain
|
return EffectAbstain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,36 +32,51 @@ func TestPolicyEngine(t *testing.T) {
|
|||||||
|
|
||||||
// Engine should fail with invalid policy
|
// Engine should fail with invalid policy
|
||||||
cfg.Auth.ACLs.Policy = "invalid_policy"
|
cfg.Auth.ACLs.Policy = "invalid_policy"
|
||||||
_, err := service.NewPolicyEngine(cfg, log)
|
_, err := NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// Engine should initialize with 'allow' policy
|
// Engine should initialize with 'allow' policy
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyAllow)
|
cfg.Auth.ACLs.Policy = string(PolicyAllow)
|
||||||
engine, err := service.NewPolicyEngine(cfg, log)
|
engine, err := NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, service.PolicyAllow, engine.Policy())
|
assert.Equal(t, PolicyAllow, engine.Policy())
|
||||||
|
|
||||||
// Engine should initialize with 'deny' policy
|
// Engine should initialize with 'deny' policy
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyDeny)
|
cfg.Auth.ACLs.Policy = string(PolicyDeny)
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
engine, err = NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, service.PolicyDeny, engine.Policy())
|
assert.Equal(t, PolicyDeny, engine.Policy())
|
||||||
|
|
||||||
// Engine should allow adding rules
|
// Engine should allow adding rules
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
engine, err = NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
engine.RegisterRule("test-rule", testRule)
|
engine.RegisterRule("test-rule", testRule)
|
||||||
_, ok := engine.Rules()["test-rule"]
|
_, ok := engine.Rules()["test-rule"]
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
// Begin allow policy tests
|
// Begin allow policy tests
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyAllow)
|
cfg.Auth.ACLs.Policy = string(PolicyAllow)
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
engine, err = NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
engine.RegisterRule("test-rule", testRule)
|
engine.RegisterRule("test-rule", testRule)
|
||||||
|
|
||||||
// With allow policy, if rule allows, access should be allowed
|
// With allow policy, if rule allows, access should be allowed
|
||||||
ctx := &service.ACLContext{Path: "/allowed"}
|
ctx := &ACLContext{Path: "/allowed"}
|
||||||
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
||||||
|
|
||||||
// With allow policy, if rule denies, access should be denied
|
// With allow policy, if rule denies, access should be denied
|
||||||
@@ -74,8 +88,11 @@ func TestPolicyEngine(t *testing.T) {
|
|||||||
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
assert.Equal(t, true, engine.Evaluate("test-rule", ctx))
|
||||||
|
|
||||||
// Begin deny policy tests
|
// Begin deny policy tests
|
||||||
cfg.Auth.ACLs.Policy = string(service.PolicyDeny)
|
cfg.Auth.ACLs.Policy = string(PolicyDeny)
|
||||||
engine, err = service.NewPolicyEngine(cfg, log)
|
engine, err = NewPolicyEngine(PolicyEngineInput{
|
||||||
|
Log: log,
|
||||||
|
Config: &cfg,
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
engine.RegisterRule("test-rule", testRule)
|
engine.RegisterRule("test-rule", testRule)
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/ding"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/repository"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Services struct {
|
|
||||||
AccessControlService *AccessControlsService
|
|
||||||
AuthService *AuthService
|
|
||||||
DockerService *DockerService
|
|
||||||
KubernetesService *KubernetesService
|
|
||||||
LDAPService *LdapService
|
|
||||||
OAuthBrokerService *OAuthBrokerService
|
|
||||||
OIDCService *OIDCService
|
|
||||||
TailscaleService *TailscaleService
|
|
||||||
PolicyEngine *PolicyEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceDependencies struct {
|
|
||||||
Log *logger.Logger
|
|
||||||
StaticConfig *model.Config
|
|
||||||
RuntimeConfig *model.RuntimeConfig
|
|
||||||
Ctx context.Context
|
|
||||||
Ding *ding.Ding
|
|
||||||
Services *Services
|
|
||||||
LabelProvider LabelProvider
|
|
||||||
Queries *repository.Store
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func simpleReq[T any](client *http.Client, ctx context.Context, url string, headers map[string]string) (*T, error) {
|
||||||
|
var decodedRes T
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range headers {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed with status: %s", res.Status)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("request failed with status: %s and body: %s", res.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &decodedRes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &decodedRes, nil
|
||||||
|
}
|
||||||
@@ -2,177 +2,243 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/ding"
|
"github.com/steveiliop56/ding"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
"tailscale.com/client/local"
|
"go.uber.org/dig"
|
||||||
"tailscale.com/tsnet"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tailscaleAPIBaseURL = "https://api.tailscale.com/api/v2"
|
||||||
|
|
||||||
|
var (
|
||||||
|
tailscaleAPIDeviceList = func(tailnet string) string {
|
||||||
|
return tailscaleAPIBaseURL + "/tailnet/" + tailnet + "/devices"
|
||||||
|
}
|
||||||
|
tailscaleAPIUserList = func(tailnet string) string {
|
||||||
|
return tailscaleAPIBaseURL + "/tailnet/" + tailnet + "/users"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type tailscaleDevice struct {
|
||||||
|
Addresses []string `json:"addresses"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tailscaleAPIDevices struct {
|
||||||
|
Devices []tailscaleDevice `json:"devices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tailscaleUser struct {
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
LoginName string `json:"loginName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tailscaleAPIUsers struct {
|
||||||
|
Users []tailscaleUser `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
type TailscaleWhoisResponse struct {
|
type TailscaleWhoisResponse struct {
|
||||||
UserID string
|
|
||||||
LoginName string
|
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
LoginName string
|
||||||
NodeName string
|
NodeName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TailscaleService struct {
|
type TailscaleService struct {
|
||||||
log *logger.Logger
|
|
||||||
config *model.Config
|
config *model.Config
|
||||||
|
log *logger.Logger
|
||||||
|
client *http.Client
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
srv *tsnet.Server
|
apiToken string
|
||||||
lc *local.Client
|
|
||||||
ln *net.Listener
|
caches struct {
|
||||||
mu sync.Mutex
|
devices *CacheStore[tailscaleAPIDevices]
|
||||||
|
users *CacheStore[tailscaleAPIUsers]
|
||||||
|
}
|
||||||
|
|
||||||
|
urls struct {
|
||||||
|
devices string
|
||||||
|
users string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTailscaleService(
|
type TailscaleServiceInput struct {
|
||||||
deps *ServiceDependencies,
|
dig.In
|
||||||
) (*TailscaleService, error) {
|
|
||||||
if !deps.StaticConfig.Tailscale.Enabled {
|
Ding *ding.Ding
|
||||||
|
Config *model.Config
|
||||||
|
Log *logger.Logger
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
|
||||||
|
if !i.Config.Tailscale.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := new(tsnet.Server)
|
if i.Config.Tailscale.Tailnet == "" {
|
||||||
|
return nil, fmt.Errorf("tailscale tailnet not set")
|
||||||
// node options
|
|
||||||
srv.Dir = deps.StaticConfig.Tailscale.Dir
|
|
||||||
srv.Hostname = deps.StaticConfig.Tailscale.Hostname
|
|
||||||
srv.AuthKey = deps.StaticConfig.Tailscale.AuthKey
|
|
||||||
srv.Ephemeral = deps.StaticConfig.Tailscale.Ephemeral
|
|
||||||
|
|
||||||
// redirect logs to zerolog
|
|
||||||
srv.Logf = deps.Log.App.Printf
|
|
||||||
srv.UserLogf = deps.Log.App.Printf
|
|
||||||
|
|
||||||
err := srv.Start()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start tailscale server: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lc, err := srv.LocalClient()
|
apiToken := utils.GetSecret(i.Config.Tailscale.APIToken, i.Config.Tailscale.APITokenFile)
|
||||||
|
|
||||||
if err != nil {
|
if apiToken == "" {
|
||||||
_ = srv.Close()
|
return nil, fmt.Errorf("tailscale api token not set")
|
||||||
return nil, fmt.Errorf("failed to get tailscale local client: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service := &TailscaleService{
|
s := &TailscaleService{
|
||||||
log: deps.Log,
|
config: i.Config,
|
||||||
config: deps.StaticConfig,
|
log: i.Log,
|
||||||
ctx: deps.Ctx,
|
ctx: i.Ctx,
|
||||||
srv: srv,
|
apiToken: apiToken,
|
||||||
lc: lc,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectCtx, cancel := context.WithTimeout(deps.Ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed
|
devicesCache := NewCacheStore[tailscaleAPIDevices](0)
|
||||||
defer cancel()
|
usersCache := NewCacheStore[tailscaleAPIUsers](0)
|
||||||
|
|
||||||
err = service.waitForConn(connectCtx)
|
s.caches.devices = devicesCache
|
||||||
|
s.caches.users = usersCache
|
||||||
|
|
||||||
if err != nil {
|
i.Ding.Go(func(ctx context.Context) {
|
||||||
_ = srv.Close()
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
|
defer ticker.Stop()
|
||||||
}
|
|
||||||
|
|
||||||
deps.Ding.Go(service.watchAndClose, ding.RingMajor)
|
|
||||||
|
|
||||||
return service, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) watchAndClose(ctx context.Context) {
|
|
||||||
<-ctx.Done()
|
|
||||||
ts.log.App.Debug().Msg("Shutting down Tailscale service")
|
|
||||||
ts.mu.Lock()
|
|
||||||
srv := ts.srv
|
|
||||||
ln := ts.ln
|
|
||||||
ts.ln = nil
|
|
||||||
ts.srv = nil
|
|
||||||
ts.mu.Unlock()
|
|
||||||
if ln != nil {
|
|
||||||
(*ln).Close()
|
|
||||||
}
|
|
||||||
if srv != nil {
|
|
||||||
srv.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) Whois(ctx context.Context, addr string) (*TailscaleWhoisResponse, error) {
|
|
||||||
who, err := ts.lc.WhoIs(ctx, addr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, local.ErrPeerNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to get client whois: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if who.Node.IsTagged() {
|
|
||||||
ts.log.App.Debug().Msgf("Skipping whois for tagged node %s", who.Node.Name)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
uid := strings.TrimPrefix(who.UserProfile.ID.String(), "userid:")
|
|
||||||
|
|
||||||
res := TailscaleWhoisResponse{
|
|
||||||
UserID: uid,
|
|
||||||
LoginName: who.UserProfile.LoginName,
|
|
||||||
DisplayName: who.UserProfile.DisplayName,
|
|
||||||
NodeName: strings.TrimSuffix(who.Node.Name, "."),
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.log.App.Debug().Interface("res", res).Msg("tailscale")
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) CreateListener() (net.Listener, error) {
|
|
||||||
ts.mu.Lock()
|
|
||||||
defer ts.mu.Unlock()
|
|
||||||
|
|
||||||
if ts.ln != nil {
|
|
||||||
return *ts.ln, nil
|
|
||||||
}
|
|
||||||
ln, err := ts.srv.ListenTLS("tcp", ":443")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ts.ln = &ln
|
|
||||||
return ln, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) GetHostname() string {
|
|
||||||
status, err := ts.lc.Status(ts.ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ts.log.App.Error().Err(err).Msg("Failed to get Tailscale status")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSuffix(status.Self.DNSName, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TailscaleService) waitForConn(ctx context.Context) error {
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
s.caches.devices.Sweep()
|
||||||
|
s.caches.users.Sweep()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return fmt.Errorf("timed out waiting for tailscale connection")
|
return
|
||||||
default:
|
}
|
||||||
ip4, _ := ts.srv.TailscaleIPs()
|
}
|
||||||
if !ip4.IsValid() {
|
}, ding.RingMinor)
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
s.urls.devices = tailscaleAPIDeviceList(i.Config.Tailscale.Tailnet)
|
||||||
|
s.urls.users = tailscaleAPIUserList(i.Config.Tailscale.Tailnet)
|
||||||
|
s.client = &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := s.getDeviceList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get device list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = s.getUsersList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.App.Info().Msg("Tailscale service initialized")
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TailscaleService) buildAuthorizationHeader() string {
|
||||||
|
return "Bearer " + s.apiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TailscaleService) getDeviceList() (*tailscaleAPIDevices, bool, error) {
|
||||||
|
cached, ok := s.caches.devices.Get("devices")
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return &cached, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := simpleReq[tailscaleAPIDevices](s.client, s.ctx, s.urls.devices, map[string]string{
|
||||||
|
"Authorization": s.buildAuthorizationHeader(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("failed to get device list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.caches.devices.Set("devices", *devices, time.Duration(s.config.Tailscale.CacheDuration)*time.Second)
|
||||||
|
|
||||||
|
return devices, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TailscaleService) getUsersList() (*tailscaleAPIUsers, bool, error) {
|
||||||
|
cached, ok := s.caches.users.Get("users")
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return &cached, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := simpleReq[tailscaleAPIUsers](s.client, s.ctx, s.urls.users, map[string]string{
|
||||||
|
"Authorization": s.buildAuthorizationHeader(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("failed to get user list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.caches.users.Set("users", *users, time.Duration(s.config.Tailscale.CacheDuration)*time.Second)
|
||||||
|
|
||||||
|
return users, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TailscaleService) Whois(addr string) (*TailscaleWhoisResponse, error) {
|
||||||
|
var device *tailscaleDevice
|
||||||
|
|
||||||
|
devices, dCacheHit, err := s.getDeviceList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get device list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range devices.Devices {
|
||||||
|
if len(d.Tags) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil
|
for _, a := range d.Addresses {
|
||||||
|
if a == addr {
|
||||||
|
device = &d
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if device == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.App.Debug().Str("device", device.Name).Bool("cache_hit", dCacheHit).Msg("Tailscale device found")
|
||||||
|
|
||||||
|
var user *tailscaleUser
|
||||||
|
|
||||||
|
users, uCacheHit, err := s.getUsersList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range users.Users {
|
||||||
|
if u.LoginName == device.User {
|
||||||
|
user = &u
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.App.Debug().Str("user", user.LoginName).Bool("cache_hit", uCacheHit).Msg("Tailscale user found")
|
||||||
|
|
||||||
|
return &TailscaleWhoisResponse{
|
||||||
|
LoginName: user.LoginName,
|
||||||
|
DisplayName: user.DisplayName,
|
||||||
|
NodeName: device.Name,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-8
@@ -43,6 +43,7 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
ACLs: model.ACLsConfig{
|
ACLs: model.ACLsConfig{
|
||||||
Policy: "allow",
|
Policy: "allow",
|
||||||
},
|
},
|
||||||
|
SubdomainsEnabled: true,
|
||||||
},
|
},
|
||||||
Database: model.DatabaseConfig{
|
Database: model.DatabaseConfig{
|
||||||
Path: filepath.Join(tempDir, "test.db"),
|
Path: filepath.Join(tempDir, "test.db"),
|
||||||
@@ -76,6 +77,50 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
Bypass: []string{"10.10.10.10"},
|
Bypass: []string{"10.10.10.10"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"ip_block": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "ip-block.example.com",
|
||||||
|
},
|
||||||
|
IP: model.AppIP{
|
||||||
|
Block: []string{"10.10.10.10"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"oauth_group": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "oauth-group.example.com",
|
||||||
|
},
|
||||||
|
OAuth: model.AppOAuth{
|
||||||
|
Whitelist: "testuser@example.com",
|
||||||
|
Groups: "group1,group2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ldap_group": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "ldap-group.example.com",
|
||||||
|
},
|
||||||
|
LDAP: model.AppLDAP{
|
||||||
|
Groups: "group1,group2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"basic_auth": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "basic-auth.example.com",
|
||||||
|
},
|
||||||
|
Response: model.AppResponse{
|
||||||
|
BasicAuth: model.AppBasicAuth{
|
||||||
|
Username: "test",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"response_headers": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "response-headers.example.com",
|
||||||
|
},
|
||||||
|
Response: model.AppResponse{
|
||||||
|
Headers: []string{"x-foo=bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,14 +166,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
CookieDomain: "example.com",
|
CookieDomain: "example.com",
|
||||||
AppURL: "https://tinyauth.example.com",
|
AppURL: "https://tinyauth.example.com",
|
||||||
SessionCookieName: "tinyauth-session",
|
SessionCookieName: "tinyauth-session",
|
||||||
OIDCClients: func() []model.OIDCClientConfig {
|
|
||||||
var clients []model.OIDCClientConfig
|
|
||||||
for id, client := range config.OIDC.Clients {
|
|
||||||
client.ID = id
|
|
||||||
clients = append(clients, client)
|
|
||||||
}
|
|
||||||
return clients
|
|
||||||
}(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, runtime
|
return config, runtime
|
||||||
|
|||||||
+22
-55
@@ -1,7 +1,6 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -10,27 +9,36 @@ import (
|
|||||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
|
// GetCookieDomain parses the app url and returns the domain value to use for cookies.
|
||||||
func GetCookieDomain(u string) (string, error) {
|
// When auth for subdomains is enabled, it strips the leftmost label
|
||||||
parsed, err := url.Parse(u)
|
// (e.g. sub1.sub2.domain.com -> sub2.domain.com), otherwise it returns the full hostname.
|
||||||
|
func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) {
|
||||||
|
u, err := url.Parse(appUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("invalid app url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
host := parsed.Hostname()
|
hostname := strings.ToLower(u.Hostname())
|
||||||
|
|
||||||
if netIP := net.ParseIP(host); netIP != nil {
|
if netIP := net.ParseIP(hostname); netIP != nil {
|
||||||
return "", errors.New("ip addresses not allowed")
|
return "", fmt.Errorf("ip addresses not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(host, ".")
|
parts := strings.Split(hostname, ".")
|
||||||
|
|
||||||
if len(parts) == 2 {
|
if len(parts) < 2 {
|
||||||
return host, nil
|
return "", fmt.Errorf("invalid app url, must be in format subdomain.domain.tld or domain.tld")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) < 3 {
|
if !subdomainsEnabled || len(parts) == 2 {
|
||||||
return "", errors.New("invalid app url, must be at least second level domain")
|
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, hostname, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("domain in public suffix list, cannot set cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
domain := strings.Join(parts[1:], ".")
|
domain := strings.Join(parts[1:], ".")
|
||||||
@@ -38,33 +46,12 @@ func GetCookieDomain(u string) (string, error) {
|
|||||||
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil)
|
_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("domain in public suffix list, cannot set cookies")
|
return "", fmt.Errorf("domain in public suffix list, cannot set cookies: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStandaloneCookieDomain(u string) (string, error) {
|
|
||||||
parsed, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
host := parsed.Hostname()
|
|
||||||
|
|
||||||
if netIP := net.ParseIP(host); netIP != nil {
|
|
||||||
return "", errors.New("ip addresses not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(host, ".")
|
|
||||||
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return "", errors.New("invalid app url")
|
|
||||||
}
|
|
||||||
|
|
||||||
return host, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseFileToLine(content string) string {
|
func ParseFileToLine(content string) string {
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
users := make([]string, 0)
|
users := make([]string, 0)
|
||||||
@@ -88,23 +75,3 @@ func Filter[T any](slice []T, test func(T) bool) (res []T) {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsRedirectSafe(redirectURL string, domain string) bool {
|
|
||||||
if redirectURL == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := url.Parse(redirectURL)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := parsed.Hostname()
|
|
||||||
|
|
||||||
if strings.HasSuffix(hostname, fmt.Sprintf(".%s", domain)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostname == domain
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,50 +11,71 @@ func TestGetRootDomain(t *testing.T) {
|
|||||||
// Normal case
|
// Normal case
|
||||||
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, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(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, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
// Invalid domain (only TLD)
|
// Invalid domain (only TLD)
|
||||||
domain = "com"
|
domain = "com"
|
||||||
_, err = utils.GetCookieDomain(domain)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
assert.ErrorContains(t, err, "invalid app url, must be at least second level domain")
|
assert.EqualError(t, err, "invalid app url, must be in format subdomain.domain.tld or domain.tld")
|
||||||
|
|
||||||
// IP address
|
// IP address
|
||||||
domain = "http://10.10.10.10"
|
domain = "http://10.10.10.10"
|
||||||
_, err = utils.GetCookieDomain(domain)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
assert.ErrorContains(t, err, "ip addresses not allowed")
|
assert.ErrorContains(t, err, "ip addresses not allowed")
|
||||||
|
|
||||||
// Invalid URL
|
// Invalid URL
|
||||||
domain = "http://[::1]:namedport"
|
domain = "http://[::1]:namedport"
|
||||||
_, err = utils.GetCookieDomain(domain)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
|
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
|
||||||
|
|
||||||
// URL with scheme and path
|
// URL with scheme and path
|
||||||
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, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(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, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, result)
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
// Domain managed by ICANN
|
// Domain managed by ICANN
|
||||||
domain = "http://example.co.uk"
|
domain = "http://example.co.uk"
|
||||||
_, err = utils.GetCookieDomain(domain)
|
_, err = utils.GetCookieDomain(domain, true)
|
||||||
assert.Error(t, err, "domain in public suffix list, cannot set cookies")
|
assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies")
|
||||||
|
|
||||||
|
// Domain without subdomain
|
||||||
|
domain = "http://tinyauth.app"
|
||||||
|
expected = "tinyauth.app"
|
||||||
|
result, err = utils.GetCookieDomain(domain, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Case insensitivity
|
||||||
|
domain = "http://Sub.Tinyauth.App"
|
||||||
|
expected = "tinyauth.app"
|
||||||
|
result, err = utils.GetCookieDomain(domain, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// Subdomains disabled
|
||||||
|
domain = "http://sub.tinyauth.app"
|
||||||
|
expected = "sub.tinyauth.app"
|
||||||
|
result, err = utils.GetCookieDomain(domain, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseFileToLine(t *testing.T) {
|
func TestParseFileToLine(t *testing.T) {
|
||||||
@@ -125,103 +146,3 @@ func TestFilter(t *testing.T) {
|
|||||||
resultStr := utils.Filter(sliceStr, testFuncStr)
|
resultStr := utils.Filter(sliceStr, testFuncStr)
|
||||||
assert.Equal(t, expectedStr, resultStr)
|
assert.Equal(t, expectedStr, resultStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsRedirectSafe(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
domain := "example.com"
|
|
||||||
|
|
||||||
// Case with no subdomain
|
|
||||||
redirectURL := "http://example.com/welcome"
|
|
||||||
result := utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.True(t, result)
|
|
||||||
|
|
||||||
// Case with different domain
|
|
||||||
redirectURL = "http://malicious.com/phishing"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.False(t, result)
|
|
||||||
|
|
||||||
// Case with subdomain
|
|
||||||
redirectURL = "http://sub.example.com/page"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.True(t, result)
|
|
||||||
|
|
||||||
// Case with sub-subdomain
|
|
||||||
redirectURL = "http://a.b.example.com/home"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.True(t, result)
|
|
||||||
|
|
||||||
// Case with empty redirect URL
|
|
||||||
redirectURL = ""
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.False(t, result)
|
|
||||||
|
|
||||||
// Case with invalid URL
|
|
||||||
redirectURL = "http://[::1]:namedport"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.False(t, result)
|
|
||||||
|
|
||||||
// Case with URL having port
|
|
||||||
redirectURL = "http://sub.example.com:8080/page"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.True(t, result)
|
|
||||||
|
|
||||||
// Case with URL having different subdomain
|
|
||||||
redirectURL = "http://another.example.com/page"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.True(t, result)
|
|
||||||
|
|
||||||
// Case with URL having different TLD
|
|
||||||
redirectURL = "http://example.org/page"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.False(t, result)
|
|
||||||
|
|
||||||
// Case with malicious domain
|
|
||||||
redirectURL = "https://malicious-example.com/yoyo"
|
|
||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
|
||||||
assert.False(t, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetStandaloneCookieDomain(t *testing.T) {
|
|
||||||
// Normal case
|
|
||||||
domain := "http://tinyauth.app"
|
|
||||||
expected := "tinyauth.app"
|
|
||||||
result, err := utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// URL with subdomain (full hostname is returned, no subdomain stripping)
|
|
||||||
domain = "http://sub.tinyauth.app"
|
|
||||||
expected = "sub.tinyauth.app"
|
|
||||||
result, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// URL with port (port should be stripped)
|
|
||||||
domain = "http://tinyauth.app:8080"
|
|
||||||
expected = "tinyauth.app"
|
|
||||||
result, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// URL with path
|
|
||||||
domain = "https://tinyauth.app/some/path"
|
|
||||||
expected = "tinyauth.app"
|
|
||||||
result, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// IP address
|
|
||||||
domain = "http://10.10.10.10"
|
|
||||||
_, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.ErrorContains(t, err, "ip addresses not allowed")
|
|
||||||
|
|
||||||
// Invalid domain (only TLD)
|
|
||||||
domain = "com"
|
|
||||||
_, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.ErrorContains(t, err, "invalid app url")
|
|
||||||
|
|
||||||
// Invalid URL
|
|
||||||
domain = "http://[::1]:namedport"
|
|
||||||
_, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user