Compare commits

..

12 Commits

Author SHA1 Message Date
Stavros 5649aea507 chore: add linter to ci 2026-05-09 18:02:08 +03:00
Stavros 3da9a5b18b feat: add golangci-lint for go linting 2026-05-09 18:00:41 +03:00
github-actions[bot] 1b18e68ce0 docs: regenerate readme sponsors list (#841)
Co-authored-by: GitHub <noreply@github.com>
2026-05-07 16:53:07 +03:00
djedditt 6602b52f85 feat: add support for oauth whitelist file (#817) (#826)
* feat: add support for oauth whitelist file (#817)

* Merge branch 'main' into feat/oauth-whitelist-file

* fix: fix conflicts

* tests: use testify for testing

---------

Co-authored-by: Stavros <steveiliop56@gmail.com>
2026-05-07 16:35:38 +03:00
dependabot[bot] a8a98bd8d5 chore(deps): bump the minor-patch group across 1 directory with 3 updates (#827)
Bumps the minor-patch group with 3 updates in the / directory: [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery), [k8s.io/client-go](https://github.com/kubernetes/client-go) and [modernc.org/sqlite](https://gitlab.com/cznic/sqlite).


Updates `k8s.io/apimachinery` from 0.32.2 to 0.36.0
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.2...v0.36.0)

Updates `k8s.io/client-go` from 0.32.2 to 0.36.0
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.32.2...v0.36.0)

Updates `modernc.org/sqlite` from 1.49.1 to 1.50.0
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.49.1...v1.50.0)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: k8s.io/client-go
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: modernc.org/sqlite
  dependency-version: 1.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 16:13:04 +03:00
Jacek Kowalski ca6a7fa551 feat: add option to run tinyauth on a top-level domain (#710)
* Add TINYAUTH_AUTH_SUBDOMAINSENABLED option

Setting it to false allows to use Tinyauth on top-level domain only,
but forbids automatic cross-app authentication using Traefik/Nginx.

* fix: inform services and controllers if subdomain cookie domain is enabled

* chore: rabbit feedback

* fix: deny ip addresses for standalone domain

---------

Co-authored-by: Stavros <steveiliop56@gmail.com>
2026-05-07 16:12:24 +03:00
Stavros 1382ab41e7 refactor: rework user context handling throughout tinyauth (#829)
* wip

* fix: fix util imports

* fix: fix bootstrap import issues

* fix: fix cli imports

* fix: context controller

* fix: use new context in user controller

* fix: fix imports and context in proxy controller

* fix: fix oauth and oidc controller imports and context

* feat: finalize context functionality

* refactor: simplify acls checking logic by passing the entire acl struct

* chore: rename get basic auth to encode basic auth for clarity

* fix: fix controller tests

* tests: fix service tests

* tests: fix utils tests

* tests: move to testify for testing in utils

* fix: fix config reference generator

* tests: add tests for context parsing

* tests: add tests for context middleware

* tests: remove error wrapper from context tests

* tests: fix log wrapper tests

* fix: fix verion setting in cd and dockerfiles

* fix: review comments batch 1

* fix: review comments batch 2

* fix: review comments batch 3

* fix: delete totp pending session cookie on totp success

* tests: fix user controller tests

* fix: don't audit login too early

* fix: own comments
2026-05-07 15:41:07 +03:00
dependabot[bot] 24f2da4e58 chore(deps): bump github/codeql-action from 4.35.2 to 4.35.3 (#837)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.2 to 4.35.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 18:49:37 +03:00
Contre 956d2f55c3 feat(access-control): Add support for Kubernetes Label (#627)
* feat(access-control): Add support for Kubernetes Label

* feat(access-control): Defaults to Docker

* feat(access-control): Remove kubeconfig fallback

* feat(watcher): Watcher for kubernetes service

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

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

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

* feat(go): go mod tidy

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

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

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

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

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

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

* feat(bootstrap): Remove dockerService from bootstrap svc

* feat(auth_svc): Remove dockerService from authservice

* feat(test): Add tests for kubernetes_services

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

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

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

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

* fix(refactor): Move interface to acess_controls_service

* feat: Autodetect labelprovider if TINYAUTH_LABELPROVIDER not set

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

* fix: service bootstrap import after merge

* fix: service bootstrap import after merge
2026-04-29 16:16:21 +03:00
Stavros 5e822d99e1 chore: fix typos in oidc service 2026-04-29 16:08:21 +03:00
Stavros 373ee8806e chore: prefer errors.is instead of comparison 2026-04-29 16:04:27 +03:00
Stavros a14d64c8ba chore: remove exp slices package and use stdlib 2026-04-29 15:56:35 +03:00
19 changed files with 59 additions and 41 deletions
+5
View File
@@ -49,6 +49,11 @@ jobs:
run: |
cp -r frontend/dist internal/assets/dist
- name: Lint backend
uses: golangci/golangci-lint-action@v9
with:
version: v2.12
- name: Run tests
run: go test -coverprofile=coverage.txt -v ./...
+17
View File
@@ -0,0 +1,17 @@
version: "2"
linters:
settings:
errcheck:
exclude-functions:
- (http.ResponseWriter).Write
- (http.ResponseWriter).WriteString
- (github.com/gin-gonic/gin.ResponseWriter).Write
- (github.com/gin-gonic/gin.ResponseWriter).WriteString
exclusions:
rules:
- linters:
- errcheck
text: "//nolint:errcheck"
- linters:
- staticcheck
text: "//nolint:staticcheck"
+1 -1
View File
@@ -65,7 +65,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
A big thank you to the following people for providing me with more coffee:
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/ax-mad"><img src="https:&#x2F;&#x2F;github.com&#x2F;ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a>&nbsp;&nbsp;<a href="https://github.com/stegratech"><img src="https:&#x2F;&#x2F;github.com&#x2F;stegratech.png" width="64px" alt="User avatar: stegratech" /></a>&nbsp;&nbsp;<!-- sponsors -->
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/ax-mad"><img src="https:&#x2F;&#x2F;github.com&#x2F;ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a>&nbsp;&nbsp;<a href="https://github.com/stegratech"><img src="https:&#x2F;&#x2F;github.com&#x2F;stegratech.png" width="64px" alt="User avatar: stegratech" /></a>&nbsp;&nbsp;<a href="https://github.com/apearson"><img src="https:&#x2F;&#x2F;github.com&#x2F;apearson.png" width="64px" alt="User avatar: apearson" /></a>&nbsp;&nbsp;<!-- sponsors -->
## Acknowledgements
+1 -4
View File
@@ -68,10 +68,7 @@ func generateTotpCmd() *cli.Command {
return fmt.Errorf("failed to parse user: %w", err)
}
docker := false
if strings.Contains(tCfg.User, "$$") {
docker = true
}
docker := strings.Contains(tCfg.User, "$$")
if user.TOTPSecret != "" {
return fmt.Errorf("user already has a TOTP secret")
+3 -3
View File
@@ -9,8 +9,8 @@ import (
"os"
"time"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/tinyauthapp/paerser/cli"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
)
type healthzResponse struct {
@@ -45,7 +45,7 @@ func healthcheckCmd() *cli.Command {
}
if appUrl == "" {
return errors.New("Could not determine app URL")
return errors.New("could not determine app url")
}
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
@@ -70,7 +70,7 @@ func healthcheckCmd() *cli.Command {
return fmt.Errorf("service is not healthy, got: %s", resp.Status)
}
defer resp.Body.Close()
defer resp.Body.Close() //nolint:errcheck
var healthResp healthzResponse
+1 -1
View File
@@ -292,7 +292,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
continue
}
res.Body.Close()
res.Body.Close() //nolint:errcheck
if res.StatusCode != 200 && res.StatusCode != 201 {
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
+1 -1
View File
@@ -36,7 +36,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
if err != nil {
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
ldapService.Unconfigure()
ldapService.Unconfigure() //nolint:errcheck
}
services.ldapService = ldapService
+6
View File
@@ -166,6 +166,12 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
user, err := controller.auth.GetOAuthUserinfo(sessionIdCookie)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
if user.Email == "" {
tlog.App.Error().Msg("OAuth provider did not return an email")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
+6 -6
View File
@@ -179,7 +179,7 @@ func TestUserController(t *testing.T) {
{
description: "Should rate limit on 3 invalid attempts",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { //nolint:staticcheck
loginReq := controller.LoginRequest{
Username: "testuser",
Password: "wrongpassword",
@@ -201,7 +201,7 @@ func TestUserController(t *testing.T) {
}
// 4th attempt should be rate limited
recorder = httptest.NewRecorder()
recorder = httptest.NewRecorder() //nolint:staticcheck
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
req.Header.Set("Content-Type", "application/json")
@@ -293,7 +293,7 @@ func TestUserController(t *testing.T) {
middlewares: []gin.HandlerFunc{
totpCtx,
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { //nolint:staticcheck
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
UUID: "test-totp-login-uuid",
Username: "test",
@@ -316,7 +316,7 @@ func TestUserController(t *testing.T) {
totpReqBody, err := json.Marshal(totpReq)
assert.NoError(t, err)
recorder = httptest.NewRecorder()
recorder = httptest.NewRecorder() //nolint:staticcheck
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
req.Header.Set("Content-Type", "application/json")
req.AddCookie(&http.Cookie{
@@ -345,7 +345,7 @@ func TestUserController(t *testing.T) {
middlewares: []gin.HandlerFunc{
totpCtx,
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { //nolint:staticcheck
for range 3 {
totpReq := controller.TotpRequest{
Code: "000000", // invalid code
@@ -354,7 +354,7 @@ func TestUserController(t *testing.T) {
totpReqBody, err := json.Marshal(totpReq)
assert.NoError(t, err)
recorder = httptest.NewRecorder()
recorder = httptest.NewRecorder() //nolint:staticcheck
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
req.Header.Set("Content-Type", "application/json")
+1 -1
View File
@@ -171,7 +171,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
}
if !m.auth.IsEmailWhitelisted(userContext.OAuth.Email) {
m.auth.DeleteSession(ctx, uuid)
m.auth.DeleteSession(ctx, uuid) //nolint:errcheck
return nil, nil, fmt.Errorf("email from session cookie not whitelisted: %s", userContext.OAuth.Email)
}
}
-7
View File
@@ -845,10 +845,3 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() {
}
auth.loginMutex.Unlock()
}
func (auth *AuthService) getCookieDomain() string {
if auth.config.SubdomainsEnabled {
return "." + auth.config.CookieDomain
}
return auth.config.CookieDomain
}
+1 -1
View File
@@ -269,7 +269,7 @@ func (ldap *LdapService) reconnect() error {
exp.Reset()
operation := func() (*ldapgo.Conn, error) {
ldap.conn.Close()
ldap.conn.Close() //nolint:errcheck
conn, err := ldap.connect()
if err != nil {
return nil, err
+1 -1
View File
@@ -92,7 +92,7 @@ func simpleReq[T any](client *http.Client, url string, headers map[string]string
if err != nil {
return nil, err
}
defer res.Body.Close()
defer res.Body.Close() //nolint:errcheck
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("request failed with status: %s", res.Status)
+1 -1
View File
@@ -18,7 +18,7 @@ func TestReadFile(t *testing.T) {
err = file.Close()
require.NoError(t, err)
defer os.Remove("/tmp/tinyauth_test_file")
defer os.Remove("/tmp/tinyauth_test_file") //nolint:errcheck
// Normal case
content, err := ReadFile("/tmp/tinyauth_test_file")
+1 -1
View File
@@ -53,7 +53,7 @@ func FilterIP(filter string, ip string) (bool, error) {
return false, errors.New("invalid IP address")
}
filter = strings.Replace(filter, "-", "/", -1)
filter = strings.ReplaceAll(filter, "-", "/")
if strings.Contains(filter, "/") {
_, cidr, err := net.ParseCIDR(filter)
+1 -1
View File
@@ -19,7 +19,7 @@ func TestGetSecret(t *testing.T) {
err = file.Close()
require.NoError(t, err)
defer os.Remove("/tmp/tinyauth_test_secret")
defer os.Remove("/tmp/tinyauth_test_secret") //nolint:errcheck
// Get from config
assert.Equal(t, "mysecret", utils.GetSecret("mysecret", ""))
+10 -10
View File
@@ -61,29 +61,29 @@ func TestCompileUserEmail(t *testing.T) {
func TestParseNonEmptyLines(t *testing.T) {
lines := utils.ParseNonEmptyLines(" first@example.com \n\n second@example.com \n \n")
assert.DeepEqual(t, []string{"first@example.com", "second@example.com"}, lines)
assert.Equal(t, []string{"first@example.com", "second@example.com"}, lines)
}
func TestGetStringList(t *testing.T) {
file, err := os.Create("/tmp/tinyauth_list_test_file")
assert.NilError(t, err)
assert.NoError(t, err)
_, err = file.WriteString(" third@example.com \n\n fourth@example.com \n")
assert.NilError(t, err)
assert.NoError(t, err)
err = file.Close()
assert.NilError(t, err)
defer os.Remove("/tmp/tinyauth_list_test_file")
assert.NoError(t, err)
defer os.Remove("/tmp/tinyauth_list_test_file") //nolint:errcheck
values, err := utils.GetStringList([]string{" first@example.com ", "", "second@example.com"}, "/tmp/tinyauth_list_test_file")
assert.NilError(t, err)
assert.DeepEqual(t, []string{"first@example.com", "second@example.com", "third@example.com", "fourth@example.com"}, values)
assert.NoError(t, err)
assert.Equal(t, []string{"first@example.com", "second@example.com", "third@example.com", "fourth@example.com"}, values)
values, err = utils.GetStringList(nil, "")
assert.NilError(t, err)
assert.DeepEqual(t, []string{}, values)
assert.NoError(t, err)
assert.Equal(t, []string{}, values)
values, err = utils.GetStringList(nil, "/tmp/non_existing_list_file")
assert.ErrorContains(t, err, "no such file or directory")
assert.DeepEqual(t, []string{}, values)
assert.Equal(t, []string{}, values)
}
+1 -1
View File
@@ -13,7 +13,7 @@ func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttribute
var users []model.LocalUser
if len(usersStr) == 0 {
return &users, nil
return nil, nil
}
for _, user := range usersStr {
+1 -1
View File
@@ -24,7 +24,7 @@ func TestGetUsers(t *testing.T) {
err = file.Close()
require.NoError(t, err)
defer os.Remove(tmpDir + "/tinyauth_users_test.txt")
defer os.Remove(tmpDir + "/tinyauth_users_test.txt") //nolint:errcheck
noAttrs := map[string]model.UserAttributes{}