mirror of
https://github.com/thomiceli/opengist.git
synced 2024-12-22 20:42:40 +00:00
Fix perms for http/ssh clone (#288)
This commit is contained in:
parent
77d87aeecd
commit
38892d8a4a
12 changed files with 225 additions and 22 deletions
|
@ -74,7 +74,7 @@ func Run(actionType int) {
|
||||||
case IndexGists:
|
case IndexGists:
|
||||||
functionToRun = indexGists
|
functionToRun = indexGists
|
||||||
default:
|
default:
|
||||||
panic("unhandled default case")
|
log.Error().Msg("Unknown action type")
|
||||||
}
|
}
|
||||||
|
|
||||||
functionToRun()
|
functionToRun()
|
||||||
|
|
|
@ -30,7 +30,7 @@ var CmdStart = cli.Command{
|
||||||
Usage: "Start Opengist server",
|
Usage: "Start Opengist server",
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
Initialize(ctx)
|
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()
|
go ssh.Start()
|
||||||
select {}
|
select {}
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
|
||||||
return sshKey, err
|
return sshKey, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
|
func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
|
||||||
sshKey := new(SSHKey)
|
var count int64
|
||||||
err := db.
|
err := db.Model(&SSHKey{}).
|
||||||
Where("content like ?", sshKeyContent+"%").
|
Where("content = ?", sshKeyContent).
|
||||||
First(&sshKey).Error
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
return sshKey, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sshKey *SSHKey) Create() error {
|
func (sshKey *SSHKey) Create() error {
|
||||||
|
|
|
@ -118,6 +118,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
|
||||||
return userMap, nil
|
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) {
|
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
|
||||||
key := new(SSHKey)
|
key := new(SSHKey)
|
||||||
err := db.
|
err := db.
|
||||||
|
|
|
@ -126,6 +126,7 @@ settings.delete-ssh-key-confirm: Confirm deletion of SSH key
|
||||||
settings.ssh-key-added-at: Added
|
settings.ssh-key-added-at: Added
|
||||||
settings.ssh-key-never-used: Never used
|
settings.ssh-key-never-used: Never used
|
||||||
settings.ssh-key-last-used: Last used
|
settings.ssh-key-last-used: Last used
|
||||||
|
settings.ssh-key-exists: SSH key already exists
|
||||||
settings.change-username: Change username
|
settings.change-username: Change username
|
||||||
settings.create-password: Create password
|
settings.create-password: Create password
|
||||||
settings.create-password-help: Create your password to login to Opengist via HTTP
|
settings.create-password-help: Create your password to login to Opengist via HTTP
|
||||||
|
|
|
@ -50,11 +50,18 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
||||||
// - gist is not found (obfuscation)
|
// - gist is not found (obfuscation)
|
||||||
// - admin setting to require login is set to true
|
// - admin setting to require login is set to true
|
||||||
if verb == "receive-pack" ||
|
if verb == "receive-pack" ||
|
||||||
gist.Private == 2 ||
|
gist.Private == db.PrivateVisibility ||
|
||||||
gist.ID == 0 ||
|
gist.ID == 0 ||
|
||||||
!allowUnauthenticated {
|
!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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
||||||
|
|
|
@ -24,8 +24,8 @@ func Start() {
|
||||||
sshConfig := &ssh.ServerConfig{
|
sshConfig := &ssh.ServerConfig{
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
||||||
_, err := db.SSHKeyDoesExists(strKey)
|
exists, err := db.SSHKeyDoesExists(strKey)
|
||||||
if err != nil {
|
if !exists {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ func gitHttp(ctx echo.Context) error {
|
||||||
|
|
||||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
|
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("impossible")
|
log.Fatal().Err(err).Msg("Cannot check if unauthenticated access is allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shows basic auth if :
|
// Shows basic auth if :
|
||||||
|
@ -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")
|
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 {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot verify password", err)
|
return errorRes(500, "Cannot verify password", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,12 +161,12 @@ type Server struct {
|
||||||
dev bool
|
dev bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(isDev bool) *Server {
|
func NewServer(isDev bool, sessionsPath string) *Server {
|
||||||
dev = isDev
|
dev = isDev
|
||||||
flashStore = sessions.NewCookieStore([]byte("opengist"))
|
flashStore = sessions.NewCookieStore([]byte("opengist"))
|
||||||
userStore = sessions.NewFilesystemStore(path.Join(config.GetHomeDir(), "sessions"),
|
userStore = sessions.NewFilesystemStore(sessionsPath,
|
||||||
utils.ReadKey(path.Join(config.GetHomeDir(), "sessions", "session-auth.key")),
|
utils.ReadKey(path.Join(sessionsPath, "session-auth.key")),
|
||||||
utils.ReadKey(path.Join(config.GetHomeDir(), "sessions", "session-encrypt.key")),
|
utils.ReadKey(path.Join(sessionsPath, "session-encrypt.key")),
|
||||||
)
|
)
|
||||||
userStore.MaxLength(10 * 1024)
|
userStore.MaxLength(10 * 1024)
|
||||||
gothic.Store = userStore
|
gothic.Store = userStore
|
||||||
|
@ -526,7 +526,7 @@ func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {
|
||||||
|
|
||||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
|
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("impossible")
|
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allow {
|
if !allow {
|
||||||
|
|
|
@ -89,6 +89,14 @@ func sshKeysProcess(ctx echo.Context) error {
|
||||||
}
|
}
|
||||||
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
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 {
|
if err := key.Create(); err != nil {
|
||||||
return errorRes(500, "Cannot add SSH key", err)
|
return errorRes(500, "Cannot add SSH key", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -147,3 +152,167 @@ func TestAnonymous(t *testing.T) {
|
||||||
require.NoError(t, err)
|
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", "http://"+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
|
||||||
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ type testServer struct {
|
||||||
|
|
||||||
func newTestServer() (*testServer, error) {
|
func newTestServer() (*testServer, error) {
|
||||||
s := &testServer{
|
s := &testServer{
|
||||||
server: web.NewServer(true),
|
server: web.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions")),
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.start()
|
go s.start()
|
||||||
|
@ -149,7 +149,7 @@ func setup(t *testing.T) {
|
||||||
homePath := config.GetHomeDir()
|
homePath := config.GetHomeDir()
|
||||||
log.Info().Msg("Data directory: " + homePath)
|
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")
|
require.NoError(t, err, "Could not create sessions directory")
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
|
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
|
||||||
|
@ -177,6 +177,9 @@ func teardown(t *testing.T, s *testServer) {
|
||||||
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos"))
|
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos"))
|
||||||
require.NoError(t, err, "Could not remove repos directory")
|
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"))
|
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
|
||||||
// require.NoError(t, err, "Could not remove repos directory")
|
// require.NoError(t, err, "Could not remove repos directory")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue