Compare commits

...

12 Commits

Author SHA1 Message Date
Scott McKendry 0244f39387 feat(db): add code gen to build sqlc-compatible wrappers 2026-05-03 14:11:54 +12:00
Scott McKendry 1d0a4627a9 refactor(db): use new store interface 2026-04-30 19:18:33 +12:00
Contre 956d2f55c3 feat(access-control): Add support for Kubernetes Label (#627)
* feat(access-control): Add support for Kubernetes Label

* feat(access-control): Defaults to Docker

* feat(access-control): Remove kubeconfig fallback

* feat(watcher): Watcher for kubernetes service

* feat(watcher): Merge with main + remove nightly fix redirect

* fix(go): Go mod + Go sum after sync with main

* fix(config): Ser default value for LabelProvider to Docker

* feat(go): go mod tidy

* feat(k8s_service): Remove logic for deprecated Ingress k8s v1.22

* feat(k8s_service): (Watcher) -> Wait 5s before breaking to outer loop again

* feat(k8s_service): Remove logic for deprecated Ingress k8s v1.22

* feat(k8s_service): Remove logic for deprecated Ingress k8s v1.22

* feat(k8s_service): Remove logic for deprecated Ingress k8s v1.22

* feat(k8s_service): Remove
var _ = unstructured.Unstructured{} + comments + msg edits

* feat(bootstrap): Remove dockerService from bootstrap svc

* feat(auth_svc): Remove dockerService from authservice

* feat(test): Add tests for kubernetes_services

* feat(test): Remove docker serivce form proxy/user test

* fix(refactor): Remove update logic from watcher and resync

* fix(refactor): Split watchGVR to make it more readable

* fix(refactor): Remove discovery + drop K 1.22 completely

* fix(refactor): Move interface to acess_controls_service

* feat: Autodetect labelprovider if TINYAUTH_LABELPROVIDER not set

* fix(test): Match testing scheme to the controllers

* fix: service bootstrap import after merge

* fix: service bootstrap import after merge
2026-04-29 16:16:21 +03:00
Stavros 5e822d99e1 chore: fix typos in oidc service 2026-04-29 16:08:21 +03:00
Stavros 373ee8806e chore: prefer errors.is instead of comparison 2026-04-29 16:04:27 +03:00
Stavros a14d64c8ba chore: remove exp slices package and use stdlib 2026-04-29 15:56:35 +03:00
Stavros d51e3efe32 fix: use pinned step versions and set workflow permissions (#825)
* fix: use pinned step versions and set workflow permissions

* fix: use contents write in sponsors list action
2026-04-28 15:52:02 +03:00
Stavros d73cc628fb chore: add openssf baseline badge to readme 2026-04-28 15:46:40 +03:00
Stavros a8737ab0bd fix: use frozen lockfile in makefile bun install 2026-04-28 15:31:07 +03:00
Stavros 11793c9869 fix: use frozen lockfile in all bun installs 2026-04-28 15:30:21 +03:00
Stavros c68a022ed0 docs: add ai policy (#821)
* docs: add ai policy

* docs: rework ai policy for more clear rules and expectations

* chore: review comments

* chore: rabbit feedback

* chore: update contributing guide to reference ai policy
2026-04-27 20:44:44 +03:00
Scott McKendry 5d95123dcb feat(oidc): support for all in-spec attributes and scopes (#777)
* feat(oidc): support for all in-spec attributes and scopes

* add tests

* assert phone/email verified when either is set

* update tests

* add claims back to userinfo

* remove redundant column drop in migration

* fix duplicate migration id

* fix clobbered imports post-rebase
2026-04-27 19:25:52 +03:00
72 changed files with 2496 additions and 377 deletions
+13 -4
View File
@@ -5,24 +5,33 @@ 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.0.2 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"
- name: Go dependencies - name: Go dependencies
run: go mod download run: go mod download
- name: Check codegen is up to date
run: |
go generate ./internal/repository/...
git diff --exit-code -- internal/repository/
git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
cd frontend cd frontend
@@ -50,6 +59,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 }}
+49 -45
View File
@@ -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.0.2 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.0.2 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.0.2 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"
@@ -85,7 +89,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v7.0.1 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.0.2 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"
@@ -131,7 +135,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v7.0.1 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.0.2 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.0.1 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.0.2 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.0.1 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.0.2 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.0.1 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.0.2 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.0.1 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
+47 -43
View File
@@ -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.0.2 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.0.2 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"
@@ -61,7 +65,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v7.0.1 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.0.2 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"
@@ -104,7 +108,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v7.0.1 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.0.2 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.0.1 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.0.2 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.0.1 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.0.2 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.0.1 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.0.2 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.0.1 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/*
+1 -1
View File
@@ -38,6 +38,6 @@ jobs:
retention-days: 5 retention-days: 5
- name: Upload to code-scanning - name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+7 -3
View File
@@ -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.0.2 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>&nbsp;&nbsp;' template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
- 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: |
+5 -1
View File
@@ -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.
-15
View File
@@ -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"
}
]
}
+27
View File
@@ -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.
+3
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -13,6 +13,7 @@
<a href="https://scorecard.dev/viewer/?uri=github.com/tinyauthapp/tinyauth" target="_blank" title="OpenSSF Scorecard"> <a href="https://scorecard.dev/viewer/?uri=github.com/tinyauthapp/tinyauth" target="_blank" title="OpenSSF Scorecard">
<img src="https://api.scorecard.dev/projects/github.com/tinyauthapp/tinyauth/badge"> <img src="https://api.scorecard.dev/projects/github.com/tinyauthapp/tinyauth/badge">
</a> </a>
<a href="https://www.bestpractices.dev/projects/12681" target="_blank" title="OSSF Best Practices"><img src="https://www.bestpractices.dev/projects/12681/baseline"></a>
</div> </div>
<br /> <br />
+1 -1
View File
@@ -6,4 +6,4 @@ It is recommended to use the [latest](https://github.com/tinyauthapp/tinyauth/re
## 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.
+522
View File
@@ -0,0 +1,522 @@
// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under
// internal/repository/<driver>/. Run via:
//
// go generate ./internal/repository/...
//
// The generator introspects *Queries methods and the model/params types in the
// driver package, then emits a store.go that wraps *Queries so it satisfies
// repository.Store using the canonical shared types in the parent package.
// This generator is specific to sqlc-generated drivers. Non-sqlc drivers should
// implement repository.Store directly by hand.
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"go/types"
"log"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"text/template"
"golang.org/x/tools/go/packages"
)
func main() {
driverPkg := flag.String("pkg", "", "import path of the driver package")
out := flag.String("out", "store.go", "output filename relative to driver package directory")
flag.Parse()
if *driverPkg == "" {
log.Fatal("-pkg is required")
}
// Resolve the driver package directory so we can overlay the output file
// with a valid stub. This prevents a stale store.go from poisoning the
// type-checker and producing cryptic "undefined" errors.
driverDir, err := pkgDir(*driverPkg)
if err != nil {
log.Fatalf("resolve driver dir: %v", err)
}
outPath := filepath.Join(driverDir, *out)
if filepath.IsAbs(*out) {
outPath = *out
}
// Stub replaces the output file during load so stale generated code is ignored.
stub := []byte("package " + filepath.Base(driverDir) + "\n")
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedImports,
Overlay: map[string][]byte{outPath: stub},
}
pkgs, err := packages.Load(cfg, *driverPkg)
if err != nil {
log.Fatalf("load %s: %v", *driverPkg, err)
}
if len(pkgs) != 1 {
log.Fatalf("expected 1 package, got %d", len(pkgs))
}
pkg := pkgs[0]
if len(pkg.Errors) > 0 {
for _, e := range pkg.Errors {
log.Printf("package error: %v", e)
}
log.Fatal("package has errors")
}
repoPkg := parentPkg(*driverPkg)
// Load the parent (repository) package so we can validate struct shapes.
repoPkgs, err := packages.Load(cfg, repoPkg)
if err != nil {
log.Fatalf("load repo pkg %s: %v", repoPkg, err)
}
if len(repoPkgs) != 1 || len(repoPkgs[0].Errors) > 0 {
log.Fatalf("could not load repo package %s cleanly", repoPkg)
}
if err := validateStructShapes(pkg.Types, repoPkgs[0].Types); err != nil {
log.Fatalf("struct shape mismatch: %v", err)
}
// Check *Queries covers every method in repository.Store before generating.
if err := validateStoreCoverage(pkg.Types, repoPkgs[0].Types); err != nil {
log.Fatalf("%v", err)
}
methods, err := collectMethods(pkg.Types)
if err != nil {
log.Fatal(err)
}
models, _ := collectTypes(pkg.Types)
data := tmplData{
PkgName: pkg.Name,
RepoPkg: repoPkg,
ModelTypes: models,
Methods: renderMethods(methods),
}
src, err := render(data)
if err != nil {
log.Fatalf("render: %v", err)
}
if err := os.WriteFile(outPath, src, 0644); err != nil {
log.Fatalf("write %s: %v", outPath, err)
}
fmt.Printf("wrote %s\n", outPath)
}
func parentPkg(imp string) string {
parts := strings.Split(imp, "/")
return strings.Join(parts[:len(parts)-1], "/")
}
// pkgDir returns the on-disk directory for an import path using `go list`.
func pkgDir(importPath string) (string, error) {
out, err := exec.Command("go", "list", "-f", "{{.Dir}}", importPath).Output()
if err != nil {
return "", fmt.Errorf("go list %s: %w", importPath, err)
}
return strings.TrimSpace(string(out)), nil
}
// validateStoreCoverage checks that every method declared in repository.Store
// exists on *Queries in the driver package. Missing methods are reported by
// name so the developer knows exactly which SQL queries need to be added.
func validateStoreCoverage(driverPkg, repoPkg *types.Package) error {
// Collect *Queries method names.
queriesObj := driverPkg.Scope().Lookup("Queries")
if queriesObj == nil {
return fmt.Errorf("Queries type not found in driver package")
}
queriesNamed := queriesObj.Type().(*types.Named)
queriesMS := types.NewMethodSet(types.NewPointer(queriesNamed))
queriesMethods := make(map[string]bool)
for m := range queriesMS.Methods() {
queriesMethods[m.Obj().Name()] = true
}
// Collect repository.Store interface methods.
storeObj := repoPkg.Scope().Lookup("Store")
if storeObj == nil {
return fmt.Errorf("Store type not found in repository package")
}
storeIface, ok := storeObj.Type().Underlying().(*types.Interface)
if !ok {
return fmt.Errorf("repository.Store is not an interface")
}
var missing []string
for i := range storeIface.NumMethods() {
name := storeIface.Method(i).Name()
if !queriesMethods[name] {
missing = append(missing, name)
}
}
if len(missing) > 0 {
sort.Strings(missing)
return fmt.Errorf(
"driver *Queries is missing %d method(s) required by repository.Store:\n - %s\n\nRun sqlc generate to regenerate query methods, or add the missing SQL queries.",
len(missing), strings.Join(missing, "\n - "),
)
}
return nil
}
type methodInfo struct {
Name string
Params []paramInfo
Results []resultInfo
}
type paramInfo struct {
Name string
TypeStr string // local (unqualified) type name
RepoType string // "repository.X" if this is a driver model/params type; else ""
}
type resultInfo struct {
TypeStr string
IsSlice bool
RepoType string // "repository.X" if driver type; else ""
}
func collectMethods(pkg *types.Package) ([]methodInfo, error) {
obj := pkg.Scope().Lookup("Queries")
if obj == nil {
return nil, fmt.Errorf("queries type not found in %s", pkg.Path())
}
named, ok := obj.Type().(*types.Named)
if !ok {
return nil, fmt.Errorf("queries is not a named type")
}
ms := types.NewMethodSet(types.NewPointer(named))
var out []methodInfo
for method := range ms.Methods() {
fn, ok := method.Obj().(*types.Func)
if !ok || fn.Name() == "WithTx" {
continue
}
sig := fn.Type().(*types.Signature)
mi := methodInfo{Name: fn.Name()}
// params: skip receiver + first (context.Context)
for i := 1; i < sig.Params().Len(); i++ {
p := sig.Params().At(i)
mi.Params = append(mi.Params, makeParam(p.Name(), p.Type(), pkg.Path()))
}
// results: skip error
for r := range sig.Results().Variables() {
if r.Type().String() == "error" {
continue
}
mi.Results = append(mi.Results, makeResult(r.Type(), pkg.Path()))
}
out = append(out, mi)
}
return out, nil
}
func makeParam(name string, t types.Type, driverPath string) paramInfo {
pi := paramInfo{Name: name}
pi.TypeStr = localName(t, driverPath)
pi.RepoType = repoName(t, driverPath)
return pi
}
func makeResult(t types.Type, driverPath string) resultInfo {
ri := resultInfo{}
if sl, ok := t.(*types.Slice); ok {
ri.IsSlice = true
t = sl.Elem()
}
ri.TypeStr = localName(t, driverPath)
ri.RepoType = repoName(t, driverPath)
return ri
}
func localName(t types.Type, driverPath string) string {
named, ok := t.(*types.Named)
if !ok {
return types.TypeString(t, nil)
}
if named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == driverPath {
return named.Obj().Name()
}
return types.TypeString(t, func(p *types.Package) string { return p.Name() })
}
func repoName(t types.Type, driverPath string) string {
named, ok := t.(*types.Named)
if !ok {
return ""
}
if named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == driverPath {
return "repository." + named.Obj().Name()
}
return ""
}
func collectTypes(pkg *types.Package) (models []string, params []string) {
for _, name := range pkg.Scope().Names() {
obj := pkg.Scope().Lookup(name)
if obj == nil {
continue
}
tn, ok := obj.(*types.TypeName)
if !ok {
continue
}
named, ok := tn.Type().(*types.Named)
if !ok {
continue
}
if _, ok := named.Underlying().(*types.Struct); !ok {
continue
}
switch name {
case "Queries", "DBTX", "Store":
continue
}
if strings.HasSuffix(name, "Params") {
params = append(params, name)
} else {
models = append(models, name)
}
}
return
}
// validateStructShapes checks that every model/params struct in the driver
// package has fields that exactly match the corresponding type in the repo
// (parent) package. This catches drift between sqlc-generated types and the
// canonical repository types before a broken cast reaches the compiler.
func validateStructShapes(driverPkg, repoPkg *types.Package) error {
var errs []string
for _, name := range driverPkg.Scope().Names() {
obj := driverPkg.Scope().Lookup(name)
if obj == nil {
continue
}
tn, ok := obj.(*types.TypeName)
if !ok {
continue
}
named, ok := tn.Type().(*types.Named)
if !ok {
continue
}
driverStruct, ok := named.Underlying().(*types.Struct)
if !ok {
continue
}
switch name {
case "Queries", "DBTX", "Store":
continue
}
repoObj := repoPkg.Scope().Lookup(name)
if repoObj == nil {
// Driver has a type not in repo — that's fine (e.g. internal helpers).
continue
}
repoNamed, ok := repoObj.Type().(*types.Named)
if !ok {
continue
}
repoStruct, ok := repoNamed.Underlying().(*types.Struct)
if !ok {
errs = append(errs, fmt.Sprintf("%s: repo type is not a struct", name))
continue
}
if err := compareStructs(name, driverStruct, repoStruct); err != nil {
errs = append(errs, err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("%s", strings.Join(errs, "\n "))
}
return nil
}
func compareStructs(name string, driver, repo *types.Struct) error {
if driver.NumFields() != repo.NumFields() {
return fmt.Errorf("%s: field count mismatch (driver=%d, repo=%d)",
name, driver.NumFields(), repo.NumFields())
}
for i := range driver.NumFields() {
df := driver.Field(i)
rf := repo.Field(i)
if df.Name() != rf.Name() {
return fmt.Errorf("%s: field %d name mismatch (driver=%q, repo=%q)",
name, i, df.Name(), rf.Name())
}
if !types.Identical(df.Type(), rf.Type()) {
return fmt.Errorf("%s.%s: type mismatch (driver=%s, repo=%s)",
name, df.Name(), df.Type(), rf.Type())
}
}
return nil
}
// converterFn: "Session" -> "sessionToRepo"
func converterFn(s string) string {
if s == "" {
return ""
}
r := []rune(s)
r[0] = []rune(strings.ToLower(string(r[0])))[0]
return string(r) + "ToRepo"
}
// renderedMethod is the pre-built method body passed to the template.
type renderedMethod struct {
Signature string
Body string
}
// renderMethods converts []methodInfo into fully pre-rendered signature+body strings.
func renderMethods(methods []methodInfo) []renderedMethod {
var out []renderedMethod
for _, m := range methods {
out = append(out, renderedMethod{
Signature: buildSig(m),
Body: buildBody(m),
})
}
return out
}
func buildSig(m methodInfo) string {
var sb strings.Builder
sb.WriteString("func (s *Store) ")
sb.WriteString(m.Name)
sb.WriteString("(ctx context.Context")
for _, p := range m.Params {
sb.WriteString(", ")
sb.WriteString(p.Name)
sb.WriteString(" ")
if p.RepoType != "" {
sb.WriteString(p.RepoType)
} else {
sb.WriteString(p.TypeStr)
}
}
sb.WriteString(") (")
for _, r := range m.Results {
if r.IsSlice {
sb.WriteString("[]")
}
if r.RepoType != "" {
sb.WriteString(r.RepoType)
} else {
sb.WriteString(r.TypeStr)
}
sb.WriteString(", ")
}
sb.WriteString("error)")
return sb.String()
}
func callArgs(m methodInfo) string {
var args []string
for _, p := range m.Params {
if p.RepoType != "" {
// convert repo type → driver type: DriverType(arg)
args = append(args, p.TypeStr+"("+p.Name+")")
} else {
args = append(args, p.Name)
}
}
if len(args) == 0 {
return "ctx"
}
return "ctx, " + strings.Join(args, ", ")
}
func buildBody(m methodInfo) string {
call := "s.q." + m.Name + "(" + callArgs(m) + ")"
// no repo-typed result → direct return
if len(m.Results) == 0 || m.Results[0].RepoType == "" {
return "\treturn " + call + "\n"
}
r := m.Results[0]
if r.IsSlice {
return fmt.Sprintf(
"\trows, err := %s\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tout := make([]%s, len(rows))\n\tfor i, row := range rows {\n\t\tout[i] = %s(row)\n\t}\n\treturn out, nil\n",
call, r.RepoType, converterFn(r.TypeStr),
)
}
return fmt.Sprintf(
"\tr, err := %s\n\tif err != nil {\n\t\treturn %s{}, err\n\t}\n\treturn %s(r), nil\n",
call, r.RepoType, converterFn(r.TypeStr),
)
}
type tmplData struct {
PkgName string
RepoPkg string
ModelTypes []string
Methods []renderedMethod
}
const storeSrc = `// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
package {{.PkgName}}
import (
"context"
"{{.RepoPkg}}"
)
// Store wraps *Queries and implements repository.Store.
type Store struct {
q *Queries
}
// NewStore wraps a *Queries to satisfy repository.Store.
func NewStore(q *Queries) repository.Store {
return &Store{q: q}
}
{{range .ModelTypes -}}
func {{converterFn .}}(v {{.}}) repository.{{.}} {
return repository.{{.}}(v)
}
{{end -}}
{{range .Methods}}{{.Signature}} {
{{.Body}}}
{{end}}`
func render(data tmplData) ([]byte, error) {
t, err := template.New("store").Funcs(template.FuncMap{
"converterFn": converterFn,
}).Parse(storeSrc)
if err != nil {
return nil, fmt.Errorf("parse template: %w", err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("execute template: %w", err)
}
formatted, err := format.Source(buf.Bytes())
if err != nil {
return buf.Bytes(), fmt.Errorf("format source: %w\nraw:\n%s", err, buf.String())
}
return formatted, nil
}
+2 -2
View File
@@ -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"]
+5 -1
View File
@@ -80,5 +80,9 @@
"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" "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."
} }
+13 -1
View File
@@ -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} />,
},
]; ];
}; };
+17 -1
View File
@@ -19,9 +19,11 @@ require (
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
github.com/weppos/publicsuffix-go v0.50.3 github.com/weppos/publicsuffix-go v0.50.3
golang.org/x/crypto v0.50.0 golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.43.0
gotest.tools/v3 v3.5.2 gotest.tools/v3 v3.5.2
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
modernc.org/sqlite v1.49.1 modernc.org/sqlite v1.49.1
) )
@@ -63,6 +65,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 +76,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 +97,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 +112,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 +124,24 @@ 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/mod v0.34.0 // 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
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
modernc.org/libc v1.72.0 // indirect modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
) )
+80
View File
@@ -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=
@@ -242,6 +269,8 @@ 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,11 +382,27 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
@@ -359,3 +433,9 @@ 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=
+1 -1
View File
@@ -11,5 +11,5 @@ var FrontendAssets embed.FS
// Migrations // Migrations
// //
//go:embed migrations/*.sql //go:embed migrations/sqlite/*.sql
var Migrations embed.FS var Migrations embed.FS
@@ -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 "{}";
+5 -8
View File
@@ -63,7 +63,7 @@ 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
@@ -130,17 +130,14 @@ func (app *BootstrapApp) Setup() error {
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name") tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
// Database // Database
db, err := app.SetupDatabase(app.config.Database.Path) store, err := app.SetupStore()
if err != nil { if err != nil {
return fmt.Errorf("failed to setup database: %w", err) return fmt.Errorf("failed to setup database: %w", err)
} }
// Queries
queries := repository.New(db)
// Services // Services
services, err := app.initServices(queries) services, err := app.initServices(store)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize services: %w", err) return fmt.Errorf("failed to initialize services: %w", err)
@@ -196,7 +193,7 @@ func (app *BootstrapApp) Setup() error {
// Start db cleanup routine // Start db cleanup routine
tlog.App.Debug().Msg("Starting database cleanup routine") tlog.App.Debug().Msg("Starting database cleanup routine")
go app.dbCleanupRoutine(queries) go app.dbCleanupRoutine(store)
// If analytics are not disabled, start heartbeat // If analytics are not disabled, start heartbeat
if app.config.Analytics.Enabled { if app.config.Analytics.Enabled {
@@ -286,7 +283,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
} }
} }
func (app *BootstrapApp) dbCleanupRoutine(queries *repository.Queries) { func (app *BootstrapApp) dbCleanupRoutine(queries repository.Store) {
ticker := time.NewTicker(time.Duration(30) * time.Minute) ticker := time.NewTicker(time.Duration(30) * time.Minute)
defer ticker.Stop() defer ticker.Stop()
ctx := context.Background() ctx := context.Background()
+16 -3
View File
@@ -7,6 +7,8 @@ import (
"path/filepath" "path/filepath"
"github.com/tinyauthapp/tinyauth/internal/assets" "github.com/tinyauthapp/tinyauth/internal/assets"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/repository/sqlite"
"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"
@@ -14,7 +16,18 @@ import (
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) { func (app *BootstrapApp) SetupStore() (repository.Store, error) {
return app.setupSQLite(app.config.Database.Path)
}
// NewSQLiteStore opens a SQLite database at the given path, runs migrations, and returns a Store.
// Useful for testing or when constructing a store outside of a BootstrapApp.
func NewSQLiteStore(databasePath string) (repository.Store, error) {
app := &BootstrapApp{}
return app.setupSQLite(databasePath)
}
func (app *BootstrapApp) setupSQLite(databasePath string) (repository.Store, error) {
dir := filepath.Dir(databasePath) dir := filepath.Dir(databasePath)
if err := os.MkdirAll(dir, 0750); err != nil { if err := os.MkdirAll(dir, 0750); err != nil {
@@ -31,7 +44,7 @@ func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) {
// if the sqlite connection starts being a bottleneck // if the sqlite connection starts being a bottleneck
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
migrations, err := iofs.New(assets.Migrations, "migrations") migrations, err := iofs.New(assets.Migrations, "migrations/sqlite")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create migrations: %w", err) return nil, fmt.Errorf("failed to create migrations: %w", err)
@@ -53,5 +66,5 @@ func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) {
return nil, fmt.Errorf("failed to migrate database: %w", err) return nil, fmt.Errorf("failed to migrate database: %w", err)
} }
return db, nil return sqlite.NewStore(sqlite.New(db)), nil
} }
+29 -9
View File
@@ -1,6 +1,8 @@
package bootstrap package bootstrap
import ( import (
"os"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
@@ -10,12 +12,13 @@ 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
} }
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) { func (app *BootstrapApp) initServices(queries repository.Store) (Services, error) {
services := Services{} services := Services{}
ldapService := service.NewLdapService(service.LdapServiceConfig{ ldapService := service.NewLdapService(service.LdapServiceConfig{
@@ -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()
@@ -80,7 +100,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
SessionCookieName: app.context.sessionCookieName, SessionCookieName: app.context.sessionCookieName,
IP: app.config.Auth.IP, IP: app.config.Auth.IP,
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL, LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
}, dockerService, services.ldapService, queries, services.oauthBrokerService) }, services.ldapService, queries, services.oauthBrokerService)
err = authService.Init() err = authService.Init()
+55 -24
View File
@@ -59,6 +59,7 @@ func NewDefaultConfiguration() *Config {
Experimental: ExperimentalConfig{ Experimental: ExperimentalConfig{
ConfigFile: "", ConfigFile: "",
}, },
LabelProvider: "auto",
} }
} }
@@ -76,25 +77,25 @@ var RedirectCookieName = "tinyauth-redirect"
var OAuthSessionCookieName = "tinyauth-oauth" var OAuthSessionCookieName = "tinyauth-oauth"
// Main app config // 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 {
Path string `description:"The path to the database, including file name." yaml:"path"` Path string `description:"The path to the SQLite database, including file name." yaml:"path"`
} }
type AnalyticsConfig struct { type AnalyticsConfig struct {
@@ -113,15 +114,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 {
@@ -228,6 +257,7 @@ type User struct {
Username string Username string
Password string Password string
TotpSecret string TotpSecret string
Attributes UserAttributes
} }
type LdapUser struct { type LdapUser struct {
@@ -254,6 +284,7 @@ type UserContext struct {
OAuthName string OAuthName string
OAuthSub string OAuthSub string
LdapGroups string LdapGroups string
Attributes UserAttributes
} }
// API responses and queries // API responses and queries
+1 -1
View File
@@ -429,7 +429,7 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token)) entry, err := controller.oidc.GetAccessToken(c, controller.oidc.Hash(token))
if err != nil { if err != nil {
if 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",
+2 -11
View File
@@ -15,7 +15,6 @@ import (
"github.com/tinyauthapp/tinyauth/internal/bootstrap" "github.com/tinyauthapp/tinyauth/internal/bootstrap"
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -848,13 +847,10 @@ func TestOIDCController(t *testing.T) {
}, },
} }
app := bootstrap.NewBootstrapApp(config.Config{}) store, err := bootstrap.NewSQLiteStore(path.Join(tempDir, "tinyauth.db"))
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err) require.NoError(t, err)
queries := repository.New(db) oidcService := service.NewOIDCService(oidcServiceCfg, store)
oidcService := service.NewOIDCService(oidcServiceCfg, queries)
err = oidcService.Init() err = oidcService.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -877,9 +873,4 @@ func TestOIDCController(t *testing.T) {
test.run(t, router, recorder) test.run(t, router, recorder)
}) })
} }
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
})
} }
+2 -12
View File
@@ -9,7 +9,6 @@ import (
"github.com/tinyauthapp/tinyauth/internal/bootstrap" "github.com/tinyauthapp/tinyauth/internal/bootstrap"
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -393,13 +392,9 @@ func TestProxyController(t *testing.T) {
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig) oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
app := bootstrap.NewBootstrapApp(config.Config{}) store, err := bootstrap.NewSQLiteStore(path.Join(tempDir, "tinyauth.db"))
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err) require.NoError(t, err)
queries := repository.New(db)
docker := service.NewDockerService() docker := service.NewDockerService()
err = docker.Init() err = docker.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -412,7 +407,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, store, broker)
err = authService.Init() err = authService.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -437,9 +432,4 @@ func TestProxyController(t *testing.T) {
test.run(t, router, recorder) test.run(t, router, recorder)
}) })
} }
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
})
} }
+35 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
@@ -105,16 +106,32 @@ func (controller *UserController) loginHandler(c *gin.Context) {
controller.auth.RecordLoginAttempt(req.Username, true) controller.auth.RecordLoginAttempt(req.Username, true)
var localUser *config.User
if userSearch.Type == "local" { if userSearch.Type == "local" {
user := controller.auth.GetLocalUser(userSearch.Username) user := controller.auth.GetLocalUser(userSearch.Username)
localUser = &user
}
if userSearch.Type == "local" && localUser != nil {
user := *localUser
if user.TotpSecret != "" { if user.TotpSecret != "" {
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification") tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
name := user.Attributes.Name
if name == "" {
name = utils.Capitalize(user.Username)
}
email := user.Attributes.Email
if email == "" {
email = utils.CompileUserEmail(user.Username, controller.config.CookieDomain)
}
err := controller.auth.CreateSessionCookie(c, &repository.Session{ err := controller.auth.CreateSessionCookie(c, &repository.Session{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(user.Username), Name: name,
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain), Email: email,
Provider: "local", Provider: "local",
TotpPending: true, TotpPending: true,
}) })
@@ -144,6 +161,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
Provider: "local", Provider: "local",
} }
if userSearch.Type == "local" && localUser != nil {
if localUser.Attributes.Name != "" {
sessionCookie.Name = localUser.Attributes.Name
}
if localUser.Attributes.Email != "" {
sessionCookie.Email = localUser.Attributes.Email
}
}
if userSearch.Type == "ldap" { if userSearch.Type == "ldap" {
sessionCookie.Provider = "ldap" sessionCookie.Provider = "ldap"
} }
@@ -258,6 +284,13 @@ 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) err = controller.auth.CreateSessionCookie(c, &sessionCookie)
+105 -27
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"net/http/httptest" "net/http/httptest"
"path" "path"
"slices"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -14,7 +13,6 @@ import (
"github.com/tinyauthapp/tinyauth/internal/bootstrap" "github.com/tinyauthapp/tinyauth/internal/bootstrap"
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -36,6 +34,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,17 +288,71 @@ 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)
app := bootstrap.NewBootstrapApp(config.Config{}) store, err := bootstrap.NewSQLiteStore(path.Join(tempDir, "tinyauth.db"))
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err) require.NoError(t, err)
queries := repository.New(db)
docker := service.NewDockerService() docker := service.NewDockerService()
err = docker.Init() err = docker.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -296,7 +365,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, store, broker)
err = authService.Init() err = authService.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -305,9 +374,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 +412,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,
})
}) })
} }
@@ -347,9 +430,4 @@ func TestUserController(t *testing.T) {
test.run(t, router, recorder) test.run(t, router, recorder)
}) })
} }
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
})
} }
+1 -1
View File
@@ -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"},
@@ -11,7 +11,6 @@ import (
"github.com/tinyauthapp/tinyauth/internal/bootstrap" "github.com/tinyauthapp/tinyauth/internal/bootstrap"
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -67,7 +66,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"},
@@ -101,14 +100,10 @@ func TestWellKnownController(t *testing.T) {
}, },
} }
app := bootstrap.NewBootstrapApp(config.Config{}) store, err := bootstrap.NewSQLiteStore(path.Join(tempDir, "tinyauth.db"))
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
require.NoError(t, err) require.NoError(t, err)
queries := repository.New(db) oidcService := service.NewOIDCService(oidcServiceCfg, store)
oidcService := service.NewOIDCService(oidcServiceCfg, queries)
err = oidcService.Init() err = oidcService.Init()
require.NoError(t, err) require.NoError(t, err)
@@ -125,9 +120,4 @@ func TestWellKnownController(t *testing.T) {
test.run(t, router, recorder) test.run(t, router, recorder)
}) })
} }
t.Cleanup(func() {
err = db.Close()
require.NoError(t, err)
})
} }
+19 -2
View File
@@ -99,6 +99,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
} }
var ldapGroups []string var ldapGroups []string
var localAttributes config.UserAttributes
if cookie.Provider == "ldap" { if cookie.Provider == "ldap" {
ldapUser, err := m.auth.GetLdapUser(userSearch.Username) ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
@@ -112,6 +113,11 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
ldapGroups = ldapUser.Groups ldapGroups = ldapUser.Groups
} }
if cookie.Provider == "local" {
localUser := m.auth.GetLocalUser(cookie.Username)
localAttributes = localUser.Attributes
}
m.auth.RefreshSessionCookie(c) m.auth.RefreshSessionCookie(c)
c.Set("context", &config.UserContext{ c.Set("context", &config.UserContext{
Username: cookie.Username, Username: cookie.Username,
@@ -120,6 +126,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
Provider: cookie.Provider, Provider: cookie.Provider,
IsLoggedIn: true, IsLoggedIn: true,
LdapGroups: strings.Join(ldapGroups, ","), LdapGroups: strings.Join(ldapGroups, ","),
Attributes: localAttributes,
}) })
c.Next() c.Next()
return return
@@ -202,13 +209,23 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
return return
} }
name := utils.Capitalize(user.Username)
if user.Attributes.Name != "" {
name = user.Attributes.Name
}
email := utils.CompileUserEmail(user.Username, m.config.CookieDomain)
if user.Attributes.Email != "" {
email = user.Attributes.Email
}
c.Set("context", &config.UserContext{ c.Set("context", &config.UserContext{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(user.Username), Name: name,
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain), Email: email,
Provider: "local", Provider: "local",
IsLoggedIn: true, IsLoggedIn: true,
IsBasicAuth: true, IsBasicAuth: true,
Attributes: user.Attributes,
}) })
c.Next() c.Next()
return return
+102 -5
View File
@@ -1,9 +1,22 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package repository package repository
// Shared model and parameter types for all storage drivers.
// sqlc-generated driver packages use these via the conversion layer in their store.go.
type Session struct {
UUID string
Username string
Email string
Name string
Provider string
TotpPending bool
OAuthGroups string
Expiry int64
CreatedAt int64
OAuthName string
OAuthSub string
}
type OidcCode struct { type OidcCode struct {
Sub string Sub string
CodeHash string CodeHash string
@@ -34,9 +47,22 @@ 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 CreateSessionParams struct {
UUID string UUID string
Username string Username string
Email string Email string
@@ -49,3 +75,74 @@ type Session struct {
OAuthName string OAuthName string
OAuthSub string OAuthSub string
} }
type UpdateSessionParams struct {
Username string
Email string
Name string
Provider string
TotpPending bool
OAuthGroups string
Expiry int64
OAuthName string
OAuthSub string
UUID string
}
type CreateOidcCodeParams struct {
Sub string
CodeHash string
Scope string
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
CodeChallenge string
}
type CreateOidcTokenParams struct {
Sub string
AccessTokenHash string
RefreshTokenHash string
Scope string
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
CodeHash string
Nonce string
}
type UpdateOidcTokenByRefreshTokenParams struct {
AccessTokenHash string
RefreshTokenHash string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
RefreshTokenHash_2 string
}
type DeleteExpiredOidcTokensParams struct {
TokenExpiresAt int64
RefreshTokenExpiresAt int64
}
type CreateOidcUserInfoParams struct {
Sub string
Name string
PreferredUsername string
Email string
Groups string
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
}
@@ -1,8 +1,8 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.0
package repository package sqlite
import ( import (
"context" "context"
+3
View File
@@ -0,0 +1,3 @@
package sqlite
//go:generate go run github.com/tinyauthapp/tinyauth/cmd/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
+64
View File
@@ -0,0 +1,64 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.0
package sqlite
type OidcCode struct {
Sub string
CodeHash string
Scope string
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
CodeChallenge string
}
type OidcToken struct {
Sub string
AccessTokenHash string
RefreshTokenHash string
CodeHash string
Scope string
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
Nonce string
}
type OidcUserinfo struct {
Sub string
Name string
PreferredUsername string
Email string
Groups string
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 {
UUID string
Username string
Email string
Name string
Provider string
TotpPending bool
OAuthGroups string
Expiry int64
CreatedAt int64
OAuthName string
OAuthSub string
}
@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.0
// source: oidc_queries.sql // source: oidc_queries.sql
package repository package sqlite
import ( import (
"context" "context"
@@ -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
} }
@@ -1,9 +1,9 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.0
// source: session_queries.sql // source: session_queries.sql
package repository package sqlite
import ( import (
"context" "context"
+206
View File
@@ -0,0 +1,206 @@
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
package sqlite
import (
"context"
"github.com/tinyauthapp/tinyauth/internal/repository"
)
// Store wraps *Queries and implements repository.Store.
type Store struct {
q *Queries
}
// NewStore wraps a *Queries to satisfy repository.Store.
func NewStore(q *Queries) repository.Store {
return &Store{q: q}
}
func oidcCodeToRepo(v OidcCode) repository.OidcCode {
return repository.OidcCode(v)
}
func oidcTokenToRepo(v OidcToken) repository.OidcToken {
return repository.OidcToken(v)
}
func oidcUserinfoToRepo(v OidcUserinfo) repository.OidcUserinfo {
return repository.OidcUserinfo(v)
}
func sessionToRepo(v Session) repository.Session {
return repository.Session(v)
}
func (s *Store) CreateOidcCode(ctx context.Context, arg repository.CreateOidcCodeParams) (repository.OidcCode, error) {
r, err := s.q.CreateOidcCode(ctx, CreateOidcCodeParams(arg))
if err != nil {
return repository.OidcCode{}, err
}
return oidcCodeToRepo(r), nil
}
func (s *Store) CreateOidcToken(ctx context.Context, arg repository.CreateOidcTokenParams) (repository.OidcToken, error) {
r, err := s.q.CreateOidcToken(ctx, CreateOidcTokenParams(arg))
if err != nil {
return repository.OidcToken{}, err
}
return oidcTokenToRepo(r), nil
}
func (s *Store) CreateOidcUserInfo(ctx context.Context, arg repository.CreateOidcUserInfoParams) (repository.OidcUserinfo, error) {
r, err := s.q.CreateOidcUserInfo(ctx, CreateOidcUserInfoParams(arg))
if err != nil {
return repository.OidcUserinfo{}, err
}
return oidcUserinfoToRepo(r), nil
}
func (s *Store) CreateSession(ctx context.Context, arg repository.CreateSessionParams) (repository.Session, error) {
r, err := s.q.CreateSession(ctx, CreateSessionParams(arg))
if err != nil {
return repository.Session{}, err
}
return sessionToRepo(r), nil
}
func (s *Store) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]repository.OidcCode, error) {
rows, err := s.q.DeleteExpiredOidcCodes(ctx, expiresAt)
if err != nil {
return nil, err
}
out := make([]repository.OidcCode, len(rows))
for i, row := range rows {
out[i] = oidcCodeToRepo(row)
}
return out, nil
}
func (s *Store) DeleteExpiredOidcTokens(ctx context.Context, arg repository.DeleteExpiredOidcTokensParams) ([]repository.OidcToken, error) {
rows, err := s.q.DeleteExpiredOidcTokens(ctx, DeleteExpiredOidcTokensParams(arg))
if err != nil {
return nil, err
}
out := make([]repository.OidcToken, len(rows))
for i, row := range rows {
out[i] = oidcTokenToRepo(row)
}
return out, nil
}
func (s *Store) DeleteExpiredSessions(ctx context.Context, expiry int64) error {
return s.q.DeleteExpiredSessions(ctx, expiry)
}
func (s *Store) DeleteOidcCode(ctx context.Context, codeHash string) error {
return s.q.DeleteOidcCode(ctx, codeHash)
}
func (s *Store) DeleteOidcCodeBySub(ctx context.Context, sub string) error {
return s.q.DeleteOidcCodeBySub(ctx, sub)
}
func (s *Store) DeleteOidcToken(ctx context.Context, accessTokenHash string) error {
return s.q.DeleteOidcToken(ctx, accessTokenHash)
}
func (s *Store) DeleteOidcTokenByCodeHash(ctx context.Context, codeHash string) error {
return s.q.DeleteOidcTokenByCodeHash(ctx, codeHash)
}
func (s *Store) DeleteOidcTokenBySub(ctx context.Context, sub string) error {
return s.q.DeleteOidcTokenBySub(ctx, sub)
}
func (s *Store) DeleteOidcUserInfo(ctx context.Context, sub string) error {
return s.q.DeleteOidcUserInfo(ctx, sub)
}
func (s *Store) DeleteSession(ctx context.Context, uuid string) error {
return s.q.DeleteSession(ctx, uuid)
}
func (s *Store) GetOidcCode(ctx context.Context, codeHash string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCode(ctx, codeHash)
if err != nil {
return repository.OidcCode{}, err
}
return oidcCodeToRepo(r), nil
}
func (s *Store) GetOidcCodeBySub(ctx context.Context, sub string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCodeBySub(ctx, sub)
if err != nil {
return repository.OidcCode{}, err
}
return oidcCodeToRepo(r), nil
}
func (s *Store) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCodeBySubUnsafe(ctx, sub)
if err != nil {
return repository.OidcCode{}, err
}
return oidcCodeToRepo(r), nil
}
func (s *Store) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCodeUnsafe(ctx, codeHash)
if err != nil {
return repository.OidcCode{}, err
}
return oidcCodeToRepo(r), nil
}
func (s *Store) GetOidcToken(ctx context.Context, accessTokenHash string) (repository.OidcToken, error) {
r, err := s.q.GetOidcToken(ctx, accessTokenHash)
if err != nil {
return repository.OidcToken{}, err
}
return oidcTokenToRepo(r), nil
}
func (s *Store) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (repository.OidcToken, error) {
r, err := s.q.GetOidcTokenByRefreshToken(ctx, refreshTokenHash)
if err != nil {
return repository.OidcToken{}, err
}
return oidcTokenToRepo(r), nil
}
func (s *Store) GetOidcTokenBySub(ctx context.Context, sub string) (repository.OidcToken, error) {
r, err := s.q.GetOidcTokenBySub(ctx, sub)
if err != nil {
return repository.OidcToken{}, err
}
return oidcTokenToRepo(r), nil
}
func (s *Store) GetOidcUserInfo(ctx context.Context, sub string) (repository.OidcUserinfo, error) {
r, err := s.q.GetOidcUserInfo(ctx, sub)
if err != nil {
return repository.OidcUserinfo{}, err
}
return oidcUserinfoToRepo(r), nil
}
func (s *Store) GetSession(ctx context.Context, uuid string) (repository.Session, error) {
r, err := s.q.GetSession(ctx, uuid)
if err != nil {
return repository.Session{}, err
}
return sessionToRepo(r), nil
}
func (s *Store) UpdateOidcTokenByRefreshToken(ctx context.Context, arg repository.UpdateOidcTokenByRefreshTokenParams) (repository.OidcToken, error) {
r, err := s.q.UpdateOidcTokenByRefreshToken(ctx, UpdateOidcTokenByRefreshTokenParams(arg))
if err != nil {
return repository.OidcToken{}, err
}
return oidcTokenToRepo(r), nil
}
func (s *Store) UpdateSession(ctx context.Context, arg repository.UpdateSessionParams) (repository.Session, error) {
r, err := s.q.UpdateSession(ctx, UpdateSessionParams(arg))
if err != nil {
return repository.Session{}, err
}
return sessionToRepo(r), nil
}
+41
View File
@@ -0,0 +1,41 @@
package repository
import "context"
// Store is the interface that all storage drivers must implement.
// The sqlc-generated *Queries struct satisfies this interface for SQLite.
// Future drivers (postgres, etc.) must return the shared types defined in this package.
type Store interface {
// Sessions
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
GetSession(ctx context.Context, uuid string) (Session, error)
UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
DeleteSession(ctx context.Context, uuid string) error
DeleteExpiredSessions(ctx context.Context, expiry int64) error
// OIDC codes
CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error)
GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error)
GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error)
GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcCode, error)
GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcCode, error)
DeleteOidcCode(ctx context.Context, codeHash string) error
DeleteOidcCodeBySub(ctx context.Context, sub string) error
DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error)
// OIDC tokens
CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error)
GetOidcToken(ctx context.Context, accessTokenHash string) (OidcToken, error)
GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (OidcToken, error)
GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken, error)
UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateOidcTokenByRefreshTokenParams) (OidcToken, error)
DeleteOidcToken(ctx context.Context, accessTokenHash string) error
DeleteOidcTokenBySub(ctx context.Context, sub string) error
DeleteOidcTokenByCodeHash(ctx context.Context, codeHash string) error
DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpiredOidcTokensParams) ([]OidcToken, error)
// OIDC userinfo
CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error)
GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo, error)
DeleteOidcUserInfo(ctx context.Context, sub string) error
}
+13 -9
View File
@@ -8,15 +8,19 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
) )
type AccessControlsService struct { type LabelProvider interface {
docker *DockerService GetLabels(appDomain string) (config.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]config.App
}
func NewAccessControlsService(labelProvider LabelProvider, static map[string]config.App) *AccessControlsService {
return &AccessControlsService{ return &AccessControlsService{
docker: docker, labelProvider: labelProvider,
static: static, static: static,
} }
} }
@@ -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)
} }
+5 -6
View File
@@ -15,10 +15,11 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"slices"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/exp/slices"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@@ -82,7 +83,6 @@ type AuthServiceConfig struct {
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
@@ -90,24 +90,23 @@ type AuthService struct {
loginMutex sync.RWMutex loginMutex sync.RWMutex
ldapGroupsMutex sync.RWMutex ldapGroupsMutex sync.RWMutex
ldap *LdapService ldap *LdapService
queries *repository.Queries queries repository.Store
oauthBroker *OAuthBrokerService oauthBroker *OAuthBrokerService
lockdown *Lockdown lockdown *Lockdown
lockdownCtx context.Context lockdownCtx context.Context
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.Store, 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),
ldap: ldap, ldap: ldap,
queries: queries, queries: queries,
oauthBroker: oauthBroker, oauthBroker: oauthBroker,
} }
} }
func (auth *AuthService) Init() error { func (auth *AuthService) Init() error {
+303
View File
@@ -0,0 +1,303 @@
package service
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/tinyauthapp/tinyauth/internal/config"
"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 config.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) (config.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 config.App{}, false
}
func (k *KubernetesService) getByAppName(appName string) (config.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 config.App{}, 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[config.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) (config.App, error) {
if !k.started {
tlog.App.Debug().Msg("Kubernetes not connected, returning empty labels")
return config.App{}, 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 config.App{}, nil
}
+186
View File
@@ -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)
})
}
}
+2 -1
View File
@@ -4,7 +4,8 @@ import (
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"golang.org/x/exp/slices" "slices"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
+88 -21
View File
@@ -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/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"golang.org/x/exp/slices"
) )
var ( var (
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 *config.AddressClaim `json:"address,omitempty"`
UpdatedAt int64 `json:"updated_at"`
} }
type TokenResponse struct { type TokenResponse struct {
@@ -95,7 +121,7 @@ type OIDCServiceConfig struct {
type OIDCService struct { type OIDCService struct {
config OIDCServiceConfig config OIDCServiceConfig
queries *repository.Queries queries repository.Store
clients map[string]config.OIDCClientConfig clients map[string]config.OIDCClientConfig
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
publicKey crypto.PublicKey publicKey crypto.PublicKey
@@ -103,7 +129,7 @@ type OIDCService struct {
isConfigured bool isConfigured bool
} }
func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDCService { func NewOIDCService(config OIDCServiceConfig, queries repository.Store) *OIDCService {
return &OIDCService{ return &OIDCService{
config: config, config: config,
queries: queries, queries: queries,
@@ -342,12 +368,30 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
} }
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 config.UserContext, req AuthorizeRequest) error {
addressJSON, err := json.Marshal(userContext.Attributes.Address)
if err != nil {
return err
}
userInfoParams := repository.CreateOidcUserInfoParams{ userInfoParams := repository.CreateOidcUserInfoParams{
Sub: sub, Sub: sub,
Name: userContext.Name, Name: userContext.Name,
Email: userContext.Email, Email: userContext.Email,
PreferredUsername: userContext.Username, PreferredUsername: userContext.Username,
UpdatedAt: time.Now().Unix(), UpdatedAt: time.Now().Unix(),
GivenName: userContext.Attributes.GivenName,
FamilyName: userContext.Attributes.FamilyName,
MiddleName: userContext.Attributes.MiddleName,
Nickname: userContext.Attributes.Nickname,
Profile: userContext.Attributes.Profile,
Picture: userContext.Attributes.Picture,
Website: userContext.Attributes.Website,
Gender: userContext.Attributes.Gender,
Birthdate: userContext.Attributes.Birthdate,
Zoneinfo: userContext.Attributes.Zoneinfo,
Locale: userContext.Attributes.Locale,
PhoneNumber: userContext.Attributes.PhoneNumber,
Address: string(addressJSON),
} }
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server // Tinyauth will pass through the groups it got from an LDAP or an OIDC server
@@ -359,7 +403,7 @@ func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContex
userInfoParams.Groups = userContext.OAuthGroups userInfoParams.Groups = userContext.OAuthGroups
} }
_, err := service.queries.CreateOidcUserInfo(c, userInfoParams) _, err = service.queries.CreateOidcUserInfo(c, userInfoParams)
return err return err
} }
@@ -486,7 +530,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 +548,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 +564,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
@@ -553,7 +597,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 +612,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 +643,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 +681,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 +707,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 config.AddressClaim
if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
userInfo.Address = &addr
}
}
return userInfo return userInfo
} }
@@ -717,7 +784,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")
+198
View File
@@ -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)
})
}
}
+6 -3
View File
@@ -9,7 +9,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
) )
func ParseUsers(usersStr []string) ([]config.User, error) { func ParseUsers(usersStr []string, userAttributes map[string]config.UserAttributes) ([]config.User, error) {
var users []config.User var users []config.User
if len(usersStr) == 0 { if len(usersStr) == 0 {
@@ -24,13 +24,16 @@ func ParseUsers(usersStr []string) ([]config.User, error) {
if err != nil { if err != nil {
return []config.User{}, err return []config.User{}, err
} }
if attrs, ok := userAttributes[parsed.Username]; ok {
parsed.Attributes = attrs
}
users = append(users, parsed) 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]config.UserAttributes) ([]config.User, error) {
var usersStr []string var usersStr []string
if len(usersCfg) == 0 && usersPath == "" { if len(usersCfg) == 0 && usersPath == "" {
@@ -59,7 +62,7 @@ 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) (config.User, error) {
+47 -52
View File
@@ -4,122 +4,117 @@ import (
"os" "os"
"testing" "testing"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/utils" "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
-31
View File
@@ -1,31 +0,0 @@
CREATE TABLE IF NOT EXISTS "oidc_codes" (
"sub" TEXT NOT NULL UNIQUE,
"code_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT "",
"code_challenge" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
"sub" TEXT NOT NULL UNIQUE,
"access_token_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
"refresh_token_hash" TEXT NOT NULL,
"code_hash" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_expires_at" INTEGER NOT NULL,
"refresh_token_expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
"name" TEXT NOT NULL,
"preferred_username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"groups" TEXT NOT NULL,
"updated_at" INTEGER NOT NULL
);
@@ -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 *;
+44
View File
@@ -0,0 +1,44 @@
CREATE TABLE IF NOT EXISTS "oidc_codes" (
"sub" TEXT NOT NULL UNIQUE,
"code_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT "",
"code_challenge" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
"sub" TEXT NOT NULL UNIQUE,
"access_token_hash" TEXT NOT NULL PRIMARY KEY UNIQUE,
"refresh_token_hash" TEXT NOT NULL,
"code_hash" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_expires_at" INTEGER NOT NULL,
"refresh_token_expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
"name" TEXT NOT NULL,
"preferred_username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"groups" TEXT 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
);
+4 -4
View File
@@ -1,12 +1,12 @@
version: "2" version: "2"
sql: sql:
- engine: "sqlite" - engine: "sqlite"
queries: "sql/*_queries.sql" queries: "sql/sqlite/*_queries.sql"
schema: "sql/*_schemas.sql" schema: "sql/sqlite/*_schemas.sql"
gen: gen:
go: go:
package: "repository" package: "sqlite"
out: "internal/repository" out: "internal/repository/sqlite"
rename: rename:
uuid: "UUID" uuid: "UUID"
oauth_groups: "OAuthGroups" oauth_groups: "OAuthGroups"