diff options
author | Runxi Yu <me@runxiyu.org> | 2025-03-07 17:10:00 +0800 |
---|---|---|
committer | Runxi Yu <me@runxiyu.org> | 2025-03-07 17:10:21 +0800 |
commit | 0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4 (patch) | |
tree | c91b09e3c3bb53f989b66b6edd56d96baf30aa92 | |
parent | hooks: Remove debug printf (diff) | |
download | forge-0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4.tar.gz forge-0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4.tar.zst forge-0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4.zip |
hooks, fedauth: Add basic federated authentication for git push
-rw-r--r-- | fedauth.go | 75 | ||||
-rw-r--r-- | git_hooks_handle.go | 47 | ||||
-rw-r--r-- | git_init.go | 1 | ||||
-rw-r--r-- | sql/schema.sql | 7 | ||||
-rw-r--r-- | ssh_handle_receive_pack.go | 44 | ||||
-rw-r--r-- | templates/group.tmpl | 1 |
6 files changed, 157 insertions, 18 deletions
diff --git a/fedauth.go b/fedauth.go new file mode 100644 index 0000000..3f403e7 --- /dev/null +++ b/fedauth.go @@ -0,0 +1,75 @@ +package main + +import ( + "bufio" + "context" + "errors" + "io" + "net/http" + "net/url" + "strings" + + "github.com/jackc/pgx/v5" +) + +func check_and_update_federated_user_status(ctx context.Context, user_id int, service, remote_username, pubkey string) (bool, error) { + switch service { + case "sr.ht": + username_escaped := url.PathEscape(remote_username) + + resp, err := http.Get("https://meta.sr.ht/~" + username_escaped + ".keys") + if err != nil { + return false, err + } + + defer func() { + _ = resp.Body.Close() + }() + buf := bufio.NewReader(resp.Body) + + matched := false + for { + line, err := buf.ReadString('\n') + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return false, err + } + + line_split := strings.Split(line, " ") + if len(line_split) < 2 { + continue + } + line = strings.Join(line_split[:2], " ") + + if line == pubkey { + matched = true + break + } + } + if !matched { + return false, nil + } + + var tx pgx.Tx + if tx, err = database.Begin(ctx); err != nil { + return false, err + } + defer func() { + _ = tx.Rollback(ctx) + }() + if _, err = tx.Exec(ctx, `UPDATE users SET type = 'federated' WHERE id = $1 AND type = 'pubkey_only'`, user_id); err != nil { + return false, err + } + if _, err = tx.Exec(ctx, `INSERT INTO federated_identities (user_id, service, remote_username) VALUES ($1, $2, $3)`, user_id, service, remote_username); err != nil { + return false, err + } + if err = tx.Commit(ctx); err != nil { + return false, err + } + + return true, nil + default: + return false, errors.New("unknown federated service") + } +} diff --git a/git_hooks_handle.go b/git_hooks_handle.go index 2adaf9a..7da6c88 100644 --- a/git_hooks_handle.go +++ b/git_hooks_handle.go @@ -151,6 +151,53 @@ func hooks_handle_connection(conn net.Conn) { var found bool var old_hash, new_hash plumbing.Hash var old_commit, new_commit *object.Commit + var git_push_option_count int + + git_push_option_count, err = strconv.Atoi(git_env["GIT_PUSH_OPTION_COUNT"]) + if err != nil { + wf_error(ssh_stderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err) + return 1 + } + + // TODO: Allow existing users (even if they are already federated or registered) to add a federated user ID... though perhaps this should be in the normal SSH interface instead of the git push interface? + // Also it'd be nice to be able to combine users or whatever + if pack_to_hook.contrib_requirements == "federated" && pack_to_hook.user_type != "federated" && pack_to_hook.user_type != "registered" { + if git_push_option_count == 0 { + wf_error(ssh_stderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu") + return 1 + } + for i := 0; i < git_push_option_count; i++ { + push_option, ok := git_env[fmt.Sprintf("GIT_PUSH_OPTION_%d", i)] + if !ok { + wf_error(ssh_stderr, "Failed to get push option %d", i) + return 1 + } + if strings.HasPrefix(push_option, "fedid=") { + federated_user_identifier := strings.TrimPrefix(push_option, "fedid=") + service, username, found := strings.Cut(federated_user_identifier, ":") + if !found { + wf_error(ssh_stderr, "Invalid federated user identifier %#v does not contain a colon", federated_user_identifier) + return 1 + } + + ok, err := check_and_update_federated_user_status(ctx, pack_to_hook.user_id, service, username, pack_to_hook.pubkey) + if err != nil { + wf_error(ssh_stderr, "Failed to verify federated user identifier %#v: %v", federated_user_identifier, err) + return 1 + } + if !ok { + wf_error(ssh_stderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", federated_user_identifier) + return 1 + } + + break + } + if i == git_push_option_count-1 { + wf_error(ssh_stderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu") + return 1 + } + } + } line, err = stdin.ReadString('\n') if errors.Is(err, io.EOF) { diff --git a/git_init.go b/git_init.go index 1e1f8c7..4313ee8 100644 --- a/git_init.go +++ b/git_init.go @@ -24,6 +24,7 @@ func git_bare_init_with_default_hooks(repo_path string) (err error) { } git_config.Raw.SetOption("core", git_format_config.NoSubsection, "hooksPath", config.Hooks.Execs) + git_config.Raw.SetOption("receive", git_format_config.NoSubsection, "advertisePushOptions", "true") if err = repo.SetConfig(git_config); err != nil { return err diff --git a/sql/schema.sql b/sql/schema.sql index d637aa3..1a038ae 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -89,3 +89,10 @@ CREATE TABLE user_group_roles ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY(user_id, group_id) ); + +CREATE TABLE federated_identities ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + service TEXT NOT NULL, + remote_username TEXT NOT NULL, + PRIMARY KEY(user_id, service) +); diff --git a/ssh_handle_receive_pack.go b/ssh_handle_receive_pack.go index 45610bb..b77b717 100644 --- a/ssh_handle_receive_pack.go +++ b/ssh_handle_receive_pack.go @@ -15,15 +15,17 @@ import ( ) type pack_to_hook_t struct { - session glider_ssh.Session - repo *git.Repository - pubkey string - direct_access bool - repo_path string - user_id int - repo_id int - group_path []string - repo_name string + session glider_ssh.Session + repo *git.Repository + pubkey string + direct_access bool + repo_path string + user_id int + user_type string + repo_id int + group_path []string + repo_name string + contrib_requirements string } var pack_to_hook_by_cookie = cmap.Map[string, pack_to_hook_t]{} @@ -65,6 +67,8 @@ func ssh_handle_receive_pack(session glider_ssh.Session, pubkey string, repo_ide return errors.New("You need to be a registered user to push to this repo.") } case "ssh_pubkey": + fallthrough + case "federated": if pubkey == "" { return errors.New("You need to have an SSH public key to push to this repo.") } @@ -74,7 +78,9 @@ func ssh_handle_receive_pack(session glider_ssh.Session, pubkey string, repo_ide return err } fmt.Fprintln(session.Stderr(), "You are now registered as user ID", user_id) + user_type = "pubkey_only" } + case "public": default: panic("unknown contrib_requirements value " + contrib_requirements) @@ -87,15 +93,17 @@ func ssh_handle_receive_pack(session glider_ssh.Session, pubkey string, repo_ide } pack_to_hook_by_cookie.Store(cookie, pack_to_hook_t{ - session: session, - pubkey: pubkey, - direct_access: direct_access, - repo_path: repo_path, - user_id: user_id, - repo_id: repo_id, - group_path: group_path, - repo_name: repo_name, - repo: repo, + session: session, + pubkey: pubkey, + direct_access: direct_access, + repo_path: repo_path, + user_id: user_id, + repo_id: repo_id, + group_path: group_path, + repo_name: repo_name, + repo: repo, + contrib_requirements: contrib_requirements, + user_type: user_type, }) defer pack_to_hook_by_cookie.Delete(cookie) // The Delete won't execute until proc.Wait returns unless something diff --git a/templates/group.tmpl b/templates/group.tmpl index 8343026..0042e0f 100644 --- a/templates/group.tmpl +++ b/templates/group.tmpl @@ -50,6 +50,7 @@ <select id="repo-contrib-input" name="repo_contrib"> <option value="public">Public</option> <option value="ssh_pubkey">SSH public key</option> + <option value="federated">Federated service</option> <option value="registered_user">Registered user</option> <option value="closed">Closed</option> </select> |