Lindenii Project Forge
Commit info | |
---|---|
ID | de1b961fbf54601f25c54c1618f11978f6618858 |
Author | Runxi Yu<me@runxiyu.org> |
Author date | Wed, 19 Feb 2025 20:14:20 +0800 |
Committer | Runxi Yu<me@runxiyu.org> |
Committer date | Wed, 19 Feb 2025 20:14:20 +0800 |
Actions | Get patch |
ssh/recv, schema: Add repos.contrib_requirements
package main import ( "context" ) // get_path_perm_by_group_repo_key returns the filesystem path and direct // access permission for a given repo and a provided ssh public key.
func get_path_perm_by_group_repo_key(ctx context.Context, group_name, repo_name, ssh_pubkey string) (filesystem_path string, access bool, err error) {
func get_path_perm_by_group_repo_key(ctx context.Context, group_name, repo_name, ssh_pubkey string) (filesystem_path string, access bool, contrib_requirements string, is_registered_user bool, err error) {
err = database.QueryRow(ctx, `SELECT r.filesystem_path, CASE WHEN ugr.user_id IS NOT NULL THEN TRUE ELSE FALSE
END AS has_role_in_group
END AS has_role_in_group, r.contrib_requirements, CASE WHEN u.id IS NOT NULL THEN TRUE ELSE FALSE END
FROM groups g JOIN repos r ON r.group_id = g.id LEFT JOIN ssh_public_keys s ON s.key_string = $3 LEFT JOIN users u ON u.id = s.user_id LEFT JOIN user_group_roles ugr ON ugr.group_id = g.id AND ugr.user_id = u.id WHERE g.name = $1 AND r.name = $2;`, group_name, repo_name, ssh_pubkey,
).Scan(&filesystem_path, &access)
).Scan(&filesystem_path, &access, &contrib_requirements, &is_registered_user)
return }
CREATE TABLE groups ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT ); CREATE TABLE repos ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE RESTRICT, -- I mean, should be CASCADE but deleting Git repos on disk also needs to be considered
contrib_requirements TEXT NOT NULL CHECK (contrib_requirements IN ('closed', 'registered_user', 'ssh_pubkey', 'public')),
name TEXT NOT NULL, UNIQUE(group_id, name), description TEXT, filesystem_path TEXT ); CREATE TABLE ticket_trackers ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE RESTRICT, name TEXT NOT NULL, UNIQUE(group_id, name), description TEXT ); CREATE TABLE tickets ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tracker_id INTEGER NOT NULL REFERENCES ticket_trackers(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT ); CREATE TABLE mailing_lists ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE RESTRICT, name TEXT NOT NULL, UNIQUE(group_id, name), description TEXT ); CREATE TABLE mailing_list_emails ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, list_id INTEGER NOT NULL REFERENCES mailing_lists(id) ON DELETE CASCADE, title TEXT NOT NULL, sender TEXT NOT NULL, date TIMESTAMP NOT NULL, content BYTEA NOT NULL ); CREATE TABLE users ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username TEXT NOT NULL UNIQUE, password TEXT ); CREATE TABLE ssh_public_keys ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, key_string TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT unique_key_string EXCLUDE USING HASH (key_string WITH =) ); CREATE TABLE sessions ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, session_id TEXT PRIMARY KEY NOT NULL, UNIQUE(user_id, session_id) ); // TODO: CREATE TABLE merge_requests ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title TEXT NOT NULL, repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE, creator INTEGER NOT NULL REFERENCES users(id) ON DELETE SET NULL, source_ref TEXT NOT NULL, destination_branch TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('open', 'merged', 'closed')), UNIQUE (repo_id, source_ref, destination_branch), UNIQUE (repo_id, id) ); CREATE TABLE user_group_roles ( group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY(user_id, group_id) );
package main import (
"errors"
"fmt" "os" "os/exec" glider_ssh "github.com/gliderlabs/ssh" "go.lindenii.runxiyu.org/lindenii-common/cmap" ) type pack_to_hook_t struct { session glider_ssh.Session pubkey string direct_access bool repo_path string } var pack_to_hook_by_cookie = cmap.Map[string, pack_to_hook_t]{} // ssh_handle_receive_pack handles attempts to push to repos. func ssh_handle_receive_pack(session glider_ssh.Session, pubkey string, repo_identifier string) (err error) { // Here "access" means direct maintainer access. access=false doesn't // necessarily mean the push is declined. This decision is delegated to // the pre-receive hook, which is then handled by git_hooks_handle.go // while being aware of the refs to be updated.
repo_path, access, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey)
repo_path, access, contrib_requirements, is_registered_user, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey)
if err != nil { return err
} if !access { switch contrib_requirements { case "closed": if !access { return errors.New("You need direct access to push to this repo.") } case "registered_user": if !is_registered_user { return errors.New("You need to be a registered user to push to this repo.") } case "ssh_pubkey": if pubkey == "" { return errors.New("You need to have an SSH public key to push to this repo.") } case "public": default: panic("unknown contrib_requirements value " + contrib_requirements) }
} cookie, err := random_urlsafe_string(16) if err != nil { fmt.Fprintln(session.Stderr(), "Error while generating cookie:", err) } pack_to_hook_by_cookie.Store(cookie, pack_to_hook_t{ session: session, pubkey: pubkey, direct_access: access, repo_path: repo_path, }) defer pack_to_hook_by_cookie.Delete(cookie) // The Delete won't execute until proc.Wait returns unless something // horribly wrong such as a panic occurs. proc := exec.CommandContext(session.Context(), "git-receive-pack", repo_path) proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket, "LINDENII_FORGE_HOOKS_COOKIE="+cookie, ) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr() err = proc.Start() if err != nil { fmt.Fprintln(session.Stderr(), "Error while starting process:", err) return err } err = proc.Wait() if exitError, ok := err.(*exec.ExitError); ok { fmt.Fprintln(session.Stderr(), "Process exited with error", exitError.ExitCode()) } else if err != nil { fmt.Fprintln(session.Stderr(), "Error while waiting for process:", err) } return err }
package main import ( "fmt" "os" "os/exec" glider_ssh "github.com/gliderlabs/ssh" ) // ssh_handle_upload_pack handles clones/fetches. It just uses git-upload-pack // and has no ACL checks. func ssh_handle_upload_pack(session glider_ssh.Session, pubkey string, repo_identifier string) (err error) {
repo_path, _, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey)
repo_path, _, _, _, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey)
if err != nil { return err } proc := exec.CommandContext(session.Context(), "git-upload-pack", repo_path) proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr() err = proc.Start() if err != nil { fmt.Fprintln(session.Stderr(), "Error while starting process:", err) return err } err = proc.Wait() if exitError, ok := err.(*exec.ExitError); ok { fmt.Fprintln(session.Stderr(), "Process exited with error", exitError.ExitCode()) } else if err != nil { fmt.Fprintln(session.Stderr(), "Error while waiting for process:", err) } return err }
package main import ( "context" "errors" "net/url" "strings" ) var err_ssh_illegal_endpoint = errors.New("illegal endpoint during SSH access")
func get_repo_path_perms_from_ssh_path_pubkey(ctx context.Context, ssh_path string, ssh_pubkey string) (repo_path string, direct_access bool, err error) {
func get_repo_path_perms_from_ssh_path_pubkey(ctx context.Context, ssh_path string, ssh_pubkey string) (repo_path string, direct_access bool, contrib_requirements string, is_registered_user bool, err error) {
segments := strings.Split(strings.TrimPrefix(ssh_path, "/"), "/") for i, segment := range segments { var err error segments[i], err = url.PathUnescape(segment) if err != nil {
return "", false, err
return "", false, "", false, err
} } if segments[0] == ":" {
return "", false, err_ssh_illegal_endpoint
return "", false, "", false, err_ssh_illegal_endpoint
} separator_index := -1 for i, part := range segments { if part == ":" { separator_index = i break } } if segments[len(segments)-1] == "" { segments = segments[:len(segments)-1] } switch { case separator_index == -1:
return "", false, err_ssh_illegal_endpoint
return "", false, "", false, err_ssh_illegal_endpoint
case len(segments) <= separator_index+2:
return "", false, err_ssh_illegal_endpoint
return "", false, "", false, err_ssh_illegal_endpoint
} group_name := segments[0] module_type := segments[separator_index+1] module_name := segments[separator_index+2] switch module_type { case "repos": return get_path_perm_by_group_repo_key(ctx, group_name, module_name, ssh_pubkey) default:
return "", false, err_ssh_illegal_endpoint
return "", false, "", false, err_ssh_illegal_endpoint
} }