Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] master from thomiceli:master #5

Merged
merged 2 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Fix perms for http/ssh clone (thomiceli#288)
  • Loading branch information
thomiceli authored May 27, 2024
commit 38892d8a4a4115b6fcb399dc6e3c0fa7ecb3dab5
2 changes: 1 addition & 1 deletion internal/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Run(actionType int) {
case IndexGists:
functionToRun = indexGists
default:
panic("unhandled default case")
log.Error().Msg("Unknown action type")
}

functionToRun()
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var CmdStart = cli.Command{
Usage: "Start Opengist server",
Action: func(ctx *cli.Context) error {
Initialize(ctx)
go web.NewServer(os.Getenv("OG_DEV") == "1").Start()
go web.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions")).Start()
go ssh.Start()
select {}
},
Expand Down
13 changes: 6 additions & 7 deletions internal/db/sshkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
return sshKey, err
}

func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
sshKey := new(SSHKey)
err := db.
Where("content like ?", sshKeyContent+"%").
First(&sshKey).Error

return sshKey, err
func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
var count int64
err := db.Model(&SSHKey{}).
Where("content = ?", sshKeyContent).
Count(&count).Error
return count > 0, err
}

func (sshKey *SSHKey) Create() error {
Expand Down
9 changes: 9 additions & 0 deletions internal/db/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
return userMap, nil
}

func GetUserFromSSHKey(sshKey string) (*User, error) {
user := new(User)
err := db.
Joins("JOIN ssh_keys ON users.id = ssh_keys.user_id").
Where("ssh_keys.content = ?", sshKey).
First(&user).Error
return user, err
}

