krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
b04b2221fd70c1d56776271e5ce33481d45c1d40
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-24T01:30:57Z
e2e/import_test.go | 13 ++++++++++++- internal/control/import.go | 22 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) @@ -89,6 +89,17 @@ func TestRepoImport(t *testing.T) { t.Fatalf("git:// import: %s", errOut) } + // Org-owned imports: allowed for org admins, refused for non-members. + if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 { + t.Fatalf("org create: %s", errOut) + } + if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "import", "imports/mirror", "--from", httpURL); code != 0 { + t.Fatalf("org import: %s", errOut) + } + if out, _, code := inst.ssh(t, aliceKey, "", "repo", "log", "imports/mirror"); code != 0 || !strings.Contains(out, "first") { + t.Fatalf("org import log: %d\n%s", code, out) + } + // Refusals: bad scheme, credentials in URL, existing name, foreign owner. cases := []struct { args []string @@ -97,7 +108,7 @@ func TestRepoImport(t *testing.T) { {[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "https://, http://, and git://"}, {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"}, {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"}, - {[]string{"repo", "import", "bob/x", "--from", httpURL}, "your own account"}, + {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"}, } for _, tc := range cases { _, errOut, code := inst.ssh(t, aliceKey, "", tc.args...) @@ -58,12 +58,28 @@ func runRepoImport(c *Ctx, args []string) int { return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url> [--private] [--token-stdin]") } owner, name, ok := strings.Cut(path, "/") - if !ok || owner != c.User.Username { - return c.fail(protocol.ExitDenied, "imports land under your own account: %s/<name>", c.User.Username) + if !ok { + return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>") } if err := policy.ValidateName(name); err != nil { return c.fail(protocol.ExitUsage, "%v", err) } + // Same ownership rule as repo create: yourself, or an org you admin. + ownerKind, ownerID := "user", c.User.ID + if owner != c.User.Username { + org, err := c.Store.OrgByName(owner) + if err != nil { + return c.fail(protocol.ExitDenied, "cannot import under %q: not you and not an organization you can see", owner) + } + role, err := c.Store.OrgRole(org.ID, c.User.ID) + if err != nil { + return c.fail(protocol.ExitFailure, "%v", err) + } + if role != "admin" { + return c.fail(protocol.ExitDenied, "only admins of %s can import repositories there", owner) + } + ownerKind, ownerID = "org", org.ID + } // Scheme allowlist. file:// (and anything else local) would read the // server's filesystem; ssh:// would use the server's own keys. @@ -107,7 +123,7 @@ func runRepoImport(c *Ctx, args []string) int { if private { visibility = "private" } - id, err := c.Store.CreateRepo("user", c.User.ID, name, visibility) + id, err := c.Store.CreateRepo(ownerKind, ownerID, name, visibility) if err != nil { return c.fail(protocol.ExitFailure, "%v", err) }