@@ -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+
96121func 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