Compare commits

...

7 Commits

Author SHA1 Message Date
Stavros faee58ca8e feat: use ding for ordered go routine shutdown order (#896) 2026-05-27 12:46:28 +03:00
Stavros e9b8ca3cf8 fix: cleanup acl logic to match stable one 2026-05-27 12:11:17 +03:00
Stavros f2c4e7932d chore: include debug symbols in nightly images (#908) 2026-05-27 11:43:43 +03:00
Stavros 4538922caf refactor: simplify error handling in oidc authorize handler (#907) 2026-05-27 11:27:10 +03:00
Stavros 672db84200 feat: make config file a stable feature (#897) 2026-05-27 11:26:09 +03:00
Scott McKendry 359000f731 feat(db): add postgresql support (#892) 2026-05-26 00:08:59 +03:00
Stavros 0a3e7bf265 fix: use policy engine in oauth whitelist check (#904) 2026-05-26 00:07:46 +03:00
45 changed files with 1935 additions and 239 deletions
+66 -1
View File
@@ -7,7 +7,9 @@ TINYAUTH_APPURL=
# database config # database config
# The path to the database, including file name. # The database driver to use. Valid values: sqlite, memory.
TINYAUTH_DATABASE_DRIVER="sqlite"
# The path to the SQLite database, including file name. Only used when driver is sqlite.
TINYAUTH_DATABASE_PATH="./tinyauth.db" TINYAUTH_DATABASE_PATH="./tinyauth.db"
# analytics config # analytics config
@@ -30,6 +32,8 @@ TINYAUTH_SERVER_PORT=3000
TINYAUTH_SERVER_ADDRESS="0.0.0.0" TINYAUTH_SERVER_ADDRESS="0.0.0.0"
# The path to the Unix socket. # The path to the Unix socket.
TINYAUTH_SERVER_SOCKETPATH= TINYAUTH_SERVER_SOCKETPATH=
# Enable listening on both TCP and Unix socket at the same time.
TINYAUTH_SERVER_CONCURRENTLISTENERSENABLED=false
# auth config # auth config
@@ -37,8 +41,52 @@ TINYAUTH_SERVER_SOCKETPATH=
TINYAUTH_AUTH_IP_ALLOW= TINYAUTH_AUTH_IP_ALLOW=
# List of blocked IPs or CIDR ranges. # List of blocked IPs or CIDR ranges.
TINYAUTH_AUTH_IP_BLOCK= TINYAUTH_AUTH_IP_BLOCK=
# List of IPs or CIDR ranges that bypass authentication entirely.
TINYAUTH_AUTH_IP_BYPASS=
# Comma-separated list of users (username:hashed_password). # Comma-separated list of users (username:hashed_password).
TINYAUTH_AUTH_USERS= TINYAUTH_AUTH_USERS=
# Enable subdomains support.
TINYAUTH_AUTH_SUBDOMAINSENABLED=true
# Full name of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_NAME=
# Given (first) name of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_GIVENNAME=
# Family (last) name of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_FAMILYNAME=
# Middle name of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_MIDDLENAME=
# Nickname of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_NICKNAME=
# URL of the user's profile page.
TINYAUTH_AUTH_USERATTRIBUTES_name_PROFILE=
# URL of the user's profile picture.
TINYAUTH_AUTH_USERATTRIBUTES_name_PICTURE=
# URL of the user's website.
TINYAUTH_AUTH_USERATTRIBUTES_name_WEBSITE=
# Email address of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_EMAIL=
# Gender of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_GENDER=
# Birthdate of the user (YYYY-MM-DD).
TINYAUTH_AUTH_USERATTRIBUTES_name_BIRTHDATE=
# Time zone of the user (e.g. Europe/Athens).
TINYAUTH_AUTH_USERATTRIBUTES_name_ZONEINFO=
# Locale of the user (e.g. en-US).
TINYAUTH_AUTH_USERATTRIBUTES_name_LOCALE=
# Phone number of the user.
TINYAUTH_AUTH_USERATTRIBUTES_name_PHONENUMBER=
# Full mailing address, formatted for display.
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_FORMATTED=
# Street address.
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_STREETADDRESS=
# City or locality.
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_LOCALITY=
# State, province, or region.
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_REGION=
# Zip or postal code.
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_POSTALCODE=
# Country.
TINYAUTH_AUTH_USERATTRIBUTES_name_ADDRESS_COUNTRY=
# Path to the users file. # Path to the users file.
TINYAUTH_AUTH_USERSFILE= TINYAUTH_AUTH_USERSFILE=
# Enable secure cookies. # Enable secure cookies.
@@ -53,6 +101,8 @@ TINYAUTH_AUTH_LOGINTIMEOUT=300
TINYAUTH_AUTH_LOGINMAXRETRIES=3 TINYAUTH_AUTH_LOGINMAXRETRIES=3
# Comma-separated list of trusted proxy addresses. # Comma-separated list of trusted proxy addresses.
TINYAUTH_AUTH_TRUSTEDPROXIES= TINYAUTH_AUTH_TRUSTEDPROXIES=
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
TINYAUTH_AUTH_ACLS_POLICY="allow"
# apps config # apps config
@@ -168,6 +218,8 @@ TINYAUTH_LDAP_AUTHCERT=
TINYAUTH_LDAP_AUTHKEY= TINYAUTH_LDAP_AUTHKEY=
# Cache duration for LDAP group membership in seconds. # Cache duration for LDAP group membership in seconds.
TINYAUTH_LDAP_GROUPCACHETTL=900 TINYAUTH_LDAP_GROUPCACHETTL=900
# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment.
TINYAUTH_LABELPROVIDER="auto"
# log config # log config
@@ -187,3 +239,16 @@ TINYAUTH_LOG_STREAMS_APP_LEVEL=
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false
# Log level for this stream. Use global if empty. # Log level for this stream. Use global if empty.
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL= TINYAUTH_LOG_STREAMS_AUDIT_LEVEL=
# tailscale config
# Enable Tailscale integration.
TINYAUTH_TAILSCALE_ENABLED=false
# Tailscale state directory.
TINYAUTH_TAILSCALE_DIR="./tailscale_state"
# Tailscale hostname.
TINYAUTH_TAILSCALE_HOSTNAME=
# Tailscale auth key.
TINYAUTH_TAILSCALE_AUTHKEY=
# Use ephemeral Tailscale node.
TINYAUTH_TAILSCALE_EPHEMERAL=false
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
- name: Build - name: Build
run: | run: |
cp -r frontend/dist internal/assets/dist cp -r frontend/dist internal/assets/dist
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth go build -ldflags "-X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 ./cmd/tinyauth
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
@@ -128,7 +128,7 @@ jobs:
- name: Build - name: Build
run: | run: |
cp -r frontend/dist internal/assets/dist cp -r frontend/dist internal/assets/dist
go build -ldflags "-s -w -X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth go build -ldflags "-X github.com/tinyauthapp/tinyauth/internal/model.Version=${{ needs.generate-metadata.outputs.VERSION }} -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 ./cmd/tinyauth
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
+4
View File
@@ -150,6 +150,7 @@ jobs:
VERSION=${{ needs.generate-metadata.outputs.VERSION }} VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
LDFLAGS="-s -w"
- name: Export digest - name: Export digest
run: | run: |
@@ -206,6 +207,7 @@ jobs:
VERSION=${{ needs.generate-metadata.outputs.VERSION }} VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
LDFLAGS="-s -w"
- name: Export digest - name: Export digest
run: | run: |
@@ -260,6 +262,7 @@ jobs:
VERSION=${{ needs.generate-metadata.outputs.VERSION }} VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
LDFLAGS="-s -w"
- name: Export digest - name: Export digest
run: | run: |
@@ -316,6 +319,7 @@ jobs:
VERSION=${{ needs.generate-metadata.outputs.VERSION }} VERSION=${{ needs.generate-metadata.outputs.VERSION }}
COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }}
BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}
LDFLAGS="-s -w"
- name: Export digest - name: Export digest
run: | run: |
+2 -1
View File
@@ -27,6 +27,7 @@ FROM golang:1.26-alpine3.23 AS builder
ARG VERSION ARG VERSION
ARG COMMIT_HASH ARG COMMIT_HASH
ARG BUILD_TIMESTAMP ARG BUILD_TIMESTAMP
ARG LDFLAGS
WORKDIR /tinyauth WORKDIR /tinyauth
@@ -39,7 +40,7 @@ COPY ./cmd ./cmd
COPY ./internal ./internal COPY ./internal ./internal
COPY --from=frontend-builder /frontend/dist ./internal/assets/dist COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN CGO_ENABLED=0 go build -ldflags "-s -w \ RUN CGO_ENABLED=0 go build -ldflags "${LDFLAGS} \
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \ -X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \ -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
+2 -1
View File
@@ -27,6 +27,7 @@ FROM golang:1.26-alpine3.23 AS builder
ARG VERSION ARG VERSION
ARG COMMIT_HASH ARG COMMIT_HASH
ARG BUILD_TIMESTAMP ARG BUILD_TIMESTAMP
ARG LDFLAGS
WORKDIR /tinyauth WORKDIR /tinyauth
@@ -41,7 +42,7 @@ COPY --from=frontend-builder /frontend/dist ./internal/assets/dist
RUN mkdir -p data RUN mkdir -p data
RUN CGO_ENABLED=0 go build -ldflags "-s -w \ RUN CGO_ENABLED=0 go build -ldflags "${LDFLAGS} \
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \ -X github.com/tinyauthapp/tinyauth/internal/model.Version=${VERSION} \
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \ -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" ./cmd/tinyauth
+2 -1
View File
@@ -8,6 +8,7 @@ TAG_NAME := $(shell git describe --abbrev=0 --exact-match 2> /dev/null || echo "
COMMIT_HASH := $(shell git rev-parse HEAD) COMMIT_HASH := $(shell git rev-parse HEAD)
BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S') BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S')
BIN_NAME := tinyauth-$(GOARCH) BIN_NAME := tinyauth-$(GOARCH)
LDFLAGS := -s -w
# Development vars # Development vars
DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.dev.yml" ) DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.dev.yml" )
@@ -36,7 +37,7 @@ webui: clean-webui
# Build the binary # Build the binary
binary: webui binary: webui
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \ CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "${LDFLAGS} \
-X github.com/tinyauthapp/tinyauth/internal/model.Version=${TAG_NAME} \ -X github.com/tinyauthapp/tinyauth/internal/model.Version=${TAG_NAME} \
-X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \ -X github.com/tinyauthapp/tinyauth/internal/model.CommitHash=${COMMIT_HASH} \
-X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \ -X github.com/tinyauthapp/tinyauth/internal/model.BuildTimestamp=${BUILD_TIMESTAMP}" \
+6
View File
@@ -12,9 +12,11 @@ require (
github.com/golang-migrate/migrate/v4 v4.19.1 github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/go-querystring v1.2.0 github.com/google/go-querystring v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/mdp/qrterminal/v3 v3.2.1 github.com/mdp/qrterminal/v3 v3.2.1
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/rs/zerolog v1.35.1 github.com/rs/zerolog v1.35.1
github.com/steveiliop56/ding v0.2.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
github.com/weppos/publicsuffix-go v0.50.3 github.com/weppos/publicsuffix-go v0.50.3
@@ -90,6 +92,10 @@ require (
github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect github.com/huin/goupnp v1.3.0 // indirect
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/compress v1.18.5 // indirect
+15
View File
@@ -143,6 +143,8 @@ github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbww
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -251,6 +253,16 @@ github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeV
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U= github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -390,12 +402,15 @@ 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.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/steveiliop56/ding v0.2.0 h1:m/Fj99wBpVVLHlpqb2RDJkWubOc5cWJ11ZYCHya3Sk0=
github.com/steveiliop56/ding v0.2.0/go.mod h1:bE2u2XH7CjhPzbb/0Ems+D8YZlf2Ae+eKhj00UR1iAY=
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 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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+1 -1
View File
@@ -11,5 +11,5 @@ var FrontendAssets embed.FS
// Migrations // Migrations
// //
//go:embed migrations/sqlite/*.sql //go:embed migrations/sqlite/*.sql migrations/postgres/*.sql
var Migrations embed.FS var Migrations embed.FS
@@ -0,0 +1,4 @@
DROP TABLE IF EXISTS "oidc_tokens";
DROP TABLE IF EXISTS "oidc_userinfo";
DROP TABLE IF EXISTS "oidc_codes";
DROP TABLE IF EXISTS "sessions";
@@ -0,0 +1,60 @@
CREATE TABLE "sessions" (
"uuid" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"totp_pending" BOOLEAN NOT NULL,
"oauth_groups" TEXT NOT NULL DEFAULT '',
"expiry" BIGINT NOT NULL,
"created_at" BIGINT NOT NULL,
"oauth_name" TEXT NOT NULL DEFAULT '',
"oauth_sub" TEXT NOT NULL DEFAULT ''
);
CREATE TABLE "oidc_codes" (
"sub" TEXT NOT NULL UNIQUE,
"code_hash" TEXT NOT NULL PRIMARY KEY,
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" BIGINT NOT NULL,
"nonce" TEXT NOT NULL DEFAULT '',
"code_challenge" TEXT NOT NULL DEFAULT ''
);
CREATE TABLE "oidc_tokens" (
"sub" TEXT NOT NULL UNIQUE,
"access_token_hash" TEXT NOT NULL PRIMARY KEY,
"refresh_token_hash" TEXT NOT NULL,
"code_hash" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_expires_at" BIGINT NOT NULL,
"refresh_token_expires_at" BIGINT NOT NULL,
"nonce" TEXT NOT NULL DEFAULT ''
);
CREATE TABLE "oidc_userinfo" (
"sub" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"preferred_username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"groups" TEXT NOT NULL,
"updated_at" BIGINT 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
);
CREATE INDEX idx_sessions_expiry ON "sessions" ("expiry");
+30 -17
View File
@@ -13,11 +13,11 @@ import (
"os/signal" "os/signal"
"sort" "sort"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
@@ -26,6 +26,12 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
// Shutdown order for go routines
// 1. Janitor routines (e.g. database cleanup, heartbeat) - ding.RingMinor
// 2. HTTP server listeners - ding.RingNormal
// 3. Networking layers, user and label providers (e.g. ailscale service, kubernetes service) - ding.RingMajor
// 4. Database connection - ding.RingCritical
type Services struct { type Services struct {
accessControlService *service.AccessControlsService accessControlService *service.AccessControlsService
authService *service.AuthService authService *service.AuthService
@@ -48,7 +54,7 @@ type BootstrapApp struct {
queries repository.Store queries repository.Store
router *gin.Engine router *gin.Engine
db *sql.DB db *sql.DB
wg sync.WaitGroup ding *ding.Ding
listeners []Listener listeners []Listener
} }
@@ -64,6 +70,10 @@ func (app *BootstrapApp) Setup() error {
app.ctx = ctx app.ctx = ctx
app.cancel = cancel app.cancel = cancel
// Create a ding instance
dg := ding.New(ctx)
app.ding = dg
// setup logger // setup logger
log := logger.NewLogger().WithConfig(app.config.Log) log := logger.NewLogger().WithConfig(app.config.Log)
log.Init() log.Init()
@@ -186,15 +196,17 @@ func (app *BootstrapApp) Setup() error {
return fmt.Errorf("failed to setup database: %w", err) return fmt.Errorf("failed to setup database: %w", err)
} }
// after this point, we start initializing dependencies so it's a good time to setup a defer app.ding.Go(func(ctx context.Context) {
// to ensure that resources are cleaned up properly in case of an error during initialization <-ctx.Done()
defer func() { app.log.App.Debug().Msg("Shutting down database connection")
app.cancel() if app.db == nil {
app.wg.Wait() // using memory store, no db instance
if app.db != nil { return
app.db.Close()
} }
}() if err := app.db.Close(); err != nil {
app.log.App.Error().Err(err).Msg("Failed to close database connection")
}
}, ding.RingCritical)
// store // store
app.queries = store app.queries = store
@@ -261,12 +273,12 @@ func (app *BootstrapApp) Setup() error {
// start db cleanup routine // start db cleanup routine
app.log.App.Debug().Msg("Starting database cleanup routine") app.log.App.Debug().Msg("Starting database cleanup routine")
app.wg.Go(app.dbCleanupRoutine) app.ding.Go(app.dbCleanupRoutine, ding.RingMinor)
// if analytics are not disabled, start heartbeat // if analytics are not disabled, start heartbeat
if app.config.Analytics.Enabled { if app.config.Analytics.Enabled {
app.log.App.Debug().Msg("Starting heartbeat routine") app.log.App.Debug().Msg("Starting heartbeat routine")
app.wg.Go(app.heartbeatRoutine) app.ding.Go(app.heartbeatRoutine, ding.RingMinor)
} }
// setup listeners // setup listeners
@@ -287,6 +299,7 @@ func (app *BootstrapApp) Setup() error {
for { for {
select { select {
case <-app.ctx.Done(): case <-app.ctx.Done():
app.ding.Wait()
app.log.App.Info().Msg("Oh, it's time for me to go, bye!") app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
return nil return nil
case err := <-lec: case err := <-lec:
@@ -297,7 +310,7 @@ func (app *BootstrapApp) Setup() error {
} }
} }
func (app *BootstrapApp) heartbeatRoutine() { func (app *BootstrapApp) heartbeatRoutine(ctx context.Context) {
ticker := time.NewTicker(time.Duration(12) * time.Hour) ticker := time.NewTicker(time.Duration(12) * time.Hour)
defer ticker.Stop() defer ticker.Stop()
@@ -350,7 +363,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
if res.StatusCode != 200 && res.StatusCode != 201 { if res.StatusCode != 200 && res.StatusCode != 201 {
app.log.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status") app.log.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
} }
case <-app.ctx.Done(): case <-ctx.Done():
app.log.App.Debug().Msg("Stopping heartbeat routine") app.log.App.Debug().Msg("Stopping heartbeat routine")
ticker.Stop() ticker.Stop()
return return
@@ -358,7 +371,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
} }
} }
func (app *BootstrapApp) dbCleanupRoutine() { func (app *BootstrapApp) dbCleanupRoutine(ctx context.Context) {
ticker := time.NewTicker(time.Duration(30) * time.Minute) ticker := time.NewTicker(time.Duration(30) * time.Minute)
defer ticker.Stop() defer ticker.Stop()
@@ -367,14 +380,14 @@ func (app *BootstrapApp) dbCleanupRoutine() {
case <-ticker.C: case <-ticker.C:
app.log.App.Debug().Msg("Running database cleanup") app.log.App.Debug().Msg("Running database cleanup")
err := app.queries.DeleteExpiredSessions(app.ctx, time.Now().Unix()) err := app.queries.DeleteExpiredSessions(ctx, time.Now().Unix())
if err != nil { if err != nil {
app.log.App.Error().Err(err).Msg("Failed to delete expired sessions") app.log.App.Error().Err(err).Msg("Failed to delete expired sessions")
} }
app.log.App.Debug().Msg("Database cleanup completed") app.log.App.Debug().Msg("Database cleanup completed")
case <-app.ctx.Done(): case <-ctx.Done():
app.log.App.Debug().Msg("Stopping database cleanup routine") app.log.App.Debug().Msg("Stopping database cleanup routine")
ticker.Stop() ticker.Stop()
return return
+57 -9
View File
@@ -6,15 +6,18 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/golang-migrate/migrate/v4"
pgxmigrate "github.com/golang-migrate/migrate/v4/database/pgx/v5"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "github.com/jackc/pgx/v5/stdlib"
_ "modernc.org/sqlite"
"github.com/tinyauthapp/tinyauth/internal/assets" "github.com/tinyauthapp/tinyauth/internal/assets"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/repository/memory" "github.com/tinyauthapp/tinyauth/internal/repository/memory"
"github.com/tinyauthapp/tinyauth/internal/repository/postgres"
"github.com/tinyauthapp/tinyauth/internal/repository/sqlite" "github.com/tinyauthapp/tinyauth/internal/repository/sqlite"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "modernc.org/sqlite"
) )
func (app *BootstrapApp) SetupStore() (repository.Store, error) { func (app *BootstrapApp) SetupStore() (repository.Store, error) {
@@ -23,8 +26,10 @@ func (app *BootstrapApp) SetupStore() (repository.Store, error) {
return memory.New(), nil return memory.New(), nil
case "sqlite", "": case "sqlite", "":
return app.setupSQLite(app.config.Database.Path) return app.setupSQLite(app.config.Database.Path)
case "postgres":
return app.setupPostgres(app.config.Database.Path)
default: default:
return nil, fmt.Errorf("unknown database driver %q: valid values are sqlite, memory", app.config.Database.Driver) return nil, fmt.Errorf("unknown database driver %q: valid values are sqlite, postgres, memory", app.config.Database.Driver)
} }
} }
@@ -41,9 +46,9 @@ func (app *BootstrapApp) setupSQLite(databasePath string) (repository.Store, err
return nil, fmt.Errorf("failed to open database: %w", err) return nil, fmt.Errorf("failed to open database: %w", err)
} }
// Close the database if there is an error during migration cleanup := true
defer func() { defer func() {
if err != nil { if cleanup {
db.Close() db.Close()
} }
}() }()
@@ -70,11 +75,54 @@ func (app *BootstrapApp) setupSQLite(databasePath string) (repository.Store, err
return nil, fmt.Errorf("failed to create migrator: %w", err) return nil, fmt.Errorf("failed to create migrator: %w", err)
} }
if err := migrator.Up(); err != nil && err != migrate.ErrNoChange { if err = migrator.Up(); err != nil && err != migrate.ErrNoChange {
return nil, fmt.Errorf("failed to migrate database: %w", err) return nil, fmt.Errorf("failed to migrate database: %w", err)
} }
cleanup = false
app.db = db app.db = db
return sqlite.NewStore(sqlite.New(db)), nil return sqlite.NewStore(sqlite.New(db)), nil
} }
func (app *BootstrapApp) setupPostgres(databaseURL string) (repository.Store, error) {
db, err := sql.Open("pgx", databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
cleanup := true
defer func() {
if cleanup {
db.Close()
}
}()
migrations, err := iofs.New(assets.Migrations, "migrations/postgres")
if err != nil {
return nil, fmt.Errorf("failed to create migrations: %w", err)
}
target, err := pgxmigrate.WithInstance(db, &pgxmigrate.Config{})
if err != nil {
return nil, fmt.Errorf("failed to create postgres instance: %w", err)
}
migrator, err := migrate.NewWithInstance("iofs", migrations, "pgx", target)
if err != nil {
return nil, fmt.Errorf("failed to create migrator: %w", err)
}
if err = migrator.Up(); err != nil && err != migrate.ErrNoChange {
return nil, fmt.Errorf("failed to migrate database: %w", err)
}
cleanup = false
app.db = db
return postgres.NewStore(postgres.New(db)), nil
}
+17 -20
View File
@@ -9,6 +9,7 @@ import (
"os" "os"
"time" "time"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/middleware" "github.com/tinyauthapp/tinyauth/internal/middleware"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
@@ -80,9 +81,9 @@ func (app *BootstrapApp) runListeners() (chan error, error) {
return nil, fmt.Errorf("failed to get listener function: %w", err) return nil, fmt.Errorf("failed to get listener function: %w", err)
} }
app.wg.Go(func() { app.ding.Go(func(ctx context.Context) {
lec <- listenerFunc() lec <- listenerFunc(ctx)
}) }, ding.RingNormal)
} }
return lec, nil return lec, nil
@@ -125,7 +126,7 @@ func (app *BootstrapApp) calculateListenerPolicy() []Listener {
return l return l
} }
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) { func (app *BootstrapApp) listenerFromType(listenerType Listener) (func(ctx context.Context) error, error) {
switch listenerType { switch listenerType {
case ListenerHTTP: case ListenerHTTP:
return app.serveHTTP, nil return app.serveHTTP, nil
@@ -138,7 +139,7 @@ func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error,
} }
} }
func (app *BootstrapApp) serveHTTP() error { func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port) address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
app.log.App.Info().Msgf("Starting server on %s", address) app.log.App.Info().Msgf("Starting server on %s", address)
@@ -154,10 +155,10 @@ func (app *BootstrapApp) serveHTTP() error {
Handler: app.router.Handler(), Handler: app.router.Handler(),
} }
return app.serve(listener, server, "http") return app.serve(listener, server, ctx, "http")
} }
func (app *BootstrapApp) serveUnix() error { func (app *BootstrapApp) serveUnix(ctx context.Context) error {
_, err := os.Stat(app.config.Server.SocketPath) _, err := os.Stat(app.config.Server.SocketPath)
if err == nil { if err == nil {
@@ -181,10 +182,10 @@ func (app *BootstrapApp) serveUnix() error {
Handler: app.router.Handler(), Handler: app.router.Handler(),
} }
return app.serve(listener, server, "unix socket") return app.serve(listener, server, ctx, "unix socket")
} }
func (app *BootstrapApp) serveTailscale() error { func (app *BootstrapApp) serveTailscale(ctx context.Context) error {
app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname())) app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname()))
listener, err := app.services.tailscaleService.CreateListener() listener, err := app.services.tailscaleService.CreateListener()
@@ -197,27 +198,23 @@ func (app *BootstrapApp) serveTailscale() error {
Handler: app.router.Handler(), Handler: app.router.Handler(),
} }
return app.serve(listener, server, "tailscale") return app.serve(listener, server, ctx, "tailscale")
} }
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error { func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx context.Context, name string) error {
shutdown := func() { shutdown := func() {
ctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second) // we use a new context for the shutdown since the main one is cancelled
sctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second)
defer cancel() defer cancel()
err := server.Shutdown(ctx) err := server.Shutdown(sctx)
if err != nil && if err != nil {
// With tailscale, the goroutine for shutting down the tailscale connection
// runs first and causes the connection the tailscale listener is running on to close
// first so, the shutdown fails
// TODO: add priority to the goroutine shutdowns
!errors.Is(err, net.ErrClosed) {
app.log.App.Error().Err(err).Msgf("Failed to shutdown %s listener gracefully", name) app.log.App.Error().Err(err).Msgf("Failed to shutdown %s listener gracefully", name)
} }
listener.Close() listener.Close()
} }
go func() { go func() {
<-app.ctx.Done() <-ctx.Done()
app.log.App.Debug().Msgf("Shutting down %s listener", name) app.log.App.Debug().Msgf("Shutting down %s listener", name)
shutdown() shutdown()
}() }()
+6 -6
View File
@@ -8,7 +8,7 @@ import (
) )
func (app *BootstrapApp) setupServices() error { func (app *BootstrapApp) setupServices() error {
ldapService, err := service.NewLdapService(app.log, app.config, app.ctx, &app.wg) ldapService, err := service.NewLdapService(app.log, app.config, app.ding)
if err != nil { if err != nil {
app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it") app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it")
@@ -22,7 +22,7 @@ func (app *BootstrapApp) setupServices() error {
return fmt.Errorf("failed to initialize label provider: %w", err) return fmt.Errorf("failed to initialize label provider: %w", err)
} }
tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, &app.wg) tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, app.ding)
if err != nil { if err != nil {
app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it") app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it")
@@ -42,10 +42,10 @@ func (app *BootstrapApp) setupServices() error {
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx) oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
app.services.oauthBrokerService = oauthBrokerService app.services.oauthBrokerService = oauthBrokerService
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService) authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, app.ding, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine)
app.services.authService = authService app.services.authService = authService
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg) oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ding)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize oidc service: %w", err) return fmt.Errorf("failed to initialize oidc service: %w", err)
@@ -69,7 +69,7 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
if useKubernetes { if useKubernetes {
app.log.App.Debug().Msg("Using Kubernetes label provider") app.log.App.Debug().Msg("Using Kubernetes label provider")
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg) kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, app.ding)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err) return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
@@ -81,7 +81,7 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
app.log.App.Debug().Msg("Using Docker label provider") app.log.App.Debug().Msg("Using Docker label provider")
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg) dockerService, err := service.NewDockerService(app.log, app.ctx, app.ding)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize docker service: %w", err) return nil, fmt.Errorf("failed to initialize docker service: %w", err)
+91 -22
View File
@@ -16,6 +16,15 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
type authorizeErrorParams struct {
err error
reason string
reasonPublic string
callback string
callbackError string
state string
}
type OIDCController struct { type OIDCController struct {
log *logger.Logger log *logger.Logger
oidc *service.OIDCService oidc *service.OIDCService
@@ -119,34 +128,55 @@ func (controller *OIDCController) GetClientInfo(c *gin.Context) {
func (controller *OIDCController) Authorize(c *gin.Context) { func (controller *OIDCController) Authorize(c *gin.Context) {
if controller.oidc == nil { if controller.oidc == nil {
controller.authorizeError(c, errors.New("err_oidc_not_configured"), "OIDC not configured", "This instance is not configured for OIDC", "", "", "") controller.authorizeError(c, authorizeErrorParams{
err: errors.New("err_oidc_not_configured"),
reason: "OIDC not configured",
reasonPublic: "This instance is not configured for OIDC",
})
return return
} }
userContext, err := new(model.UserContext).NewFromGin(c) userContext, err := new(model.UserContext).NewFromGin(c)
if err != nil { if err != nil {
controller.authorizeError(c, err, "Failed to get user context", "User is not logged in or the session is invalid", "", "", "") controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to get user context",
reasonPublic: "User is not logged in or the session is invalid",
})
return return
} }
if !userContext.Authenticated { if !userContext.Authenticated {
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "") controller.authorizeError(c, authorizeErrorParams{
err: errors.New("err user not logged in"),
reason: "User not logged in",
reasonPublic: "The user is not logged in",
})
return return
} }
var req service.AuthorizeRequest var req service.AuthorizeRequest
err = c.BindJSON(&req) err = c.Bind(&req)
if err != nil { if err != nil {
controller.authorizeError(c, err, "Failed to bind JSON", "The client provided an invalid authorization request", "", "", "") controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to bind JSON",
reasonPublic: "The client provided an invalid authorization request",
})
return return
} }
client, ok := controller.oidc.GetClient(req.ClientID) client, ok := controller.oidc.GetClient(req.ClientID)
if !ok { if !ok {
controller.authorizeError(c, fmt.Errorf("client not found: %s", req.ClientID), "Client not found", "The client ID is invalid", "", "", "") controller.authorizeError(c, authorizeErrorParams{
err: fmt.Errorf("client not found: %s", req.ClientID),
reason: "Client not found",
reasonPublic: "The client ID is invalid",
})
return return
} }
@@ -155,10 +185,21 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
if err != nil { if err != nil {
controller.log.App.Warn().Err(err).Msg("Failed to validate authorize params") controller.log.App.Warn().Err(err).Msg("Failed to validate authorize params")
if err.Error() != "invalid_request_uri" { if err.Error() != "invalid_request_uri" {
controller.authorizeError(c, err, "Failed validate authorize params", "Invalid request parameters", req.RedirectURI, err.Error(), req.State) controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed validate authorize params",
reasonPublic: "Invalid request parameters",
callback: req.RedirectURI,
callbackError: err.Error(),
state: req.State,
})
return return
} }
controller.authorizeError(c, err, "Redirect URI not trusted", "The provided redirect URI is not trusted", "", "", "") controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Redirect URI not trusted",
reasonPublic: "The provided redirect URI is not trusted",
})
return return
} }
@@ -169,14 +210,28 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
// Before storing the code, delete old session // Before storing the code, delete old session
err = controller.oidc.DeleteOldSession(c, sub) err = controller.oidc.DeleteOldSession(c, sub)
if err != nil { if err != nil {
controller.authorizeError(c, err, "Failed to delete old sessions", "Failed to delete old sessions", req.RedirectURI, "server_error", req.State) controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to delete old sessions",
reasonPublic: "Failed to delete old sessions",
callback: req.RedirectURI,
callbackError: "server_error",
state: req.State,
})
return return
} }
err = controller.oidc.StoreCode(c, sub, code, req) err = controller.oidc.StoreCode(c, sub, code, req)
if err != nil { if err != nil {
controller.authorizeError(c, err, "Failed to store code", "Failed to store code", req.RedirectURI, "server_error", req.State) controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to store code",
reasonPublic: "Failed to store code",
callback: req.RedirectURI,
callbackError: "server_error",
state: req.State,
})
return return
} }
@@ -186,7 +241,14 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
if err != nil { if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to store user info") controller.log.App.Error().Err(err).Msg("Failed to store user info")
controller.authorizeError(c, err, "Failed to store user info", "Failed to store user info", req.RedirectURI, "server_error", req.State) controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to store user info",
reasonPublic: "Failed to store user info",
callback: req.RedirectURI,
callbackError: "server_error",
state: req.State,
})
return return
} }
} }
@@ -197,7 +259,14 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
}) })
if err != nil { if err != nil {
controller.authorizeError(c, err, "Failed to build query", "Failed to build query", req.RedirectURI, "server_error", req.State) controller.authorizeError(c, authorizeErrorParams{
err: err,
reason: "Failed to build query",
reasonPublic: "Failed to build query",
callback: req.RedirectURI,
callbackError: "server_error",
state: req.State,
})
return return
} }
@@ -478,20 +547,20 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
c.JSON(200, controller.oidc.CompileUserinfo(user, entry.Scope)) c.JSON(200, controller.oidc.CompileUserinfo(user, entry.Scope))
} }
func (controller *OIDCController) authorizeError(c *gin.Context, err error, reason string, reasonUser string, callback string, callbackError string, state string) { func (controller *OIDCController) authorizeError(c *gin.Context, params authorizeErrorParams) {
controller.log.App.Warn().Err(err).Str("reason", reason).Msg("Authorization error") controller.log.App.Error().Err(params.err).Str("reason", params.reason).Msg("Authorization error")
if callback != "" { if params.callback != "" {
errorQueries := CallbackError{ errorQueries := CallbackError{
Error: callbackError, Error: params.callbackError,
} }
if reasonUser != "" { if params.reasonPublic != "" {
errorQueries.ErrorDescription = reasonUser errorQueries.ErrorDescription = params.reasonPublic
} }
if state != "" { if params.state != "" {
errorQueries.State = state errorQueries.State = params.state
} }
queries, err := query.Values(errorQueries) queries, err := query.Values(errorQueries)
@@ -503,13 +572,13 @@ func (controller *OIDCController) authorizeError(c *gin.Context, err error, reas
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"redirect_uri": fmt.Sprintf("%s?%s", callback, queries.Encode()), "redirect_uri": fmt.Sprintf("%s?%s", params.callback, queries.Encode()),
}) })
return return
} }
errorQueries := ErrorScreen{ errorQueries := ErrorScreen{
Error: reasonUser, Error: params.reasonPublic,
} }
queries, err := query.Values(errorQueries) queries, err := query.Values(errorQueries)
+3 -3
View File
@@ -8,11 +8,11 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
"github.com/steveiliop56/ding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
@@ -840,9 +840,9 @@ func TestOIDCController(t *testing.T) {
store := memory.New() store := memory.New()
wg := &sync.WaitGroup{} dg := ding.New(context.TODO())
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, context.TODO(), wg) oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg)
require.NoError(t, err) require.NoError(t, err)
for _, test := range tests { for _, test := range tests {
+4 -3
View File
@@ -3,10 +3,10 @@ package controller_test
import ( import (
"context" "context"
"net/http/httptest" "net/http/httptest"
"sync"
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/steveiliop56/ding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
@@ -353,11 +353,10 @@ func TestProxyController(t *testing.T) {
store := memory.New() store := memory.New()
wg := &sync.WaitGroup{}
ctx := context.TODO() ctx := context.TODO()
dg := ding.New(ctx)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil)
aclsService := service.NewAccessControlsService(log, cfg, nil) aclsService := service.NewAccessControlsService(log, cfg, nil)
policyEngine, err := service.NewPolicyEngine(cfg, log) policyEngine, err := service.NewPolicyEngine(cfg, log)
@@ -383,6 +382,8 @@ func TestProxyController(t *testing.T) {
Log: log, Log: log,
}) })
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
router := gin.Default() router := gin.Default()
+6 -3
View File
@@ -6,12 +6,12 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/steveiliop56/ding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
@@ -412,10 +412,13 @@ func TestUserController(t *testing.T) {
} }
ctx := context.TODO() ctx := context.TODO()
wg := &sync.WaitGroup{} dg := ding.New(ctx)
policyEngine, err := service.NewPolicyEngine(cfg, log)
require.NoError(t, err)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil) authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
beforeEach := func() { beforeEach := func() {
// Clear failed login attempts before each test // Clear failed login attempts before each test
@@ -5,10 +5,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http/httptest" "net/http/httptest"
"sync"
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/steveiliop56/ding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
@@ -89,11 +89,11 @@ func TestWellKnownController(t *testing.T) {
} }
ctx := context.TODO() ctx := context.TODO()
wg := &sync.WaitGroup{} dg := ding.New(ctx)
store := memory.New() store := memory.New()
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, ctx, wg) oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg)
require.NoError(t, err) require.NoError(t, err)
for _, test := range tests { for _, test := range tests {
@@ -5,11 +5,11 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/steveiliop56/ding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/middleware" "github.com/tinyauthapp/tinyauth/internal/middleware"
@@ -250,12 +250,15 @@ func TestContextMiddleware(t *testing.T) {
} }
ctx := context.TODO() ctx := context.TODO()
wg := &sync.WaitGroup{} dg := ding.New(ctx)
store := memory.New() store := memory.New()
policyEngine, err := service.NewPolicyEngine(cfg, log)
require.NoError(t, err)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil) authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil) contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil)
+5 -8
View File
@@ -62,9 +62,6 @@ func NewDefaultConfiguration() *Config {
PrivateKeyPath: "./tinyauth_oidc_key", PrivateKeyPath: "./tinyauth_oidc_key",
PublicKeyPath: "./tinyauth_oidc_key.pub", PublicKeyPath: "./tinyauth_oidc_key.pub",
}, },
Experimental: ExperimentalConfig{
ConfigFile: "",
},
Tailscale: TailscaleConfig{ Tailscale: TailscaleConfig{
Dir: "./tailscale_state", Dir: "./tailscale_state",
}, },
@@ -88,11 +85,12 @@ type Config struct {
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"` LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
Log LogConfig `description:"Logging configuration." yaml:"log"` Log LogConfig `description:"Logging configuration." yaml:"log"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"` Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
ConfigFile string `description:"Path to config file." yaml:"-"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
Driver string `description:"The database driver to use. Valid values: sqlite, memory." yaml:"driver"` Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver"`
Path string `description:"The path to the SQLite database, including file name. Only used when driver is sqlite." yaml:"path"` Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path"`
} }
type AnalyticsConfig struct { type AnalyticsConfig struct {
@@ -208,9 +206,8 @@ type LogStreamConfig struct {
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"` Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
} }
type ExperimentalConfig struct { // no experimental features
ConfigFile string `description:"Path to config file." yaml:"-"` type ExperimentalConfig struct{}
}
type TailscaleConfig struct { type TailscaleConfig struct {
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"` Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"`
+31
View File
@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package postgres
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
+3
View File
@@ -0,0 +1,3 @@
package postgres
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
+64
View File
@@ -0,0 +1,64 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package postgres
type OidcCode struct {
Sub string
CodeHash string
Scope string
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
CodeChallenge string
}
type OidcToken struct {
Sub string
AccessTokenHash string
RefreshTokenHash string
CodeHash string
Scope string
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
Nonce string
}
type OidcUserinfo struct {
Sub string
Name string
PreferredUsername string
Email string
Groups string
UpdatedAt int64
GivenName string
FamilyName string
MiddleName string
Nickname string
Profile string
Picture string
Website string
Gender string
Birthdate string
Zoneinfo string
Locale string
PhoneNumber string
Address string
}
type Session struct {
UUID string
Username string
Email string
Name string
Provider string
TotpPending bool
OAuthGroups string
Expiry int64
CreatedAt int64
OAuthName string
OAuthSub string
}
@@ -0,0 +1,581 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: oidc_queries.sql
package postgres
import (
"context"
)
const createOidcCode = `-- name: CreateOidcCode :one
INSERT INTO "oidc_codes" (
"sub",
"code_hash",
"scope",
"redirect_uri",
"client_id",
"expires_at",
"nonce",
"code_challenge"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
`
type CreateOidcCodeParams struct {
Sub string
CodeHash string
Scope string
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
CodeChallenge string
}
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
row := q.db.QueryRowContext(ctx, createOidcCode,
arg.Sub,
arg.CodeHash,
arg.Scope,
arg.RedirectURI,
arg.ClientID,
arg.ExpiresAt,
arg.Nonce,
arg.CodeChallenge,
)
var i OidcCode
err := row.Scan(
&i.Sub,
&i.CodeHash,
&i.Scope,
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
&i.CodeChallenge,
)
return i, err
}
const createOidcToken = `-- name: CreateOidcToken :one
INSERT INTO "oidc_tokens" (
"sub",
"access_token_hash",
"refresh_token_hash",
"scope",
"client_id",
"token_expires_at",
"refresh_token_expires_at",
"code_hash",
"nonce"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9
)
RETURNING sub, access_token_hash, refresh_token_hash, code_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type CreateOidcTokenParams struct {
Sub string
AccessTokenHash string
RefreshTokenHash string
Scope string
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
CodeHash string
Nonce string
}
func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error) {
row := q.db.QueryRowContext(ctx, createOidcToken,
arg.Sub,
arg.AccessTokenHash,
arg.RefreshTokenHash,
arg.Scope,
arg.ClientID,
arg.TokenExpiresAt,
arg.RefreshTokenExpiresAt,
arg.CodeHash,
arg.Nonce,
)
var i OidcToken
err := row.Scan(
&i.Sub,
&i.AccessTokenHash,
&i.RefreshTokenHash,
&i.CodeHash,
&i.Scope,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const createOidcUserInfo = `-- name: CreateOidcUserInfo :one
INSERT INTO "oidc_userinfo" (
"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"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
)
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 {
Sub string
Name string
PreferredUsername string
Email string
Groups string
UpdatedAt int64
GivenName string
FamilyName string
MiddleName string
Nickname string
Profile string
Picture string
Website string
Gender string
Birthdate string
Zoneinfo string
Locale string
PhoneNumber string
Address string
}
func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfoParams) (OidcUserinfo, error) {
row := q.db.QueryRowContext(ctx, createOidcUserInfo,
arg.Sub,
arg.Name,
arg.PreferredUsername,
arg.Email,
arg.Groups,
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
err := row.Scan(
&i.Sub,
&i.Name,
&i.PreferredUsername,
&i.Email,
&i.Groups,
&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
}
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
DELETE FROM "oidc_codes"
WHERE "expires_at" < $1
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
`
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
rows, err := q.db.QueryContext(ctx, deleteExpiredOidcCodes, expiresAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []OidcCode
for rows.Next() {
var i OidcCode
if err := rows.Scan(
&i.Sub,
&i.CodeHash,
&i.Scope,
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
&i.CodeChallenge,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteExpiredOidcTokens = `-- name: DeleteExpiredOidcTokens :many
DELETE FROM "oidc_tokens"
WHERE "token_expires_at" < $1 AND "refresh_token_expires_at" < $2
RETURNING sub, access_token_hash, refresh_token_hash, code_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type DeleteExpiredOidcTokensParams struct {
TokenExpiresAt int64
RefreshTokenExpiresAt int64
}
func (q *Queries) DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpiredOidcTokensParams) ([]OidcToken, error) {
rows, err := q.db.QueryContext(ctx, deleteExpiredOidcTokens, arg.TokenExpiresAt, arg.RefreshTokenExpiresAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []OidcToken
for rows.Next() {
var i OidcToken
if err := rows.Scan(
&i.Sub,
&i.AccessTokenHash,
&i.RefreshTokenHash,
&i.CodeHash,
&i.Scope,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteOidcCode = `-- name: DeleteOidcCode :exec
DELETE FROM "oidc_codes"
WHERE "code_hash" = $1
`
func (q *Queries) DeleteOidcCode(ctx context.Context, codeHash string) error {
_, err := q.db.ExecContext(ctx, deleteOidcCode, codeHash)
return err
}
const deleteOidcCodeBySub = `-- name: DeleteOidcCodeBySub :exec
DELETE FROM "oidc_codes"
WHERE "sub" = $1
`
func (q *Queries) DeleteOidcCodeBySub(ctx context.Context, sub string) error {
_, err := q.db.ExecContext(ctx, deleteOidcCodeBySub, sub)
return err
}
const deleteOidcToken = `-- name: DeleteOidcToken :exec
DELETE FROM "oidc_tokens"
WHERE "access_token_hash" = $1
`
func (q *Queries) DeleteOidcToken(ctx context.Context, accessTokenHash string) error {
_, err := q.db.ExecContext(ctx, deleteOidcToken, accessTokenHash)
return err
}
const deleteOidcTokenByCodeHash = `-- name: DeleteOidcTokenByCodeHash :exec
DELETE FROM "oidc_tokens"
WHERE "code_hash" = $1
`
func (q *Queries) DeleteOidcTokenByCodeHash(ctx context.Context, codeHash string) error {
_, err := q.db.ExecContext(ctx, deleteOidcTokenByCodeHash, codeHash)
return err
}
const deleteOidcTokenBySub = `-- name: DeleteOidcTokenBySub :exec
DELETE FROM "oidc_tokens"
WHERE "sub" = $1
`
func (q *Queries) DeleteOidcTokenBySub(ctx context.Context, sub string) error {
_, err := q.db.ExecContext(ctx, deleteOidcTokenBySub, sub)
return err
}
const deleteOidcUserInfo = `-- name: DeleteOidcUserInfo :exec
DELETE FROM "oidc_userinfo"
WHERE "sub" = $1
`
func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
_, err := q.db.ExecContext(ctx, deleteOidcUserInfo, sub)
return err
}
const getOidcCode = `-- name: GetOidcCode :one
DELETE FROM "oidc_codes"
WHERE "code_hash" = $1
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
`
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
row := q.db.QueryRowContext(ctx, getOidcCode, codeHash)
var i OidcCode
err := row.Scan(
&i.Sub,
&i.CodeHash,
&i.Scope,
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
&i.CodeChallenge,
)
return i, err
}
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
DELETE FROM "oidc_codes"
WHERE "sub" = $1
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
`
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
row := q.db.QueryRowContext(ctx, getOidcCodeBySub, sub)
var i OidcCode
err := row.Scan(
&i.Sub,
&i.CodeHash,
&i.Scope,
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
&i.CodeChallenge,
)
return i, err
}
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
WHERE "sub" = $1
`
func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcCode, error) {
row := q.db.QueryRowContext(ctx, getOidcCodeBySubUnsafe, sub)
var i OidcCode
err := row.Scan(
&i.Sub,
&i.CodeHash,
&i.Scope,
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
&i.CodeChallenge,
)
return i, err
}
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
WHERE "code_hash" = $1
`
func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcCode, error) {
row := q.db.QueryRowContext(ctx, getOidcCodeUnsafe, codeHash)
var i OidcCode
err := row.Scan(
&i.Sub,
&i.CodeHash,
&i.Scope,
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
&i.CodeChallenge,
)
return i, err
}
const getOidcToken = `-- name: GetOidcToken :one
SELECT sub, access_token_hash, refresh_token_hash, code_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "access_token_hash" = $1
`
func (q *Queries) GetOidcToken(ctx context.Context, accessTokenHash string) (OidcToken, error) {
row := q.db.QueryRowContext(ctx, getOidcToken, accessTokenHash)
var i OidcToken
err := row.Scan(
&i.Sub,
&i.AccessTokenHash,
&i.RefreshTokenHash,
&i.CodeHash,
&i.Scope,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcTokenByRefreshToken = `-- name: GetOidcTokenByRefreshToken :one
SELECT sub, access_token_hash, refresh_token_hash, code_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "refresh_token_hash" = $1
`
func (q *Queries) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (OidcToken, error) {
row := q.db.QueryRowContext(ctx, getOidcTokenByRefreshToken, refreshTokenHash)
var i OidcToken
err := row.Scan(
&i.Sub,
&i.AccessTokenHash,
&i.RefreshTokenHash,
&i.CodeHash,
&i.Scope,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcTokenBySub = `-- name: GetOidcTokenBySub :one
SELECT sub, access_token_hash, refresh_token_hash, code_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "sub" = $1
`
func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken, error) {
row := q.db.QueryRowContext(ctx, getOidcTokenBySub, sub)
var i OidcToken
err := row.Scan(
&i.Sub,
&i.AccessTokenHash,
&i.RefreshTokenHash,
&i.CodeHash,
&i.Scope,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcUserInfo = `-- name: GetOidcUserInfo :one
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" = $1
`
func (q *Queries) GetOidcUserInfo(ctx context.Context, sub string) (OidcUserinfo, error) {
row := q.db.QueryRowContext(ctx, getOidcUserInfo, sub)
var i OidcUserinfo
err := row.Scan(
&i.Sub,
&i.Name,
&i.PreferredUsername,
&i.Email,
&i.Groups,
&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
}
const updateOidcTokenByRefreshToken = `-- name: UpdateOidcTokenByRefreshToken :one
UPDATE "oidc_tokens" SET
"access_token_hash" = $1,
"refresh_token_hash" = $2,
"token_expires_at" = $3,
"refresh_token_expires_at" = $4
WHERE "refresh_token_hash" = $5
RETURNING sub, access_token_hash, refresh_token_hash, code_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type UpdateOidcTokenByRefreshTokenParams struct {
AccessTokenHash string
RefreshTokenHash string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
RefreshTokenHash_2 string
}
func (q *Queries) UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateOidcTokenByRefreshTokenParams) (OidcToken, error) {
row := q.db.QueryRowContext(ctx, updateOidcTokenByRefreshToken,
arg.AccessTokenHash,
arg.RefreshTokenHash,
arg.TokenExpiresAt,
arg.RefreshTokenExpiresAt,
arg.RefreshTokenHash_2,
)
var i OidcToken
err := row.Scan(
&i.Sub,
&i.AccessTokenHash,
&i.RefreshTokenHash,
&i.CodeHash,
&i.Scope,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -0,0 +1,176 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: session_queries.sql
package postgres
import (
"context"
)
const createSession = `-- name: CreateSession :one
INSERT INTO "sessions" (
"uuid",
"username",
"email",
"name",
"provider",
"totp_pending",
"oauth_groups",
"expiry",
"created_at",
"oauth_name",
"oauth_sub"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
)
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
`
type CreateSessionParams struct {
UUID string
Username string
Email string
Name string
Provider string
TotpPending bool
OAuthGroups string
Expiry int64
CreatedAt int64
OAuthName string
OAuthSub string
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
row := q.db.QueryRowContext(ctx, createSession,
arg.UUID,
arg.Username,
arg.Email,
arg.Name,
arg.Provider,
arg.TotpPending,
arg.OAuthGroups,
arg.Expiry,
arg.CreatedAt,
arg.OAuthName,
arg.OAuthSub,
)
var i Session
err := row.Scan(
&i.UUID,
&i.Username,
&i.Email,
&i.Name,
&i.Provider,
&i.TotpPending,
&i.OAuthGroups,
&i.Expiry,
&i.CreatedAt,
&i.OAuthName,
&i.OAuthSub,
)
return i, err
}
const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
DELETE FROM "sessions"
WHERE "expiry" < $1
`
func (q *Queries) DeleteExpiredSessions(ctx context.Context, expiry int64) error {
_, err := q.db.ExecContext(ctx, deleteExpiredSessions, expiry)
return err
}
const deleteSession = `-- name: DeleteSession :exec
DELETE FROM "sessions"
WHERE "uuid" = $1
`
func (q *Queries) DeleteSession(ctx context.Context, uuid string) error {
_, err := q.db.ExecContext(ctx, deleteSession, uuid)
return err
}
const getSession = `-- name: GetSession :one
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub FROM "sessions"
WHERE "uuid" = $1
`
func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error) {
row := q.db.QueryRowContext(ctx, getSession, uuid)
var i Session
err := row.Scan(
&i.UUID,
&i.Username,
&i.Email,
&i.Name,
&i.Provider,
&i.TotpPending,
&i.OAuthGroups,
&i.Expiry,
&i.CreatedAt,
&i.OAuthName,
&i.OAuthSub,
)
return i, err
}
const updateSession = `-- name: UpdateSession :one
UPDATE "sessions" SET
"username" = $1,
"email" = $2,
"name" = $3,
"provider" = $4,
"totp_pending" = $5,
"oauth_groups" = $6,
"expiry" = $7,
"oauth_name" = $8,
"oauth_sub" = $9
WHERE "uuid" = $10
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
`
type UpdateSessionParams struct {
Username string
Email string
Name string
Provider string
TotpPending bool
OAuthGroups string
Expiry int64
OAuthName string
OAuthSub string
UUID string
}
func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) {
row := q.db.QueryRowContext(ctx, updateSession,
arg.Username,
arg.Email,
arg.Name,
arg.Provider,
arg.TotpPending,
arg.OAuthGroups,
arg.Expiry,
arg.OAuthName,
arg.OAuthSub,
arg.UUID,
)
var i Session
err := row.Scan(
&i.UUID,
&i.Username,
&i.Email,
&i.Name,
&i.Provider,
&i.TotpPending,
&i.OAuthGroups,
&i.Expiry,
&i.CreatedAt,
&i.OAuthName,
&i.OAuthSub,
)
return i, err
}
+209
View File
@@ -0,0 +1,209 @@
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
package postgres
import (
"context"
"database/sql"
"errors"
"github.com/tinyauthapp/tinyauth/internal/repository"
)
// Store wraps *Queries and implements repository.Store.
type Store struct {
q *Queries
}
// NewStore wraps a *Queries to satisfy repository.Store.
func NewStore(q *Queries) repository.Store {
return &Store{q: q}
}
var errorMap = map[error]error{
sql.ErrNoRows: repository.ErrNotFound,
}
func mapErr(err error) error {
for from, to := range errorMap {
if errors.Is(err, from) {
return to
}
}
return err
}
func (s *Store) CreateOidcCode(ctx context.Context, arg repository.CreateOidcCodeParams) (repository.OidcCode, error) {
r, err := s.q.CreateOidcCode(ctx, CreateOidcCodeParams(arg))
if err != nil {
return repository.OidcCode{}, mapErr(err)
}
return repository.OidcCode(r), nil
}
func (s *Store) CreateOidcToken(ctx context.Context, arg repository.CreateOidcTokenParams) (repository.OidcToken, error) {
r, err := s.q.CreateOidcToken(ctx, CreateOidcTokenParams(arg))
if err != nil {
return repository.OidcToken{}, mapErr(err)
}
return repository.OidcToken(r), nil
}
func (s *Store) CreateOidcUserInfo(ctx context.Context, arg repository.CreateOidcUserInfoParams) (repository.OidcUserinfo, error) {
r, err := s.q.CreateOidcUserInfo(ctx, CreateOidcUserInfoParams(arg))
if err != nil {
return repository.OidcUserinfo{}, mapErr(err)
}
return repository.OidcUserinfo(r), nil
}
func (s *Store) CreateSession(ctx context.Context, arg repository.CreateSessionParams) (repository.Session, error) {
r, err := s.q.CreateSession(ctx, CreateSessionParams(arg))
if err != nil {
return repository.Session{}, mapErr(err)
}
return repository.Session(r), nil
}
func (s *Store) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]repository.OidcCode, error) {
rows, err := s.q.DeleteExpiredOidcCodes(ctx, expiresAt)
if err != nil {
return nil, mapErr(err)
}
out := make([]repository.OidcCode, len(rows))
for i, row := range rows {
out[i] = repository.OidcCode(row)
}
return out, nil
}
func (s *Store) DeleteExpiredOidcTokens(ctx context.Context, arg repository.DeleteExpiredOidcTokensParams) ([]repository.OidcToken, error) {
rows, err := s.q.DeleteExpiredOidcTokens(ctx, DeleteExpiredOidcTokensParams(arg))
if err != nil {
return nil, mapErr(err)
}
out := make([]repository.OidcToken, len(rows))
for i, row := range rows {
out[i] = repository.OidcToken(row)
}
return out, nil
}
func (s *Store) DeleteExpiredSessions(ctx context.Context, expiry int64) error {
return mapErr(s.q.DeleteExpiredSessions(ctx, expiry))
}
func (s *Store) DeleteOidcCode(ctx context.Context, codeHash string) error {
return mapErr(s.q.DeleteOidcCode(ctx, codeHash))
}
func (s *Store) DeleteOidcCodeBySub(ctx context.Context, sub string) error {
return mapErr(s.q.DeleteOidcCodeBySub(ctx, sub))
}
func (s *Store) DeleteOidcToken(ctx context.Context, accessTokenHash string) error {
return mapErr(s.q.DeleteOidcToken(ctx, accessTokenHash))
}
func (s *Store) DeleteOidcTokenByCodeHash(ctx context.Context, codeHash string) error {
return mapErr(s.q.DeleteOidcTokenByCodeHash(ctx, codeHash))
}
func (s *Store) DeleteOidcTokenBySub(ctx context.Context, sub string) error {
return mapErr(s.q.DeleteOidcTokenBySub(ctx, sub))
}
func (s *Store) DeleteOidcUserInfo(ctx context.Context, sub string) error {
return mapErr(s.q.DeleteOidcUserInfo(ctx, sub))
}
func (s *Store) DeleteSession(ctx context.Context, uuid string) error {
return mapErr(s.q.DeleteSession(ctx, uuid))
}
func (s *Store) GetOidcCode(ctx context.Context, codeHash string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCode(ctx, codeHash)
if err != nil {
return repository.OidcCode{}, mapErr(err)
}
return repository.OidcCode(r), nil
}
func (s *Store) GetOidcCodeBySub(ctx context.Context, sub string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCodeBySub(ctx, sub)
if err != nil {
return repository.OidcCode{}, mapErr(err)
}
return repository.OidcCode(r), nil
}
func (s *Store) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCodeBySubUnsafe(ctx, sub)
if err != nil {
return repository.OidcCode{}, mapErr(err)
}
return repository.OidcCode(r), nil
}
func (s *Store) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (repository.OidcCode, error) {
r, err := s.q.GetOidcCodeUnsafe(ctx, codeHash)
if err != nil {
return repository.OidcCode{}, mapErr(err)
}
return repository.OidcCode(r), nil
}
func (s *Store) GetOidcToken(ctx context.Context, accessTokenHash string) (repository.OidcToken, error) {
r, err := s.q.GetOidcToken(ctx, accessTokenHash)
if err != nil {
return repository.OidcToken{}, mapErr(err)
}
return repository.OidcToken(r), nil
}
func (s *Store) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHash string) (repository.OidcToken, error) {
r, err := s.q.GetOidcTokenByRefreshToken(ctx, refreshTokenHash)
if err != nil {
return repository.OidcToken{}, mapErr(err)
}
return repository.OidcToken(r), nil
}
func (s *Store) GetOidcTokenBySub(ctx context.Context, sub string) (repository.OidcToken, error) {
r, err := s.q.GetOidcTokenBySub(ctx, sub)
if err != nil {
return repository.OidcToken{}, mapErr(err)
}
return repository.OidcToken(r), nil
}
func (s *Store) GetOidcUserInfo(ctx context.Context, sub string) (repository.OidcUserinfo, error) {
r, err := s.q.GetOidcUserInfo(ctx, sub)
if err != nil {
return repository.OidcUserinfo{}, mapErr(err)
}
return repository.OidcUserinfo(r), nil
}
func (s *Store) GetSession(ctx context.Context, uuid string) (repository.Session, error) {
r, err := s.q.GetSession(ctx, uuid)
if err != nil {
return repository.Session{}, mapErr(err)
}
return repository.Session(r), nil
}
func (s *Store) UpdateOidcTokenByRefreshToken(ctx context.Context, arg repository.UpdateOidcTokenByRefreshTokenParams) (repository.OidcToken, error) {
r, err := s.q.UpdateOidcTokenByRefreshToken(ctx, UpdateOidcTokenByRefreshTokenParams(arg))
if err != nil {
return repository.OidcToken{}, mapErr(err)
}
return repository.OidcToken(r), nil
}
func (s *Store) UpdateSession(ctx context.Context, arg repository.UpdateSessionParams) (repository.Session, error) {
r, err := s.q.UpdateSession(ctx, UpdateSessionParams(arg))
if err != nil {
return repository.Session{}, mapErr(err)
}
return repository.Session(r), nil
}
+45 -12
View File
@@ -9,6 +9,12 @@ import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
// For LDAP and OAuth groups and IP allow/deny, we default to allow even with a deny policy.
// This is because we can't force the user to use groups in LDAP and OAuth if they would like to use
// a deny policy. As for IP checks, we can't reliably get the client IP (most of Tinyauth instances are
// behind a Docker bridge network) so to make it easier for users to use a deny policy without
// issues with IPs we allow by default.
type RuleName string type RuleName string
const ( const (
@@ -25,7 +31,11 @@ type UserAllowedRule struct {
} }
func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect { func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
if ctx.ACLs == nil || ctx.UserContext == nil { if ctx.UserContext == nil {
return EffectDeny
}
if ctx.ACLs == nil {
return EffectAbstain return EffectAbstain
} }
@@ -34,7 +44,7 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
match, err := utils.CheckFilter(ctx.ACLs.OAuth.Whitelist, ctx.UserContext.OAuth.Email) match, err := utils.CheckFilter(ctx.ACLs.OAuth.Whitelist, ctx.UserContext.OAuth.Email)
if err != nil { if err != nil {
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist") rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist")
return EffectAbstain return EffectDeny
} }
if match { if match {
rule.Log.App.Debug().Str("email", ctx.UserContext.OAuth.Email).Msg("User is in OAuth whitelist, allowing access") rule.Log.App.Debug().Str("email", ctx.UserContext.OAuth.Email).Msg("User is in OAuth whitelist, allowing access")
@@ -48,7 +58,7 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
match, err := utils.CheckFilter(ctx.ACLs.Users.Block, ctx.UserContext.GetUsername()) match, err := utils.CheckFilter(ctx.ACLs.Users.Block, ctx.UserContext.GetUsername())
if err != nil { if err != nil {
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users block list") rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users block list")
return EffectAbstain return EffectDeny
} }
if match { if match {
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users block list, denying access") rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users block list, denying access")
@@ -62,9 +72,12 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername()) match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername())
if err != nil { if err != nil {
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list") if err == utils.ErrFilterEmpty {
return EffectAbstain return EffectAbstain
} }
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list")
return EffectDeny
}
if match { if match {
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users allow list, allowing access") rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users allow list, allowing access")
@@ -80,13 +93,22 @@ type OAuthGroupRule struct {
} }
func (rule *OAuthGroupRule) Evaluate(ctx *ACLContext) Effect { func (rule *OAuthGroupRule) Evaluate(ctx *ACLContext) Effect {
if ctx.ACLs == nil || ctx.UserContext == nil { if ctx.UserContext == nil {
return EffectAbstain return EffectDeny
}
if ctx.ACLs == nil {
return EffectAllow
} }
if !ctx.UserContext.IsOAuth() { if !ctx.UserContext.IsOAuth() {
rule.Log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check") rule.Log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
return EffectAbstain return EffectAllow
}
if len(ctx.ACLs.OAuth.Groups) == 0 {
rule.Log.App.Debug().Msg("No OAuth groups specified in ACLs, allowing access")
return EffectAllow
} }
if _, ok := model.OverrideProviders[ctx.UserContext.OAuth.ID]; ok { if _, ok := model.OverrideProviders[ctx.UserContext.OAuth.ID]; ok {
@@ -97,7 +119,8 @@ func (rule *OAuthGroupRule) Evaluate(ctx *ACLContext) Effect {
for _, group := range ctx.UserContext.OAuth.Groups { for _, group := range ctx.UserContext.OAuth.Groups {
match, err := utils.CheckFilter(ctx.ACLs.OAuth.Groups, strings.TrimSpace(group)) match, err := utils.CheckFilter(ctx.ACLs.OAuth.Groups, strings.TrimSpace(group))
if err != nil { if err != nil {
return EffectAbstain rule.Log.App.Warn().Err(err).Str("item", group).Msg("Invalid entry in OAuth groups ACL")
return EffectDeny
} }
if match { if match {
rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.OAuth.Groups).Msg("User group matched, allowing access") rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.OAuth.Groups).Msg("User group matched, allowing access")
@@ -114,19 +137,29 @@ type LDAPGroupRule struct {
} }
func (rule *LDAPGroupRule) Evaluate(ctx *ACLContext) Effect { func (rule *LDAPGroupRule) Evaluate(ctx *ACLContext) Effect {
if ctx == nil || ctx.UserContext == nil || ctx.ACLs == nil { if ctx.UserContext == nil {
return EffectAbstain return EffectDeny
}
if ctx.ACLs == nil {
return EffectAllow
} }
if !ctx.UserContext.IsLDAP() { if !ctx.UserContext.IsLDAP() {
rule.Log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check") rule.Log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
return EffectAbstain return EffectAllow
}
if len(ctx.ACLs.LDAP.Groups) == 0 {
rule.Log.App.Debug().Msg("No LDAP groups specified in ACLs, allowing access")
return EffectAllow
} }
for _, group := range ctx.UserContext.LDAP.Groups { for _, group := range ctx.UserContext.LDAP.Groups {
match, err := utils.CheckFilter(ctx.ACLs.LDAP.Groups, strings.TrimSpace(group)) match, err := utils.CheckFilter(ctx.ACLs.LDAP.Groups, strings.TrimSpace(group))
if err != nil { if err != nil {
return EffectAbstain rule.Log.App.Warn().Err(err).Str("item", group).Msg("Invalid entry in LDAP groups ACL")
return EffectDeny
} }
if match { if match {
rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.LDAP.Groups).Msg("User group matched, allowing access") rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.LDAP.Groups).Msg("User group matched, allowing access")
+95 -42
View File
@@ -21,6 +21,16 @@ func TestUserAllowedRule(t *testing.T) {
ctx *ACLContext ctx *ACLContext
expected Effect expected Effect
}{ }{
{
name: "denies when user context is nil",
ctx: &ACLContext{
ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: "alice"},
},
UserContext: nil,
},
expected: EffectDeny,
},
{ {
name: "abstains when ACLs are nil", name: "abstains when ACLs are nil",
ctx: &ACLContext{ ctx: &ACLContext{
@@ -34,16 +44,6 @@ func TestUserAllowedRule(t *testing.T) {
}, },
expected: EffectAbstain, expected: EffectAbstain,
}, },
{
name: "abstains when user context is nil",
ctx: &ACLContext{
ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: "alice"},
},
UserContext: nil,
},
expected: EffectAbstain,
},
{ {
name: "allows OAuth user when email matches whitelist", name: "allows OAuth user when email matches whitelist",
ctx: &ACLContext{ ctx: &ACLContext{
@@ -78,7 +78,7 @@ func TestUserAllowedRule(t *testing.T) {
expected: EffectDeny, expected: EffectDeny,
}, },
{ {
name: "abstains for OAuth user when whitelist filter is invalid", name: "denies for OAuth user when whitelist filter is invalid",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: "/[/"}, OAuth: model.AppOAuth{Whitelist: "/[/"},
@@ -90,7 +90,7 @@ func TestUserAllowedRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectDeny,
}, },
{ {
name: "denies local user when username matches block list", name: "denies local user when username matches block list",
@@ -123,7 +123,7 @@ func TestUserAllowedRule(t *testing.T) {
expected: EffectAllow, expected: EffectAllow,
}, },
{ {
name: "abstains when block list filter is invalid", name: "denies when block list filter is invalid",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
Users: model.AppUsers{Block: "/[/"}, Users: model.AppUsers{Block: "/[/"},
@@ -135,6 +135,21 @@ func TestUserAllowedRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectDeny,
},
{
name: "abstains when allow list is empty",
ctx: &ACLContext{
ACLs: &model.App{
Users: model.AppUsers{Allow: ""},
},
UserContext: &model.UserContext{
Provider: model.ProviderLocal,
Local: &model.LocalContext{
BaseContext: model.BaseContext{Username: "alice"},
},
},
},
expected: EffectAbstain, expected: EffectAbstain,
}, },
{ {
@@ -168,7 +183,7 @@ func TestUserAllowedRule(t *testing.T) {
expected: EffectDeny, expected: EffectDeny,
}, },
{ {
name: "abstains when allow list filter is invalid", name: "denies when allow list filter is invalid",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
Users: model.AppUsers{Allow: "/[/"}, Users: model.AppUsers{Allow: "/[/"},
@@ -180,7 +195,7 @@ func TestUserAllowedRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectDeny,
}, },
} }
@@ -203,7 +218,17 @@ func TestOAuthGroupRule(t *testing.T) {
expected Effect expected Effect
}{ }{
{ {
name: "abstains when ACLs are nil", name: "denies when user context is nil",
ctx: &ACLContext{
ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: "alice"},
},
UserContext: nil,
},
expected: EffectDeny,
},
{
name: "allows when ACLs are nil",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: nil, ACLs: nil,
UserContext: &model.UserContext{ UserContext: &model.UserContext{
@@ -213,20 +238,10 @@ func TestOAuthGroupRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectAllow,
}, },
{ {
name: "abstains when user context is nil", name: "allows when user is not OAuth",
ctx: &ACLContext{
ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: "alice"},
},
UserContext: nil,
},
expected: EffectAbstain,
},
{
name: "abstains when user is not OAuth",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
OAuth: model.AppOAuth{Groups: "admins"}, OAuth: model.AppOAuth{Groups: "admins"},
@@ -238,7 +253,22 @@ func TestOAuthGroupRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectAllow,
},
{
name: "allows when group filter is empty",
ctx: &ACLContext{
ACLs: &model.App{
OAuth: model.AppOAuth{Groups: ""},
},
UserContext: &model.UserContext{
Provider: model.ProviderOAuth,
OAuth: &model.OAuthContext{
BaseContext: model.BaseContext{Username: "alice"},
},
},
},
expected: EffectAllow,
}, },
{ {
name: "allows when provider is an override provider regardless of groups", name: "allows when provider is an override provider regardless of groups",
@@ -305,7 +335,7 @@ func TestOAuthGroupRule(t *testing.T) {
expected: EffectDeny, expected: EffectDeny,
}, },
{ {
name: "abstains when groups filter is invalid", name: "denies when groups filter is invalid",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
OAuth: model.AppOAuth{Groups: "/[/"}, OAuth: model.AppOAuth{Groups: "/[/"},
@@ -318,7 +348,7 @@ func TestOAuthGroupRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectDeny,
}, },
} }
@@ -341,22 +371,30 @@ func TestLDAPGroupRule(t *testing.T) {
expected Effect expected Effect
}{ }{
{ {
name: "abstains when context is nil", name: "denies when user context is nil",
ctx: nil,
expected: EffectAbstain,
},
{
name: "abstains when user context is nil",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: "alice"}, OAuth: model.AppOAuth{Whitelist: "alice"},
}, },
UserContext: nil, UserContext: nil,
}, },
expected: EffectAbstain, expected: EffectDeny,
}, },
{ {
name: "abstains when user is not LDAP", name: "allows when acls are nil",
ctx: &ACLContext{
ACLs: nil,
UserContext: &model.UserContext{
Provider: model.ProviderLocal,
Local: &model.LocalContext{
BaseContext: model.BaseContext{Username: "alice"},
},
},
},
expected: EffectAllow,
},
{
name: "allows when user is not LDAP",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
LDAP: model.AppLDAP{Groups: "admins"}, LDAP: model.AppLDAP{Groups: "admins"},
@@ -368,7 +406,22 @@ func TestLDAPGroupRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectAllow,
},
{
name: "allows when group filter is empty",
ctx: &ACLContext{
ACLs: &model.App{
LDAP: model.AppLDAP{Groups: ""},
},
UserContext: &model.UserContext{
Provider: model.ProviderLDAP,
LDAP: &model.LDAPContext{
BaseContext: model.BaseContext{Username: "alice"},
},
},
},
expected: EffectAllow,
}, },
{ {
name: "allows LDAP user when a group matches", name: "allows LDAP user when a group matches",
@@ -416,7 +469,7 @@ func TestLDAPGroupRule(t *testing.T) {
expected: EffectDeny, expected: EffectDeny,
}, },
{ {
name: "abstains when groups filter is invalid", name: "denies when groups filter is invalid",
ctx: &ACLContext{ ctx: &ACLContext{
ACLs: &model.App{ ACLs: &model.App{
LDAP: model.AppLDAP{Groups: "/[/"}, LDAP: model.AppLDAP{Groups: "/[/"},
@@ -428,7 +481,7 @@ func TestLDAPGroupRule(t *testing.T) {
}, },
}, },
}, },
expected: EffectAbstain, expected: EffectDeny,
}, },
} }
+21 -8
View File
@@ -9,6 +9,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
@@ -79,6 +80,7 @@ type AuthService struct {
queries repository.Store queries repository.Store
oauthBroker *OAuthBrokerService oauthBroker *OAuthBrokerService
tailscale *TailscaleService tailscale *TailscaleService
policyEngine *PolicyEngine
loginAttempts map[string]*LoginAttempt loginAttempts map[string]*LoginAttempt
ldapGroupsCache map[string]*LdapGroupsCache ldapGroupsCache map[string]*LdapGroupsCache
@@ -96,11 +98,12 @@ func NewAuthService(
config model.Config, config model.Config,
runtime model.RuntimeConfig, runtime model.RuntimeConfig,
ctx context.Context, ctx context.Context,
wg *sync.WaitGroup, dg *ding.Ding,
ldap *LdapService, ldap *LdapService,
queries repository.Store, queries repository.Store,
oauthBroker *OAuthBrokerService, oauthBroker *OAuthBrokerService,
tailscale *TailscaleService, tailscale *TailscaleService,
policy *PolicyEngine,
) *AuthService { ) *AuthService {
service := &AuthService{ service := &AuthService{
log: log, log: log,
@@ -114,9 +117,10 @@ func NewAuthService(
queries: queries, queries: queries,
oauthBroker: oauthBroker, oauthBroker: oauthBroker,
tailscale: tailscale, tailscale: tailscale,
policyEngine: policy,
} }
wg.Go(service.CleanupOAuthSessionsRoutine) dg.Go(service.cleanupOAuthSessions, ding.RingMinor)
return service return service
} }
@@ -285,18 +289,27 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
} }
} }
// We could also directly access the policyEngine.effectToAccess but
// I believe it's better to use the exported functions instead
func (auth *AuthService) IsEmailWhitelisted(provider string, email string) bool { func (auth *AuthService) IsEmailWhitelisted(provider string, email string) bool {
return auth.policyEngine.EvaluateFunc(func() Effect {
whitelist := auth.runtime.OAuthWhitelist whitelist := auth.runtime.OAuthWhitelist
if providerConfig, ok := auth.runtime.OAuthProviders[provider]; ok && len(providerConfig.Whitelist) > 0 { if providerConfig, ok := auth.runtime.OAuthProviders[provider]; ok && len(providerConfig.Whitelist) > 0 {
whitelist = providerConfig.Whitelist whitelist = providerConfig.Whitelist
} }
match, err := utils.CheckFilter(strings.Join(whitelist, ","), email) match, err := utils.CheckFilter(strings.Join(whitelist, ","), email)
if err != nil { if err != nil {
auth.log.App.Warn().Err(err).Str("provider", provider).Str("email", email).Msg("Invalid email filter pattern") if err == utils.ErrFilterEmpty {
return false return EffectAbstain
} }
return match auth.log.App.Error().Err(err).Str("email", email).Msg("Failed to evaluate email whitelist filter, defaulting to deny")
return EffectDeny
}
if match {
return EffectAllow
}
return EffectDeny
})
} }
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) { func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
@@ -589,7 +602,7 @@ func (auth *AuthService) EndOAuthSession(sessionId string) {
auth.oauthMutex.Unlock() auth.oauthMutex.Unlock()
} }
func (auth *AuthService) CleanupOAuthSessionsRoutine() { func (auth *AuthService) cleanupOAuthSessions(ctx context.Context) {
auth.log.App.Debug().Msg("Starting OAuth session cleanup routine") auth.log.App.Debug().Msg("Starting OAuth session cleanup routine")
ticker := time.NewTicker(30 * time.Minute) ticker := time.NewTicker(30 * time.Minute)
@@ -612,7 +625,7 @@ func (auth *AuthService) CleanupOAuthSessionsRoutine() {
auth.oauthMutex.Unlock() auth.oauthMutex.Unlock()
auth.log.App.Debug().Msg("OAuth session cleanup completed") auth.log.App.Debug().Msg("OAuth session cleanup completed")
case <-auth.context.Done(): case <-ctx.Done():
auth.log.App.Debug().Msg("Stopping OAuth session cleanup routine") auth.log.App.Debug().Msg("Stopping OAuth session cleanup routine")
return return
} }
+5 -5
View File
@@ -3,8 +3,8 @@ package service
import ( import (
"context" "context"
"strings" "strings"
"sync"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders" "github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
@@ -24,7 +24,7 @@ type DockerService struct {
func NewDockerService( func NewDockerService(
log *logger.Logger, log *logger.Logger,
ctx context.Context, ctx context.Context,
wg *sync.WaitGroup, dg *ding.Ding,
) (*DockerService, error) { ) (*DockerService, error) {
client, err := client.NewClientWithOpts(client.FromEnv) client, err := client.NewClientWithOpts(client.FromEnv)
@@ -50,7 +50,7 @@ func NewDockerService(
service.isConnected = true service.isConnected = true
service.log.App.Debug().Msg("Docker connected successfully") service.log.App.Debug().Msg("Docker connected successfully")
wg.Go(service.watchAndClose) dg.Go(service.watchAndClose, ding.RingMajor)
return service, nil return service, nil
} }
@@ -108,8 +108,8 @@ func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) {
return nil, nil return nil, nil
} }
func (docker *DockerService) watchAndClose() { func (docker *DockerService) watchAndClose(ctx context.Context) {
<-docker.context.Done() <-ctx.Done()
docker.log.App.Debug().Msg("Closing Docker client") docker.log.App.Debug().Msg("Closing Docker client")
if docker.client != nil { if docker.client != nil {
err := docker.client.Close() err := docker.client.Close()
+16 -17
View File
@@ -8,6 +8,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders" "github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
@@ -38,7 +39,6 @@ type ingressApp struct {
type KubernetesService struct { type KubernetesService struct {
log *logger.Logger log *logger.Logger
ctx context.Context
client dynamic.Interface client dynamic.Interface
started bool started bool
@@ -51,7 +51,7 @@ type KubernetesService struct {
func NewKubernetesService( func NewKubernetesService(
log *logger.Logger, log *logger.Logger,
ctx context.Context, ctx context.Context,
wg *sync.WaitGroup, dg *ding.Ding,
) (*KubernetesService, error) { ) (*KubernetesService, error) {
cfg, err := rest.InClusterConfig() cfg, err := rest.InClusterConfig()
if err != nil { if err != nil {
@@ -82,16 +82,15 @@ func NewKubernetesService(
service := &KubernetesService{ service := &KubernetesService{
log: log, log: log,
ctx: ctx,
client: client, client: client,
ingressApps: make(map[ingressKey][]ingressApp), ingressApps: make(map[ingressKey][]ingressApp),
domainIndex: make(map[string]ingressAppKey), domainIndex: make(map[string]ingressAppKey),
appNameIndex: make(map[string]ingressAppKey), appNameIndex: make(map[string]ingressAppKey),
} }
wg.Go(func() { dg.Go(func(ctx context.Context) {
service.watchGVR(gvr) service.watchGVR(gvr, ctx)
}) }, ding.RingMajor)
service.started = true service.started = true
log.App.Debug().Msg("Kubernetes label provider started successfully") log.App.Debug().Msg("Kubernetes label provider started successfully")
@@ -271,8 +270,8 @@ func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
} }
} }
func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource) error { func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource, ctx context.Context) error {
ctx, cancel := context.WithTimeout(k.ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
list, err := k.client.Resource(gvr).List(ctx, metav1.ListOptions{}) list, err := k.client.Resource(gvr).List(ctx, metav1.ListOptions{})
@@ -289,10 +288,10 @@ func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource) error {
// runWatcher drains events from an active watcher until it closes or the context is done. // 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. // 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 { func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch.Interface, resyncTicker *time.Ticker, ctx context.Context) bool {
for { for {
select { select {
case <-k.ctx.Done(): case <-ctx.Done():
w.Stop() w.Stop()
return false return false
case event, ok := <-w.ResultChan(): case event, ok := <-w.ResultChan():
@@ -314,33 +313,33 @@ func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch.
k.removeIngress(item.GetNamespace(), item.GetName()) k.removeIngress(item.GetNamespace(), item.GetName())
} }
case <-resyncTicker.C: case <-resyncTicker.C:
if err := k.resyncGVR(gvr); err != nil { if err := k.resyncGVR(gvr, ctx); err != nil {
k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed during watcher run") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed during watcher run")
} }
} }
} }
} }
func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource) { func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource, ctx context.Context) {
resyncTicker := time.NewTicker(5 * time.Minute) resyncTicker := time.NewTicker(5 * time.Minute)
defer resyncTicker.Stop() defer resyncTicker.Stop()
if err := k.resyncGVR(gvr); err != nil { if err := k.resyncGVR(gvr, ctx); err != nil {
k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Initial resync failed, will retry") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Initial resync failed, will retry")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
} }
for { for {
select { select {
case <-k.ctx.Done(): case <-ctx.Done():
k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Shutting down kubernetes watcher") k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Shutting down kubernetes watcher")
return return
case <-resyncTicker.C: case <-resyncTicker.C:
if err := k.resyncGVR(gvr); err != nil { if err := k.resyncGVR(gvr, ctx); err != nil {
k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed, will retry") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed, will retry")
} }
default: default:
ctx, cancel := context.WithCancel(k.ctx) ctx, cancel := context.WithCancel(ctx)
watcher, err := k.client.Resource(gvr).Watch(ctx, metav1.ListOptions{}) watcher, err := k.client.Resource(gvr).Watch(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to start watcher, will retry") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to start watcher, will retry")
@@ -349,7 +348,7 @@ func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource) {
continue continue
} }
k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher started successfully") k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher started successfully")
if !k.runWatcher(gvr, watcher, resyncTicker) { if !k.runWatcher(gvr, watcher, resyncTicker, ctx) {
cancel() cancel()
return return
} }
+5 -7
View File
@@ -9,6 +9,7 @@ import (
"github.com/cenkalti/backoff/v5" "github.com/cenkalti/backoff/v5"
ldapgo "github.com/go-ldap/ldap/v3" ldapgo "github.com/go-ldap/ldap/v3"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
) )
@@ -16,7 +17,6 @@ import (
type LdapService struct { type LdapService struct {
log *logger.Logger log *logger.Logger
config model.Config config model.Config
context context.Context
conn *ldapgo.Conn conn *ldapgo.Conn
mutex sync.RWMutex mutex sync.RWMutex
@@ -26,8 +26,7 @@ type LdapService struct {
func NewLdapService( func NewLdapService(
log *logger.Logger, log *logger.Logger,
config model.Config, config model.Config,
ctx context.Context, dg *ding.Ding,
wg *sync.WaitGroup,
) (*LdapService, error) { ) (*LdapService, error) {
if config.LDAP.Address == "" { if config.LDAP.Address == "" {
return nil, nil return nil, nil
@@ -36,7 +35,6 @@ func NewLdapService(
ldap := &LdapService{ ldap := &LdapService{
log: log, log: log,
config: config, config: config,
context: ctx,
} }
// Check whether authentication with client certificate is possible // Check whether authentication with client certificate is possible
@@ -69,7 +67,7 @@ func NewLdapService(
return nil, fmt.Errorf("failed to connect to ldap server: %w", err) return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
} }
wg.Go(func() { dg.Go(func(ctx context.Context) {
ldap.log.App.Debug().Msg("Starting LDAP connection heartbeat routine") ldap.log.App.Debug().Msg("Starting LDAP connection heartbeat routine")
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
@@ -87,12 +85,12 @@ func NewLdapService(
} }
ldap.log.App.Info().Msg("Successfully reconnected to LDAP server") ldap.log.App.Info().Msg("Successfully reconnected to LDAP server")
} }
case <-ldap.context.Done(): case <-ctx.Done():
ldap.log.App.Debug().Msg("LDAP service context cancelled, stopping heartbeat") ldap.log.App.Debug().Msg("LDAP service context cancelled, stopping heartbeat")
return return
} }
} }
}) }, ding.RingMajor)
return ldap, nil return ldap, nil
} }
+10 -13
View File
@@ -15,13 +15,13 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"slices" "slices"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
@@ -116,7 +116,6 @@ type OIDCService struct {
config model.Config config model.Config
runtime model.RuntimeConfig runtime model.RuntimeConfig
queries repository.Store queries repository.Store
context context.Context
clients map[string]model.OIDCClientConfig clients map[string]model.OIDCClientConfig
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
@@ -129,8 +128,7 @@ func NewOIDCService(
config model.Config, config model.Config,
runtime model.RuntimeConfig, runtime model.RuntimeConfig,
queries repository.Store, queries repository.Store,
ctx context.Context, dg *ding.Ding) (*OIDCService, error) {
wg *sync.WaitGroup) (*OIDCService, error) {
// If not configured, skip init // If not configured, skip init
if len(runtime.OIDCClients) == 0 { if len(runtime.OIDCClients) == 0 {
return nil, nil return nil, nil
@@ -276,7 +274,6 @@ func NewOIDCService(
config: config, config: config,
runtime: runtime, runtime: runtime,
queries: queries, queries: queries,
context: ctx,
clients: clients, clients: clients,
privateKey: privateKey, privateKey: privateKey,
@@ -285,7 +282,7 @@ func NewOIDCService(
} }
// Start cleanup routine // Start cleanup routine
wg.Go(service.cleanupRoutine) dg.Go(service.cleanupRoutine, ding.RingMinor)
return service, nil return service, nil
} }
@@ -759,7 +756,7 @@ func (service *OIDCService) DeleteOldSession(ctx context.Context, sub string) er
} }
// Cleanup routine - Resource heavy due to the linked tables // Cleanup routine - Resource heavy due to the linked tables
func (service *OIDCService) cleanupRoutine() { func (service *OIDCService) cleanupRoutine(ctx context.Context) {
service.log.App.Debug().Msg("Starting OIDC cleanup routine") service.log.App.Debug().Msg("Starting OIDC cleanup routine")
ticker := time.NewTicker(time.Duration(30) * time.Minute) ticker := time.NewTicker(time.Duration(30) * time.Minute)
defer ticker.Stop() defer ticker.Stop()
@@ -772,7 +769,7 @@ func (service *OIDCService) cleanupRoutine() {
currentTime := time.Now().Unix() currentTime := time.Now().Unix()
// For the OIDC tokens, if they are expired we delete the userinfo and codes // For the OIDC tokens, if they are expired we delete the userinfo and codes
expiredTokens, err := service.queries.DeleteExpiredOidcTokens(service.context, repository.DeleteExpiredOidcTokensParams{ expiredTokens, err := service.queries.DeleteExpiredOidcTokens(ctx, repository.DeleteExpiredOidcTokensParams{
TokenExpiresAt: currentTime, TokenExpiresAt: currentTime,
RefreshTokenExpiresAt: currentTime, RefreshTokenExpiresAt: currentTime,
}) })
@@ -782,21 +779,21 @@ func (service *OIDCService) cleanupRoutine() {
} }
for _, expiredToken := range expiredTokens { for _, expiredToken := range expiredTokens {
err := service.DeleteOldSession(service.context, expiredToken.Sub) err := service.DeleteOldSession(ctx, expiredToken.Sub)
if err != nil { if err != nil {
service.log.App.Warn().Err(err).Msg("Failed to delete session for expired token") service.log.App.Warn().Err(err).Msg("Failed to delete session for expired token")
} }
} }
// For expired codes, we need to get the sub, check if tokens are expired and if they are remove everything // For expired codes, we need to get the sub, check if tokens are expired and if they are remove everything
expiredCodes, err := service.queries.DeleteExpiredOidcCodes(service.context, currentTime) expiredCodes, err := service.queries.DeleteExpiredOidcCodes(ctx, currentTime)
if err != nil { if err != nil {
service.log.App.Warn().Err(err).Msg("Failed to delete expired codes") service.log.App.Warn().Err(err).Msg("Failed to delete expired codes")
} }
for _, expiredCode := range expiredCodes { for _, expiredCode := range expiredCodes {
token, err := service.queries.GetOidcTokenBySub(service.context, expiredCode.Sub) token, err := service.queries.GetOidcTokenBySub(ctx, expiredCode.Sub)
if err != nil { if err != nil {
if !errors.Is(err, repository.ErrNotFound) { if !errors.Is(err, repository.ErrNotFound) {
@@ -806,7 +803,7 @@ func (service *OIDCService) cleanupRoutine() {
} }
if token.TokenExpiresAt < currentTime && token.RefreshTokenExpiresAt < currentTime { if token.TokenExpiresAt < currentTime && token.RefreshTokenExpiresAt < currentTime {
err := service.DeleteOldSession(service.context, expiredCode.Sub) err := service.DeleteOldSession(ctx, expiredCode.Sub)
if err != nil { if err != nil {
service.log.App.Warn().Err(err).Msg("Failed to delete session for expired code") service.log.App.Warn().Err(err).Msg("Failed to delete session for expired code")
} }
@@ -814,7 +811,7 @@ func (service *OIDCService) cleanupRoutine() {
} }
service.log.App.Debug().Msg("Finished OIDC cleanup routine") service.log.App.Debug().Msg("Finished OIDC cleanup routine")
case <-service.context.Done(): case <-ctx.Done():
service.log.App.Debug().Msg("Stopping OIDC cleanup routine") service.log.App.Debug().Msg("Stopping OIDC cleanup routine")
return return
} }
+3 -3
View File
@@ -3,9 +3,9 @@ package service_test
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"sync"
"testing" "testing"
"github.com/steveiliop56/ding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -70,9 +70,9 @@ func TestCompileUserinfo(t *testing.T) {
log.Init() log.Init()
ctx := context.TODO() ctx := context.TODO()
wg := &sync.WaitGroup{} dg := ding.New(ctx)
svc, err := service.NewOIDCService(log, cfg, runtime, nil, ctx, wg) svc, err := service.NewOIDCService(log, cfg, runtime, nil, dg)
require.NoError(t, err) require.NoError(t, err)
type testCase struct { type testCase struct {
+4
View File
@@ -108,3 +108,7 @@ func (engine *PolicyEngine) Policy() Policy {
func (engine *PolicyEngine) Rules() map[RuleName]Rule { func (engine *PolicyEngine) Rules() map[RuleName]Rule {
return engine.rules return engine.rules
} }
func (engine *PolicyEngine) EvaluateFunc(f func() Effect) bool {
return engine.effectToAccess(f())
}
+5 -6
View File
@@ -9,6 +9,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"tailscale.com/client/local" "tailscale.com/client/local"
@@ -25,7 +26,6 @@ type TailscaleWhoisResponse struct {
type TailscaleService struct { type TailscaleService struct {
log *logger.Logger log *logger.Logger
wg *sync.WaitGroup
config model.Config config model.Config
ctx context.Context ctx context.Context
@@ -35,7 +35,7 @@ type TailscaleService struct {
mu sync.Mutex mu sync.Mutex
} }
func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, wg *sync.WaitGroup) (*TailscaleService, error) { func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, dg *ding.Ding) (*TailscaleService, error) {
if !config.Tailscale.Enabled { if !config.Tailscale.Enabled {
return nil, nil return nil, nil
} }
@@ -67,7 +67,6 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co
service := &TailscaleService{ service := &TailscaleService{
log: log, log: log,
wg: wg,
config: config, config: config,
ctx: ctx, ctx: ctx,
srv: srv, srv: srv,
@@ -84,13 +83,13 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co
return nil, fmt.Errorf("failed to connect to tailscale network: %w", err) return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
} }
wg.Go(service.watchAndClose) dg.Go(service.watchAndClose, ding.RingMajor)
return service, nil return service, nil
} }
func (ts *TailscaleService) watchAndClose() { func (ts *TailscaleService) watchAndClose(ctx context.Context) {
<-ts.ctx.Done() <-ctx.Done()
ts.log.App.Debug().Msg("Shutting down Tailscale service") ts.log.App.Debug().Msg("Shutting down Tailscale service")
ts.mu.Lock() ts.mu.Lock()
srv := ts.srv srv := ts.srv
+2 -5
View File
@@ -3,7 +3,6 @@ package loaders
import ( import (
"os" "os"
"github.com/rs/zerolog/log"
"github.com/tinyauthapp/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/tinyauthapp/paerser/file" "github.com/tinyauthapp/paerser/file"
"github.com/tinyauthapp/paerser/flag" "github.com/tinyauthapp/paerser/flag"
@@ -19,8 +18,8 @@ func (f *FileLoader) Load(args []string, cmd *cli.Command) (bool, error) {
} }
// I guess we are using traefik as the root name (we can't change it) // I guess we are using traefik as the root name (we can't change it)
configFileFlag := "traefik.experimental.configfile" configFileFlag := "traefik.configfile"
envVar := "TINYAUTH_EXPERIMENTAL_CONFIGFILE" envVar := "TINYAUTH_CONFIGFILE"
if _, ok := flags[configFileFlag]; !ok { if _, ok := flags[configFileFlag]; !ok {
if value := os.Getenv(envVar); value != "" { if value := os.Getenv(envVar); value != "" {
@@ -30,8 +29,6 @@ func (f *FileLoader) Load(args []string, cmd *cli.Command) (bool, error) {
} }
} }
log.Warn().Msg("Using experimental file config loader, this feature is experimental and may change or be removed in future releases")
err = file.Decode(flags[configFileFlag], cmd.Configuration) err = file.Decode(flags[configFileFlag], cmd.Configuration)
if err != nil { if err != nil {
+6 -1
View File
@@ -3,6 +3,7 @@ package utils
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"net" "net"
"regexp" "regexp"
@@ -11,6 +12,10 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
var (
ErrFilterEmpty = errors.New("filter is empty")
)
func GetSecret(conf string, file string) string { func GetSecret(conf string, file string) string {
if conf == "" && file == "" { if conf == "" && file == "" {
return "" return ""
@@ -78,7 +83,7 @@ func CheckIPFilter(filter string, ip string) (bool, error) {
func CheckFilter(filter string, input string) (bool, error) { func CheckFilter(filter string, input string) (bool, error) {
if len(strings.TrimSpace(filter)) == 0 { if len(strings.TrimSpace(filter)) == 0 {
return false, fmt.Errorf("filter is empty") return false, ErrFilterEmpty
} }
if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") { if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") {
+133
View File
@@ -0,0 +1,133 @@
-- name: CreateOidcCode :one
INSERT INTO "oidc_codes" (
"sub",
"code_hash",
"scope",
"redirect_uri",
"client_id",
"expires_at",
"nonce",
"code_challenge"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
RETURNING *;
-- name: GetOidcCodeUnsafe :one
SELECT * FROM "oidc_codes"
WHERE "code_hash" = $1;
-- name: GetOidcCode :one
DELETE FROM "oidc_codes"
WHERE "code_hash" = $1
RETURNING *;
-- name: GetOidcCodeBySubUnsafe :one
SELECT * FROM "oidc_codes"
WHERE "sub" = $1;
-- name: GetOidcCodeBySub :one
DELETE FROM "oidc_codes"
WHERE "sub" = $1
RETURNING *;
-- name: DeleteOidcCode :exec
DELETE FROM "oidc_codes"
WHERE "code_hash" = $1;
-- name: DeleteOidcCodeBySub :exec
DELETE FROM "oidc_codes"
WHERE "sub" = $1;
-- name: CreateOidcToken :one
INSERT INTO "oidc_tokens" (
"sub",
"access_token_hash",
"refresh_token_hash",
"scope",
"client_id",
"token_expires_at",
"refresh_token_expires_at",
"code_hash",
"nonce"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9
)
RETURNING *;
-- name: UpdateOidcTokenByRefreshToken :one
UPDATE "oidc_tokens" SET
"access_token_hash" = $1,
"refresh_token_hash" = $2,
"token_expires_at" = $3,
"refresh_token_expires_at" = $4
WHERE "refresh_token_hash" = $5
RETURNING *;
-- name: GetOidcToken :one
SELECT * FROM "oidc_tokens"
WHERE "access_token_hash" = $1;
-- name: GetOidcTokenByRefreshToken :one
SELECT * FROM "oidc_tokens"
WHERE "refresh_token_hash" = $1;
-- name: GetOidcTokenBySub :one
SELECT * FROM "oidc_tokens"
WHERE "sub" = $1;
-- name: DeleteOidcTokenByCodeHash :exec
DELETE FROM "oidc_tokens"
WHERE "code_hash" = $1;
-- name: DeleteOidcToken :exec
DELETE FROM "oidc_tokens"
WHERE "access_token_hash" = $1;
-- name: DeleteOidcTokenBySub :exec
DELETE FROM "oidc_tokens"
WHERE "sub" = $1;
-- name: CreateOidcUserInfo :one
INSERT INTO "oidc_userinfo" (
"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"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
)
RETURNING *;
-- name: GetOidcUserInfo :one
SELECT * FROM "oidc_userinfo"
WHERE "sub" = $1;
-- name: DeleteOidcUserInfo :exec
DELETE FROM "oidc_userinfo"
WHERE "sub" = $1;
-- name: DeleteExpiredOidcCodes :many
DELETE FROM "oidc_codes"
WHERE "expires_at" < $1
RETURNING *;
-- name: DeleteExpiredOidcTokens :many
DELETE FROM "oidc_tokens"
WHERE "token_expires_at" < $1 AND "refresh_token_expires_at" < $2
RETURNING *;
+44
View File
@@ -0,0 +1,44 @@
CREATE TABLE IF NOT EXISTS "oidc_codes" (
"sub" TEXT NOT NULL UNIQUE,
"code_hash" TEXT NOT NULL PRIMARY KEY,
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" BIGINT NOT NULL,
"nonce" TEXT NOT NULL DEFAULT '',
"code_challenge" TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
"sub" TEXT NOT NULL UNIQUE,
"access_token_hash" TEXT NOT NULL PRIMARY KEY,
"refresh_token_hash" TEXT NOT NULL,
"code_hash" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_expires_at" BIGINT NOT NULL,
"refresh_token_expires_at" BIGINT NOT NULL,
"nonce" TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
"sub" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"preferred_username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"groups" TEXT NOT NULL,
"updated_at" BIGINT 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
);
+43
View File
@@ -0,0 +1,43 @@
-- name: CreateSession :one
INSERT INTO "sessions" (
"uuid",
"username",
"email",
"name",
"provider",
"totp_pending",
"oauth_groups",
"expiry",
"created_at",
"oauth_name",
"oauth_sub"
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
)
RETURNING *;
-- name: GetSession :one
SELECT * FROM "sessions"
WHERE "uuid" = $1;
-- name: DeleteSession :exec
DELETE FROM "sessions"
WHERE "uuid" = $1;
-- name: UpdateSession :one
UPDATE "sessions" SET
"username" = $1,
"email" = $2,
"name" = $3,
"provider" = $4,
"totp_pending" = $5,
"oauth_groups" = $6,
"expiry" = $7,
"oauth_name" = $8,
"oauth_sub" = $9
WHERE "uuid" = $10
RETURNING *;
-- name: DeleteExpiredSessions :exec
DELETE FROM "sessions"
WHERE "expiry" < $1;
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS "sessions" (
"uuid" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"totp_pending" BOOLEAN NOT NULL,
"oauth_groups" TEXT NOT NULL DEFAULT '',
"expiry" BIGINT NOT NULL,
"created_at" BIGINT NOT NULL,
"oauth_name" TEXT NOT NULL DEFAULT '',
"oauth_sub" TEXT NOT NULL DEFAULT ''
);
+13
View File
@@ -28,3 +28,16 @@ sql:
go_type: "string" go_type: "string"
- column: "oidc_codes.code_challenge" - column: "oidc_codes.code_challenge"
go_type: "string" go_type: "string"
- engine: "postgresql"
queries: "sql/postgres/*_queries.sql"
schema: "sql/postgres/*_schemas.sql"
gen:
go:
package: "postgres"
out: "internal/repository/postgres"
rename:
uuid: "UUID"
oauth_groups: "OAuthGroups"
oauth_name: "OAuthName"
oauth_sub: "OAuthSub"
redirect_uri: "RedirectURI"