func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
key := new(SSHKey)
err := db.
Expand Down
1 change: 1 addition & 0 deletions internal/i18n/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ settings.delete-ssh-key-confirm: Confirm deletion of SSH key
settings.ssh-key-added-at: Added
settings.ssh-key-never-used: Never used
settings.ssh-key-last-used: Last used
settings.ssh-key-exists: SSH key already exists
settings.change-username: Change username
settings.create-password: Create password
settings.create-password-help: Create your password to login to Opengist via HTTP
Expand Down
11 changes: 9 additions & 2 deletions internal/ssh/git_ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,18 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
// - gist is not found (obfuscation)
// - admin setting to require login is set to true
if verb == "receive-pack" ||
gist.Private == 2 ||
gist.Private == db.PrivateVisibility ||
gist.ID == 0 ||
!allowUnauthenticated {

pubKey, err := db.SSHKeyExistsForUser(key, gist.UserID)
var userToCheckPermissions *db.User
if gist.Private != db.PrivateVisibility && verb == "upload-pack" {
userToCheckPermissions, _ = db.GetUserFromSSHKey(key)
} else {
userToCheckPermissions = &gist.User
}

pubKey, err := db.SSHKeyExistsForUser(key, userToCheckPermissions.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
Expand Down
4 changes: 2 additions & 2 deletions internal/ssh/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func Start() {
sshConfig := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
_, err := db.SSHKeyDoesExists(strKey)
if err != nil {
exists, err := db.SSHKeyDoesExists(strKey)
if !exists {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
Expand Down
11 changes: 9 additions & 2 deletions internal/web/git_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func gitHttp(ctx echo.Context) error {

allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
if err != nil {
panic("impossible")
log.Fatal().Err(err).Msg("Cannot check if unauthenticated access is allowed")
}

// Shows basic auth if :
Expand Down Expand Up @@ -105,7 +105,14 @@ func gitHttp(ctx echo.Context) error {
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
}

if ok, err := utils.Argon2id.Verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
var userToCheckPermissions *db.User
if gist.Private != db.PrivateVisibility && isPull {
userToCheckPermissions, _ = db.GetUserByUsername(authUsername)
} else {
userToCheckPermissions = &gist.User
}

if ok, err := utils.Argon2id.Verify(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
return errorRes(500, "Cannot verify password", err)
}
Expand Down
10 changes: 5 additions & 5 deletions internal/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,12 @@ type Server struct {
dev bool
}

func NewServer(isDev bool) *Server {
func NewServer(isDev bool, sessionsPath string) *Server {
dev = isDev
flashStore = sessions.NewCookieStore([]byte("opengist"))
userStore = sessions.NewFilesystemStore(path.Join(config.GetHomeDir(), "sessions"),
utils.ReadKey(path.Join(config.GetHomeDir(), "sessions", "session-auth.key")),
utils.ReadKey(path.Join(config.GetHomeDir(), "sessions", "session-encrypt.key")),
userStore = sessions.NewFilesystemStore(sessionsPath,
utils.ReadKey(path.Join(sessionsPath, "session-auth.key")),
utils.ReadKey(path.Join(sessionsPath, "session-encrypt.key")),
)
userStore.MaxLength(10 * 1024)
gothic.Store = userStore
Expand Down Expand Up @@ -526,7 +526,7 @@ func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {

allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
if err != nil {
panic("impossible")
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
}

if !allow {
Expand Down
8 changes: 8 additions & 0 deletions internal/web/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ func sshKeysProcess(ctx echo.Context) error {
}
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))

if exists, err := db.SSHKeyDoesExists(key.Content); exists {
if err != nil {
return errorRes(500, "Cannot check if SSH key exists", err)
}
addFlash(ctx, tr(ctx, "settings.ssh-key-exists"), "error")
return redirect(ctx, "/settings")
}

if err := key.Create(); err != nil {
return errorRes(500, "Cannot add SSH key", err)
}
Expand Down
169 changes: 169 additions & 0 deletions internal/web/test/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package test

import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"os"
"os/exec"
"path"
"testing"
)

Expand Down Expand Up @@ -147,3 +152,167 @@ func TestAnonymous(t *testing.T) {
require.NoError(t, err)

}

func TestGitOperations(t *testing.T) {
setup(t)
s, err := newTestServer()
require.NoError(t, err, "Failed to create test server")
defer teardown(t, s)

admin := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, admin)
s.sessionCookie = ""
register(t, s, db.UserDTO{Username: "fujiwara", Password: "fujiwara"})
s.sessionCookie = ""
register(t, s, db.UserDTO{Username: "kaguya", Password: "kaguya"})

gist1 := db.GistDTO{
Title: "kaguya-pub-gist",
URL: "kaguya-pub-gist",
Description: "kaguya's first gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PublicVisibility,
},
Name: []string{"kaguya-file.txt"},
Content: []string{
"yeah",
},
}
err = s.request("POST", "/", gist1, 302)
require.NoError(t, err)

gist2 := db.GistDTO{
Title: "kaguya-unl-gist",
URL: "kaguya-unl-gist",
Description: "kaguya's second gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.UnlistedVisibility,
},
Name: []string{"kaguya-file.txt"},
Content: []string{
"cool",
},
}
err = s.request("POST", "/", gist2, 302)
require.NoError(t, err)

gist3 := db.GistDTO{
Title: "kaguya-priv-gist",
URL: "kaguya-priv-gist",
Description: "kaguya's second gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"kaguya-file.txt"},
Content: []string{
"super",
},
}
err = s.request("POST", "/", gist3, 302)
require.NoError(t, err)

gitOperations := func(credentials, owner, url, filename string, expectErrorClone, expectErrorCheck, expectErrorPush bool) {
fmt.Println("Testing", credentials, url, expectErrorClone, expectErrorCheck, expectErrorPush)
err := clientGitClone(credentials, owner, url)
if expectErrorClone {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientCheckRepo(url, filename)
if expectErrorCheck {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientGitPush(url)
if expectErrorPush {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}

tests := []struct {
credentials string
user string
url string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "kaguya-pub-gist", false, false, true},
{":", "kaguya", "kaguya-unl-gist", false, false, true},
{":", "kaguya", "kaguya-priv-gist", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
}

for _, test := range tests {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}

login(t, s, admin)
err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
require.NoError(t, err)

testsRequireLogin := []struct {
credentials string
user string
url string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "kaguya-pub-gist", true, true, true},
{":", "kaguya", "kaguya-unl-gist", true, true, true},
{":", "kaguya", "kaguya-priv-gist", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
}

for _, test := range testsRequireLogin {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}

login(t, s, admin)
err = s.request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
require.NoError(t, err)

for _, test := range tests {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
}

func clientGitClone(creds string, user string, url string) error {
return exec.Command("git", "clone", "https://"+creds+"@localhost:6157/"+user+"/"+url, path.Join(config.GetHomeDir(), "tmp", url)).Run()
}

func clientGitPush(url string) error {
f, err := os.Create(path.Join(config.GetHomeDir(), "tmp", url, "newfile.txt"))
if err != nil {
return err
}
f.Close()

_ = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "add", "newfile.txt").Run()
_ = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "commit", "-m", "new file").Run()
err = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "push", "origin", "master").Run()

_ = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", url))

return err
}

func clientCheckRepo(url string, file string) error {
_, err := os.ReadFile(path.Join(config.GetHomeDir(), "tmp", url, file))
return err
}
7 changes: 5 additions & 2 deletions internal/web/test/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type testServer struct {

func newTestServer() (*testServer, error) {
s := &testServer{
server: web.NewServer(true),
server: web.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions")),
}

go s.start()
Expand Down Expand Up @@ -149,7 +149,7 @@ func setup(t *testing.T) {
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)

err = os.MkdirAll(filepath.Join(homePath, "sessions"), 0755)
err = os.MkdirAll(filepath.Join(homePath, "tmp", "sessions"), 0755)
require.NoError(t, err, "Could not create sessions directory")

err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
Expand Down Expand Up @@ -177,6 +177,9 @@ func teardown(t *testing.T, s *testServer) {
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos"))
require.NoError(t, err, "Could not remove repos directory")

err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "sessions"))
require.NoError(t, err, "Could not remove repos directory")

// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
// require.NoError(t, err, "Could not remove repos directory")

Expand Down