mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-30 09:28:11 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c299c3ef95 | |||
| 956d2f55c3 | |||
| 5e822d99e1 | |||
| 373ee8806e | |||
| a14d64c8ba | |||
| d51e3efe32 | |||
| d73cc628fb | |||
| a8737ab0bd | |||
| 11793c9869 | |||
| c68a022ed0 | |||
| 5d95123dcb |
@@ -5,18 +5,21 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6.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"
|
||||||
|
|
||||||
@@ -50,6 +53,6 @@ jobs:
|
|||||||
run: go test -coverprofile=coverage.txt -v ./...
|
run: go test -coverprofile=coverage.txt -v ./...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v6
|
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.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
|
||||||
|
|||||||
@@ -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/*
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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> '
|
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a> '
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: |
|
commit-message: |
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: 0 10 * * *
|
- cron: 0 10 * * *
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||||
with:
|
with:
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
|
stale-pr-message: This PR has been inactive for 30 days and will be marked as stale.
|
||||||
|
|||||||
Vendored
-15
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Connect to server",
|
|
||||||
"type": "go",
|
|
||||||
"request": "attach",
|
|
||||||
"mode": "remote",
|
|
||||||
"remotePath": "/tinyauth",
|
|
||||||
"port": 4000,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"debugAdapter": "legacy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +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
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
modernc.org/sqlite v1.49.1
|
k8s.io/apimachinery v0.36.0
|
||||||
|
k8s.io/client-go v0.36.0
|
||||||
|
modernc.org/sqlite v1.50.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -63,6 +64,7 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.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
|
||||||
@@ -90,8 +92,9 @@ require (
|
|||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
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 +109,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
|
||||||
@@ -116,16 +120,27 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/term v0.42.0 // indirect
|
golang.org/x/term v0.42.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // 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.140.0 // indirect
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // 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-20250730193827-2d320260d730 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/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=
|
||||||
@@ -132,6 +142,8 @@ 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/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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
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=
|
||||||
@@ -162,6 +174,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
|||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
@@ -176,6 +190,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=
|
||||||
@@ -203,12 +219,15 @@ github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFL
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
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,9 +261,12 @@ 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.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/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=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -261,6 +283,8 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
|||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
@@ -287,6 +311,10 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu
|
|||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
@@ -308,8 +336,8 @@ 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.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.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
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=
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
@@ -319,16 +347,32 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.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.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
||||||
|
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
||||||
|
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||||
|
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
||||||
|
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
|
||||||
|
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
|
||||||
|
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||||
|
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
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=
|
||||||
@@ -351,11 +395,19 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||||
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
|
|||||||
@@ -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 "{}";
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,6 +12,7 @@ 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
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
+54
-23
@@ -59,6 +59,7 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Experimental: ExperimentalConfig{
|
Experimental: ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
|
LabelProvider: "auto",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,21 +77,21 @@ 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 {
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
err = broker.Init()
|
err = broker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -36,6 +35,23 @@ func TestUserController(t *testing.T) {
|
|||||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Username: "attruser",
|
||||||
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
|
Attributes: config.UserAttributes{
|
||||||
|
Name: "Alice Smith",
|
||||||
|
Email: "alice@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Username: "attrtotpuser",
|
||||||
|
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||||
|
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||||
|
Attributes: config.UserAttributes{
|
||||||
|
Name: "Bob Jones",
|
||||||
|
Email: "bob@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SessionExpiry: 10, // 10 seconds, useful for testing
|
SessionExpiry: 10, // 10 seconds, useful for testing
|
||||||
CookieDomain: "example.com",
|
CookieDomain: "example.com",
|
||||||
@@ -273,6 +289,64 @@ func TestUserController(t *testing.T) {
|
|||||||
assert.Contains(t, recorder.Body.String(), "Too many failed TOTP attempts.")
|
assert.Contains(t, recorder.Body.String(), "Too many failed TOTP attempts.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Login uses name and email from user attributes",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
loginReq := controller.LoginRequest{Username: "attruser", Password: "password"}
|
||||||
|
body, err := json.Marshal(loginReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
require.Equal(t, 200, recorder.Code)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
assert.Equal(t, "tinyauth-session", cookies[0].Name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Login with TOTP uses name and email from user attributes in pending session",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
loginReq := controller.LoginRequest{Username: "attrtotpuser", Password: "password"}
|
||||||
|
body, err := json.Marshal(loginReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
require.Equal(t, 200, recorder.Code)
|
||||||
|
var res map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
|
||||||
|
assert.Equal(t, true, res["totpPending"])
|
||||||
|
require.Len(t, recorder.Result().Cookies(), 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "TOTP completion uses name and email from user attributes",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
totpReq := controller.TotpRequest{Code: code}
|
||||||
|
body, err := json.Marshal(totpReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(body)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
require.Equal(t, 200, recorder.Code)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
assert.Equal(t, "tinyauth-session", cookies[0].Name)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||||
@@ -296,7 +370,7 @@ func TestUserController(t *testing.T) {
|
|||||||
err = broker.Init()
|
err = broker.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
authService := service.NewAuthService(authServiceCfg, ldap, queries, broker)
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -305,9 +379,31 @@ func TestUserController(t *testing.T) {
|
|||||||
authService.ClearRateLimitsTestingOnly()
|
authService.ClearRateLimitsTestingOnly()
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotpMiddlewareOverrides := []string{
|
setTotpMiddlewareOverrides := map[string]config.UserContext{
|
||||||
"Should be able to login with totp",
|
"Should be able to login with totp": {
|
||||||
"Totp should rate limit on multiple invalid attempts",
|
Username: "totpuser",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
"Totp should rate limit on multiple invalid attempts": {
|
||||||
|
Username: "totpuser",
|
||||||
|
Name: "Totpuser",
|
||||||
|
Email: "totpuser@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
|
"TOTP completion uses name and email from user attributes": {
|
||||||
|
Username: "attrtotpuser",
|
||||||
|
Name: "Bob Jones",
|
||||||
|
Email: "bob@example.com",
|
||||||
|
Provider: "local",
|
||||||
|
TotpPending: true,
|
||||||
|
TotpEnabled: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -321,18 +417,10 @@ func TestUserController(t *testing.T) {
|
|||||||
|
|
||||||
// Gin is stupid and doesn't allow setting a middleware after the groups
|
// Gin is stupid and doesn't allow setting a middleware after the groups
|
||||||
// so we need to do some stupid overrides here
|
// so we need to do some stupid overrides here
|
||||||
if slices.Contains(setTotpMiddlewareOverrides, test.description) {
|
if ctx, ok := setTotpMiddlewareOverrides[test.description]; ok {
|
||||||
// Assuming the cookie is set, it should be picked up by the
|
ctx := ctx
|
||||||
// context middleware
|
|
||||||
router.Use(func(c *gin.Context) {
|
router.Use(func(c *gin.Context) {
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &ctx)
|
||||||
Username: "totpuser",
|
|
||||||
Name: "Totpuser",
|
|
||||||
Email: "totpuser@example.com",
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: true,
|
|
||||||
TotpEnabled: true,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func TestWellKnownController(t *testing.T) {
|
|||||||
SubjectTypesSupported: []string{"pairwise"},
|
SubjectTypesSupported: []string{"pairwise"},
|
||||||
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
||||||
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"},
|
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups", "phone_number", "phone_number_verified", "address", "given_name", "family_name", "middle_name", "nickname", "profile", "picture", "website", "gender", "birthdate", "zoneinfo", "locale"},
|
||||||
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
|
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
|
||||||
RequestParameterSupported: true,
|
RequestParameterSupported: true,
|
||||||
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
RequestObjectSigningAlgValuesSupported: []string{"none"},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ type OidcUserinfo struct {
|
|||||||
Email string
|
Email string
|
||||||
Groups string
|
Groups string
|
||||||
UpdatedAt int64
|
UpdatedAt int64
|
||||||
|
GivenName string
|
||||||
|
FamilyName string
|
||||||
|
MiddleName string
|
||||||
|
Nickname string
|
||||||
|
Profile string
|
||||||
|
Picture string
|
||||||
|
Website string
|
||||||
|
Gender string
|
||||||
|
Birthdate string
|
||||||
|
Zoneinfo string
|
||||||
|
Locale string
|
||||||
|
PhoneNumber string
|
||||||
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
|||||||
@@ -124,11 +124,24 @@ INSERT INTO "oidc_userinfo" (
|
|||||||
"preferred_username",
|
"preferred_username",
|
||||||
"email",
|
"email",
|
||||||
"groups",
|
"groups",
|
||||||
"updated_at"
|
"updated_at",
|
||||||
|
"given_name",
|
||||||
|
"family_name",
|
||||||
|
"middle_name",
|
||||||
|
"nickname",
|
||||||
|
"profile",
|
||||||
|
"picture",
|
||||||
|
"website",
|
||||||
|
"gender",
|
||||||
|
"birthdate",
|
||||||
|
"zoneinfo",
|
||||||
|
"locale",
|
||||||
|
"phone_number",
|
||||||
|
"address"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING sub, name, preferred_username, email, "groups", updated_at
|
RETURNING sub, name, preferred_username, email, "groups", updated_at, given_name, family_name, middle_name, nickname, profile, picture, website, gender, birthdate, zoneinfo, locale, phone_number, address
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateOidcUserInfoParams struct {
|
type CreateOidcUserInfoParams struct {
|
||||||
@@ -138,6 +151,19 @@ type CreateOidcUserInfoParams struct {
|
|||||||
Email string
|
Email string
|
||||||
Groups string
|
Groups string
|
||||||
UpdatedAt int64
|
UpdatedAt int64
|
||||||
|
GivenName string
|
||||||
|
FamilyName string
|
||||||
|
MiddleName string
|
||||||
|
Nickname string
|
||||||
|
Profile string
|
||||||
|
Picture string
|
||||||
|
Website string
|
||||||
|
Gender string
|
||||||
|
Birthdate string
|
||||||
|
Zoneinfo string
|
||||||
|
Locale string
|
||||||
|
PhoneNumber string
|
||||||
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
|
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
|
||||||
@@ -148,6 +174,19 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
|
|||||||
arg.Email,
|
arg.Email,
|
||||||
arg.Groups,
|
arg.Groups,
|
||||||
arg.UpdatedAt,
|
arg.UpdatedAt,
|
||||||
|
arg.GivenName,
|
||||||
|
arg.FamilyName,
|
||||||
|
arg.MiddleName,
|
||||||
|
arg.Nickname,
|
||||||
|
arg.Profile,
|
||||||
|
arg.Picture,
|
||||||
|
arg.Website,
|
||||||
|
arg.Gender,
|
||||||
|
arg.Birthdate,
|
||||||
|
arg.Zoneinfo,
|
||||||
|
arg.Locale,
|
||||||
|
arg.PhoneNumber,
|
||||||
|
arg.Address,
|
||||||
)
|
)
|
||||||
var i OidcUserinfo
|
var i OidcUserinfo
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -157,6 +196,19 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
|
|||||||
&i.Email,
|
&i.Email,
|
||||||
&i.Groups,
|
&i.Groups,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.GivenName,
|
||||||
|
&i.FamilyName,
|
||||||
|
&i.MiddleName,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.Profile,
|
||||||
|
&i.Picture,
|
||||||
|
&i.Website,
|
||||||
|
&i.Gender,
|
||||||
|
&i.Birthdate,
|
||||||
|
&i.Zoneinfo,
|
||||||
|
&i.Locale,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Address,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -456,7 +508,7 @@ func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
|
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
|
||||||
SELECT sub, name, preferred_username, email, "groups", updated_at FROM "oidc_userinfo"
|
SELECT sub, name, preferred_username, email, "groups", updated_at, given_name, family_name, middle_name, nickname, profile, picture, website, gender, birthdate, zoneinfo, locale, phone_number, address FROM "oidc_userinfo"
|
||||||
WHERE "sub" = ?
|
WHERE "sub" = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -470,6 +522,19 @@ func (q *Queries) GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo
|
|||||||
&i.Email,
|
&i.Email,
|
||||||
&i.Groups,
|
&i.Groups,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.GivenName,
|
||||||
|
&i.FamilyName,
|
||||||
|
&i.MiddleName,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.Profile,
|
||||||
|
&i.Picture,
|
||||||
|
&i.Website,
|
||||||
|
&i.Gender,
|
||||||
|
&i.Birthdate,
|
||||||
|
&i.Zoneinfo,
|
||||||
|
&i.Locale,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Address,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -97,17 +97,16 @@ type AuthService struct {
|
|||||||
lockdownCancelFunc context.CancelFunc
|
lockdownCancelFunc context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
func NewAuthService(config AuthServiceConfig, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
config: config,
|
config: config,
|
||||||
docker: docker,
|
|
||||||
loginAttempts: make(map[string]*LoginAttempt),
|
loginAttempts: make(map[string]*LoginAttempt),
|
||||||
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||||
oauthPendingSessions: make(map[string]*OAuthPendingSession),
|
oauthPendingSessions: make(map[string]*OAuthPendingSession),
|
||||||
ldap: ldap,
|
ldap: ldap,
|
||||||
queries: queries,
|
queries: queries,
|
||||||
oauthBroker: oauthBroker,
|
oauthBroker: oauthBroker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) Init() error {
|
func (auth *AuthService) Init() error {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+15
-2
@@ -95,9 +95,22 @@ INSERT INTO "oidc_userinfo" (
|
|||||||
"preferred_username",
|
"preferred_username",
|
||||||
"email",
|
"email",
|
||||||
"groups",
|
"groups",
|
||||||
"updated_at"
|
"updated_at",
|
||||||
|
"given_name",
|
||||||
|
"family_name",
|
||||||
|
"middle_name",
|
||||||
|
"nickname",
|
||||||
|
"profile",
|
||||||
|
"picture",
|
||||||
|
"website",
|
||||||
|
"gender",
|
||||||
|
"birthdate",
|
||||||
|
"zoneinfo",
|
||||||
|
"locale",
|
||||||
|
"phone_number",
|
||||||
|
"address"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|||||||
+19
-6
@@ -22,10 +22,23 @@ CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
|
||||||
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"preferred_username" TEXT NOT NULL,
|
"preferred_username" TEXT NOT NULL,
|
||||||
"email" TEXT NOT NULL,
|
"email" TEXT NOT NULL,
|
||||||
"groups" TEXT NOT NULL,
|
"groups" TEXT NOT NULL,
|
||||||
"updated_at" INTEGER NOT NULL
|
"updated_at" INTEGER NOT NULL,
|
||||||
|
"given_name" TEXT NOT NULL,
|
||||||
|
"family_name" TEXT NOT NULL,
|
||||||
|
"middle_name" TEXT NOT NULL,
|
||||||
|
"nickname" TEXT NOT NULL,
|
||||||
|
"profile" TEXT NOT NULL,
|
||||||
|
"picture" TEXT NOT NULL,
|
||||||
|
"website" TEXT NOT NULL,
|
||||||
|
"gender" TEXT NOT NULL,
|
||||||
|
"birthdate" TEXT NOT NULL,
|
||||||
|
"zoneinfo" TEXT NOT NULL,
|
||||||
|
"locale" TEXT NOT NULL,
|
||||||
|
"phone_number" TEXT NOT NULL,
|
||||||
|
"address" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user