Skip to content

Commit fcc9d95

Browse files
committed
fix(registry): enforce group/repo push and auto-create missing group
Block root-level image pushes by requiring a group prefix, and automatically ensure group metadata/folder are created during push flow so users can push without pre-creating groups in the UI.
1 parent 5fd9ccb commit fcc9d95

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

backend/internal/database/database.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,15 @@ func (d *Database) CreateGroup(name string) error {
587587
return err
588588
}
589589

590+
// EnsureGroup inserts the group row if missing (idempotent). Used when a push uses group/repo before the group exists in the UI.
591+
func (d *Database) EnsureGroup(name string) error {
592+
_, err := d.db.Exec(`
593+
INSERT OR IGNORE INTO groups (name, created_at)
594+
VALUES (?, CURRENT_TIMESTAMP)
595+
`, name)
596+
return err
597+
}
598+
590599
// GetGroups returns all unique groups from both the groups table and repository names
591600
func (d *Database) GetGroups() ([]string, error) {
592601
// Get groups from groups table

backend/internal/registry/handler.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ func validateRepoName(name string) bool {
9393
return validRepoName.MatchString(name)
9494
}
9595

96+
// validateRepoNameForPush requires group/repo (at least one '/') so images are not pushed at registry root.
97+
func validateRepoNameForPush(name string) bool {
98+
return validateRepoName(name) && strings.Contains(name, "/")
99+
}
100+
101+
// autoEnsureGroupForPush records the path prefix as a group (DB + SFTP) so docker push group/repo works without pre-creating the group in the UI.
102+
func autoEnsureGroupForPush(repoName string) {
103+
idx := strings.IndexByte(repoName, '/')
104+
if idx <= 0 {
105+
return
106+
}
107+
group := repoName[:idx]
108+
ctx := context.TODO()
109+
if db != nil {
110+
if err := db.EnsureGroup(group); err != nil {
111+
log.Printf("autoEnsureGroupForPush: EnsureGroup %q: %v", group, err)
112+
}
113+
}
114+
if sftpDriver != nil {
115+
if err := sftpDriver.CreateGroupFolder(ctx, group); err != nil {
116+
log.Printf("autoEnsureGroupForPush: CreateGroupFolder %q: %v", group, err)
117+
}
118+
}
119+
}
120+
96121
func validateManifestRef(ref string) bool {
97122
if ref == "" || len(ref) > 128 || strings.ContainsAny(ref, "../\\") || strings.Contains(ref, "\x00") {
98123
return false
@@ -179,6 +204,11 @@ func initiateBlobUpload(w http.ResponseWriter, r *http.Request, path string) {
179204
w.Write([]byte("invalid repository name"))
180205
return
181206
}
207+
if !validateRepoNameForPush(name) {
208+
registryError(w, "NAME_INVALID", "repository name must include a group prefix, e.g. mygroup/myimage", http.StatusBadRequest)
209+
return
210+
}
211+
autoEnsureGroupForPush(name)
182212

183213
// Auto-create repository if it doesn't exist (Docker registry standard behavior)
184214
if db != nil {
@@ -353,6 +383,11 @@ func uploadBlobData(w http.ResponseWriter, r *http.Request, path string) {
353383
w.Write([]byte("invalid repository name"))
354384
return
355385
}
386+
if !validateRepoNameForPush(name) {
387+
registryError(w, "NAME_INVALID", "repository name must include a group prefix, e.g. mygroup/myimage", http.StatusBadRequest)
388+
return
389+
}
390+
autoEnsureGroupForPush(name)
356391
uploadID := strings.TrimSuffix(parts[1], "/")
357392
if idx := strings.Index(uploadID, "?"); idx >= 0 {
358393
uploadID = uploadID[:idx]
@@ -547,6 +582,11 @@ func handleManifest(w http.ResponseWriter, r *http.Request, path string) {
547582
manifestPath = strings.TrimLeft(manifestPath, "/")
548583
switch r.Method {
549584
case http.MethodPut:
585+
if !validateRepoNameForPush(name) {
586+
registryError(w, "NAME_INVALID", "repository name must include a group prefix, e.g. mygroup/myimage", http.StatusBadRequest)
587+
return
588+
}
589+
autoEnsureGroupForPush(name)
550590
// Auto-create repository if it doesn't exist (Docker registry standard behavior)
551591
if db != nil {
552592
if _, err := db.GetRepository(name); err != nil {
@@ -567,8 +607,7 @@ func handleManifest(w http.ResponseWriter, r *http.Request, path string) {
567607
}
568608
}
569609
}
570-
571-
// Skip group folder check for now - it's causing 403 errors
610+
572611
manifest, err := io.ReadAll(r.Body)
573612
if err != nil {
574613
w.WriteHeader(http.StatusInternalServerError)
@@ -761,6 +800,11 @@ func commitBlobUpload(w http.ResponseWriter, r *http.Request, path string) {
761800
w.Write([]byte("invalid repository name"))
762801
return
763802
}
803+
if !validateRepoNameForPush(name) {
804+
registryError(w, "NAME_INVALID", "repository name must include a group prefix, e.g. mygroup/myimage", http.StatusBadRequest)
805+
return
806+
}
807+
autoEnsureGroupForPush(name)
764808
uploadID := parts[1]
765809
if idx := strings.Index(uploadID, "?"); idx >= 0 {
766810
uploadID = uploadID[:idx]

0 commit comments

Comments
 (0)