Add a setting to allow anonymous access to individual gists while still RequireLogin everywhere else (#229)

* Add a setting to allow accessing individual gists without auth

This is a middle ground between the existing setting "Require Login",
which requires login to do anything at all, and having it off, which
shows a public list of gists and more generally allows discovering info
about the users/gists of the instance without login.

The idea of this setting is that it is "require login" for everything
except individual gists.

Fixes #228.


Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
This commit is contained in:
Jade Lovelace 2024-05-12 14:40:11 -07:00 committed by GitHub
parent 2fd053a077
commit 22052bd38f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 187 additions and 37 deletions

18
internal/auth/auth.go Normal file
View file

@ -0,0 +1,18 @@
package auth
type AuthInfoProvider interface {
RequireLogin() (bool, error)
AllowGistsWithoutLogin() (bool, error)
}
func ShouldAllowUnauthenticatedGistAccess(prov AuthInfoProvider, isSingleGistAccess bool) (bool, error) {
require, err := prov.RequireLogin()
if err != nil {
return false, err
}
allow, err := prov.AllowGistsWithoutLogin()
if err != nil {
return false, err
}
return !require || (isSingleGistAccess && allow), nil
}

View file

@ -12,6 +12,7 @@ type AdminSetting struct {
const (
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
)
@ -62,3 +63,21 @@ func initAdminSettings(settings map[string]string) error {
return nil
}
type DBAuthInfo struct{}
func (auth DBAuthInfo) RequireLogin() (bool, error) {
s, err := GetSetting(SettingRequireLogin)
if err != nil {
return true, err
}
return s == "1", nil
}
func (auth DBAuthInfo) AllowGistsWithoutLogin() (bool, error) {
s, err := GetSetting(SettingAllowGistsWithoutLogin)
if err != nil {
return false, err
}
return s == "1", nil
}

View file

@ -54,6 +54,7 @@ func Setup(dbPath string, sharedCache bool) error {
return initAdminSettings(map[string]string{
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingAllowGistsWithoutLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
})

View file

@ -173,7 +173,8 @@ admin.disable-login: Zakázat přihlášení
admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele.
admin.disable-gravatar: Zakázat Gravatar
admin.disable-gravatar_help: Zakázat použití Gravataru jako poskytovatele avatara.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Opravdu chcete smazat tohoto uživatele?
admin.gists.title: Titulek

View file

@ -185,7 +185,8 @@ admin.disable-login: 'Login-Maske deaktivieren'
admin.disable-login_help: 'Login über Login-Maske verbieten und Benutzung von OAuth Providern erzwingen.'
admin.disable-gravatar: 'Gravatar deaktivieren'
admin.disable-gravatar_help: 'Gravatar als Avatar-Anbieter deaktivieren.'
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: 'Willst du diesen Benutzer löschen?'
admin.gists.title: 'Titel'

View file

@ -203,6 +203,8 @@ admin.disable-signup: Disable signup
admin.disable-signup_help: Forbid the creation of new accounts.
admin.require-login: Require login
admin.require-login_help: Enforce users to be logged in to see gists.
admin.allow-gists-without-login: Allow individual gists without login
admin.allow-gists-without-login_help: Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
admin.disable-login: Disable login form
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
admin.disable-gravatar: Disable Gravatar

View file

@ -165,7 +165,8 @@ admin.disable-login: Deshabilitar formulario de inicio de sesión
admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar.
admin.disable-gravatar: Deshabilitar Gravatar
admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
admin.gists.title: Título

View file

@ -166,7 +166,8 @@ admin.disable-login: Désactiver le formulaire de connexion
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
admin.disable-gravatar: Désactiver Gravatar
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
admin.gists.title: Titre

View file

@ -186,7 +186,8 @@ admin.disable-login: Bejelentkezés űrlap letiltása
admin.disable-login_help: Letiltja a bejelentkezés űrlapon keresztüli bejelentkezéseket, OAuth szolgáltatókat ajánlva helyette.
admin.disable-gravatar: Gravatar kikapcsolása
admin.disable-gravatar_help: Tiltsd le a Gravatar-t mint profilkép szolgáltató.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Biztosan törlöd ezt a felhasználót?
admin.gists.title: Cím

View file

@ -163,7 +163,8 @@ admin.disable-login: Desabilitar formulário de login
admin.disable-login_help: Proibir o login através do formulário de login para forçar o uso de provedores de OAuth no lugar.
admin.disable-gravatar: Desabilitar Gravatar
admin.disable-gravatar_help: Desabilitar o uso do Gravatar como provedor de avatar.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Quer excluir este usuário?
admin.gists.title: Título

View file

@ -166,7 +166,8 @@ admin.disable-login: Запретить авторизацию по паролю
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
admin.disable-gravatar: Запретить Gravatar
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
admin.gists.title: Название

View file

@ -207,7 +207,8 @@ admin.disable-login: Disable login form
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
admin.disable-gravatar: Disable Gravatar
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: Do you want to delete this user ?
admin.gists.title: Title

View file

@ -166,7 +166,8 @@ admin.disable-login: 禁用登录表单
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
admin.disable-gravatar: 禁用 Gravatar
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: 你想要删除此用户吗?
admin.gists.title: 标题

View file

@ -177,7 +177,8 @@ admin.disable-login: 關閉登錄頁面
admin.disable-login_help: 關閉通過登錄頁面登錄,強制使用 OAuth 提供者。
admin.disable-gravatar: 禁用 Gravatar
admin.disable-gravatar_help: 禁止使用 Gravatar 作為頭像提供者。
admin.allow-gists-without-login:
admin.allow-gists-without-login_help:
admin.users.delete_confirm: 您要刪除這個使用者嗎?
admin.gists.title: 標題

View file

@ -2,14 +2,16 @@ package ssh
import (
"errors"
"io"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"io"
"os/exec"
"strings"
)
func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
@ -37,7 +39,7 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
return errors.New("gist not found")
}
requireLogin, err := db.GetSetting(db.SettingRequireLogin)
allowUnauthenticated, err := auth.ShouldAllowUnauthenticatedGistAccess(db.DBAuthInfo{}, true)
if err != nil {
return errors.New("internal server error")
}
@ -50,7 +52,7 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
if verb == "receive-pack" ||
gist.Private == 2 ||
gist.ID == 0 ||
requireLogin == "1" {
!allowUnauthenticated {
pubKey, err := db.SSHKeyExistsForUser(key, gist.UserID)
if err != nil {

View file

@ -442,3 +442,15 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
}
return ""
}
type ContextAuthInfo struct {
context echo.Context
}
func (auth ContextAuthInfo) RequireLogin() (bool, error) {
return getData(auth.context, "RequireLogin") == true, nil
}
func (auth ContextAuthInfo) AllowGistsWithoutLogin() (bool, error) {
return getData(auth.context, "AllowGistsWithoutLogin") == true, nil
}

View file

@ -19,6 +19,7 @@ import (
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
@ -70,12 +71,17 @@ func gitHttp(ctx echo.Context) error {
setData(ctx, "repositoryPath", repositoryPath)
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
if err != nil {
panic("impossible")
}
// Shows basic auth if :
// - user wants to push the gist
// - user wants to clone/pull a private gist
// - gist is not found (obfuscation)
// - admin setting to require login is set to true
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) {
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && allow {
return route.handler(ctx)
}

View file

@ -27,6 +27,7 @@ import (
"github.com/labstack/echo/v4/middleware"
"github.com/markbates/goth/gothic"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
@ -309,7 +310,7 @@ func NewServer(isDev bool) *Server {
g3 := g1.Group("/:user/:gistname")
{
g3.Use(checkRequireLogin, gistInit)
g3.Use(makeCheckRequireLogin(true), gistInit)
g3.GET("", gistIndex)
g3.GET("/rev/:revision", gistIndex)
g3.GET("/revisions", revisions)
@ -321,9 +322,9 @@ func NewServer(isDev bool) *Server {
g3.GET("/edit", edit, logged, writePermission)
g3.POST("/edit", processCreate, logged, writePermission)
g3.POST("/like", like, logged)
g3.GET("/likes", likes)
g3.GET("/likes", likes, checkRequireLogin)
g3.POST("/fork", fork, logged)
g3.GET("/forks", forks)
g3.GET("/forks", forks, checkRequireLogin)
g3.PUT("/checkbox", checkbox, logged, writePermission)
}
}
@ -516,19 +517,29 @@ func logged(next echo.HandlerFunc) echo.HandlerFunc {
}
}
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if user := getUserLogged(ctx); user != nil {
return next(ctx)
}
require := getData(ctx, "RequireLogin")
if require == true {
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
if err != nil {
panic("impossible")
}
if !allow {
addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error")
return redirect(ctx, "/login")
}
return next(ctx)
}
}
}
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
return makeCheckRequireLogin(false)(next)
}
func noRouteFound(echo.Context) error {

View file

@ -89,3 +89,61 @@ func login(t *testing.T, s *testServer, user db.UserDTO) {
err := s.request("POST", "/login", user, 302)
require.NoError(t, err)
}
type settingSet struct {
key string `form:"key"`
value string `form:"value"`
}
func TestAnonymous(t *testing.T) {
setup(t)
s, err := newTestServer()
require.NoError(t, err, "Failed to create test server")
defer teardown(t, s)
user := db.UserDTO{Username: "thomas", Password: "azeaze"}
register(t, s, user)
err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
require.NoError(t, err)
gist1 := db.GistDTO{
Title: "gist1",
Description: "my first gist",
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
err = s.request("GET", "/all", nil, 200)
require.NoError(t, err)
cookie := s.sessionCookie
s.sessionCookie = ""
err = s.request("GET", "/all", nil, 302)
require.NoError(t, err)
// Should redirect to login if RequireLogin
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 302)
require.NoError(t, err)
s.sessionCookie = cookie
err = s.request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
require.NoError(t, err)
s.sessionCookie = ""
// Should return results
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
require.NoError(t, err)
}

View file

@ -93,6 +93,17 @@
</button>
</div>
</li>
<li class="list-none gap-x-4 py-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span class="text-sm font-medium leading-6 text-slate-700 dark:text-slate-300">{{ .locale.Tr "admin.allow-gists-without-login" }}</span>
<span class="text-sm text-gray-400 dark:text-gray-400">{{ .locale.Tr "admin.allow-gists-without-login_help" }}</span>
</span>
<button type="button" id="allow-gists-without-login" data-bool="{{ .AllowGistsWithoutLogin }}" class="toggle-button {{ if .AllowGistsWithoutLogin }}bg-primary-600{{else}}bg-gray-300 dark:bg-gray-400{{end}} relative inline-flex h-6 w-11 ml-4 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description">
<span aria-hidden="true" class="{{ if .AllowGistsWithoutLogin }}translate-x-5{{else}}translate-x-0{{end}} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</li>
<li class="list-none gap-x-4 py-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">