aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-03-07 17:10:00 +0800
committerRunxi Yu <me@runxiyu.org>2025-03-07 17:10:21 +0800
commit0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4 (patch)
treec91b09e3c3bb53f989b66b6edd56d96baf30aa92
parenthooks: Remove debug printf (diff)
downloadforge-0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4.tar.gz
forge-0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4.tar.zst
forge-0c5f8b4b639e48176f1cbf78b732cb20d5abf0a4.zip
hooks, fedauth: Add basic federated authentication for git push
-rw-r--r--fedauth.go75
-rw-r--r--git_hooks_handle.go47
-rw-r--r--git_init.go1
-rw-r--r--sql/schema.sql7
-rw-r--r--ssh_handle_receive_pack.go44
-rw-r--r--templates/group.tmpl1
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>