mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-30 09:28:11 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62ffd2fd11 | |||
| a3ec07230c | |||
| b4eb7090bd | |||
| 2f24f823eb | |||
| 9a219046ac | |||
| 97d58b376d | |||
| b426a1529e | |||
| c7efb71a5a | |||
| eec75a6f49 | |||
| 956d2f55c3 | |||
| 5e822d99e1 | |||
| 373ee8806e | |||
| a14d64c8ba | |||
| d51e3efe32 | |||
| d73cc628fb | |||
| a8737ab0bd | |||
| 11793c9869 | |||
| c68a022ed0 | |||
| 5d95123dcb | |||
| c364b8682c | |||
| ab7c81f63b | |||
| a9a782a9e4 | |||
| 399dee2ee5 | |||
| 6422d5e491 | |||
| a96ee13876 | |||
| 92b435d8cb | |||
| 03164f6c97 | |||
| f3186571cc | |||
| 3906e50925 | |||
| ff81f91366 | |||
| 479f165781 |
@@ -3,7 +3,8 @@ name: Bug report
|
|||||||
about: Create a report to help improve Tinyauth
|
about: Create a report to help improve Tinyauth
|
||||||
title: "[BUG]"
|
title: "[BUG]"
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: steveiliop56
|
assignees:
|
||||||
|
- steveiliop56
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ name: Feature request
|
|||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: "[FEATURE]"
|
title: "[FEATURE]"
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: steveiliop56
|
assignees:
|
||||||
|
- steveiliop56
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Setup bun
|
- name: Setup bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||||
|
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.0"
|
go-version: "^1.26.0"
|
||||||
|
|
||||||
@@ -50,6 +53,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@v6
|
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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
|
||||||
@@ -19,7 +23,7 @@ jobs:
|
|||||||
REPO: ${{ github.event.repository.name }}
|
REPO: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: nightly
|
tag_name: nightly
|
||||||
@@ -33,7 +37,7 @@ jobs:
|
|||||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
@@ -51,15 +55,15 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Install bun
|
- name: Install bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.0"
|
go-version: "^1.26.0"
|
||||||
|
|
||||||
@@ -80,12 +84,12 @@ 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/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: tinyauth-amd64
|
name: tinyauth-amd64
|
||||||
path: tinyauth-amd64
|
path: tinyauth-amd64
|
||||||
@@ -97,15 +101,15 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Install bun
|
- name: Install bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.0"
|
go-version: "^1.26.0"
|
||||||
|
|
||||||
@@ -126,12 +130,12 @@ 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/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: tinyauth-arm64
|
name: tinyauth-arm64
|
||||||
path: tinyauth-arm64
|
path: tinyauth-arm64
|
||||||
@@ -143,28 +147,28 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -186,7 +190,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-amd64
|
name: digests-linux-amd64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -201,28 +205,28 @@ jobs:
|
|||||||
- image-build
|
- image-build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -245,7 +249,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-distroless-linux-amd64
|
name: digests-distroless-linux-amd64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -259,28 +263,28 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
@@ -302,7 +306,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-arm64
|
name: digests-linux-arm64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -317,28 +321,28 @@ jobs:
|
|||||||
- image-build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: nightly
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
@@ -361,7 +365,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-distroless-linux-arm64
|
name: digests-distroless-linux-arm64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -375,25 +379,25 @@ jobs:
|
|||||||
- image-build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -414,25 +418,25 @@ jobs:
|
|||||||
- image-build-arm-distroless
|
- image-build-arm-distroless
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-distroless-*
|
pattern: digests-distroless-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -452,14 +456,14 @@ jobs:
|
|||||||
- binary-build
|
- binary-build
|
||||||
- binary-build-arm
|
- binary-build-arm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v8
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
pattern: tinyauth-*
|
pattern: tinyauth-*
|
||||||
path: binaries
|
path: binaries
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||||
with:
|
with:
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
tag_name: nightly
|
tag_name: nightly
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-metadata:
|
generate-metadata:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -14,7 +18,7 @@ jobs:
|
|||||||
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
BUILD_TIMESTAMP: ${{ steps.metadata.outputs.BUILD_TIMESTAMP }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Generate metadata
|
- name: Generate metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
@@ -29,13 +33,13 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install bun
|
- name: Install bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.0"
|
go-version: "^1.26.0"
|
||||||
|
|
||||||
@@ -56,12 +60,12 @@ 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/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: tinyauth-amd64
|
name: tinyauth-amd64
|
||||||
path: tinyauth-amd64
|
path: tinyauth-amd64
|
||||||
@@ -72,13 +76,13 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install bun
|
- name: Install bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.26.0"
|
go-version: "^1.26.0"
|
||||||
|
|
||||||
@@ -99,12 +103,12 @@ 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/steveiliop56/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: tinyauth-arm64
|
name: tinyauth-arm64
|
||||||
path: tinyauth-arm64
|
path: tinyauth-arm64
|
||||||
@@ -115,26 +119,26 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -156,7 +160,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-amd64
|
name: digests-linux-amd64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -170,26 +174,26 @@ jobs:
|
|||||||
- image-build
|
- image-build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -212,7 +216,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-distroless-linux-amd64
|
name: digests-distroless-linux-amd64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -225,26 +229,26 @@ jobs:
|
|||||||
- generate-metadata
|
- generate-metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
@@ -266,7 +270,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-linux-arm64
|
name: digests-linux-arm64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -280,26 +284,26 @@ jobs:
|
|||||||
- image-build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # 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@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
@@ -322,7 +326,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-distroless-linux-arm64
|
name: digests-distroless-linux-arm64
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -336,25 +340,25 @@ jobs:
|
|||||||
- image-build-arm
|
- image-build-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -377,25 +381,25 @@ jobs:
|
|||||||
- image-build-arm-distroless
|
- image-build-arm-distroless
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-distroless-*
|
pattern: digests-distroless-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 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: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
images: ghcr.io/${{ github.repository_owner }}/tinyauth
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -419,13 +423,13 @@ jobs:
|
|||||||
- binary-build
|
- binary-build
|
||||||
- binary-build-arm
|
- binary-build-arm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v8
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
pattern: tinyauth-*
|
pattern: tinyauth-*
|
||||||
path: binaries
|
path: binaries
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||||
with:
|
with:
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Scorecard supply-chain security
|
||||||
|
on:
|
||||||
|
branch_protection_rule:
|
||||||
|
schedule:
|
||||||
|
- cron: "31 17 * * 5"
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
name: Scorecard analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run analysis
|
||||||
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a
|
||||||
|
with:
|
||||||
|
results_file: results.sarif
|
||||||
|
results_format: sarif
|
||||||
|
publish_results: true
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
||||||
|
with:
|
||||||
|
name: SARIF file
|
||||||
|
path: results.sarif
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
- name: Upload to code-scanning
|
||||||
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
@@ -2,15 +2,19 @@ name: Generate Sponsors List
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-sponsors:
|
generate-sponsors:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Generate Sponsors
|
- name: Generate Sponsors
|
||||||
uses: JamesIves/github-sponsors-readme-action@v1
|
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.SPONSORS_GENERATOR_PAT }}
|
token: ${{ secrets.SPONSORS_GENERATOR_PAT }}
|
||||||
active-only: false
|
active-only: false
|
||||||
@@ -18,7 +22,7 @@ jobs:
|
|||||||
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a> '
|
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a> '
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: |
|
commit-message: |
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: 0 10 * * *
|
- cron: 0 10 * * *
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||||
with:
|
with:
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
|
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
|
||||||
|
|||||||
Vendored
-15
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Connect to server",
|
|
||||||
"type": "go",
|
|
||||||
"request": "attach",
|
|
||||||
"mode": "remote",
|
|
||||||
"remotePath": "/tinyauth",
|
|
||||||
"port": 4000,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"debugAdapter": "legacy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# AI Usage Policy
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> By Tinyauth, we refer to the entire Tinyauth ([tinyauthapp](https://github.com/tinyauthapp)) organization and all of the repositories under it.
|
||||||
|
|
||||||
|
## How we utilize AI in Tinyauth
|
||||||
|
|
||||||
|
In Tinyauth, we see AI as another tool designed to help developers accelerate their work, ***not*** as something that should be doing the development for them. The ways we utilize large language models in Tinyauth are the following:
|
||||||
|
|
||||||
|
- **Pull request reviews**: We utilize [CodeRabbit](https://www.coderabbit.ai/) for reviews in our pull requests which helps us find and fix issues faster, minimizing the time maintainers have to spend reviewing.
|
||||||
|
- **Documentation and Issues**: We use [Dosu](https://dosu.dev/) to help resolve duplicate issues faster and automatically update our documentation based on changes in the code base.
|
||||||
|
- **In-Line Suggestions**: GitHub's [Copilot](https://github.com/features/copilot) is partially used to fill in boilerplate code through in-line suggestions.
|
||||||
|
|
||||||
|
## How we expect the community to use AI
|
||||||
|
|
||||||
|
We expect the Tinyauth community to use AI as a tool for faster development and not as a way to implement entire features through prompts. For this reason, the following guidelines are in place for AI generated content:
|
||||||
|
|
||||||
|
- **All usage must be clearly labeled**: Any content generated by AI must be clearly labeled as such. In the case that a pull request is clearly generated by AI and the author fails to disclose its use, it will be rejected.
|
||||||
|
- **All generated content should be completely understood by the account holder**: The human who utilized the large language model to generate content must have a thorough understanding of it. This includes understanding the resulting output to the full extent and being able to explain it in detail in case it's needed.
|
||||||
|
- **Automated systems are not allowed**: All forms of automated systems that utilize large language models to generate content without human oversight are forbidden. This includes any system that generates content without a human being directly involved in the process like for example with OpenClaw.
|
||||||
|
- **No generated content other than text is allowed**: Images, videos, audio and any other form of content generated by AI other than text is not allowed in Tinyauth.
|
||||||
|
- **AI pull requests are not guaranteed to be accepted or prioritized**: Any pull request that contains AI generated content is not guaranteed to be accepted and/or prioritized. The maintainers are responsible for reviewing all pull requests and determining whether or not they meet the standards of the project. AI generated content will be reviewed with the same standards as any other content, and may be rejected if it does not meet those standards.
|
||||||
|
- **Large generated pull requests will be rejected**: Any pull request that contains a large amount of generated content will be rejected. This is because it is difficult for the maintainers to review and verify large amounts of generated content.
|
||||||
|
|
||||||
|
## Tinyauth is developed by humans, for humans
|
||||||
|
|
||||||
|
Please remember that Tinyauth is developed by humans. While AI can be a useful tool for **assisting** in the development process, it should not be used in place of the human brain. Moving forward, we are committed to ensuring that most, if not all the content in Tinyauth is created and reviewed by humans, and that AI is only used as a tool to assist in the development process.
|
||||||
+4
-1
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
Contributing to Tinyauth is straightforward. Follow the steps below to set up a development server.
|
Contributing to Tinyauth is straightforward. Follow the steps below to set up a development server.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you are using large language models to contribute to the project, please ensure that you have read and understood the [AI Policy](AI_POLICY.md).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Bun
|
- Bun
|
||||||
@@ -15,7 +18,7 @@ Contributing to Tinyauth is straightforward. Follow the steps below to set up a
|
|||||||
Start by cloning the repository:
|
Start by cloning the repository:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/steveiliop56/tinyauth
|
git clone https://github.com/tinyauthapp/tinyauth
|
||||||
cd tinyauth
|
cd tinyauth
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.3.12-alpine AS frontend-builder
|
FROM oven/bun:1.3.13-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ COPY ./internal ./internal
|
|||||||
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.Version=${VERSION} \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM alpine:3.23 AS runner
|
FROM alpine:3.23 AS runner
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Site builder
|
# Site builder
|
||||||
FROM oven/bun:1.3.12-alpine AS frontend-builder
|
FROM oven/bun:1.3.13-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
@@ -40,9 +40,9 @@ COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
|
|||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.Version=${VERSION} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.Version=${VERSION} \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
-X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM gcr.io/distroless/static-debian12:latest AS runner
|
FROM gcr.io/distroless/static-debian12:latest AS runner
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
|||||||
|
|
||||||
# Deps
|
# Deps
|
||||||
deps:
|
deps:
|
||||||
bun install --cwd frontend
|
bun install --frozen-lockfile --cwd frontend
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
# Clean data
|
# Clean data
|
||||||
@@ -37,9 +37,9 @@ webui: clean-webui
|
|||||||
# Build the binary
|
# Build the binary
|
||||||
binary: webui
|
binary: webui
|
||||||
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.Version=${TAG_NAME} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.Version=${TAG_NAME} \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
-X github.com/tinyauthapp/tinyauth/internal/config.CommitHash=${COMMIT_HASH} \
|
||||||
-X github.com/steveiliop56/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
-X github.com/tinyauthapp/tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \
|
||||||
-o ${BIN_NAME} ./cmd/tinyauth
|
-o ${BIN_NAME} ./cmd/tinyauth
|
||||||
|
|
||||||
# Build for amd64
|
# Build for amd64
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="License" src="https://img.shields.io/github/license/steveiliop56/tinyauth">
|
<img alt="License" src="https://img.shields.io/github/license/tinyauthapp/tinyauth">
|
||||||
<img alt="Release" src="https://img.shields.io/github/v/release/steveiliop56/tinyauth">
|
<img alt="Release" src="https://img.shields.io/github/v/release/tinyauthapp/tinyauth">
|
||||||
<img alt="Issues" src="https://img.shields.io/github/issues/steveiliop56/tinyauth">
|
<img alt="Issues" src="https://img.shields.io/github/issues/tinyauthapp/tinyauth">
|
||||||
<img alt="Tinyauth CI" src="https://github.com/steveiliop56/tinyauth/actions/workflows/ci.yml/badge.svg">
|
<img alt="Tinyauth CI" src="https://github.com/tinyauthapp/tinyauth/actions/workflows/ci.yml/badge.svg">
|
||||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/tinyauth"><img src="https://badges.crowdin.net/tinyauth/localized.svg"></a>
|
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/tinyauth"><img src="https://badges.crowdin.net/tinyauth/localized.svg"></a>
|
||||||
|
<a href="https://scorecard.dev/viewer/?uri=github.com/tinyauthapp/tinyauth" target="_blank" title="OpenSSF Scorecard">
|
||||||
|
<img src="https://api.scorecard.dev/projects/github.com/tinyauthapp/tinyauth/badge">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.bestpractices.dev/projects/12681" target="_blank" title="OSSF Best Practices"><img src="https://www.bestpractices.dev/projects/12681/baseline"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
@@ -39,7 +43,7 @@ If you are still not sure if Tinyauth suits your needs you can try out the [demo
|
|||||||
|
|
||||||
You can find documentation and guides on all of the available configuration of Tinyauth in the [website](https://tinyauth.app).
|
You can find documentation and guides on all of the available configuration of Tinyauth in the [website](https://tinyauth.app).
|
||||||
|
|
||||||
If you wish to contribute to the documentation head over to the [repository](https://github.com/steveiliop56/tinyauth-docs).
|
If you wish to contribute to the documentation head over to the [repository](https://github.com/tinyauthapp/docs).
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
|
|
||||||
@@ -47,7 +51,7 @@ Tinyauth has a [Discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
All contributions to the codebase are welcome! If you have any free time, feel free to pick up an [issue](https://github.com/steveiliop56/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running.
|
All contributions to the codebase are welcome! If you have any free time, feel free to pick up an [issue](https://github.com/tinyauthapp/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running.
|
||||||
|
|
||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
@@ -72,4 +76,4 @@ A big thank you to the following people for providing me with more coffee:
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://www.star-history.com/#steveiliop56/tinyauth&Date)
|
[](https://www.star-history.com/#tinyauthapp/tinyauth&Date)
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
It is recommended to use the [latest](https://github.com/steveiliop56/tinyauth/releases/latest) available version of tinyauth. This is because it includes security fixes, new features and dependency updates. Older versions, especially major ones, are not supported and won't receive security or patch updates.
|
It is recommended to use the [latest](https://github.com/tinyauthapp/tinyauth/releases/latest) available version of tinyauth. This is because it includes security fixes, new features and dependency updates. Older versions, especially major ones, are not supported and won't receive security or patch updates.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Due to the nature of this app, it needs to be secure. If you discover any security issues or vulnerabilities in the app please contact me as soon as possible at <steve@doesmycode.work>. Please do not use the issues section to report security issues as I won't be able to patch them in time and they may get exploited by malicious actors.
|
Due to the nature of this app, it needs to be secure. If you discover any security issues or vulnerabilities in the app please contact me as soon as possible at <security@tinyauth.app>. Please do not use the issues section to report security issues as I won't be able to patch them in time and they may get exploited by malicious actors.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"embeds": [
|
"embeds": [
|
||||||
{
|
{
|
||||||
"title": "Welcome to Tinyauth Discord!",
|
"title": "Welcome to Tinyauth Discord!",
|
||||||
"description": "Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy.\n\n**Information**\n\n• Github: <https://github.com/steveiliop56/tinyauth>\n• Website: <https://tinyauth.app>",
|
"description": "Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github and any provider to all of your docker apps. It supports all the popular proxies like Traefik, Nginx and Caddy.\n\n**Information**\n\n• Github: <https://github.com/tinyauthapp/tinyauth>\n• Website: <https://tinyauth.app>",
|
||||||
"url": "https://tinyauth.app",
|
"url": "https://tinyauth.app",
|
||||||
"color": 7002085,
|
"color": 7002085,
|
||||||
"author": {
|
"author": {
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
},
|
},
|
||||||
"timestamp": "2025-06-06T12:25:27.629Z",
|
"timestamp": "2025-06-06T12:25:27.629Z",
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"url": "https://github.com/steveiliop56/tinyauth/blob/main/assets/logo.png?raw=true"
|
"url": "https://github.com/tinyauthapp/tinyauth/blob/main/assets/logo.png?raw=true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"attachments": []
|
"attachments": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/mdp/qrterminal/v3"
|
"github.com/mdp/qrterminal/v3"
|
||||||
@@ -73,7 +73,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
docker = true
|
docker = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.TotpSecret != "" {
|
if user.TOTPSecret != "" {
|
||||||
return fmt.Errorf("user already has a TOTP secret")
|
return fmt.Errorf("user already has a TOTP secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +102,14 @@ func generateTotpCmd() *cli.Command {
|
|||||||
|
|
||||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||||
|
|
||||||
user.TotpSecret = secret
|
user.TOTPSecret = secret
|
||||||
|
|
||||||
// If using docker escape re-escape it
|
// If using docker escape re-escape it
|
||||||
if docker {
|
if docker {
|
||||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/loaders"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
tConfig := config.NewDefaultConfiguration()
|
tConfig := model.NewDefaultConfiguration()
|
||||||
|
|
||||||
loaders := []cli.ResourceLoader{
|
loaders := []cli.ResourceLoader{
|
||||||
&loaders.FileLoader{},
|
&loaders.FileLoader{},
|
||||||
@@ -108,11 +108,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCmd(cfg config.Config) error {
|
func runCmd(cfg model.Config) error {
|
||||||
logger := tlog.NewLogger(cfg.Log)
|
logger := tlog.NewLogger(cfg.Log)
|
||||||
logger.Init()
|
logger.Init()
|
||||||
|
|
||||||
tlog.App.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
tlog.App.Info().Str("version", model.Version).Msg("Starting tinyauth")
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(cfg)
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"charm.land/huh/v2"
|
"charm.land/huh/v2"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -95,7 +95,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
return fmt.Errorf("password is incorrect: %w", err)
|
return fmt.Errorf("password is incorrect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.TotpSecret == "" {
|
if user.TOTPSecret == "" {
|
||||||
if tCfg.Totp != "" {
|
if tCfg.Totp != "" {
|
||||||
tlog.App.Warn().Msg("User does not have TOTP secret")
|
tlog.App.Warn().Msg("User does not have TOTP secret")
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ok := totp.Validate(tCfg.Totp, user.TotpSecret)
|
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("TOTP code incorrect")
|
return fmt.Errorf("TOTP code incorrect")
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func versionCmd() *cli.Command {
|
func versionCmd() *cli.Command {
|
||||||
@@ -15,9 +14,9 @@ func versionCmd() *cli.Command {
|
|||||||
Configuration: nil,
|
Configuration: nil,
|
||||||
Resources: nil,
|
Resources: nil,
|
||||||
Run: func(_ []string) error {
|
Run: func(_ []string) error {
|
||||||
fmt.Printf("Version: %s\n", config.Version)
|
fmt.Printf("Version: %s\n", model.Version)
|
||||||
fmt.Printf("Commit Hash: %s\n", config.CommitHash)
|
fmt.Printf("Commit Hash: %s\n", model.CommitHash)
|
||||||
fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
|
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ services:
|
|||||||
traefik.http.routers.whoami.middlewares: tinyauth
|
traefik.http.routers.whoami.middlewares: tinyauth
|
||||||
|
|
||||||
tinyauth:
|
tinyauth:
|
||||||
image: ghcr.io/steveiliop56/tinyauth:v5
|
image: ghcr.io/tinyauthapp/tinyauth:v5
|
||||||
environment:
|
environment:
|
||||||
- TINYAUTH_APPURL=https://tinyauth.example.com
|
- TINYAUTH_APPURL=https://tinyauth.example.com
|
||||||
- TINYAUTH_AUTH_USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
- TINYAUTH_AUTH_USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ WORKDIR /frontend
|
|||||||
COPY ./frontend/package.json ./
|
COPY ./frontend/package.json ./
|
||||||
COPY ./frontend/bun.lock ./
|
COPY ./frontend/bun.lock ./
|
||||||
|
|
||||||
RUN bun install
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
COPY ./frontend/public ./public
|
COPY ./frontend/public ./public
|
||||||
COPY ./frontend/src ./src
|
COPY ./frontend/src ./src
|
||||||
@@ -19,4 +19,4 @@ COPY ./frontend/vite.config.ts ./
|
|||||||
|
|
||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
|
|
||||||
ENTRYPOINT ["bun", "run", "dev"]
|
ENTRYPOINT ["bun", "run", "dev"]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
onSubmit: (data: LoginSchema) => void;
|
onSubmit: (data: LoginSchema) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
formId?: string;
|
formId?: string;
|
||||||
|
params?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginForm = (props: Props) => {
|
export const LoginForm = (props: Props) => {
|
||||||
@@ -71,6 +72,12 @@ export const LoginForm = (props: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<a
|
<a
|
||||||
href="/forgot-password"
|
href="/forgot-password"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.replace(
|
||||||
|
`/forgot-password${props.params ? `${props.params}` : ""}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm absolute right-0 bottom-[2.565rem]" // 2.565 is *just* perfect
|
className="text-muted-foreground hover:text-muted-foreground/80 text-sm absolute right-0 bottom-[2.565rem]" // 2.565 is *just* perfect
|
||||||
>
|
>
|
||||||
{t("forgotPasswordTitle")}
|
{t("forgotPasswordTitle")}
|
||||||
|
|||||||
@@ -79,5 +79,6 @@
|
|||||||
"profileScopeName": "Profile",
|
"profileScopeName": "Profile",
|
||||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
"groupsScopeName": "Groups",
|
"groupsScopeName": "Groups",
|
||||||
"groupsScopeDescription": "Allows the app to access your group information."
|
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||||
|
"backToLoginButton": "Back to login"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,5 +79,10 @@
|
|||||||
"profileScopeName": "Profile",
|
"profileScopeName": "Profile",
|
||||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||||
"groupsScopeName": "Groups",
|
"groupsScopeName": "Groups",
|
||||||
"groupsScopeDescription": "Allows the app to access your group information."
|
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||||
|
"backToLoginButton": "Back to login",
|
||||||
|
"phoneScopeName": "Phone",
|
||||||
|
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||||
|
"addressScopeName": "Address",
|
||||||
|
"addressScopeDescription": "Allows the app to access your address."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { toast } from "sonner";
|
|||||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import { Mail, Shield, User, Users } from "lucide-react";
|
import { Mail, MapPin, Phone, Shield, User, Users } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -61,6 +61,18 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
|
|||||||
description: t("groupsScopeDescription"),
|
description: t("groupsScopeDescription"),
|
||||||
icon: <Users {...scopeMapIconProps} />,
|
icon: <Users {...scopeMapIconProps} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "phone",
|
||||||
|
name: t("phoneScopeName"),
|
||||||
|
description: t("phoneScopeDescription"),
|
||||||
|
icon: <Phone {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "address",
|
||||||
|
name: t("addressScopeName"),
|
||||||
|
description: t("addressScopeDescription"),
|
||||||
|
icon: <MapPin {...scopeMapIconProps} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { useAppContext } from "@/context/app-context";
|
import { useAppContext } from "@/context/app-context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { useNavigate } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
export const ForgotPasswordPage = () => {
|
export const ForgotPasswordPage = () => {
|
||||||
const { forgotPasswordMessage } = useAppContext();
|
const { forgotPasswordMessage } = useAppContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const { search } = useLocation();
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -36,10 +37,13 @@ export const ForgotPasswordPage = () => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/login");
|
const eparams = searchParams.toString();
|
||||||
|
window.location.replace(
|
||||||
|
`/login${eparams.length > 0 ? `?${eparams}` : ""}`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("notFoundButton")}
|
{t("backToLoginButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -264,6 +264,10 @@ export const LoginPage = () => {
|
|||||||
onSubmit={(values) => loginMutate(values)}
|
onSubmit={(values) => loginMutate(values)}
|
||||||
loading={loginIsPending || oauthIsPending}
|
loading={loginIsPending || oauthIsPending}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
|
params={(() => {
|
||||||
|
const eparams = searchParams.toString();
|
||||||
|
return eparams.length > 0 ? `?${eparams}` : "";
|
||||||
|
})()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{providers.length == 0 && (
|
{providers.length == 0 && (
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvEntry struct {
|
type EnvEntry struct {
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarkdownEntry struct {
|
type MarkdownEntry struct {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/steveiliop56/tinyauth
|
module github.com/tinyauthapp/tinyauth
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
@@ -14,15 +14,16 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/rs/zerolog v1.35.0
|
github.com/rs/zerolog v1.35.1
|
||||||
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.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
modernc.org/sqlite v1.48.2
|
k8s.io/apimachinery v0.32.2
|
||||||
|
k8s.io/client-go v0.32.2
|
||||||
|
modernc.org/sqlite v1.49.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -30,7 +31,7 @@ require (
|
|||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.1.0 // 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.3.0 // indirect
|
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||||
@@ -63,6 +64,7 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
@@ -73,7 +75,9 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
@@ -92,6 +96,7 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
@@ -106,6 +111,7 @@ require (
|
|||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
@@ -117,15 +123,23 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/term v0.42.0 // indirect
|
golang.org/x/term v0.42.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||||
|
modernc.org/libc v1.72.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ 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=
|
||||||
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.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
@@ -97,10 +97,14 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
|||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
@@ -118,6 +122,12 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -130,14 +140,23 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||||
|
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -162,8 +181,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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -176,6 +199,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -209,6 +234,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -234,14 +261,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
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.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
github.com/rs/zerolog v1.35.0/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/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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -261,8 +290,12 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
|||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
@@ -289,29 +322,54 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
@@ -324,15 +382,31 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
|
||||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
|
||||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||||
|
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
|
||||||
|
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
|
||||||
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||||
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||||
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
|
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||||
|
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||||
|
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||||
|
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||||
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=
|
||||||
@@ -341,8 +415,8 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/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.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||||
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=
|
||||||
@@ -351,11 +425,17 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/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.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
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=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||||
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||||
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
|
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "given_name";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "family_name";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "middle_name";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "nickname";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "profile";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "picture";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "website";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "gender";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "birthdate";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "zoneinfo";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "locale";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "phone_number";
|
||||||
|
ALTER TABLE "oidc_userinfo" DROP COLUMN "address";
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "given_name" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "family_name" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "middle_name" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "nickname" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "profile" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "picture" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "website" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "gender" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "birthdate" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "zoneinfo" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "locale" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "phone_number" TEXT NOT NULL DEFAULT "";
|
||||||
|
ALTER TABLE "oidc_userinfo" ADD COLUMN "address" TEXT NOT NULL DEFAULT "{}";
|
||||||
@@ -12,15 +12,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config config.Config
|
config model.Config
|
||||||
context struct {
|
context struct {
|
||||||
appUrl string
|
appUrl string
|
||||||
uuid string
|
uuid string
|
||||||
@@ -29,24 +29,26 @@ type BootstrapApp struct {
|
|||||||
csrfCookieName string
|
csrfCookieName string
|
||||||
redirectCookieName string
|
redirectCookieName string
|
||||||
oauthSessionCookieName string
|
oauthSessionCookieName string
|
||||||
users []config.User
|
localUsers []model.LocalUser
|
||||||
oauthProviders map[string]config.OAuthServiceConfig
|
oauthProviders map[string]model.OAuthServiceConfig
|
||||||
configuredProviders []controller.Provider
|
configuredProviders []controller.Provider
|
||||||
oidcClients []config.OIDCClientConfig
|
oidcClients []model.OIDCClientConfig
|
||||||
}
|
}
|
||||||
services Services
|
services Services
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBootstrapApp(config config.Config) *BootstrapApp {
|
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
||||||
return &BootstrapApp{
|
return &BootstrapApp{
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) Setup() error {
|
func (app *BootstrapApp) Setup() error {
|
||||||
fmt.Println("Tinyauth is moving to an organization! All versions after v5.0.7 will be released under ghcr.io/tinyauthapp/tinyauth. Existing images will continue to work but new features and updates (including security ones) will only be released under the new image path.")
|
|
||||||
|
|
||||||
// get app url
|
// get app url
|
||||||
|
if app.config.AppURL == "" {
|
||||||
|
return fmt.Errorf("app URL cannot be empty, perhaps config loading failed")
|
||||||
|
}
|
||||||
|
|
||||||
appUrl, err := url.Parse(app.config.AppURL)
|
appUrl, err := url.Parse(app.config.AppURL)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61,13 +63,13 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse users
|
// Parse users
|
||||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile, app.config.Auth.UserAttributes)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
app.context.users = users
|
app.context.localUsers = *users
|
||||||
|
|
||||||
// Setup OAuth providers
|
// Setup OAuth providers
|
||||||
app.context.oauthProviders = app.config.OAuth.Providers
|
app.context.oauthProviders = app.config.OAuth.Providers
|
||||||
@@ -86,7 +88,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
for id, provider := range app.context.oauthProviders {
|
for id, provider := range app.context.oauthProviders {
|
||||||
if provider.Name == "" {
|
if provider.Name == "" {
|
||||||
if name, ok := config.OverrideProviders[id]; ok {
|
if name, ok := model.OverrideProviders[id]; ok {
|
||||||
provider.Name = name
|
provider.Name = name
|
||||||
} else {
|
} else {
|
||||||
provider.Name = utils.Capitalize(id)
|
provider.Name = utils.Capitalize(id)
|
||||||
@@ -113,14 +115,14 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
// Cookie names
|
// Cookie names
|
||||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
app.context.sessionCookieName = fmt.Sprintf("%s-%s", model.SessionCookieName, cookieId)
|
||||||
app.context.csrfCookieName = fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
|
app.context.csrfCookieName = fmt.Sprintf("%s-%s", model.CSRFCookieName, cookieId)
|
||||||
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
app.context.redirectCookieName = fmt.Sprintf("%s-%s", model.RedirectCookieName, cookieId)
|
||||||
app.context.oauthSessionCookieName = fmt.Sprintf("%s-%s", config.OAuthSessionCookieName, cookieId)
|
app.context.oauthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId)
|
||||||
|
|
||||||
// Dumps
|
// Dumps
|
||||||
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
||||||
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
|
tlog.App.Trace().Interface("users", app.context.localUsers).Msg("Users dump")
|
||||||
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||||
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||||
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||||
@@ -169,7 +171,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if services.authService.LdapAuthConfigured() {
|
if services.authService.LDAPAuthConfigured() {
|
||||||
configuredProviders = append(configuredProviders, controller.Provider{
|
configuredProviders = append(configuredProviders, controller.Provider{
|
||||||
Name: "LDAP",
|
Name: "LDAP",
|
||||||
ID: "ldap",
|
ID: "ldap",
|
||||||
@@ -242,7 +244,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
|||||||
var body heartbeat
|
var body heartbeat
|
||||||
|
|
||||||
body.UUID = app.context.uuid
|
body.UUID = app.context.uuid
|
||||||
body.Version = config.Version
|
body.Version = model.Version
|
||||||
|
|
||||||
bodyJson, err := json.Marshal(body)
|
bodyJson, err := json.Marshal(body)
|
||||||
|
|
||||||
@@ -255,7 +257,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
|||||||
Timeout: 30 * time.Second, // The server should never take more than 30 seconds to respond
|
Timeout: 30 * time.Second, // The server should never take more than 30 seconds to respond
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
heartbeatURL := model.APIServer + "/v1/instances/heartbeat"
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
tlog.App.Debug().Msg("Sending heartbeat")
|
tlog.App.Debug().Msg("Sending heartbeat")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
"github.com/tinyauthapp/tinyauth/internal/assets"
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||||
"github.com/steveiliop56/tinyauth/internal/middleware"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
var DEV_MODES = []string{"main", "test", "development"}
|
var DEV_MODES = []string{"main", "test", "development"}
|
||||||
|
|
||||||
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
||||||
if !slices.Contains(DEV_MODES, config.Version) {
|
if !slices.Contains(DEV_MODES, model.Version) {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,8 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
}, app.services.authService, app.services.oauthBrokerService)
|
}, app.services.authService, app.services.oauthBrokerService)
|
||||||
|
|
||||||
err := contextMiddleware.Init()
|
err := contextMiddleware.Init()
|
||||||
@@ -98,7 +99,8 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
proxyController.SetupRoutes()
|
proxyController.SetupRoutes()
|
||||||
|
|
||||||
userController := controller.NewUserController(controller.UserControllerConfig{
|
userController := controller.NewUserController(controller.UserControllerConfig{
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
}, apiRouter, app.services.authService)
|
}, apiRouter, app.services.authService)
|
||||||
|
|
||||||
userController.SetupRoutes()
|
userController.SetupRoutes()
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"os"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
accessControlService *service.AccessControlsService
|
accessControlService *service.AccessControlsService
|
||||||
authService *service.AuthService
|
authService *service.AuthService
|
||||||
dockerService *service.DockerService
|
dockerService *service.DockerService
|
||||||
|
kubernetesService *service.KubernetesService
|
||||||
ldapService *service.LdapService
|
ldapService *service.LdapService
|
||||||
oauthBrokerService *service.OAuthBrokerService
|
oauthBrokerService *service.OAuthBrokerService
|
||||||
oidcService *service.OIDCService
|
oidcService *service.OIDCService
|
||||||
@@ -19,14 +22,14 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
services := Services{}
|
services := Services{}
|
||||||
|
|
||||||
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
ldapService := service.NewLdapService(service.LdapServiceConfig{
|
||||||
Address: app.config.Ldap.Address,
|
Address: app.config.LDAP.Address,
|
||||||
BindDN: app.config.Ldap.BindDN,
|
BindDN: app.config.LDAP.BindDN,
|
||||||
BindPassword: app.config.Ldap.BindPassword,
|
BindPassword: app.config.LDAP.BindPassword,
|
||||||
BaseDN: app.config.Ldap.BaseDN,
|
BaseDN: app.config.LDAP.BaseDN,
|
||||||
Insecure: app.config.Ldap.Insecure,
|
Insecure: app.config.LDAP.Insecure,
|
||||||
SearchFilter: app.config.Ldap.SearchFilter,
|
SearchFilter: app.config.LDAP.SearchFilter,
|
||||||
AuthCert: app.config.Ldap.AuthCert,
|
AuthCert: app.config.LDAP.AuthCert,
|
||||||
AuthKey: app.config.Ldap.AuthKey,
|
AuthKey: app.config.LDAP.AuthKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
err := ldapService.Init()
|
err := ldapService.Init()
|
||||||
@@ -38,17 +41,34 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
|
|
||||||
services.ldapService = ldapService
|
services.ldapService = ldapService
|
||||||
|
|
||||||
dockerService := service.NewDockerService()
|
var labelProvider service.LabelProvider
|
||||||
|
var dockerService *service.DockerService
|
||||||
|
var kubernetesService *service.KubernetesService
|
||||||
|
|
||||||
err = dockerService.Init()
|
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
||||||
|
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
||||||
|
|
||||||
if err != nil {
|
if useKubernetes {
|
||||||
return Services{}, err
|
tlog.App.Debug().Msg("Using Kubernetes label provider")
|
||||||
|
kubernetesService = service.NewKubernetesService()
|
||||||
|
err = kubernetesService.Init()
|
||||||
|
if err != nil {
|
||||||
|
return Services{}, err
|
||||||
|
}
|
||||||
|
services.kubernetesService = kubernetesService
|
||||||
|
labelProvider = kubernetesService
|
||||||
|
} else {
|
||||||
|
tlog.App.Debug().Msg("Using Docker label provider")
|
||||||
|
dockerService = service.NewDockerService()
|
||||||
|
err = dockerService.Init()
|
||||||
|
if err != nil {
|
||||||
|
return Services{}, err
|
||||||
|
}
|
||||||
|
services.dockerService = dockerService
|
||||||
|
labelProvider = dockerService
|
||||||
}
|
}
|
||||||
|
|
||||||
services.dockerService = dockerService
|
accessControlsService := service.NewAccessControlsService(labelProvider, app.config.Apps)
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(dockerService, app.config.Apps)
|
|
||||||
|
|
||||||
err = accessControlsService.Init()
|
err = accessControlsService.Init()
|
||||||
|
|
||||||
@@ -69,7 +89,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
services.oauthBrokerService = oauthBrokerService
|
services.oauthBrokerService = oauthBrokerService
|
||||||
|
|
||||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||||
Users: app.context.users,
|
LocalUsers: app.context.localUsers,
|
||||||
OauthWhitelist: app.config.OAuth.Whitelist,
|
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
||||||
@@ -79,8 +99,8 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||||
SessionCookieName: app.context.sessionCookieName,
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
IP: app.config.Auth.IP,
|
IP: app.config.Auth.IP,
|
||||||
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
|
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
|
||||||
}, dockerService, services.ldapService, queries, services.oauthBrokerService)
|
}, services.ldapService, queries, services.oauthBrokerService)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -19,7 +19,7 @@ type UserContextResponse struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
OAuth bool `json:"oauth"`
|
OAuth bool `json:"oauth"`
|
||||||
TotpPending bool `json:"totpPending"`
|
TOTPPending bool `json:"totpPending"`
|
||||||
OAuthName string `json:"oauthName"`
|
OAuthName string `json:"oauthName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,28 +76,29 @@ func (controller *ContextController) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||||
context, err := utils.GetContext(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Msg("No user context found in request")
|
||||||
|
c.JSON(200, UserContextResponse{
|
||||||
|
Status: 401,
|
||||||
|
Message: "Unauthorized",
|
||||||
|
IsLoggedIn: false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userContext := UserContextResponse{
|
userContext := UserContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
IsLoggedIn: context.IsLoggedIn,
|
IsLoggedIn: context.Authenticated,
|
||||||
Username: context.Username,
|
Username: context.GetUsername(),
|
||||||
Name: context.Name,
|
Name: context.GetName(),
|
||||||
Email: context.Email,
|
Email: context.GetEmail(),
|
||||||
Provider: context.Provider,
|
Provider: context.ProviderName(),
|
||||||
OAuth: context.OAuth,
|
OAuth: context.IsOAuth(),
|
||||||
TotpPending: context.TotpPending,
|
TOTPPending: context.TOTPPending(),
|
||||||
OAuthName: context.OAuthName,
|
OAuthName: context.OAuthName(),
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Debug().Err(err).Msg("No user context found in request")
|
|
||||||
userContext.Status = 401
|
|
||||||
userContext.Message = "Unauthorized"
|
|
||||||
userContext.IsLoggedIn = false
|
|
||||||
c.JSON(200, userContext)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, userContext)
|
c.JSON(200, userContext)
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
type UnauthorizedQuery struct {
|
||||||
|
Username string `url:"username"`
|
||||||
|
Resource string `url:"resource"`
|
||||||
|
GroupErr bool `url:"groupErr"`
|
||||||
|
IP string `url:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedirectQuery struct {
|
||||||
|
RedirectURI string `url:"redirect_uri"`
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
@@ -176,7 +175,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
|
||||||
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
|
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
|
||||||
|
|
||||||
queries, err := query.Values(config.UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Username: user.Email,
|
Username: user.Email,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -236,7 +235,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
@@ -244,6 +243,8 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
||||||
|
|
||||||
if controller.isOidcRequest(oauthPendingSession.CallbackParams) {
|
if controller.isOidcRequest(oauthPendingSession.CallbackParams) {
|
||||||
@@ -259,7 +260,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oauthPendingSession.CallbackParams.RedirectURI != "" {
|
if oauthPendingSession.CallbackParams.RedirectURI != "" {
|
||||||
queries, err := query.Values(config.RedirectQuery{
|
queries, err := query.Values(RedirectQuery{
|
||||||
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
|
RedirectURI: oauthPendingSession.CallbackParams.RedirectURI,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OIDCControllerConfig struct{}
|
type OIDCControllerConfig struct{}
|
||||||
@@ -111,14 +112,14 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userContext, err := utils.GetContext(c)
|
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userContext.IsLoggedIn {
|
if !userContext.Authenticated {
|
||||||
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "")
|
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,7 +152,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
// WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too.
|
||||||
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.Username, client.ID))
|
sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.GetUsername(), client.ID))
|
||||||
code := utils.GenerateString(32)
|
code := utils.GenerateString(32)
|
||||||
|
|
||||||
// Before storing the code, delete old session
|
// Before storing the code, delete old session
|
||||||
@@ -170,7 +171,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
|
|
||||||
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
||||||
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
||||||
err = controller.oidc.StoreUserinfo(c, sub, userContext, req)
|
err = controller.oidc.StoreUserinfo(c, sub, *userContext, req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to insert user info into database")
|
tlog.App.Error().Err(err).Msg("Failed to insert user info into database")
|
||||||
@@ -429,7 +430,7 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == service.ErrTokenNotFound {
|
if errors.Is(err, service.ErrTokenNotFound) {
|
||||||
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"error": "invalid_grant",
|
"error": "invalid_grant",
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
@@ -99,12 +99,16 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if acls == nil {
|
||||||
|
acls = &model.App{}
|
||||||
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
if controller.auth.IsBypassedIP(acls.IP, clientIP) {
|
if controller.auth.IsBypassedIP(&acls.IP, clientIP) {
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, *acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -112,7 +116,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls.Path)
|
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, &acls.Path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||||
@@ -122,7 +126,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !authEnabled {
|
if !authEnabled {
|
||||||
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
|
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, *acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Authenticated",
|
"message": "Authenticated",
|
||||||
@@ -130,8 +134,8 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.CheckIP(acls.IP, clientIP) {
|
if !controller.auth.CheckIP(&acls.IP, clientIP) {
|
||||||
queries, err := query.Values(config.UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
IP: clientIP,
|
IP: clientIP,
|
||||||
})
|
})
|
||||||
@@ -157,28 +161,24 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var userContext config.UserContext
|
userContext, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
context, err := utils.GetContext(c)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Debug().Msg("No user context found in request, treating as not logged in")
|
tlog.App.Debug().Err(err).Msg("No user context found in request, treating as unauthenticated")
|
||||||
userContext = config.UserContext{
|
userContext = &model.UserContext{
|
||||||
IsLoggedIn: false,
|
Authenticated: false,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
userContext = context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||||
|
|
||||||
if userContext.IsLoggedIn {
|
if userContext.Authenticated {
|
||||||
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
||||||
|
|
||||||
if !userAllowed {
|
if !userAllowed {
|
||||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
|
tlog.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User not allowed to access resource")
|
||||||
|
|
||||||
queries, err := query.Values(config.UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -188,10 +188,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.OAuth {
|
if userContext.IsOAuth() {
|
||||||
queries.Set("username", userContext.Email)
|
queries.Set("username", userContext.GetEmail())
|
||||||
} else {
|
} else {
|
||||||
queries.Set("username", userContext.Username)
|
queries.Set("username", userContext.GetUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
||||||
@@ -209,19 +209,19 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.OAuth || userContext.Provider == "ldap" {
|
if userContext.IsOAuth() || userContext.IsLDAP() {
|
||||||
var groupOK bool
|
var groupOK bool
|
||||||
|
|
||||||
if userContext.OAuth {
|
if userContext.IsOAuth() {
|
||||||
groupOK = controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls.OAuth.Groups)
|
||||||
} else {
|
} else {
|
||||||
groupOK = controller.auth.IsInLdapGroup(c, userContext, acls.LDAP.Groups)
|
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls.LDAP.Groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
|
tlog.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User groups do not match resource requirements")
|
||||||
|
|
||||||
queries, err := query.Values(config.UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
GroupErr: true,
|
GroupErr: true,
|
||||||
})
|
})
|
||||||
@@ -232,10 +232,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.OAuth {
|
if userContext.IsOAuth() {
|
||||||
queries.Set("username", userContext.Email)
|
queries.Set("username", userContext.GetEmail())
|
||||||
} else {
|
} else {
|
||||||
queries.Set("username", userContext.Username)
|
queries.Set("username", userContext.GetUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
redirectURL := fmt.Sprintf("%s/unauthorized?%s", controller.config.AppURL, queries.Encode())
|
||||||
@@ -254,19 +254,20 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
c.Header("Remote-User", utils.SanitizeHeader(userContext.GetUsername()))
|
||||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
c.Header("Remote-Name", utils.SanitizeHeader(userContext.GetName()))
|
||||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
c.Header("Remote-Email", utils.SanitizeHeader(userContext.GetEmail()))
|
||||||
|
|
||||||
if userContext.Provider == "ldap" {
|
if userContext.IsLDAP() {
|
||||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.LdapGroups))
|
c.Header("Remote-Groups", utils.SanitizeHeader(strings.Join(userContext.LDAP.Groups, ",")))
|
||||||
} else if userContext.Provider != "local" {
|
|
||||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
if userContext.IsOAuth() {
|
||||||
|
c.Header("Remote-Groups", utils.SanitizeHeader(strings.Join(userContext.OAuth.Groups, ",")))
|
||||||
|
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuth.Sub))
|
||||||
|
}
|
||||||
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, *acls)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -275,7 +276,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
queries, err := query.Values(config.RedirectQuery{
|
queries, err := query.Values(RedirectQuery{
|
||||||
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
|
RedirectURI: fmt.Sprintf("%s://%s%s", proxyCtx.Proto, proxyCtx.Host, proxyCtx.Path),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
|
func (controller *ProxyController) setHeaders(c *gin.Context, acls model.App) {
|
||||||
c.Header("Authorization", c.Request.Header.Get("Authorization"))
|
c.Header("Authorization", c.Request.Header.Get("Authorization"))
|
||||||
|
|
||||||
headers := utils.ParseHeaders(acls.Response.Headers)
|
headers := utils.ParseHeaders(acls.Response.Headers)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -412,7 +412,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
err = broker.Init()
|
err = broker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -23,7 +26,8 @@ type TotpRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserControllerConfig struct {
|
type UserControllerConfig struct {
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
|
SessionCookieName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserController struct {
|
type UserController struct {
|
||||||
@@ -76,20 +80,28 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSearch := controller.auth.SearchUser(req.Username)
|
search, err := controller.auth.SearchUser(req.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" {
|
if err != nil {
|
||||||
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
if errors.Is(err, service.ErrUserNotFound) {
|
||||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
|
||||||
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
c.JSON(401, gin.H{
|
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
||||||
"status": 401,
|
c.JSON(401, gin.H{
|
||||||
"message": "Unauthorized",
|
"status": 401,
|
||||||
|
"message": "Unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tlog.App.Error().Err(err).Str("username", req.Username).Msg("Error searching for user")
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": 500,
|
||||||
|
"message": "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
|
||||||
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
|
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
|
||||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
||||||
@@ -105,16 +117,28 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
controller.auth.RecordLoginAttempt(req.Username, true)
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
if userSearch.Type == "local" {
|
var localUser *model.LocalUser
|
||||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
|
||||||
|
|
||||||
if user.TotpSecret != "" {
|
if search.Type == model.UserLocal {
|
||||||
|
localUser = controller.auth.GetLocalUser(req.Username)
|
||||||
|
|
||||||
|
if localUser.TOTPSecret != "" {
|
||||||
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
|
||||||
|
|
||||||
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
name := localUser.Attributes.Name
|
||||||
Username: user.Username,
|
if name == "" {
|
||||||
Name: utils.Capitalize(user.Username),
|
name = utils.Capitalize(localUser.Username)
|
||||||
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
}
|
||||||
|
|
||||||
|
email := localUser.Attributes.Email
|
||||||
|
if email == "" {
|
||||||
|
email = utils.CompileUserEmail(localUser.Username, controller.config.CookieDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := controller.auth.CreateSession(c, repository.Session{
|
||||||
|
Username: localUser.Username,
|
||||||
|
Name: name,
|
||||||
|
Email: email,
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
})
|
})
|
||||||
@@ -128,6 +152,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "TOTP required",
|
"message": "TOTP required",
|
||||||
@@ -144,13 +170,22 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
Provider: "local",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSearch.Type == "ldap" {
|
if search.Type == model.UserLocal {
|
||||||
|
if localUser.Attributes.Name != "" {
|
||||||
|
sessionCookie.Name = localUser.Attributes.Name
|
||||||
|
}
|
||||||
|
if localUser.Attributes.Email != "" {
|
||||||
|
sessionCookie.Email = localUser.Attributes.Email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Type == model.UserLDAP {
|
||||||
sessionCookie.Provider = "ldap"
|
sessionCookie.Provider = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
@@ -161,6 +196,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
@@ -170,13 +207,51 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
func (controller *UserController) logoutHandler(c *gin.Context) {
|
func (controller *UserController) logoutHandler(c *gin.Context) {
|
||||||
tlog.App.Debug().Msg("Logout request received")
|
tlog.App.Debug().Msg("Logout request received")
|
||||||
|
|
||||||
controller.auth.DeleteSessionCookie(c)
|
uuid, err := c.Cookie(controller.config.SessionCookieName)
|
||||||
|
|
||||||
context, err := utils.GetContext(c)
|
if err != nil {
|
||||||
if err == nil && context.IsLoggedIn {
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
tlog.AuditLogout(c, context.Username, context.Provider)
|
tlog.App.Warn().Msg("No session cookie found on logout request")
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Logout successful",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tlog.App.Error().Err(err).Msg("Error retrieving session cookie on logout")
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": 500,
|
||||||
|
"message": "Internal Server Error",
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Failed to get user context on logout")
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": 500,
|
||||||
|
"message": "Internal Server Error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := controller.auth.DeleteSession(c, uuid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Msg("Error deleting session on logout")
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": 500,
|
||||||
|
"message": "Internal Server Error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
|
||||||
|
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Logout successful",
|
"message": "Logout successful",
|
||||||
@@ -196,7 +271,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context, err := utils.GetContext(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
tlog.App.Error().Err(err).Msg("Failed to get user context")
|
||||||
@@ -207,7 +282,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.TotpPending {
|
if !context.TOTPPending() {
|
||||||
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
@@ -216,12 +291,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
|
tlog.App.Debug().Str("username", context.GetUsername()).Msg("TOTP verification attempt")
|
||||||
|
|
||||||
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
isLocked, remaining := controller.auth.IsAccountLocked(context.GetUsername())
|
||||||
|
|
||||||
if isLocked {
|
if isLocked {
|
||||||
tlog.App.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
|
tlog.App.Warn().Str("username", context.GetUsername()).Msg("Account is locked due to too many failed TOTP attempts")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.JSON(429, gin.H{
|
c.JSON(429, gin.H{
|
||||||
@@ -231,14 +306,14 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := controller.auth.GetLocalUser(context.Username)
|
user := controller.auth.GetLocalUser(context.GetUsername())
|
||||||
|
|
||||||
ok := totp.Validate(req.Code, user.TotpSecret)
|
ok := totp.Validate(req.Code, user.TOTPSecret)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
tlog.App.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
|
tlog.App.Warn().Str("username", context.GetUsername()).Msg("Invalid TOTP code")
|
||||||
controller.auth.RecordLoginAttempt(context.Username, false)
|
controller.auth.RecordLoginAttempt(context.GetUsername(), false)
|
||||||
tlog.AuditLoginFailure(c, context.Username, "totp", "invalid totp code")
|
tlog.AuditLoginFailure(c, context.GetUsername(), "totp", "invalid totp code")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -246,10 +321,10 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Info().Str("username", context.Username).Msg("TOTP verification successful")
|
tlog.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful")
|
||||||
tlog.AuditLoginSuccess(c, context.Username, "totp")
|
tlog.AuditLoginSuccess(c, context.GetUsername(), "totp")
|
||||||
|
|
||||||
controller.auth.RecordLoginAttempt(context.Username, true)
|
controller.auth.RecordLoginAttempt(context.GetUsername(), true)
|
||||||
|
|
||||||
sessionCookie := repository.Session{
|
sessionCookie := repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
@@ -258,9 +333,16 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
Provider: "local",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Attributes.Name != "" {
|
||||||
|
sessionCookie.Name = user.Attributes.Name
|
||||||
|
}
|
||||||
|
if user.Attributes.Email != "" {
|
||||||
|
sessionCookie.Email = user.Attributes.Email
|
||||||
|
}
|
||||||
|
|
||||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
cookie, err := controller.auth.CreateSession(c, sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
@@ -271,6 +353,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Login successful",
|
"message": "Login successful",
|
||||||
|
|||||||
@@ -4,19 +4,18 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -36,6 +35,23 @@ func TestUserController(t *testing.T) {
|
|||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Username: "attruser",
|
||||||
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
|
Attributes: config.UserAttributes{
|
||||||
|
Name: "Alice Smith",
|
||||||
|
Email: "alice@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Username: "attrtotpuser",
|
||||||
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
|
Attributes: config.UserAttributes{
|
||||||
|
Name: "Bob Jones",
|
||||||
|
Email: "bob@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SessionExpiry: 10, // 10 seconds, useful for testing
|
SessionExpiry: 10, // 10 seconds, useful for testing
|
||||||
CookieDomain: "example.com",
|
CookieDomain: "example.com",
|
||||||
@@ -273,6 +289,64 @@ func TestUserController(t *testing.T) {
|
|||||||
assert.Contains(t, recorder.Body.String(), "Too many failed TOTP attempts.")
|
assert.Contains(t, recorder.Body.String(), "Too many failed TOTP attempts.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Login uses name and email from user attributes",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
loginReq := controller.LoginRequest{Username: "attruser", Password: "password"}
|
||||||
|
body, err := json.Marshal(loginReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
require.Equal(t, 200, recorder.Code)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
assert.Equal(t, "tinyauth-session", cookies[0].Name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Login with TOTP uses name and email from user attributes in pending session",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
loginReq := controller.LoginRequest{Username: "attrtotpuser", Password: "password"}
|
||||||
|
body, err := json.Marshal(loginReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
require.Equal(t, 200, recorder.Code)
|
||||||
|
var res map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||||
|
assert.Equal(t, true, res["totpPending"])
|
||||||
|
require.Len(t, recorder.Result().Cookies(), 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "TOTP completion uses name and email from user attributes",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
totpReq := controller.TotpRequest{Code: code}
|
||||||
|
body, err := json.Marshal(totpReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
require.Equal(t, 200, recorder.Code)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
assert.Equal(t, "tinyauth-session", cookies[0].Name)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||||
@@ -296,7 +370,7 @@ func TestUserController(t *testing.T) {
|
|||||||
err = broker.Init()
|
err = broker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -305,9 +379,31 @@ func TestUserController(t *testing.T) {
|
|||||||
authService.ClearRateLimitsTestingOnly()
|
authService.ClearRateLimitsTestingOnly()
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotpMiddlewareOverrides := []string{
|
setTotpMiddlewareOverrides := map[string]config.UserContext{
|
||||||
"Should be able to login with totp",
|
"Should be able to login with totp": {
|
||||||
"Totp should rate limit on multiple invalid attempts",
|
Username: "totpuser",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
"Totp should rate limit on multiple invalid attempts": {
|
||||||
|
Username: "totpuser",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
"TOTP completion uses name and email from user attributes": {
|
||||||
|
Username: "attrtotpuser",
|
||||||
|
Name: "Bob Jones",
|
||||||
|
Email: "bob@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -321,18 +417,10 @@ func TestUserController(t *testing.T) {
|
|||||||
|
|
||||||
// Gin is stupid and doesn't allow setting a middleware after the groups
|
// Gin is stupid and doesn't allow setting a middleware after the groups
|
||||||
// so we need to do some stupid overrides here
|
// so we need to do some stupid overrides here
|
||||||
if slices.Contains(setTotpMiddlewareOverrides, test.description) {
|
if ctx, ok := setTotpMiddlewareOverrides[test.description]; ok {
|
||||||
// Assuming the cookie is set, it should be picked up by the
|
ctx := ctx
|
||||||
// context middleware
|
|
||||||
router.Use(func(c *gin.Context) {
|
router.Use(func(c *gin.Context) {
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &ctx)
|
||||||
Username: "totpuser",
|
|
||||||
Name: "Totpuser",
|
|
||||||
Email: "totpuser@example.com",
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: true,
|
|
||||||
TotpEnabled: true,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenIDConnectConfiguration struct {
|
type OpenIDConnectConfiguration struct {
|
||||||
@@ -61,7 +61,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
|
|||||||
SubjectTypesSupported: []string{"pairwise"},
|
SubjectTypesSupported: []string{"pairwise"},
|
||||||
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||||
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"},
|
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,
|
RequestParameterSupported: true,
|
||||||
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/tinyauthapp/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -67,7 +67,7 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
SubjectTypesSupported: []string{"pairwise"},
|
SubjectTypesSupported: []string{"pairwise"},
|
||||||
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||||
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"},
|
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,
|
RequestParameterSupported: true,
|
||||||
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -33,7 +36,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ContextMiddlewareConfig struct {
|
type ContextMiddlewareConfig struct {
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
|
SessionCookieName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextMiddleware struct {
|
type ContextMiddleware struct {
|
||||||
@@ -61,177 +65,43 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := m.auth.GetSessionCookie(c)
|
uuid, err := c.Cookie(m.config.SessionCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err == nil {
|
||||||
tlog.App.Debug().Err(err).Msg("No valid session cookie found")
|
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid)
|
||||||
goto basic
|
|
||||||
}
|
|
||||||
|
|
||||||
if cookie.TotpPending {
|
|
||||||
c.Set("context", &config.UserContext{
|
|
||||||
Username: cookie.Username,
|
|
||||||
Name: cookie.Name,
|
|
||||||
Email: cookie.Email,
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: true,
|
|
||||||
TotpEnabled: true,
|
|
||||||
})
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cookie.Provider {
|
|
||||||
case "local", "ldap":
|
|
||||||
userSearch := m.auth.SearchUser(cookie.Username)
|
|
||||||
|
|
||||||
if userSearch.Type == "unknown" {
|
|
||||||
tlog.App.Debug().Msg("User from session cookie not found")
|
|
||||||
m.auth.DeleteSessionCookie(c)
|
|
||||||
goto basic
|
|
||||||
}
|
|
||||||
|
|
||||||
if userSearch.Type != cookie.Provider {
|
|
||||||
tlog.App.Warn().Msg("User type from session cookie does not match user search type")
|
|
||||||
m.auth.DeleteSessionCookie(c)
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var ldapGroups []string
|
|
||||||
|
|
||||||
if cookie.Provider == "ldap" {
|
|
||||||
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Error().Err(err).Msg("Error retrieving LDAP user details")
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ldapGroups = ldapUser.Groups
|
|
||||||
}
|
|
||||||
|
|
||||||
m.auth.RefreshSessionCookie(c)
|
|
||||||
c.Set("context", &config.UserContext{
|
|
||||||
Username: cookie.Username,
|
|
||||||
Name: cookie.Name,
|
|
||||||
Email: cookie.Email,
|
|
||||||
Provider: cookie.Provider,
|
|
||||||
IsLoggedIn: true,
|
|
||||||
LdapGroups: strings.Join(ldapGroups, ","),
|
|
||||||
})
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
_, exists := m.broker.GetService(cookie.Provider)
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
tlog.App.Debug().Msg("OAuth provider from session cookie not found")
|
|
||||||
m.auth.DeleteSessionCookie(c)
|
|
||||||
goto basic
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.auth.IsEmailWhitelisted(cookie.Email) {
|
|
||||||
tlog.App.Debug().Msg("Email from session cookie not whitelisted")
|
|
||||||
m.auth.DeleteSessionCookie(c)
|
|
||||||
goto basic
|
|
||||||
}
|
|
||||||
|
|
||||||
m.auth.RefreshSessionCookie(c)
|
|
||||||
c.Set("context", &config.UserContext{
|
|
||||||
Username: cookie.Username,
|
|
||||||
Name: cookie.Name,
|
|
||||||
Email: cookie.Email,
|
|
||||||
Provider: cookie.Provider,
|
|
||||||
OAuthGroups: cookie.OAuthGroups,
|
|
||||||
OAuthName: cookie.OAuthName,
|
|
||||||
OAuthSub: cookie.OAuthSub,
|
|
||||||
IsLoggedIn: true,
|
|
||||||
OAuth: true,
|
|
||||||
})
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
basic:
|
|
||||||
basic := m.auth.GetBasicAuth(c)
|
|
||||||
|
|
||||||
if basic == nil {
|
|
||||||
tlog.App.Debug().Msg("No basic auth provided")
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
|
||||||
|
|
||||||
if locked {
|
|
||||||
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSearch := m.auth.SearchUser(basic.Username)
|
|
||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
|
||||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
|
||||||
tlog.App.Debug().Msg("User from basic auth not found")
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
|
||||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
|
||||||
tlog.App.Debug().Msg("Invalid password for basic auth user")
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.auth.RecordLoginAttempt(basic.Username, true)
|
|
||||||
|
|
||||||
switch userSearch.Type {
|
|
||||||
case "local":
|
|
||||||
tlog.App.Debug().Msg("Basic auth user is local")
|
|
||||||
|
|
||||||
user := m.auth.GetLocalUser(basic.Username)
|
|
||||||
|
|
||||||
if user.TotpSecret != "" {
|
|
||||||
tlog.App.Debug().Msg("User with TOTP not allowed to login via basic auth")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
|
||||||
Username: user.Username,
|
|
||||||
Name: utils.Capitalize(user.Username),
|
|
||||||
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
|
||||||
Provider: "local",
|
|
||||||
IsLoggedIn: true,
|
|
||||||
IsBasicAuth: true,
|
|
||||||
})
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
case "ldap":
|
|
||||||
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
|
||||||
|
|
||||||
ldapUser, err := m.auth.GetLdapUser(basic.Username)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Debug().Err(err).Msg("Error retrieving LDAP user details")
|
tlog.App.Error().Msgf("Error authenticating session cookie: %v", err)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
if cookie != nil {
|
||||||
Username: basic.Username,
|
http.SetCookie(c.Writer, cookie)
|
||||||
Name: utils.Capitalize(basic.Username),
|
}
|
||||||
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
|
||||||
Provider: "ldap",
|
tlog.App.Trace().Msgf("Authenticated user from session cookie: %s", userContext.GetUsername())
|
||||||
IsLoggedIn: true,
|
c.Set("context", userContext)
|
||||||
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
c.Next()
|
||||||
IsBasicAuth: true,
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
|
basic, err := m.auth.GetBasicAuth(c.Request)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
userContext, headers, err := m.basicAuth(c.Request.Context(), basic)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Msgf("Error authenticating basic auth: %v", err)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
c.Header(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("context", userContext)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -240,6 +110,150 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model.UserContext, *http.Cookie, error) {
|
||||||
|
session, err := m.auth.GetSession(ctx, uuid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error retrieving session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext, err := new(model.UserContext).NewFromSession(session)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error creating user context from session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userContext.Provider == model.ProviderLocal &&
|
||||||
|
userContext.Local.TOTPPending {
|
||||||
|
userContext.Local.TOTPEnabled = true
|
||||||
|
return userContext, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch userContext.Provider {
|
||||||
|
case model.ProviderLocal:
|
||||||
|
user := m.auth.GetLocalUser(userContext.Local.Username)
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return nil, nil, fmt.Errorf("local user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext.Local.Attributes = user.Attributes
|
||||||
|
|
||||||
|
if userContext.Local.Attributes.Name == "" {
|
||||||
|
userContext.Local.Attributes.Name = utils.Capitalize(user.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userContext.Local.Attributes.Email == "" {
|
||||||
|
userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.config.CookieDomain)
|
||||||
|
}
|
||||||
|
case model.ProviderLDAP:
|
||||||
|
search, err := m.auth.SearchUser(userContext.LDAP.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error searching for ldap user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Type != model.UserLDAP {
|
||||||
|
return nil, nil, fmt.Errorf("user from session cookie is not ldap")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := m.auth.GetLDAPUser(search.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext.LDAP.Groups = user.Groups
|
||||||
|
userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username)
|
||||||
|
userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.config.CookieDomain)
|
||||||
|
case model.ProviderOAuth:
|
||||||
|
_, exists := m.broker.GetService(userContext.OAuth.ID)
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, nil, fmt.Errorf("oauth provider from session cookie not found: %s", userContext.OAuth.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.auth.IsEmailWhitelisted(userContext.OAuth.Email) {
|
||||||
|
m.auth.DeleteSession(ctx, uuid)
|
||||||
|
return nil, nil, fmt.Errorf("email from session cookie not whitelisted: %s", userContext.OAuth.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := m.auth.RefreshSession(ctx, uuid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error refreshing session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userContext, cookie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ContextMiddleware) basicAuth(ctx context.Context, basic *model.LocalUser) (*model.UserContext, map[string]string, error) {
|
||||||
|
headers := make(map[string]string)
|
||||||
|
userContext := new(model.UserContext)
|
||||||
|
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
|
||||||
|
headers["x-tinyauth-lock-locked"] = "true"
|
||||||
|
headers["x-tinyauth-lock-reset"] = time.Now().Add(time.Duration(remaining) * time.Second).Format(time.RFC3339)
|
||||||
|
return nil, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
search, err := m.auth.SearchUser(basic.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error searching for user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.auth.CheckUserPassword(*search, basic.Password)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, false)
|
||||||
|
return nil, nil, fmt.Errorf("invalid password for basic auth user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.auth.RecordLoginAttempt(basic.Username, true)
|
||||||
|
|
||||||
|
switch search.Type {
|
||||||
|
case model.UserLocal:
|
||||||
|
user := m.auth.GetLocalUser(basic.Username)
|
||||||
|
|
||||||
|
if user.TOTPSecret != "" {
|
||||||
|
return nil, nil, fmt.Errorf("user with totp not allowed to login via basic auth: %s", basic.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext.Local = &model.LocalContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: user.Username,
|
||||||
|
Name: utils.Capitalize(user.Username),
|
||||||
|
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
||||||
|
},
|
||||||
|
Attributes: user.Attributes,
|
||||||
|
}
|
||||||
|
userContext.Provider = model.ProviderLocal
|
||||||
|
case model.UserLDAP:
|
||||||
|
user, err := m.auth.GetLDAPUser(basic.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext.LDAP = &model.LDAPContext{
|
||||||
|
BaseContext: model.BaseContext{
|
||||||
|
Username: basic.Username,
|
||||||
|
Name: utils.Capitalize(basic.Username),
|
||||||
|
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||||
|
},
|
||||||
|
Groups: user.Groups,
|
||||||
|
}
|
||||||
|
userContext.Provider = model.ProviderLDAP
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext.Authenticated = true
|
||||||
|
return userContext, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
||||||
for _, prefix := range contextSkipPathsPrefix {
|
for _, prefix := range contextSkipPathsPrefix {
|
||||||
if strings.HasPrefix(path, prefix) {
|
if strings.HasPrefix(path, prefix) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/assets"
|
"github.com/tinyauthapp/tinyauth/internal/assets"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// See context middleware for explanation of why we have to do this
|
// See context middleware for explanation of why we have to do this
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package model
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
func NewDefaultConfiguration() *Config {
|
func NewDefaultConfiguration() *Config {
|
||||||
@@ -29,7 +29,7 @@ func NewDefaultConfiguration() *Config {
|
|||||||
BackgroundImage: "/background.jpg",
|
BackgroundImage: "/background.jpg",
|
||||||
WarningsEnabled: true,
|
WarningsEnabled: true,
|
||||||
},
|
},
|
||||||
Ldap: LdapConfig{
|
LDAP: LDAPConfig{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
SearchFilter: "(uid=%s)",
|
SearchFilter: "(uid=%s)",
|
||||||
GroupCacheTTL: 900, // 15 minutes
|
GroupCacheTTL: 900, // 15 minutes
|
||||||
@@ -59,38 +59,25 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Experimental: ExperimentalConfig{
|
Experimental: ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
|
LabelProvider: "auto",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version information, set at build time
|
|
||||||
|
|
||||||
var Version = "development"
|
|
||||||
var CommitHash = "development"
|
|
||||||
var BuildTimestamp = "0000-00-00T00:00:00Z"
|
|
||||||
|
|
||||||
// Cookie name templates
|
|
||||||
|
|
||||||
var SessionCookieName = "tinyauth-session"
|
|
||||||
var CSRFCookieName = "tinyauth-csrf"
|
|
||||||
var RedirectCookieName = "tinyauth-redirect"
|
|
||||||
var OAuthSessionCookieName = "tinyauth-oauth"
|
|
||||||
|
|
||||||
// Main app config
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
||||||
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
|
||||||
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"`
|
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"`
|
||||||
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"`
|
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"`
|
||||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||||
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
|
||||||
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
|
||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, or kubernetes). auto detects the environment." yaml:"labelProvider"`
|
||||||
|
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
@@ -113,15 +100,43 @@ type ServerConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||||
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||||
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
|
||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||||
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAttributes struct {
|
||||||
|
Name string `description:"Full name of the user." yaml:"name"`
|
||||||
|
GivenName string `description:"Given (first) name of the user." yaml:"givenName"`
|
||||||
|
FamilyName string `description:"Family (last) name of the user." yaml:"familyName"`
|
||||||
|
MiddleName string `description:"Middle name of the user." yaml:"middleName"`
|
||||||
|
Nickname string `description:"Nickname of the user." yaml:"nickname"`
|
||||||
|
Profile string `description:"URL of the user's profile page." yaml:"profile"`
|
||||||
|
Picture string `description:"URL of the user's profile picture." yaml:"picture"`
|
||||||
|
Website string `description:"URL of the user's website." yaml:"website"`
|
||||||
|
Email string `description:"Email address of the user." yaml:"email"`
|
||||||
|
Gender string `description:"Gender of the user." yaml:"gender"`
|
||||||
|
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"`
|
||||||
|
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"`
|
||||||
|
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"`
|
||||||
|
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"`
|
||||||
|
Address AddressClaim `description:"Address of the user." yaml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddressClaim struct {
|
||||||
|
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted" json:"formatted,omitempty"`
|
||||||
|
StreetAddress string `description:"Street address." yaml:"streetAddress" json:"street_address,omitempty"`
|
||||||
|
Locality string `description:"City or locality." yaml:"locality" json:"locality,omitempty"`
|
||||||
|
Region string `description:"State, province, or region." yaml:"region" json:"region,omitempty"`
|
||||||
|
PostalCode string `description:"Zip or postal code." yaml:"postalCode" json:"postal_code,omitempty"`
|
||||||
|
Country string `description:"Country." yaml:"country" json:"country,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IPConfig struct {
|
type IPConfig struct {
|
||||||
@@ -148,7 +163,7 @@ type UIConfig struct {
|
|||||||
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LdapConfig struct {
|
type LDAPConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
||||||
@@ -181,20 +196,6 @@ type ExperimentalConfig struct {
|
|||||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config loader options
|
|
||||||
|
|
||||||
const DefaultNamePrefix = "TINYAUTH_"
|
|
||||||
|
|
||||||
// OAuth/OIDC config
|
|
||||||
|
|
||||||
type Claims struct {
|
|
||||||
Sub string `json:"sub"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
PreferredUsername string `json:"preferred_username"`
|
|
||||||
Groups any `json:"groups"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OAuthServiceConfig struct {
|
type OAuthServiceConfig struct {
|
||||||
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
ClientID string `description:"OAuth client ID." yaml:"clientId"`
|
||||||
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
|
||||||
@@ -217,58 +218,6 @@ type OIDCClientConfig struct {
|
|||||||
Name string `description:"Client name in UI." yaml:"name"`
|
Name string `description:"Client name in UI." yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var OverrideProviders = map[string]string{
|
|
||||||
"google": "Google",
|
|
||||||
"github": "GitHub",
|
|
||||||
}
|
|
||||||
|
|
||||||
// User/session related stuff
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
TotpSecret string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LdapUser struct {
|
|
||||||
DN string
|
|
||||||
Groups []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSearch struct {
|
|
||||||
Username string
|
|
||||||
Type string // local, ldap or unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserContext struct {
|
|
||||||
Username string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
IsLoggedIn bool
|
|
||||||
IsBasicAuth bool
|
|
||||||
OAuth bool
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
TotpEnabled bool
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
LdapGroups string
|
|
||||||
}
|
|
||||||
|
|
||||||
// API responses and queries
|
|
||||||
|
|
||||||
type UnauthorizedQuery struct {
|
|
||||||
Username string `url:"username"`
|
|
||||||
Resource string `url:"resource"`
|
|
||||||
GroupErr bool `url:"groupErr"`
|
|
||||||
IP string `url:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedirectQuery struct {
|
|
||||||
RedirectURI string `url:"redirect_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
@@ -324,7 +273,3 @@ type AppPath struct {
|
|||||||
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"`
|
||||||
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
Block string `description:"Comma-separated list of blocked paths." yaml:"block"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// API server
|
|
||||||
|
|
||||||
var ApiServer = "https://api.tinyauth.app"
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
const DefaultNamePrefix = "TINYAUTH_"
|
||||||
|
|
||||||
|
const APIServer = "https://api.tinyauth.app"
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Groups any `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var OverrideProviders = map[string]string{
|
||||||
|
"google": "Google",
|
||||||
|
"github": "GitHub",
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionCookieName = "tinyauth-session"
|
||||||
|
const CSRFCookieName = "tinyauth-csrf"
|
||||||
|
const RedirectCookieName = "tinyauth-redirect"
|
||||||
|
const OAuthSessionCookieName = "tinyauth-oauth"
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderLocal ProviderType = iota
|
||||||
|
ProviderBasicAuth
|
||||||
|
ProviderOAuth
|
||||||
|
ProviderLDAP
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserContext struct {
|
||||||
|
Authenticated bool
|
||||||
|
Provider ProviderType
|
||||||
|
Local *LocalContext
|
||||||
|
OAuth *OAuthContext
|
||||||
|
LDAP *LDAPContext
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseContext struct {
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalContext struct {
|
||||||
|
BaseContext
|
||||||
|
TOTPPending bool
|
||||||
|
TOTPEnabled bool
|
||||||
|
Attributes UserAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type OAuthContext struct {
|
||||||
|
BaseContext
|
||||||
|
Groups []string
|
||||||
|
Sub string
|
||||||
|
DisplayName string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LDAPContext struct {
|
||||||
|
BaseContext
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) IsAuthenticated() bool {
|
||||||
|
return c.Authenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) IsLocal() bool {
|
||||||
|
return c.Provider == ProviderLocal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) IsOAuth() bool {
|
||||||
|
return c.Provider == ProviderOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) IsLDAP() bool {
|
||||||
|
return c.Provider == ProviderLDAP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) IsBasicAuth() bool {
|
||||||
|
return c.Provider == ProviderBasicAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) {
|
||||||
|
userContextValue, exists := ginctx.Get("context")
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("failed to get user context")
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext, ok := userContextValue.(*UserContext)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid user context type")
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = *userContext
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compatability layer until we get an excuse to drop in database migrations
|
||||||
|
func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, error) {
|
||||||
|
switch session.Provider {
|
||||||
|
case "local":
|
||||||
|
c.Provider = ProviderLocal
|
||||||
|
c.Local = &LocalContext{
|
||||||
|
BaseContext: BaseContext{
|
||||||
|
Username: session.Username,
|
||||||
|
Name: session.Name,
|
||||||
|
Email: session.Email,
|
||||||
|
},
|
||||||
|
TOTPPending: session.TotpPending,
|
||||||
|
}
|
||||||
|
case "ldap":
|
||||||
|
c.Provider = ProviderLDAP
|
||||||
|
c.LDAP = &LDAPContext{
|
||||||
|
BaseContext: BaseContext{
|
||||||
|
Username: session.Username,
|
||||||
|
Name: session.Name,
|
||||||
|
Email: session.Email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// By default we assume an unkown name which is oauth
|
||||||
|
default:
|
||||||
|
c.Provider = ProviderOAuth
|
||||||
|
c.OAuth = &OAuthContext{
|
||||||
|
BaseContext: BaseContext{
|
||||||
|
Username: session.Username,
|
||||||
|
Name: session.Name,
|
||||||
|
Email: session.Email,
|
||||||
|
},
|
||||||
|
Groups: strings.Split(session.OAuthGroups, ","),
|
||||||
|
Sub: session.OAuthSub,
|
||||||
|
DisplayName: session.OAuthName,
|
||||||
|
ID: session.Provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !session.TotpPending {
|
||||||
|
c.Authenticated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) GetUsername() string {
|
||||||
|
switch c.Provider {
|
||||||
|
case ProviderLocal:
|
||||||
|
return c.Local.Username
|
||||||
|
case ProviderLDAP:
|
||||||
|
return c.LDAP.Username
|
||||||
|
case ProviderBasicAuth:
|
||||||
|
return c.Local.Username
|
||||||
|
case ProviderOAuth:
|
||||||
|
return c.OAuth.Username
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) GetEmail() string {
|
||||||
|
switch c.Provider {
|
||||||
|
case ProviderLocal:
|
||||||
|
return c.Local.Email
|
||||||
|
case ProviderLDAP:
|
||||||
|
return c.LDAP.Email
|
||||||
|
case ProviderBasicAuth:
|
||||||
|
return c.Local.Email
|
||||||
|
case ProviderOAuth:
|
||||||
|
return c.OAuth.Email
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) GetName() string {
|
||||||
|
switch c.Provider {
|
||||||
|
case ProviderLocal:
|
||||||
|
return c.Local.Name
|
||||||
|
case ProviderLDAP:
|
||||||
|
return c.LDAP.Name
|
||||||
|
case ProviderBasicAuth:
|
||||||
|
return c.Local.Name
|
||||||
|
case ProviderOAuth:
|
||||||
|
return c.OAuth.Name
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) ProviderName() string {
|
||||||
|
switch c.Provider {
|
||||||
|
case ProviderBasicAuth, ProviderLocal:
|
||||||
|
return "local"
|
||||||
|
case ProviderLDAP:
|
||||||
|
return "ldap"
|
||||||
|
case ProviderOAuth:
|
||||||
|
return c.OAuth.DisplayName // compatability
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) TOTPPending() bool {
|
||||||
|
if c.Provider == ProviderLocal {
|
||||||
|
return c.Local.TOTPPending
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserContext) OAuthName() string {
|
||||||
|
if c.Provider == ProviderOAuth {
|
||||||
|
return c.OAuth.DisplayName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type UserSearchType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserLocal UserSearchType = iota
|
||||||
|
UserLDAP
|
||||||
|
)
|
||||||
|
|
||||||
|
type LDAPUser struct {
|
||||||
|
DN string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalUser struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
TOTPSecret string
|
||||||
|
Attributes UserAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSearch struct {
|
||||||
|
Username string
|
||||||
|
Type UserSearchType
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
var Version = "development"
|
||||||
|
var CommitHash = "development"
|
||||||
|
var BuildTimestamp = "0000-00-00T00:00:00Z"
|
||||||
@@ -34,6 +34,19 @@ type OidcUserinfo struct {
|
|||||||
Email string
|
Email string
|
||||||
Groups string
|
Groups string
|
||||||
UpdatedAt int64
|
UpdatedAt int64
|
||||||
|
GivenName string
|
||||||
|
FamilyName string
|
||||||
|
MiddleName string
|
||||||
|
Nickname string
|
||||||
|
Profile string
|
||||||
|
Picture string
|
||||||
|
Website string
|
||||||
|
Gender string
|
||||||
|
Birthdate string
|
||||||
|
Zoneinfo string
|
||||||
|
Locale string
|
||||||
|
PhoneNumber string
|
||||||
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
|||||||
@@ -124,11 +124,24 @@ INSERT INTO "oidc_userinfo" (
|
|||||||
"preferred_username",
|
"preferred_username",
|
||||||
"email",
|
"email",
|
||||||
"groups",
|
"groups",
|
||||||
"updated_at"
|
"updated_at",
|
||||||
|
"given_name",
|
||||||
|
"family_name",
|
||||||
|
"middle_name",
|
||||||
|
"nickname",
|
||||||
|
"profile",
|
||||||
|
"picture",
|
||||||
|
"website",
|
||||||
|
"gender",
|
||||||
|
"birthdate",
|
||||||
|
"zoneinfo",
|
||||||
|
"locale",
|
||||||
|
"phone_number",
|
||||||
|
"address"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING sub, name, preferred_username, email, "groups", updated_at
|
RETURNING sub, name, preferred_username, email, "groups", updated_at, given_name, family_name, middle_name, nickname, profile, picture, website, gender, birthdate, zoneinfo, locale, phone_number, address
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateOidcUserInfoParams struct {
|
type CreateOidcUserInfoParams struct {
|
||||||
@@ -138,6 +151,19 @@ type CreateOidcUserInfoParams struct {
|
|||||||
Email string
|
Email string
|
||||||
Groups string
|
Groups string
|
||||||
UpdatedAt int64
|
UpdatedAt int64
|
||||||
|
GivenName string
|
||||||
|
FamilyName string
|
||||||
|
MiddleName string
|
||||||
|
Nickname string
|
||||||
|
Profile string
|
||||||
|
Picture string
|
||||||
|
Website string
|
||||||
|
Gender string
|
||||||
|
Birthdate string
|
||||||
|
Zoneinfo string
|
||||||
|
Locale string
|
||||||
|
PhoneNumber string
|
||||||
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
|
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
|
||||||
@@ -148,6 +174,19 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
|
|||||||
arg.Email,
|
arg.Email,
|
||||||
arg.Groups,
|
arg.Groups,
|
||||||
arg.UpdatedAt,
|
arg.UpdatedAt,
|
||||||
|
arg.GivenName,
|
||||||
|
arg.FamilyName,
|
||||||
|
arg.MiddleName,
|
||||||
|
arg.Nickname,
|
||||||
|
arg.Profile,
|
||||||
|
arg.Picture,
|
||||||
|
arg.Website,
|
||||||
|
arg.Gender,
|
||||||
|
arg.Birthdate,
|
||||||
|
arg.Zoneinfo,
|
||||||
|
arg.Locale,
|
||||||
|
arg.PhoneNumber,
|
||||||
|
arg.Address,
|
||||||
)
|
)
|
||||||
var i OidcUserinfo
|
var i OidcUserinfo
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -157,6 +196,19 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
|
|||||||
&i.Email,
|
&i.Email,
|
||||||
&i.Groups,
|
&i.Groups,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.GivenName,
|
||||||
|
&i.FamilyName,
|
||||||
|
&i.MiddleName,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.Profile,
|
||||||
|
&i.Picture,
|
||||||
|
&i.Website,
|
||||||
|
&i.Gender,
|
||||||
|
&i.Birthdate,
|
||||||
|
&i.Zoneinfo,
|
||||||
|
&i.Locale,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Address,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -456,7 +508,7 @@ func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
|
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
|
||||||
SELECT sub, name, preferred_username, email, "groups", updated_at FROM "oidc_userinfo"
|
SELECT sub, name, preferred_username, email, "groups", updated_at, given_name, family_name, middle_name, nickname, profile, picture, website, gender, birthdate, zoneinfo, locale, phone_number, address FROM "oidc_userinfo"
|
||||||
WHERE "sub" = ?
|
WHERE "sub" = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -470,6 +522,19 @@ func (q *Queries) GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo
|
|||||||
&i.Email,
|
&i.Email,
|
||||||
&i.Groups,
|
&i.Groups,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.GivenName,
|
||||||
|
&i.FamilyName,
|
||||||
|
&i.MiddleName,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.Profile,
|
||||||
|
&i.Picture,
|
||||||
|
&i.Website,
|
||||||
|
&i.Gender,
|
||||||
|
&i.Birthdate,
|
||||||
|
&i.Zoneinfo,
|
||||||
|
&i.Locale,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Address,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,23 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccessControlsService struct {
|
type LabelProvider interface {
|
||||||
docker *DockerService
|
GetLabels(appDomain string) (*model.App, error)
|
||||||
static map[string]config.App
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(docker *DockerService, static map[string]config.App) *AccessControlsService {
|
type AccessControlsService struct {
|
||||||
|
labelProvider LabelProvider
|
||||||
|
static map[string]model.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessControlsService(labelProvider LabelProvider, static map[string]model.App) *AccessControlsService {
|
||||||
return &AccessControlsService{
|
return &AccessControlsService{
|
||||||
docker: docker,
|
labelProvider: labelProvider,
|
||||||
static: static,
|
static: static,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,22 +28,22 @@ func (acls *AccessControlsService) Init() error {
|
|||||||
return nil // No initialization needed
|
return nil // No initialization needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
|
func (acls *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) {
|
||||||
for app, config := range acls.static {
|
for app, config := range acls.static {
|
||||||
if config.Config.Domain == domain {
|
if config.Config.Domain == domain {
|
||||||
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||||
return config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||||
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||||
return config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return config.App{}, errors.New("no results")
|
return nil, errors.New("no results")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) GetAccessControls(domain string) (config.App, error) {
|
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||||
// First check in the static config
|
// First check in the static config
|
||||||
app, err := acls.lookupStaticACLs(domain)
|
app, err := acls.lookupStaticACLs(domain)
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ func (acls *AccessControlsService) GetAccessControls(domain string) (config.App,
|
|||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Docker labels
|
// Fallback to label provider
|
||||||
tlog.App.Debug().Msg("Falling back to Docker labels for ACLs")
|
tlog.App.Debug().Msg("Falling back to label provider for ACLs")
|
||||||
return acls.docker.GetLabels(domain)
|
return acls.labelProvider.GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-144
@@ -5,20 +5,22 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +30,10 @@ const MaxOAuthPendingSessions = 256
|
|||||||
const OAuthCleanupCount = 16
|
const OAuthCleanupCount = 16
|
||||||
const MaxLoginAttemptRecords = 256
|
const MaxLoginAttemptRecords = 256
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
)
|
||||||
|
|
||||||
// slightly modified version of the AuthorizeRequest from the OIDC service to basically accept all
|
// slightly modified version of the AuthorizeRequest from the OIDC service to basically accept all
|
||||||
// parameters and pass them to the authorize page if needed
|
// parameters and pass them to the authorize page if needed
|
||||||
type OAuthURLParams struct {
|
type OAuthURLParams struct {
|
||||||
@@ -67,7 +73,7 @@ type Lockdown struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthServiceConfig struct {
|
type AuthServiceConfig struct {
|
||||||
Users []config.User
|
LocalUsers []model.LocalUser
|
||||||
OauthWhitelist []string
|
OauthWhitelist []string
|
||||||
SessionExpiry int
|
SessionExpiry int
|
||||||
SessionMaxLifetime int
|
SessionMaxLifetime int
|
||||||
@@ -76,13 +82,12 @@ type AuthServiceConfig struct {
|
|||||||
LoginTimeout int
|
LoginTimeout int
|
||||||
LoginMaxRetries int
|
LoginMaxRetries int
|
||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
IP config.IPConfig
|
IP model.IPConfig
|
||||||
LDAPGroupsCacheTTL int
|
LDAPGroupsCacheTTL int
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
config AuthServiceConfig
|
config AuthServiceConfig
|
||||||
docker *DockerService
|
|
||||||
loginAttempts map[string]*LoginAttempt
|
loginAttempts map[string]*LoginAttempt
|
||||||
ldapGroupsCache map[string]*LdapGroupsCache
|
ldapGroupsCache map[string]*LdapGroupsCache
|
||||||
oauthPendingSessions map[string]*OAuthPendingSession
|
oauthPendingSessions map[string]*OAuthPendingSession
|
||||||
@@ -97,10 +102,9 @@ type AuthService struct {
|
|||||||
lockdownCancelFunc context.CancelFunc
|
lockdownCancelFunc context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
func NewAuthService(config AuthServiceConfig, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
config: config,
|
config: config,
|
||||||
docker: docker,
|
|
||||||
loginAttempts: make(map[string]*LoginAttempt),
|
loginAttempts: make(map[string]*LoginAttempt),
|
||||||
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||||
oauthPendingSessions: make(map[string]*OAuthPendingSession),
|
oauthPendingSessions: make(map[string]*OAuthPendingSession),
|
||||||
@@ -115,79 +119,67 @@ func (auth *AuthService) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {
|
||||||
if auth.GetLocalUser(username).Username != "" {
|
if auth.GetLocalUser(username).Username != "" {
|
||||||
return config.UserSearch{
|
return &model.UserSearch{
|
||||||
Username: username,
|
Username: username,
|
||||||
Type: "local",
|
Type: model.UserLocal,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.ldap.IsConfigured() {
|
if auth.ldap.IsConfigured() {
|
||||||
userDN, err := auth.ldap.GetUserDN(username)
|
userDN, err := auth.ldap.GetUserDN(username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
return nil, fmt.Errorf("failed to get ldap user: %w", err)
|
||||||
return config.UserSearch{
|
|
||||||
Type: "unknown",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.UserSearch{
|
return &model.UserSearch{
|
||||||
Username: userDN,
|
Username: userDN,
|
||||||
Type: "ldap",
|
Type: model.UserLDAP,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.UserSearch{
|
return nil, ErrUserNotFound
|
||||||
Type: "unknown",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) VerifyUser(search config.UserSearch, password string) bool {
|
func (auth *AuthService) CheckUserPassword(search model.UserSearch, password string) error {
|
||||||
switch search.Type {
|
switch search.Type {
|
||||||
case "local":
|
case model.UserLocal:
|
||||||
user := auth.GetLocalUser(search.Username)
|
user := auth.GetLocalUser(search.Username)
|
||||||
return auth.CheckPassword(user, password)
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||||
case "ldap":
|
case model.UserLDAP:
|
||||||
if auth.ldap.IsConfigured() {
|
if auth.ldap.IsConfigured() {
|
||||||
err := auth.ldap.Bind(search.Username, password)
|
err := auth.ldap.Bind(search.Username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
|
return fmt.Errorf("failed to bind to ldap user: %w", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.ldap.BindService(true)
|
err = auth.ldap.BindService(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
return fmt.Errorf("failed to bind to ldap service account: %w", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
tlog.App.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
|
return errors.New("unknown user search type")
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return errors.New("user authentication failed")
|
||||||
tlog.App.Warn().Str("username", search.Username).Msg("User authentication failed")
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetLocalUser(username string) config.User {
|
func (auth *AuthService) GetLocalUser(username string) *model.LocalUser {
|
||||||
for _, user := range auth.config.Users {
|
for _, user := range auth.config.LocalUsers {
|
||||||
if user.Username == username {
|
if user.Username == username {
|
||||||
return user
|
return &user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
tlog.App.Warn().Str("username", username).Msg("Local user not found")
|
|
||||||
return config.User{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
|
||||||
if !auth.ldap.IsConfigured() {
|
if !auth.ldap.IsConfigured() {
|
||||||
return config.LdapUser{}, errors.New("LDAP service not initialized")
|
return nil, errors.New("ldap service not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.ldapGroupsMutex.RLock()
|
auth.ldapGroupsMutex.RLock()
|
||||||
@@ -195,7 +187,7 @@ func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
|||||||
auth.ldapGroupsMutex.RUnlock()
|
auth.ldapGroupsMutex.RUnlock()
|
||||||
|
|
||||||
if exists && time.Now().Before(entry.Expires) {
|
if exists && time.Now().Before(entry.Expires) {
|
||||||
return config.LdapUser{
|
return &model.LDAPUser{
|
||||||
DN: userDN,
|
DN: userDN,
|
||||||
Groups: entry.Groups,
|
Groups: entry.Groups,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -204,7 +196,7 @@ func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
|||||||
groups, err := auth.ldap.GetUserGroups(userDN)
|
groups, err := auth.ldap.GetUserGroups(userDN)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.LdapUser{}, err
|
return nil, fmt.Errorf("failed to get ldap groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.ldapGroupsMutex.Lock()
|
auth.ldapGroupsMutex.Lock()
|
||||||
@@ -214,16 +206,12 @@ func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
|||||||
}
|
}
|
||||||
auth.ldapGroupsMutex.Unlock()
|
auth.ldapGroupsMutex.Unlock()
|
||||||
|
|
||||||
return config.LdapUser{
|
return &model.LDAPUser{
|
||||||
DN: userDN,
|
DN: userDN,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
|
||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
|
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
|
||||||
auth.loginMutex.RLock()
|
auth.loginMutex.RLock()
|
||||||
defer auth.loginMutex.RUnlock()
|
defer auth.loginMutex.RUnlock()
|
||||||
@@ -292,11 +280,11 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
|||||||
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
|
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("failed to generate session uuid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var expiry int
|
var expiry int
|
||||||
@@ -321,28 +309,30 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Se
|
|||||||
OAuthSub: data.OAuthSub,
|
OAuthSub: data.OAuthSub,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = auth.queries.CreateSession(c, session)
|
_, err = auth.queries.CreateSession(ctx, session)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
return &http.Cookie{
|
||||||
|
Name: auth.config.SessionCookieName,
|
||||||
return nil
|
Value: session.UUID,
|
||||||
|
Path: "/",
|
||||||
|
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
||||||
|
Expires: time.Now().Add(time.Duration(expiry) * time.Second),
|
||||||
|
MaxAge: expiry,
|
||||||
|
Secure: auth.config.SecureCookie,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http.Cookie, error) {
|
||||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
session, err := auth.queries.GetSession(ctx, uuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("failed to retrieve session: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
session, err := auth.queries.GetSession(c, cookie)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
@@ -356,12 +346,12 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if session.Expiry-currentTime > refreshThreshold {
|
if session.Expiry-currentTime > refreshThreshold {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newExpiry := session.Expiry + refreshThreshold
|
newExpiry := session.Expiry + refreshThreshold
|
||||||
|
|
||||||
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
|
_, err = auth.queries.UpdateSession(ctx, repository.UpdateSessionParams{
|
||||||
Username: session.Username,
|
Username: session.Username,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
Name: session.Name,
|
Name: session.Name,
|
||||||
@@ -375,120 +365,121 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("failed to update session expiry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
return &http.Cookie{
|
||||||
tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
|
Name: auth.config.SessionCookieName,
|
||||||
|
Value: session.UUID,
|
||||||
|
Path: "/",
|
||||||
|
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
||||||
|
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
|
||||||
|
MaxAge: auth.config.SessionExpiry,
|
||||||
|
Secure: auth.config.SecureCookie,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}, nil
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.Cookie, error) {
|
||||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
err := auth.queries.DeleteSession(ctx, uuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.queries.DeleteSession(c, cookie)
|
return &http.Cookie{
|
||||||
|
Name: auth.config.SessionCookieName,
|
||||||
if err != nil {
|
Value: "",
|
||||||
return err
|
Path: "/",
|
||||||
}
|
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
|
||||||
|
Expires: time.Now(),
|
||||||
c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
|
MaxAge: -1,
|
||||||
|
Secure: auth.config.SecureCookie,
|
||||||
return nil
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
|
func (auth *AuthService) GetSession(ctx context.Context, uuid string) (*repository.Session, error) {
|
||||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
session, err := auth.queries.GetSession(ctx, uuid)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return repository.Session{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := auth.queries.GetSession(c, cookie)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return repository.Session{}, fmt.Errorf("session not found")
|
return nil, errors.New("session not found")
|
||||||
}
|
}
|
||||||
return repository.Session{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
|
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
|
||||||
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
||||||
err = auth.queries.DeleteSession(c, cookie)
|
err = auth.queries.DeleteSession(ctx, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
|
return nil, fmt.Errorf("failed to delete expired session: %w", err)
|
||||||
}
|
}
|
||||||
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
return nil, fmt.Errorf("session max lifetime exceeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentTime > session.Expiry {
|
if currentTime > session.Expiry {
|
||||||
err = auth.queries.DeleteSession(c, cookie)
|
err = auth.queries.DeleteSession(ctx, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
|
return nil, fmt.Errorf("failed to delete expired session: %w", err)
|
||||||
}
|
}
|
||||||
return repository.Session{}, fmt.Errorf("session expired")
|
return nil, fmt.Errorf("session expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
return repository.Session{
|
return &session, nil
|
||||||
UUID: session.UUID,
|
|
||||||
Username: session.Username,
|
|
||||||
Email: session.Email,
|
|
||||||
Name: session.Name,
|
|
||||||
Provider: session.Provider,
|
|
||||||
TotpPending: session.TotpPending,
|
|
||||||
OAuthGroups: session.OAuthGroups,
|
|
||||||
OAuthName: session.OAuthName,
|
|
||||||
OAuthSub: session.OAuthSub,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) LocalAuthConfigured() bool {
|
func (auth *AuthService) LocalAuthConfigured() bool {
|
||||||
return len(auth.config.Users) > 0
|
return len(auth.config.LocalUsers) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) LdapAuthConfigured() bool {
|
func (auth *AuthService) LDAPAuthConfigured() bool {
|
||||||
return auth.ldap.IsConfigured()
|
return auth.ldap.IsConfigured()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
||||||
if context.OAuth {
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.Provider == model.ProviderOAuth {
|
||||||
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
||||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if acls.Users.Block != "" {
|
if acls.Users.Block != "" {
|
||||||
tlog.App.Debug().Msg("Checking blocked users")
|
tlog.App.Debug().Msg("Checking blocked users")
|
||||||
if utils.CheckFilter(acls.Users.Block, context.Username) {
|
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Debug().Msg("Checking users")
|
tlog.App.Debug().Msg("Checking users")
|
||||||
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
|
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, requiredGroups string) bool {
|
||||||
if requiredGroups == "" {
|
if requiredGroups == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for id := range config.OverrideProviders {
|
if !context.IsOAuth() {
|
||||||
if context.Provider == id {
|
tlog.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
||||||
tlog.App.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
|
return false
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
||||||
|
tlog.App.Debug().Msg("Provider override for OAuth groups enabled, skipping group check")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userGroup := range context.OAuth.Groups {
|
||||||
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
return true
|
return true
|
||||||
@@ -499,12 +490,17 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
|
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, requiredGroups string) bool {
|
||||||
if requiredGroups == "" {
|
if requiredGroups == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for userGroup := range strings.SplitSeq(context.LdapGroups, ",") {
|
if !context.IsLDAP() {
|
||||||
|
tlog.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userGroup := range context.LDAP.Groups {
|
||||||
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||||
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||||
return true
|
return true
|
||||||
@@ -515,7 +511,11 @@ func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContex
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) {
|
func (auth *AuthService) IsAuthEnabled(uri string, path *model.AppPath) (bool, error) {
|
||||||
|
if path == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Check for block list
|
// Check for block list
|
||||||
if path.Block != "" {
|
if path.Block != "" {
|
||||||
regex, err := regexp.Compile(path.Block)
|
regex, err := regexp.Compile(path.Block)
|
||||||
@@ -545,19 +545,26 @@ func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, e
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
// local user is used only as a medium to pass the basic auth credentials, user can be ldap too
|
||||||
username, password, ok := c.Request.BasicAuth()
|
func (auth *AuthService) GetBasicAuth(req *http.Request) (*model.LocalUser, error) {
|
||||||
if !ok {
|
if req == nil {
|
||||||
tlog.App.Debug().Msg("No basic auth provided")
|
return nil, errors.New("request is nil")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return &config.User{
|
username, password, ok := req.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("no basic auth credentials provided")
|
||||||
|
}
|
||||||
|
return &model.LocalUser{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
func (auth *AuthService) CheckIP(acls *model.AppIP, ip string) bool {
|
||||||
|
if acls == nil {
|
||||||
|
acls = &model.AppIP{}
|
||||||
|
}
|
||||||
|
|
||||||
// Merge the global and app IP filter
|
// Merge the global and app IP filter
|
||||||
blockedIps := append(auth.config.IP.Block, acls.Block...)
|
blockedIps := append(auth.config.IP.Block, acls.Block...)
|
||||||
allowedIPs := append(auth.config.IP.Allow, acls.Allow...)
|
allowedIPs := append(auth.config.IP.Allow, acls.Allow...)
|
||||||
@@ -595,7 +602,11 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
|
func (auth *AuthService) IsBypassedIP(acls *model.AppIP, ip string) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for _, bypassed := range acls.Bypass {
|
for _, bypassed := range acls.Bypass {
|
||||||
res, err := utils.FilterIP(bypassed, ip)
|
res, err := utils.FilterIP(bypassed, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -675,21 +686,21 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, error) {
|
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, error) {
|
||||||
session, err := auth.GetOAuthPendingSession(sessionId)
|
session, err := auth.GetOAuthPendingSession(sessionId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.Claims{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.Token == nil {
|
if session.Token == nil {
|
||||||
return config.Claims{}, 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 config.Claims{}, fmt.Errorf("failed to get userinfo: %w", err)
|
return nil, fmt.Errorf("failed to get userinfo: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
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"
|
||||||
@@ -66,41 +66,41 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins
|
|||||||
return inspect, nil
|
return inspect, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) {
|
||||||
if !docker.isConnected {
|
if !docker.isConnected {
|
||||||
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
|
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
|
||||||
return config.App{}, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err := docker.getContainers()
|
containers, err := docker.getContainers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.App{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
inspect, err := docker.inspectContainer(ctr.ID)
|
inspect, err := docker.inspectContainer(ctr.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.App{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
labels, err := decoders.DecodeLabels[config.Apps](inspect.Config.Labels, "apps")
|
labels, err := decoders.DecodeLabels[model.Apps](inspect.Config.Labels, "apps")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.App{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for appName, appLabels := range labels.Apps {
|
for appName, appLabels := range labels.Apps {
|
||||||
if appLabels.Config.Domain == appDomain {
|
if appLabels.Config.Domain == appDomain {
|
||||||
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
|
||||||
return appLabels, nil
|
return &appLabels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
||||||
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
|
||||||
return appLabels, nil
|
return &appLabels, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Debug().Msg("No matching container found, returning empty labels")
|
tlog.App.Debug().Msg("No matching container found, returning empty labels")
|
||||||
return config.App{}, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ingressKey struct {
|
||||||
|
namespace string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ingressAppKey struct {
|
||||||
|
ingressKey
|
||||||
|
appName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ingressApp struct {
|
||||||
|
domain string
|
||||||
|
appName string
|
||||||
|
app model.App
|
||||||
|
}
|
||||||
|
|
||||||
|
type KubernetesService struct {
|
||||||
|
client dynamic.Interface
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
started bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
ingressApps map[ingressKey][]ingressApp
|
||||||
|
domainIndex map[string]ingressAppKey
|
||||||
|
appNameIndex map[string]ingressAppKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetesService() *KubernetesService {
|
||||||
|
return &KubernetesService{
|
||||||
|
ingressApps: make(map[ingressKey][]ingressApp),
|
||||||
|
domainIndex: make(map[string]ingressAppKey),
|
||||||
|
appNameIndex: make(map[string]ingressAppKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) addIngressApps(namespace, name string, apps []ingressApp) {
|
||||||
|
k.mu.Lock()
|
||||||
|
defer k.mu.Unlock()
|
||||||
|
|
||||||
|
key := ingressKey{namespace, name}
|
||||||
|
// Remove existing entries for this ingress
|
||||||
|
if existing, ok := k.ingressApps[key]; ok {
|
||||||
|
for _, app := range existing {
|
||||||
|
delete(k.domainIndex, app.domain)
|
||||||
|
delete(k.appNameIndex, app.appName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add new entries
|
||||||
|
k.ingressApps[key] = apps
|
||||||
|
for _, app := range apps {
|
||||||
|
appKey := ingressAppKey{key, app.appName}
|
||||||
|
k.domainIndex[app.domain] = appKey
|
||||||
|
k.appNameIndex[app.appName] = appKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) removeIngress(namespace, name string) {
|
||||||
|
k.mu.Lock()
|
||||||
|
defer k.mu.Unlock()
|
||||||
|
|
||||||
|
key := ingressKey{namespace, name}
|
||||||
|
if apps, ok := k.ingressApps[key]; ok {
|
||||||
|
for _, app := range apps {
|
||||||
|
delete(k.domainIndex, app.domain)
|
||||||
|
delete(k.appNameIndex, app.appName)
|
||||||
|
}
|
||||||
|
delete(k.ingressApps, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) getByDomain(domain string) (*model.App, bool) {
|
||||||
|
k.mu.RLock()
|
||||||
|
defer k.mu.RUnlock()
|
||||||
|
|
||||||
|
if appKey, ok := k.domainIndex[domain]; ok {
|
||||||
|
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
||||||
|
for _, app := range apps {
|
||||||
|
if app.domain == domain && app.appName == appKey.appName {
|
||||||
|
return &app.app, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) getByAppName(appName string) (*model.App, bool) {
|
||||||
|
k.mu.RLock()
|
||||||
|
defer k.mu.RUnlock()
|
||||||
|
|
||||||
|
if appKey, ok := k.appNameIndex[appName]; ok {
|
||||||
|
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
||||||
|
for _, app := range apps {
|
||||||
|
if app.appName == appName {
|
||||||
|
return &app.app, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
|
||||||
|
namespace := item.GetNamespace()
|
||||||
|
name := item.GetName()
|
||||||
|
annotations := item.GetAnnotations()
|
||||||
|
if annotations == nil {
|
||||||
|
k.removeIngress(namespace, name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Msg("Failed to decode labels from annotations")
|
||||||
|
k.removeIngress(namespace, name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var apps []ingressApp
|
||||||
|
for appName, appLabels := range labels.Apps {
|
||||||
|
if appLabels.Config.Domain == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apps = append(apps, ingressApp{
|
||||||
|
domain: appLabels.Config.Domain,
|
||||||
|
appName: appName,
|
||||||
|
app: appLabels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(apps) == 0 {
|
||||||
|
k.removeIngress(namespace, name)
|
||||||
|
} else {
|
||||||
|
k.addIngressApps(namespace, name, apps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource) error {
|
||||||
|
ctx, cancel := context.WithTimeout(k.ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
list, err := k.client.Resource(gvr).List(ctx, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to list ingresses during resync")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range list.Items {
|
||||||
|
k.updateFromItem(&list.Items[i])
|
||||||
|
}
|
||||||
|
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Int("count", len(list.Items)).Msg("Resynced ingress cache")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runWatcher drains events from an active watcher until it closes or the context is done.
|
||||||
|
// Returns true if the caller should restart the watcher, false if it should exit.
|
||||||
|
func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch.Interface, resyncTicker *time.Ticker) bool {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-k.ctx.Done():
|
||||||
|
w.Stop()
|
||||||
|
return false
|
||||||
|
case event, ok := <-w.ResultChan():
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher channel closed, restarting in 5 seconds")
|
||||||
|
w.Stop()
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
item, ok := event.Object.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
tlog.App.Warn().Str("api", gvr.GroupVersion().String()).Msg("Failed to cast watched object")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch event.Type {
|
||||||
|
case watch.Added, watch.Modified:
|
||||||
|
k.updateFromItem(item)
|
||||||
|
case watch.Deleted:
|
||||||
|
k.removeIngress(item.GetNamespace(), item.GetName())
|
||||||
|
}
|
||||||
|
case <-resyncTicker.C:
|
||||||
|
if err := k.resyncGVR(gvr); err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource) {
|
||||||
|
resyncTicker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer resyncTicker.Stop()
|
||||||
|
|
||||||
|
if err := k.resyncGVR(gvr); err != nil {
|
||||||
|
tlog.App.Error().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Initial resync failed, retrying in 30 seconds")
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-k.ctx.Done():
|
||||||
|
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Stopping watcher")
|
||||||
|
return
|
||||||
|
case <-resyncTicker.C:
|
||||||
|
if err := k.resyncGVR(gvr); err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ctx, cancel := context.WithCancel(k.ctx)
|
||||||
|
watcher, err := k.client.Resource(gvr).Watch(ctx, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Error().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to start watcher")
|
||||||
|
cancel()
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tlog.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher started")
|
||||||
|
if !k.runWatcher(gvr, watcher, resyncTicker) {
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) Init() error {
|
||||||
|
var cfg *rest.Config
|
||||||
|
var err error
|
||||||
|
|
||||||
|
cfg, err = rest.InClusterConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get in-cluster Kubernetes config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := dynamic.NewForConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create Kubernetes client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
k.client = client
|
||||||
|
k.ctx, k.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
gvr := schema.GroupVersionResource{
|
||||||
|
Group: "networking.k8s.io",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "ingresses",
|
||||||
|
}
|
||||||
|
|
||||||
|
accessCtx, accessCancel := context.WithTimeout(k.ctx, 5*time.Second)
|
||||||
|
defer accessCancel()
|
||||||
|
_, err = k.client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Warn().Err(err).Msg("Insufficient permissions for networking.k8s.io/v1 Ingress, Kubernetes label provider will not work")
|
||||||
|
k.started = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Debug().Msg("networking.k8s.io/v1 Ingress API accessible")
|
||||||
|
go k.watchGVR(gvr)
|
||||||
|
|
||||||
|
k.started = true
|
||||||
|
tlog.App.Info().Msg("Kubernetes label provider initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KubernetesService) GetLabels(appDomain string) (*model.App, error) {
|
||||||
|
if !k.started {
|
||||||
|
tlog.App.Debug().Msg("Kubernetes not connected, returning empty labels")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check cache
|
||||||
|
if app, found := k.getByDomain(appDomain); found {
|
||||||
|
tlog.App.Debug().Str("domain", appDomain).Msg("Found labels in cache by domain")
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
appName := strings.SplitN(appDomain, ".", 2)[0]
|
||||||
|
if app, found := k.getByAppName(appName); found {
|
||||||
|
tlog.App.Debug().Str("domain", appDomain).Str("appName", appName).Msg("Found labels in cache by app name")
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Debug().Str("domain", appDomain).Msg("Cache miss, no matching ingress found")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKubernetesService(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
run func(t *testing.T, svc *KubernetesService)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
description: "Cache by domain returns app and misses unknown domain",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
app := config.App{Config: config.AppConfig{Domain: "foo.example.com"}}
|
||||||
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
|
{domain: "foo.example.com", appName: "foo", app: app},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, ok := svc.getByDomain("foo.example.com")
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "foo.example.com", got.Config.Domain)
|
||||||
|
|
||||||
|
_, ok = svc.getByDomain("notfound.example.com")
|
||||||
|
assert.False(t, ok)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Cache by app name returns app and misses unknown name",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
app := config.App{Config: config.AppConfig{Domain: "bar.example.com"}}
|
||||||
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
|
{domain: "bar.example.com", appName: "bar", app: app},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, ok := svc.getByAppName("bar")
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "bar.example.com", got.Config.Domain)
|
||||||
|
|
||||||
|
_, ok = svc.getByAppName("notfound")
|
||||||
|
assert.False(t, ok)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "RemoveIngress clears domain and app name entries",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
app := config.App{Config: config.AppConfig{Domain: "baz.example.com"}}
|
||||||
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
|
{domain: "baz.example.com", appName: "baz", app: app},
|
||||||
|
})
|
||||||
|
|
||||||
|
svc.removeIngress("default", "my-ingress")
|
||||||
|
|
||||||
|
_, ok := svc.getByDomain("baz.example.com")
|
||||||
|
assert.False(t, ok)
|
||||||
|
_, ok = svc.getByAppName("baz")
|
||||||
|
assert.False(t, ok)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "AddIngressApps replaces stale entries for the same ingress",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
old := config.App{Config: config.AppConfig{Domain: "old.example.com"}}
|
||||||
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
|
{domain: "old.example.com", appName: "old", app: old},
|
||||||
|
})
|
||||||
|
|
||||||
|
updated := config.App{Config: config.AppConfig{Domain: "new.example.com"}}
|
||||||
|
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||||
|
{domain: "new.example.com", appName: "new", app: updated},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, ok := svc.getByDomain("old.example.com")
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
got, ok := svc.getByDomain("new.example.com")
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "new.example.com", got.Config.Domain)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "GetLabels returns app from cache when started",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
svc.started = true
|
||||||
|
|
||||||
|
app := config.App{Config: config.AppConfig{Domain: "hit.example.com"}}
|
||||||
|
svc.addIngressApps("default", "ing", []ingressApp{
|
||||||
|
{domain: "hit.example.com", appName: "hit", app: app},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := svc.GetLabels("hit.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "hit.example.com", got.Config.Domain)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "GetLabels returns empty app on cache miss when started",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
svc.started = true
|
||||||
|
|
||||||
|
got, err := svc.GetLabels("notfound.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, config.App{}, got)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "GetLabels resolves app by app name",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
svc.started = true
|
||||||
|
|
||||||
|
app := config.App{Config: config.AppConfig{Domain: "myapp.internal.example.com"}}
|
||||||
|
svc.addIngressApps("default", "ing", []ingressApp{
|
||||||
|
{domain: "myapp.internal.example.com", appName: "myapp", app: app},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := svc.GetLabels("myapp.internal.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "myapp.internal.example.com", got.Config.Domain)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "GetLabels returns empty app when service not yet started",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
got, err := svc.GetLabels("anything.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, config.App{}, got)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "UpdateFromItem parses annotations and populates cache",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
item := unstructured.Unstructured{}
|
||||||
|
item.SetNamespace("default")
|
||||||
|
item.SetName("test-ingress")
|
||||||
|
item.SetAnnotations(map[string]string{
|
||||||
|
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
|
||||||
|
"tinyauth.apps.myapp.users.allow": "alice",
|
||||||
|
})
|
||||||
|
|
||||||
|
svc.updateFromItem(&item)
|
||||||
|
|
||||||
|
got, ok := svc.getByDomain("myapp.example.com")
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "myapp.example.com", got.Config.Domain)
|
||||||
|
assert.Equal(t, "alice", got.Users.Allow)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "UpdateFromItem with no annotations removes existing cache entries",
|
||||||
|
run: func(t *testing.T, svc *KubernetesService) {
|
||||||
|
app := config.App{Config: config.AppConfig{Domain: "todelete.example.com"}}
|
||||||
|
svc.addIngressApps("default", "test-ingress", []ingressApp{
|
||||||
|
{domain: "todelete.example.com", appName: "todelete", app: app},
|
||||||
|
})
|
||||||
|
|
||||||
|
item := unstructured.Unstructured{}
|
||||||
|
item.SetNamespace("default")
|
||||||
|
item.SetName("test-ingress")
|
||||||
|
|
||||||
|
svc.updateFromItem(&item)
|
||||||
|
|
||||||
|
_, ok := svc.getByDomain("todelete.example.com")
|
||||||
|
assert.False(t, ok)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.description, func(t *testing.T) {
|
||||||
|
svc := &KubernetesService{
|
||||||
|
ingressApps: make(map[ingressKey][]ingressApp),
|
||||||
|
domainIndex: make(map[string]ingressAppKey),
|
||||||
|
appNameIndex: make(map[string]ingressAppKey),
|
||||||
|
}
|
||||||
|
test.run(t, svc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cenkalti/backoff/v5"
|
"github.com/cenkalti/backoff/v5"
|
||||||
ldapgo "github.com/go-ldap/ldap/v3"
|
ldapgo "github.com/go-ldap/ldap/v3"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LdapServiceConfig struct {
|
type LdapServiceConfig struct {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,20 +15,20 @@ type OAuthServiceImpl interface {
|
|||||||
NewRandom() string
|
NewRandom() string
|
||||||
GetAuthURL(state string, verifier string) string
|
GetAuthURL(state string, verifier string) string
|
||||||
GetToken(code string, verifier string) (*oauth2.Token, error)
|
GetToken(code string, verifier string) (*oauth2.Token, error)
|
||||||
GetUserinfo(token *oauth2.Token) (config.Claims, error)
|
GetUserinfo(token *oauth2.Token) (*model.Claims, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthBrokerService struct {
|
type OAuthBrokerService struct {
|
||||||
services map[string]OAuthServiceImpl
|
services map[string]OAuthServiceImpl
|
||||||
configs map[string]config.OAuthServiceConfig
|
configs map[string]model.OAuthServiceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
var presets = map[string]func(config config.OAuthServiceConfig) *OAuthService{
|
var presets = map[string]func(config model.OAuthServiceConfig) *OAuthService{
|
||||||
"github": newGitHubOAuthService,
|
"github": newGitHubOAuthService,
|
||||||
"google": newGoogleOAuthService,
|
"google": newGoogleOAuthService,
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
|
func NewOAuthBrokerService(configs map[string]model.OAuthServiceConfig) *OAuthBrokerService {
|
||||||
return &OAuthBrokerService{
|
return &OAuthBrokerService{
|
||||||
services: make(map[string]OAuthServiceImpl),
|
services: make(map[string]OAuthServiceImpl),
|
||||||
configs: configs,
|
configs: configs,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GithubEmailResponse []struct {
|
type GithubEmailResponse []struct {
|
||||||
@@ -22,32 +22,32 @@ type GithubUserInfoResponse struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultExtractor(client *http.Client, url string) (config.Claims, error) {
|
func defaultExtractor(client *http.Client, url string) (*model.Claims, error) {
|
||||||
return simpleReq[config.Claims](client, url, nil)
|
return simpleReq[model.Claims](client, url, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubExtractor(client *http.Client, url string) (config.Claims, error) {
|
func githubExtractor(client *http.Client, url string) (*model.Claims, error) {
|
||||||
var user config.Claims
|
var user model.Claims
|
||||||
|
|
||||||
userInfo, err := simpleReq[GithubUserInfoResponse](client, "https://api.github.com/user", map[string]string{
|
userInfo, err := simpleReq[GithubUserInfoResponse](client, "https://api.github.com/user", map[string]string{
|
||||||
"accept": "application/vnd.github+json",
|
"accept": "application/vnd.github+json",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.Claims{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userEmails, err := simpleReq[GithubEmailResponse](client, "https://api.github.com/user/emails", map[string]string{
|
userEmails, err := simpleReq[GithubEmailResponse](client, "https://api.github.com/user/emails", map[string]string{
|
||||||
"accept": "application/vnd.github+json",
|
"accept": "application/vnd.github+json",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.Claims{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(userEmails) == 0 {
|
if len(*userEmails) == 0 {
|
||||||
return user, errors.New("no emails found")
|
return nil, errors.New("no emails found")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, email := range userEmails {
|
for _, email := range *userEmails {
|
||||||
if email.Primary {
|
if email.Primary {
|
||||||
user.Email = email.Email
|
user.Email = email.Email
|
||||||
break
|
break
|
||||||
@@ -56,22 +56,22 @@ func githubExtractor(client *http.Client, url string) (config.Claims, error) {
|
|||||||
|
|
||||||
// Use first available email if no primary email was found
|
// Use first available email if no primary email was found
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
user.Email = userEmails[0].Email
|
user.Email = (*userEmails)[0].Email
|
||||||
}
|
}
|
||||||
|
|
||||||
user.PreferredUsername = userInfo.Login
|
user.PreferredUsername = userInfo.Login
|
||||||
user.Name = userInfo.Name
|
user.Name = userInfo.Name
|
||||||
user.Sub = strconv.Itoa(userInfo.ID)
|
user.Sub = strconv.Itoa(userInfo.ID)
|
||||||
|
|
||||||
return user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (T, error) {
|
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (*T, error) {
|
||||||
var decodedRes T
|
var decodedRes T
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return decodedRes, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range headers {
|
for key, value := range headers {
|
||||||
@@ -80,23 +80,23 @@ func simpleReq[T any](client *http.Client, url string, headers map[string]string
|
|||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return decodedRes, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
return decodedRes, fmt.Errorf("request failed with status: %s", res.Status)
|
return nil, fmt.Errorf("request failed with status: %s", res.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return decodedRes, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, &decodedRes)
|
err = json.Unmarshal(body, &decodedRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return decodedRes, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodedRes, nil
|
return &decodedRes, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"golang.org/x/oauth2/endpoints"
|
"golang.org/x/oauth2/endpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
func newGoogleOAuthService(config model.OAuthServiceConfig) *OAuthService {
|
||||||
scopes := []string{"openid", "email", "profile"}
|
scopes := []string{"openid", "email", "profile"}
|
||||||
config.Scopes = scopes
|
config.Scopes = scopes
|
||||||
config.AuthURL = endpoints.Google.AuthURL
|
config.AuthURL = endpoints.Google.AuthURL
|
||||||
@@ -14,7 +14,7 @@ func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
|||||||
return NewOAuthService(config, "google")
|
return NewOAuthService(config, "google")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
func newGitHubOAuthService(config model.OAuthServiceConfig) *OAuthService {
|
||||||
scopes := []string{"read:user", "user:email"}
|
scopes := []string{"read:user", "user:email"}
|
||||||
config.Scopes = scopes
|
config.Scopes = scopes
|
||||||
config.AuthURL = endpoints.GitHub.AuthURL
|
config.AuthURL = endpoints.GitHub.AuthURL
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserinfoExtractor func(client *http.Client, url string) (config.Claims, error)
|
type UserinfoExtractor func(client *http.Client, url string) (*model.Claims, error)
|
||||||
|
|
||||||
type OAuthService struct {
|
type OAuthService struct {
|
||||||
serviceCfg config.OAuthServiceConfig
|
serviceCfg model.OAuthServiceConfig
|
||||||
config *oauth2.Config
|
config *oauth2.Config
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
userinfoExtractor UserinfoExtractor
|
userinfoExtractor UserinfoExtractor
|
||||||
id string
|
id string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOAuthService(config config.OAuthServiceConfig, id string) *OAuthService {
|
func NewOAuthService(config model.OAuthServiceConfig, id string) *OAuthService {
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
@@ -78,7 +78,7 @@ func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, er
|
|||||||
return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
|
return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (config.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.serviceCfg.UserinfoURL)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,17 +18,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-jose/go-jose/v4"
|
"github.com/go-jose/go-jose/v4"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
SupportedScopes = []string{"openid", "profile", "email", "groups"}
|
SupportedScopes = []string{"openid", "profile", "email", "phone", "address", "groups"}
|
||||||
SupportedResponseTypes = []string{"code"}
|
SupportedResponseTypes = []string{"code"}
|
||||||
SupportedGrantTypes = []string{"authorization_code", "refresh_token"}
|
SupportedGrantTypes = []string{"authorization_code", "refresh_token"}
|
||||||
)
|
)
|
||||||
@@ -48,6 +49,17 @@ type ClaimSet struct {
|
|||||||
Iat int64 `json:"iat"`
|
Iat int64 `json:"iat"`
|
||||||
Exp int64 `json:"exp"`
|
Exp int64 `json:"exp"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
GivenName string `json:"given_name,omitempty"`
|
||||||
|
FamilyName string `json:"family_name,omitempty"`
|
||||||
|
MiddleName string `json:"middle_name,omitempty"`
|
||||||
|
Nickname string `json:"nickname,omitempty"`
|
||||||
|
Profile string `json:"profile,omitempty"`
|
||||||
|
Picture string `json:"picture,omitempty"`
|
||||||
|
Website string `json:"website,omitempty"`
|
||||||
|
Gender string `json:"gender,omitempty"`
|
||||||
|
Birthdate string `json:"birthdate,omitempty"`
|
||||||
|
Zoneinfo string `json:"zoneinfo,omitempty"`
|
||||||
|
Locale string `json:"locale,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
EmailVerified bool `json:"email_verified,omitempty"`
|
EmailVerified bool `json:"email_verified,omitempty"`
|
||||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||||
@@ -56,13 +68,27 @@ type ClaimSet struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserinfoResponse struct {
|
type UserinfoResponse struct {
|
||||||
Sub string `json:"sub"`
|
Sub string `json:"sub"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
GivenName string `json:"given_name,omitempty"`
|
||||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
FamilyName string `json:"family_name,omitempty"`
|
||||||
Groups []string `json:"groups,omitempty"`
|
MiddleName string `json:"middle_name,omitempty"`
|
||||||
EmailVerified bool `json:"email_verified,omitempty"`
|
Nickname string `json:"nickname,omitempty"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
Profile string `json:"profile,omitempty"`
|
||||||
|
Picture string `json:"picture,omitempty"`
|
||||||
|
Website string `json:"website,omitempty"`
|
||||||
|
Gender string `json:"gender,omitempty"`
|
||||||
|
Birthdate string `json:"birthdate,omitempty"`
|
||||||
|
Zoneinfo string `json:"zoneinfo,omitempty"`
|
||||||
|
Locale string `json:"locale,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
EmailVerified bool `json:"email_verified,omitempty"`
|
||||||
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
|
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
|
||||||
|
Address *model.AddressClaim `json:"address,omitempty"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
@@ -86,7 +112,7 @@ type AuthorizeRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OIDCServiceConfig struct {
|
type OIDCServiceConfig struct {
|
||||||
Clients map[string]config.OIDCClientConfig
|
Clients map[string]model.OIDCClientConfig
|
||||||
PrivateKeyPath string
|
PrivateKeyPath string
|
||||||
PublicKeyPath string
|
PublicKeyPath string
|
||||||
Issuer string
|
Issuer string
|
||||||
@@ -96,7 +122,7 @@ type OIDCServiceConfig struct {
|
|||||||
type OIDCService struct {
|
type OIDCService struct {
|
||||||
config OIDCServiceConfig
|
config OIDCServiceConfig
|
||||||
queries *repository.Queries
|
queries *repository.Queries
|
||||||
clients map[string]config.OIDCClientConfig
|
clients map[string]model.OIDCClientConfig
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
publicKey crypto.PublicKey
|
publicKey crypto.PublicKey
|
||||||
issuer string
|
issuer string
|
||||||
@@ -229,7 +255,7 @@ func (service *OIDCService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We will reorganize the client into a map with the client ID as the key
|
// We will reorganize the client into a map with the client ID as the key
|
||||||
service.clients = make(map[string]config.OIDCClientConfig)
|
service.clients = make(map[string]model.OIDCClientConfig)
|
||||||
|
|
||||||
for id, client := range service.config.Clients {
|
for id, client := range service.config.Clients {
|
||||||
client.ID = id
|
client.ID = id
|
||||||
@@ -257,7 +283,7 @@ func (service *OIDCService) GetIssuer() string {
|
|||||||
return service.issuer
|
return service.issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) GetClient(id string) (config.OIDCClientConfig, bool) {
|
func (service *OIDCService) GetClient(id string) (model.OIDCClientConfig, bool) {
|
||||||
client, ok := service.clients[id]
|
client, ok := service.clients[id]
|
||||||
return client, ok
|
return client, ok
|
||||||
}
|
}
|
||||||
@@ -341,22 +367,42 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext config.UserContext, req AuthorizeRequest) error {
|
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext model.UserContext, req AuthorizeRequest) error {
|
||||||
userInfoParams := repository.CreateOidcUserInfoParams{
|
userInfoParams := repository.CreateOidcUserInfoParams{
|
||||||
Sub: sub,
|
Sub: sub,
|
||||||
Name: userContext.Name,
|
Name: userContext.GetName(),
|
||||||
Email: userContext.Email,
|
Email: userContext.GetEmail(),
|
||||||
PreferredUsername: userContext.Username,
|
PreferredUsername: userContext.GetUsername(),
|
||||||
UpdatedAt: time.Now().Unix(),
|
UpdatedAt: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server
|
if userContext.IsLocal() {
|
||||||
if userContext.Provider == "ldap" {
|
addressJSON, err := json.Marshal(userContext.Local.Attributes.Address)
|
||||||
userInfoParams.Groups = userContext.LdapGroups
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userInfoParams.GivenName = userContext.Local.Attributes.GivenName
|
||||||
|
userInfoParams.FamilyName = userContext.Local.Attributes.FamilyName
|
||||||
|
userInfoParams.MiddleName = userContext.Local.Attributes.MiddleName
|
||||||
|
userInfoParams.Nickname = userContext.Local.Attributes.Nickname
|
||||||
|
userInfoParams.Profile = userContext.Local.Attributes.Profile
|
||||||
|
userInfoParams.Picture = userContext.Local.Attributes.Picture
|
||||||
|
userInfoParams.Website = userContext.Local.Attributes.Website
|
||||||
|
userInfoParams.Gender = userContext.Local.Attributes.Gender
|
||||||
|
userInfoParams.Birthdate = userContext.Local.Attributes.Birthdate
|
||||||
|
userInfoParams.Zoneinfo = userContext.Local.Attributes.Zoneinfo
|
||||||
|
userInfoParams.Locale = userContext.Local.Attributes.Locale
|
||||||
|
userInfoParams.PhoneNumber = userContext.Local.Attributes.PhoneNumber
|
||||||
|
userInfoParams.Address = string(addressJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
if userContext.OAuth && len(userContext.OAuthGroups) > 0 {
|
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server
|
||||||
userInfoParams.Groups = userContext.OAuthGroups
|
if userContext.IsLDAP() {
|
||||||
|
userInfoParams.Groups = strings.Join(userContext.LDAP.Groups, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if userContext.IsOAuth() {
|
||||||
|
userInfoParams.Groups = strings.Join(userContext.OAuth.Groups, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := service.queries.CreateOidcUserInfo(c, userInfoParams)
|
_, err := service.queries.CreateOidcUserInfo(c, userInfoParams)
|
||||||
@@ -401,7 +447,7 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, client
|
|||||||
return oidcCode, nil
|
return oidcCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
|
func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
|
||||||
createdAt := time.Now().Unix()
|
createdAt := time.Now().Unix()
|
||||||
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
@@ -467,7 +513,7 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
|
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client model.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
|
||||||
user, err := service.GetUserinfo(c, codeEntry.Sub)
|
user, err := service.GetUserinfo(c, codeEntry.Sub)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -486,7 +532,7 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OI
|
|||||||
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
// Refresh token lives double the time of an access token but can't be used to access userinfo
|
// Refresh token lives double the time of an access token but can't be used to access userinfo
|
||||||
refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
refreshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
||||||
|
|
||||||
tokenResponse := TokenResponse{
|
tokenResponse := TokenResponse{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
@@ -504,7 +550,7 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OI
|
|||||||
ClientID: client.ClientID,
|
ClientID: client.ClientID,
|
||||||
Scope: codeEntry.Scope,
|
Scope: codeEntry.Scope,
|
||||||
TokenExpiresAt: tokenExpiresAt,
|
TokenExpiresAt: tokenExpiresAt,
|
||||||
RefreshTokenExpiresAt: refrshTokenExpiresAt,
|
RefreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||||
Nonce: codeEntry.Nonce,
|
Nonce: codeEntry.Nonce,
|
||||||
CodeHash: codeEntry.CodeHash,
|
CodeHash: codeEntry.CodeHash,
|
||||||
})
|
})
|
||||||
@@ -520,7 +566,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return TokenResponse{}, ErrTokenNotFound
|
return TokenResponse{}, ErrTokenNotFound
|
||||||
}
|
}
|
||||||
return TokenResponse{}, err
|
return TokenResponse{}, err
|
||||||
@@ -541,7 +587,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
return TokenResponse{}, err
|
return TokenResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, err := service.generateIDToken(config.OIDCClientConfig{
|
idToken, err := service.generateIDToken(model.OIDCClientConfig{
|
||||||
ClientID: entry.ClientID,
|
ClientID: entry.ClientID,
|
||||||
}, user, entry.Scope, entry.Nonce)
|
}, user, entry.Scope, entry.Nonce)
|
||||||
|
|
||||||
@@ -553,7 +599,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
newRefreshToken := utils.GenerateString(32)
|
newRefreshToken := utils.GenerateString(32)
|
||||||
|
|
||||||
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
refreshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix()
|
||||||
|
|
||||||
tokenResponse := TokenResponse{
|
tokenResponse := TokenResponse{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
@@ -568,7 +614,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
|
|||||||
AccessTokenHash: service.Hash(accessToken),
|
AccessTokenHash: service.Hash(accessToken),
|
||||||
RefreshTokenHash: service.Hash(newRefreshToken),
|
RefreshTokenHash: service.Hash(newRefreshToken),
|
||||||
TokenExpiresAt: tokenExpiresAt,
|
TokenExpiresAt: tokenExpiresAt,
|
||||||
RefreshTokenExpiresAt: refrshTokenExpiresAt,
|
RefreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||||
RefreshTokenHash_2: service.Hash(refreshToken), // that's the selector, it's not stored in the db
|
RefreshTokenHash_2: service.Hash(refreshToken), // that's the selector, it's not stored in the db
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -599,7 +645,7 @@ func (service *OIDCService) GetAccessToken(c *gin.Context, tokenHash string) (re
|
|||||||
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
entry, err := service.queries.GetOidcToken(c, tokenHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return repository.OidcToken{}, ErrTokenNotFound
|
return repository.OidcToken{}, ErrTokenNotFound
|
||||||
}
|
}
|
||||||
return repository.OidcToken{}, err
|
return repository.OidcToken{}, err
|
||||||
@@ -637,12 +683,22 @@ func (service *OIDCService) CompileUserinfo(user repository.OidcUserinfo, scope
|
|||||||
if slices.Contains(scopes, "profile") {
|
if slices.Contains(scopes, "profile") {
|
||||||
userInfo.Name = user.Name
|
userInfo.Name = user.Name
|
||||||
userInfo.PreferredUsername = user.PreferredUsername
|
userInfo.PreferredUsername = user.PreferredUsername
|
||||||
|
userInfo.GivenName = user.GivenName
|
||||||
|
userInfo.FamilyName = user.FamilyName
|
||||||
|
userInfo.MiddleName = user.MiddleName
|
||||||
|
userInfo.Nickname = user.Nickname
|
||||||
|
userInfo.Profile = user.Profile
|
||||||
|
userInfo.Picture = user.Picture
|
||||||
|
userInfo.Website = user.Website
|
||||||
|
userInfo.Gender = user.Gender
|
||||||
|
userInfo.Birthdate = user.Birthdate
|
||||||
|
userInfo.Zoneinfo = user.Zoneinfo
|
||||||
|
userInfo.Locale = user.Locale
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "email") {
|
if slices.Contains(scopes, "email") {
|
||||||
userInfo.Email = user.Email
|
userInfo.Email = user.Email
|
||||||
// We can set this as a configuration option in the future but for now it's a good idea to assume it's true
|
userInfo.EmailVerified = user.Email != ""
|
||||||
userInfo.EmailVerified = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "groups") {
|
if slices.Contains(scopes, "groups") {
|
||||||
@@ -653,6 +709,19 @@ func (service *OIDCService) CompileUserinfo(user repository.OidcUserinfo, scope
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if slices.Contains(scopes, "phone") {
|
||||||
|
userInfo.PhoneNumber = user.PhoneNumber
|
||||||
|
verified := user.PhoneNumber != ""
|
||||||
|
userInfo.PhoneNumberVerified = &verified
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(scopes, "address") {
|
||||||
|
var addr model.AddressClaim
|
||||||
|
if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
|
||||||
|
userInfo.Address = &addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return userInfo
|
return userInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +786,7 @@ func (service *OIDCService) Cleanup() {
|
|||||||
token, err := service.queries.GetOidcTokenBySub(ctx, expiredCode.Sub)
|
token, err := service.queries.GetOidcTokenBySub(ctx, expiredCode.Sub)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to get OIDC token by sub")
|
tlog.App.Warn().Err(err).Msg("Failed to get OIDC token by sub")
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/repository"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestUser() repository.OidcUserinfo {
|
||||||
|
addr := config.AddressClaim{
|
||||||
|
Formatted: "123 Main St",
|
||||||
|
StreetAddress: "123 Main St",
|
||||||
|
Locality: "Springfield",
|
||||||
|
Region: "IL",
|
||||||
|
PostalCode: "62701",
|
||||||
|
Country: "US",
|
||||||
|
}
|
||||||
|
addrJSON, _ := json.Marshal(addr)
|
||||||
|
|
||||||
|
return repository.OidcUserinfo{
|
||||||
|
Sub: "test-sub",
|
||||||
|
Name: "Test User",
|
||||||
|
PreferredUsername: "testuser",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Groups: "admins,users",
|
||||||
|
UpdatedAt: 1234567890,
|
||||||
|
GivenName: "Test",
|
||||||
|
FamilyName: "User",
|
||||||
|
MiddleName: "M",
|
||||||
|
Nickname: "testy",
|
||||||
|
Profile: "https://example.com/testuser",
|
||||||
|
Picture: "https://example.com/testuser.jpg",
|
||||||
|
Website: "https://testuser.example.com",
|
||||||
|
Gender: "male",
|
||||||
|
Birthdate: "1990-01-01",
|
||||||
|
Zoneinfo: "America/Chicago",
|
||||||
|
Locale: "en-US",
|
||||||
|
PhoneNumber: "+15555550100",
|
||||||
|
Address: string(addrJSON),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileUserinfo(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
svc := service.NewOIDCService(service.OIDCServiceConfig{
|
||||||
|
PrivateKeyPath: dir + "/key.pem",
|
||||||
|
PublicKeyPath: dir + "/key.pub",
|
||||||
|
Issuer: "https://tinyauth.example.com",
|
||||||
|
SessionExpiry: 3600,
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, svc.Init())
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
mutate func(u *repository.OidcUserinfo)
|
||||||
|
scope string
|
||||||
|
run func(t *testing.T, info service.UserinfoResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
description: "openid scope only returns sub and updated_at",
|
||||||
|
scope: "openid",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, "test-sub", info.Sub)
|
||||||
|
assert.Equal(t, int64(1234567890), info.UpdatedAt)
|
||||||
|
assert.Empty(t, info.Name)
|
||||||
|
assert.Empty(t, info.Email)
|
||||||
|
assert.Nil(t, info.Groups)
|
||||||
|
assert.Nil(t, info.PhoneNumberVerified)
|
||||||
|
assert.Nil(t, info.Address)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "profile scope returns all profile fields",
|
||||||
|
scope: "openid,profile",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, "Test User", info.Name)
|
||||||
|
assert.Equal(t, "testuser", info.PreferredUsername)
|
||||||
|
assert.Equal(t, "Test", info.GivenName)
|
||||||
|
assert.Equal(t, "User", info.FamilyName)
|
||||||
|
assert.Equal(t, "M", info.MiddleName)
|
||||||
|
assert.Equal(t, "testy", info.Nickname)
|
||||||
|
assert.Equal(t, "https://example.com/testuser", info.Profile)
|
||||||
|
assert.Equal(t, "https://example.com/testuser.jpg", info.Picture)
|
||||||
|
assert.Equal(t, "https://testuser.example.com", info.Website)
|
||||||
|
assert.Equal(t, "male", info.Gender)
|
||||||
|
assert.Equal(t, "1990-01-01", info.Birthdate)
|
||||||
|
assert.Equal(t, "America/Chicago", info.Zoneinfo)
|
||||||
|
assert.Equal(t, "en-US", info.Locale)
|
||||||
|
assert.Empty(t, info.Email)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "email scope sets email and email_verified true when email present",
|
||||||
|
scope: "openid,email",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, "test@example.com", info.Email)
|
||||||
|
assert.True(t, info.EmailVerified)
|
||||||
|
assert.Empty(t, info.Name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "email scope sets email_verified false when email absent",
|
||||||
|
scope: "openid,email",
|
||||||
|
mutate: func(u *repository.OidcUserinfo) { u.Email = "" },
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Empty(t, info.Email)
|
||||||
|
assert.False(t, info.EmailVerified)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "phone scope sets phone_number_verified true when phone present",
|
||||||
|
scope: "openid,phone",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, "+15555550100", info.PhoneNumber)
|
||||||
|
require.NotNil(t, info.PhoneNumberVerified)
|
||||||
|
assert.True(t, *info.PhoneNumberVerified)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "phone scope sets phone_number_verified false when phone absent",
|
||||||
|
scope: "openid,phone",
|
||||||
|
mutate: func(u *repository.OidcUserinfo) { u.PhoneNumber = "" },
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
require.NotNil(t, info.PhoneNumberVerified)
|
||||||
|
assert.False(t, *info.PhoneNumberVerified)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "address scope returns parsed address",
|
||||||
|
scope: "openid,address",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
require.NotNil(t, info.Address)
|
||||||
|
assert.Equal(t, "123 Main St", info.Address.Formatted)
|
||||||
|
assert.Equal(t, "123 Main St", info.Address.StreetAddress)
|
||||||
|
assert.Equal(t, "Springfield", info.Address.Locality)
|
||||||
|
assert.Equal(t, "IL", info.Address.Region)
|
||||||
|
assert.Equal(t, "62701", info.Address.PostalCode)
|
||||||
|
assert.Equal(t, "US", info.Address.Country)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "address scope with invalid JSON omits address",
|
||||||
|
scope: "openid,address",
|
||||||
|
mutate: func(u *repository.OidcUserinfo) { u.Address = "not-valid-json" },
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Nil(t, info.Address)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "groups scope returns split groups",
|
||||||
|
scope: "openid,groups",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, []string{"admins", "users"}, info.Groups)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "groups scope returns empty slice when no groups",
|
||||||
|
scope: "openid,groups",
|
||||||
|
mutate: func(u *repository.OidcUserinfo) { u.Groups = "" },
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, []string{}, info.Groups)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "all scopes return all fields",
|
||||||
|
scope: "openid,profile,email,phone,address,groups",
|
||||||
|
run: func(t *testing.T, info service.UserinfoResponse) {
|
||||||
|
assert.Equal(t, "Test User", info.Name)
|
||||||
|
assert.Equal(t, "test@example.com", info.Email)
|
||||||
|
assert.Equal(t, "+15555550100", info.PhoneNumber)
|
||||||
|
require.NotNil(t, info.PhoneNumberVerified)
|
||||||
|
assert.True(t, *info.PhoneNumberVerified)
|
||||||
|
require.NotNil(t, info.Address)
|
||||||
|
assert.Equal(t, "Springfield", info.Address.Locality)
|
||||||
|
assert.Equal(t, []string{"admins", "users"}, info.Groups)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.description, func(t *testing.T) {
|
||||||
|
user := newTestUser()
|
||||||
|
if test.mutate != nil {
|
||||||
|
test.mutate(&user)
|
||||||
|
}
|
||||||
|
info := svc.CompileUserinfo(user, test.scope)
|
||||||
|
test.run(t, info)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,22 +71,6 @@ func Filter[T any](slice []T, test func(T) bool) (res []T) {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContext(c *gin.Context) (config.UserContext, error) {
|
|
||||||
userContextValue, exists := c.Get("context")
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return config.UserContext{}, errors.New("no user context in request")
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext, ok := userContextValue.(*config.UserContext)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return config.UserContext{}, errors.New("invalid user context in request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return *userContext, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsRedirectSafe(redirectURL string, domain string) bool {
|
func IsRedirectSafe(redirectURL string, domain string) bool {
|
||||||
if redirectURL == "" {
|
if redirectURL == "" {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,28 +127,6 @@ func TestFilter(t *testing.T) {
|
|||||||
assert.DeepEqual(t, expectedStr, resultStr)
|
assert.DeepEqual(t, expectedStr, resultStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetContext(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
c, _ := gin.CreateTestContext(nil)
|
|
||||||
|
|
||||||
// Normal case
|
|
||||||
c.Set("context", &config.UserContext{Username: "testuser"})
|
|
||||||
result, err := utils.GetContext(c)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Equal(t, "testuser", result.Username)
|
|
||||||
|
|
||||||
// Case with no context
|
|
||||||
c.Set("context", nil)
|
|
||||||
_, err = utils.GetContext(c)
|
|
||||||
assert.Error(t, err, "invalid user context in request")
|
|
||||||
|
|
||||||
// Case with invalid context type
|
|
||||||
c.Set("context", "invalid type")
|
|
||||||
_, err = utils.GetContext(c)
|
|
||||||
assert.Error(t, err, "invalid user context in request")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsRedirectSafe(t *testing.T) {
|
func TestIsRedirectSafe(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
domain := "example.com"
|
domain := "example.com"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package decoders_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,21 +4,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
|
||||||
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
"github.com/tinyauthapp/paerser/env"
|
"github.com/tinyauthapp/paerser/env"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvLoader struct{}
|
type EnvLoader struct{}
|
||||||
|
|
||||||
func (e *EnvLoader) Load(_ []string, cmd *cli.Command) (bool, error) {
|
func (e *EnvLoader) Load(_ []string, cmd *cli.Command) (bool, error) {
|
||||||
vars := env.FindPrefixedEnvVars(os.Environ(), config.DefaultNamePrefix, cmd.Configuration)
|
vars := env.FindPrefixedEnvVars(os.Environ(), model.DefaultNamePrefix, cmd.Configuration)
|
||||||
if len(vars) == 0 {
|
if len(vars) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := env.Decode(vars, config.DefaultNamePrefix, cmd.Configuration); err != nil {
|
if err := env.Decode(vars, model.DefaultNamePrefix, cmd.Configuration); err != nil {
|
||||||
return false, fmt.Errorf("failed to decode configuration from environment variables: %w", err)
|
return false, fmt.Errorf("failed to decode configuration from environment variables: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package utils_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
@@ -22,7 +22,7 @@ var (
|
|||||||
App zerolog.Logger
|
App zerolog.Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLogger(cfg config.LogConfig) *Logger {
|
func NewLogger(cfg model.LogConfig) *Logger {
|
||||||
baseLogger := log.With().
|
baseLogger := log.With().
|
||||||
Timestamp().
|
Timestamp().
|
||||||
Caller().
|
Caller().
|
||||||
@@ -44,24 +44,24 @@ func NewLogger(cfg config.LogConfig) *Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewSimpleLogger() *Logger {
|
func NewSimpleLogger() *Logger {
|
||||||
return NewLogger(config.LogConfig{
|
return NewLogger(model.LogConfig{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
Json: false,
|
Json: false,
|
||||||
Streams: config.LogStreams{
|
Streams: model.LogStreams{
|
||||||
HTTP: config.LogStreamConfig{Enabled: true},
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
App: config.LogStreamConfig{Enabled: true},
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
Audit: config.LogStreamConfig{Enabled: false},
|
Audit: model.LogStreamConfig{Enabled: false},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestLogger() *Logger {
|
func NewTestLogger() *Logger {
|
||||||
return NewLogger(config.LogConfig{
|
return NewLogger(model.LogConfig{
|
||||||
Level: "trace",
|
Level: "trace",
|
||||||
Streams: config.LogStreams{
|
Streams: model.LogStreams{
|
||||||
HTTP: config.LogStreamConfig{Enabled: true},
|
HTTP: model.LogStreamConfig{Enabled: true},
|
||||||
App: config.LogStreamConfig{Enabled: true},
|
App: model.LogStreamConfig{Enabled: true},
|
||||||
Audit: config.LogStreamConfig{Enabled: true},
|
Audit: model.LogStreamConfig{Enabled: true},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ func (l *Logger) Init() {
|
|||||||
App = l.App
|
App = l.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLogger(component string, streamCfg config.LogStreamConfig, baseLogger zerolog.Logger) zerolog.Logger {
|
func createLogger(component string, streamCfg model.LogStreamConfig, baseLogger zerolog.Logger) zerolog.Logger {
|
||||||
if !streamCfg.Enabled {
|
if !streamCfg.Enabled {
|
||||||
return zerolog.Nop()
|
return zerolog.Nop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseUsers(usersStr []string) ([]config.User, error) {
|
func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) {
|
||||||
var users []config.User
|
var users []model.LocalUser
|
||||||
|
|
||||||
if len(usersStr) == 0 {
|
if len(usersStr) == 0 {
|
||||||
return []config.User{}, nil
|
return &users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range usersStr {
|
for _, user := range usersStr {
|
||||||
@@ -22,19 +22,22 @@ func ParseUsers(usersStr []string) ([]config.User, error) {
|
|||||||
}
|
}
|
||||||
parsed, err := ParseUser(strings.TrimSpace(user))
|
parsed, err := ParseUser(strings.TrimSpace(user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []config.User{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
users = append(users, parsed)
|
if attrs, ok := userAttributes[parsed.Username]; ok {
|
||||||
|
parsed.Attributes = attrs
|
||||||
|
}
|
||||||
|
users = append(users, *parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return &users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
|
func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) {
|
||||||
var usersStr []string
|
var usersStr []string
|
||||||
|
|
||||||
if len(usersCfg) == 0 && usersPath == "" {
|
if len(usersCfg) == 0 && usersPath == "" {
|
||||||
return []config.User{}, nil
|
return &[]model.LocalUser{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(usersCfg) > 0 {
|
if len(usersCfg) > 0 {
|
||||||
@@ -45,7 +48,7 @@ func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
|
|||||||
contents, err := ReadFile(usersPath)
|
contents, err := ReadFile(usersPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []config.User{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.SplitSeq(contents, "\n")
|
lines := strings.SplitSeq(contents, "\n")
|
||||||
@@ -59,10 +62,10 @@ func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseUsers(usersStr)
|
return ParseUsers(usersStr, userAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseUser(userStr string) (config.User, error) {
|
func ParseUser(userStr string) (*model.LocalUser, error) {
|
||||||
if strings.Contains(userStr, "$$") {
|
if strings.Contains(userStr, "$$") {
|
||||||
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
||||||
}
|
}
|
||||||
@@ -70,27 +73,27 @@ func ParseUser(userStr string) (config.User, error) {
|
|||||||
parts := strings.SplitN(userStr, ":", 4)
|
parts := strings.SplitN(userStr, ":", 4)
|
||||||
|
|
||||||
if len(parts) < 2 || len(parts) > 3 {
|
if len(parts) < 2 || len(parts) > 3 {
|
||||||
return config.User{}, errors.New("invalid user format")
|
return nil, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
trimmed := strings.TrimSpace(part)
|
trimmed := strings.TrimSpace(part)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
return config.User{}, errors.New("invalid user format")
|
return nil, errors.New("invalid user format")
|
||||||
}
|
}
|
||||||
parts[i] = trimmed
|
parts[i] = trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
user := config.User{
|
user := model.LocalUser{
|
||||||
Username: parts[0],
|
Username: parts[0],
|
||||||
Password: parts[1],
|
Password: parts[1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
user.TotpSecret = parts[2]
|
user.TOTPSecret = parts[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompileUserEmail(username string, domain string) string {
|
func CompileUserEmail(username string, domain string) string {
|
||||||
|
|||||||
@@ -4,122 +4,117 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/tinyauthapp/tinyauth/internal/config"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetUsers(t *testing.T) {
|
func TestGetUsers(t *testing.T) {
|
||||||
|
hash := "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
file, err := os.Create("/tmp/tinyauth_users_test.txt")
|
file, err := os.Create("/tmp/tinyauth_users_test.txt")
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
_, err = file.WriteString(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G \n user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ") // Spacing is on purpose
|
_, err = file.WriteString(" user1:" + hash + " \n user2:" + hash + " ") // Spacing is on purpose
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
defer os.Remove("/tmp/tinyauth_users_test.txt")
|
defer os.Remove("/tmp/tinyauth_users_test.txt")
|
||||||
|
|
||||||
// Test file
|
noAttrs := map[string]config.UserAttributes{}
|
||||||
users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt")
|
|
||||||
|
// Test file only
|
||||||
|
users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt", noAttrs)
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 2, len(users))
|
assert.Equal(t, 2, len(users))
|
||||||
|
|
||||||
assert.Equal(t, "user1", users[0].Username)
|
assert.Equal(t, "user1", users[0].Username)
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
|
assert.Equal(t, hash, users[0].Password)
|
||||||
assert.Equal(t, "user2", users[1].Username)
|
assert.Equal(t, "user2", users[1].Username)
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
assert.Equal(t, hash, users[1].Password)
|
||||||
|
|
||||||
// Test config
|
// Test inline config only
|
||||||
users, err = utils.GetUsers([]string{"user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "")
|
users, err = utils.GetUsers([]string{"user3:" + hash, "user4:" + hash}, "", noAttrs)
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 2, len(users))
|
assert.Equal(t, 2, len(users))
|
||||||
|
|
||||||
assert.Equal(t, "user3", users[0].Username)
|
assert.Equal(t, "user3", users[0].Username)
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
|
|
||||||
assert.Equal(t, "user4", users[1].Username)
|
assert.Equal(t, "user4", users[1].Username)
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
|
||||||
|
|
||||||
// Test both
|
// Test both
|
||||||
users, err = utils.GetUsers([]string{"user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "/tmp/tinyauth_users_test.txt")
|
users, err = utils.GetUsers([]string{"user5:" + hash}, "/tmp/tinyauth_users_test.txt", noAttrs)
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 3, len(users))
|
assert.Equal(t, 3, len(users))
|
||||||
|
|
||||||
assert.Equal(t, "user5", users[0].Username)
|
usernames := map[string]bool{}
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
|
for _, u := range users {
|
||||||
assert.Equal(t, "user1", users[1].Username)
|
usernames[u.Username] = true
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
}
|
||||||
assert.Equal(t, "user2", users[2].Username)
|
assert.Assert(t, usernames["user1"])
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password)
|
assert.Assert(t, usernames["user2"])
|
||||||
|
assert.Assert(t, usernames["user5"])
|
||||||
|
|
||||||
|
// Test attributes applied from userAttributes map
|
||||||
|
attrs := map[string]config.UserAttributes{
|
||||||
|
"user1": {Name: "User One", Email: "user1@example.com"},
|
||||||
|
}
|
||||||
|
users, err = utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt", attrs)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, 2, len(users))
|
||||||
|
|
||||||
|
for _, u := range users {
|
||||||
|
if u.Username == "user1" {
|
||||||
|
assert.Equal(t, "User One", u.Attributes.Name)
|
||||||
|
assert.Equal(t, "user1@example.com", u.Attributes.Email)
|
||||||
|
}
|
||||||
|
if u.Username == "user2" {
|
||||||
|
assert.Equal(t, "", u.Attributes.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test empty
|
// Test empty
|
||||||
users, err = utils.GetUsers([]string{}, "")
|
users, err = utils.GetUsers([]string{}, "", noAttrs)
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 0, len(users))
|
assert.Equal(t, 0, len(users))
|
||||||
|
|
||||||
// Test non-existent file
|
// Test non-existent file
|
||||||
users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt")
|
users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt", noAttrs)
|
||||||
|
|
||||||
assert.ErrorContains(t, err, "no such file or directory")
|
assert.ErrorContains(t, err, "no such file or directory")
|
||||||
|
|
||||||
assert.Equal(t, 0, len(users))
|
assert.Equal(t, 0, len(users))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseUsers(t *testing.T) {
|
|
||||||
// Valid users
|
|
||||||
users, err := utils.ParseUsers([]string{"user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF"}) // user2 has TOTP
|
|
||||||
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, 2, len(users))
|
|
||||||
|
|
||||||
assert.Equal(t, "user1", users[0].Username)
|
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
|
|
||||||
assert.Equal(t, "", users[0].TotpSecret)
|
|
||||||
assert.Equal(t, "user2", users[1].Username)
|
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
|
||||||
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
|
|
||||||
|
|
||||||
// Valid weirdly spaced users
|
|
||||||
users, err = utils.ParseUsers([]string{" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ", " user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF "}) // Spacing is on purpose
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, 2, len(users))
|
|
||||||
|
|
||||||
assert.Equal(t, "user1", users[0].Username)
|
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password)
|
|
||||||
assert.Equal(t, "", users[0].TotpSecret)
|
|
||||||
assert.Equal(t, "user2", users[1].Username)
|
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
|
|
||||||
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseUser(t *testing.T) {
|
func TestParseUser(t *testing.T) {
|
||||||
|
hash := "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"
|
||||||
|
|
||||||
// Valid user without TOTP
|
// Valid user without TOTP
|
||||||
user, err := utils.ParseUser("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G")
|
user, err := utils.ParseUser("user1:" + hash)
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "user1", user.Username)
|
assert.Equal(t, "user1", user.Username)
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password)
|
assert.Equal(t, hash, user.Password)
|
||||||
assert.Equal(t, "", user.TotpSecret)
|
assert.Equal(t, "", user.TotpSecret)
|
||||||
|
|
||||||
// Valid user with TOTP
|
// Valid user with TOTP
|
||||||
user, err = utils.ParseUser("user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF")
|
user, err = utils.ParseUser("user2:" + hash + ":ABCDEF")
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "user2", user.Username)
|
assert.Equal(t, "user2", user.Username)
|
||||||
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password)
|
assert.Equal(t, hash, user.Password)
|
||||||
assert.Equal(t, "ABCDEF", user.TotpSecret)
|
assert.Equal(t, "ABCDEF", user.TotpSecret)
|
||||||
|
|
||||||
// Valid user with $$ in password
|
// Valid user with $$ in password
|
||||||
|
|||||||
+15
-2
@@ -95,9 +95,22 @@ INSERT INTO "oidc_userinfo" (
|
|||||||
"preferred_username",
|
"preferred_username",
|
||||||
"email",
|
"email",
|
||||||
"groups",
|
"groups",
|
||||||
"updated_at"
|
"updated_at",
|
||||||
|
"given_name",
|
||||||
|
"family_name",
|
||||||
|
"middle_name",
|
||||||
|
"nickname",
|
||||||
|
"profile",
|
||||||
|
"picture",
|
||||||
|
"website",
|
||||||
|
"gender",
|
||||||
|
"birthdate",
|
||||||
|
"zoneinfo",
|
||||||
|
"locale",
|
||||||
|
"phone_number",
|
||||||
|
"address"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|||||||
+19
-6
@@ -22,10 +22,23 @@ CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
||||||
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"preferred_username" TEXT NOT NULL,
|
"preferred_username" TEXT NOT NULL,
|
||||||
"email" TEXT NOT NULL,
|
"email" TEXT NOT NULL,
|
||||||
"groups" TEXT NOT NULL,
|
"groups" TEXT NOT NULL,
|
||||||
"updated_at" INTEGER NOT NULL
|
"updated_at" INTEGER NOT NULL,
|
||||||
|
"given_name" TEXT NOT NULL,
|
||||||
|
"family_name" TEXT NOT NULL,
|
||||||
|
"middle_name" TEXT NOT NULL,
|
||||||
|
"nickname" TEXT NOT NULL,
|
||||||
|
"profile" TEXT NOT NULL,
|
||||||
|
"picture" TEXT NOT NULL,
|
||||||
|
"website" TEXT NOT NULL,
|
||||||
|
"gender" TEXT NOT NULL,
|
||||||
|
"birthdate" TEXT NOT NULL,
|
||||||
|
"zoneinfo" TEXT NOT NULL,
|
||||||
|
"locale" TEXT NOT NULL,
|
||||||
|
"phone_number" TEXT NOT NULL,
|
||||||
|
"address" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user