Commit b04b2221fd

b04b2221fd70c1d56776271e5ce33481d45c1d40

parent: e1063b0300

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24 01:30 UTC

repo import: allow org owners

Same ownership rule as repo create — yourself, or an org you admin.
The self-only restriction predated orgs and was never relaxed; e2e
covers the org path and the updated refusal message.

Layout: unified · split

e2e/import_test.go +12 −1
@@ -89,6 +89,17 @@ func TestRepoImport(t *testing.T) {
89 t.Fatalf("git:// import: %s", errOut) 89 t.Fatalf("git:// import: %s", errOut)
90 } 90 }
91 91
92 // Org-owned imports: allowed for org admins, refused for non-members.
93 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 {
94 t.Fatalf("org create: %s", errOut)
95 }
96 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "import", "imports/mirror", "--from", httpURL); code != 0 {
97 t.Fatalf("org import: %s", errOut)
98 }
99 if out, _, code := inst.ssh(t, aliceKey, "", "repo", "log", "imports/mirror"); code != 0 || !strings.Contains(out, "first") {
100 t.Fatalf("org import log: %d\n%s", code, out)
101 }
102
92 // Refusals: bad scheme, credentials in URL, existing name, foreign owner. 103 // Refusals: bad scheme, credentials in URL, existing name, foreign owner.
93 cases := []struct { 104 cases := []struct {
94 args []string 105 args []string
@@ -97,7 +108,7 @@ func TestRepoImport(t *testing.T) {
97 {[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "https://, http://, and git://"}, 108 {[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "https://, http://, and git://"},
98 {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"}, 109 {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"},
99 {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"}, 110 {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"},
100 {[]string{"repo", "import", "bob/x", "--from", httpURL}, "your own account"}, 111 {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"},
101 } 112 }
102 for _, tc := range cases { 113 for _, tc := range cases {
103 _, errOut, code := inst.ssh(t, aliceKey, "", tc.args...) 114 _, errOut, code := inst.ssh(t, aliceKey, "", tc.args...)
internal/control/import.go +19 −3
@@ -58,12 +58,28 @@ func runRepoImport(c *Ctx, args []string) int {
58 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url> [--private] [--token-stdin]") 58 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url> [--private] [--token-stdin]")
59 } 59 }
60 owner, name, ok := strings.Cut(path, "/") 60 owner, name, ok := strings.Cut(path, "/")
61 if !ok || owner != c.User.Username { 61 if !ok {
62 return c.fail(protocol.ExitDenied, "imports land under your own account: %s/<name>", c.User.Username) 62 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>")
63 } 63 }
64 if err := policy.ValidateName(name); err != nil { 64 if err := policy.ValidateName(name); err != nil {
65 return c.fail(protocol.ExitUsage, "%v", err) 65 return c.fail(protocol.ExitUsage, "%v", err)
66 } 66 }
67 // Same ownership rule as repo create: yourself, or an org you admin.
68 ownerKind, ownerID := "user", c.User.ID
69 if owner != c.User.Username {
70 org, err := c.Store.OrgByName(owner)
71 if err != nil {
72 return c.fail(protocol.ExitDenied, "cannot import under %q: not you and not an organization you can see", owner)
73 }
74 role, err := c.Store.OrgRole(org.ID, c.User.ID)
75 if err != nil {
76 return c.fail(protocol.ExitFailure, "%v", err)
77 }
78 if role != "admin" {
79 return c.fail(protocol.ExitDenied, "only admins of %s can import repositories there", owner)
80 }
81 ownerKind, ownerID = "org", org.ID
82 }
67 83
68 // Scheme allowlist. file:// (and anything else local) would read the 84 // Scheme allowlist. file:// (and anything else local) would read the
69 // server's filesystem; ssh:// would use the server's own keys. 85 // server's filesystem; ssh:// would use the server's own keys.
@@ -107,7 +123,7 @@ func runRepoImport(c *Ctx, args []string) int {
107 if private { 123 if private {
108 visibility = "private" 124 visibility = "private"
109 } 125 }
110 id, err := c.Store.CreateRepo("user", c.User.ID, name, visibility) 126 id, err := c.Store.CreateRepo(ownerKind, ownerID, name, visibility)
111 if err != nil { 127 if err != nil {
112 return c.fail(protocol.ExitFailure, "%v", err) 128 return c.fail(protocol.ExitFailure, "%v", err)
113 } 129 }