feat(repo): enforce dot-prefixed repos as always-private system repos
compliance / files-changed (pull_request) Successful in 2m48s
pr-title / lint-pr-title (pull_request) Successful in 5s
db-tests / files-changed (pull_request) Successful in 2m53s
docker-dryrun / files-changed (pull_request) Successful in 3m7s
e2e-tests / files-changed (pull_request) Successful in 3m8s
compliance / lint-on-demand (pull_request) Successful in 1m23s
compliance / lint-backend (pull_request) Failing after 4m50s
compliance / frontend (pull_request) Has been skipped
compliance / checks-backend (pull_request) Failing after 5m20s
compliance / backend (pull_request) Failing after 4m15s
db-tests / test-pgsql (pull_request) Failing after 4m7s
db-tests / test-sqlite (pull_request) Failing after 4m29s
db-tests / test-unit (pull_request) Failing after 5m53s
db-tests / test-mysql (pull_request) Failing after 5m21s
docker-dryrun / container-amd64 (pull_request) Has been skipped
docker-dryrun / container-arm64 (pull_request) Has been skipped
docker-dryrun / container-riscv64 (pull_request) Has been skipped
db-tests / test-mssql (pull_request) Failing after 6m30s
e2e-tests / test-e2e (pull_request) Failing after 4m53s
compliance / lint-go-gogit (pull_request) Failing after 33m59s
compliance / lint-go-windows (pull_request) Failing after 33m59s

Repositories with names starting with "." are now treated as system
repositories that are always private and cannot be made public. This is
enforced at every code path: API create, web create, migrate, template
create, push-to-create, API edit, web settings, and public access
settings. On creation paths, privacy is silently forced. On edit paths,
a clear error is returned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-12 19:12:00 -05:00
parent 809e9d2bf3
commit c5eb8df8a2
10 changed files with 52 additions and 14 deletions
+6
View File
@@ -303,6 +303,12 @@ func (repo *Repository) IsBeingCreated() bool {
return repo.IsBeingMigrated()
}
// IsSystemRepo returns true when the repository name starts with "."
// System repositories are always private and cannot be made public.
func (repo *Repository) IsSystemRepo() bool {
return strings.HasPrefix(repo.Name, ".")
}
// IsBroken indicates that repository is broken
func (repo *Repository) IsBroken() bool {
return repo.Status == RepositoryBroken
+1
View File
@@ -2496,6 +2496,7 @@
"repo.settings.visibility.success": "Repository visibility changed.",
"repo.settings.visibility.error": "An error occurred while trying to change the repo visibility.",
"repo.settings.visibility.fork_error": "Can't change the visibility of a forked repo.",
"repo.settings.visibility.system_repo_private": "System repositories (dot-prefixed names) are always private and cannot be made public.",
"repo.settings.archive.button": "Archive Repo",
"repo.settings.archive.header": "Archive This Repo",
"repo.settings.archive.text": "Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests.",
+2 -2
View File
@@ -139,7 +139,7 @@ func Migrate(ctx *context.APIContext) {
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Private: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
Mirror: form.Mirror,
LFS: form.LFS,
LFSEndpoint: form.LFSEndpoint,
@@ -174,7 +174,7 @@ func Migrate(ctx *context.APIContext) {
Description: opts.Description,
OriginalURL: form.CloneAddr,
GitServiceType: gitServiceType,
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
IsPrivate: opts.Private || setting.Repository.ForcePrivate || strings.HasPrefix(opts.RepoName, "."),
IsMirror: opts.Mirror,
Status: repo_model.RepositoryBeingMigrated,
}, false)
+10 -1
View File
@@ -253,6 +253,9 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
return
}
// System repos (dot-prefixed names) are always private.
isPrivate := opt.Private || setting.Repository.ForcePrivate || strings.HasPrefix(opt.Name, ".")
repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{
Name: opt.Name,
Description: opt.Description,
@@ -260,7 +263,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
Gitignores: opt.Gitignores,
License: opt.License,
Readme: opt.Readme,
IsPrivate: opt.Private || setting.Repository.ForcePrivate,
IsPrivate: isPrivate,
AutoInit: opt.AutoInit,
DefaultBranch: opt.DefaultBranch,
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
@@ -700,6 +703,12 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
}
visibilityChanged = repo.IsPrivate != *opts.Private
// System repos (dot-prefixed) cannot be made public, regardless of user role.
if visibilityChanged && !*opts.Private && repo.IsSystemRepo() {
err := errors.New("system repositories (dot-prefixed) cannot be made public")
ctx.APIError(http.StatusForbidden, err)
return err
}
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
err := errors.New("cannot change private repository to public")
+9 -4
View File
@@ -210,10 +210,15 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
// FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag
// These options should only be used for "push-to-create"
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
// TODO: it needs to do more work
repo.IsPrivate = isPrivate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
// System repos (dot-prefixed) are always private — ignore attempts to make them public.
if !isPrivate.Value() && repo.IsSystemRepo() {
log.Warn("Ignoring push option to make system repo %s public", repo.FullName())
} else {
// TODO: it needs to do more work
repo.IsPrivate = isPrivate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
}
}
}
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
+1 -1
View File
@@ -210,7 +210,7 @@ func MigratePost(ctx *context.Context) {
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Private: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
Mirror: form.Mirror,
LFS: form.LFS,
LFSEndpoint: form.LFSEndpoint,
+2 -2
View File
@@ -243,7 +243,7 @@ func CreatePost(ctx *context.Context) {
opts := repo_service.GenerateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Private: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
GitContent: form.GitContent,
Topics: form.Topics,
GitHooks: form.GitHooks,
@@ -282,7 +282,7 @@ func CreatePost(ctx *context.Context) {
IssueLabels: form.IssueLabels,
License: form.License,
Readme: form.Readme,
IsPrivate: form.Private || setting.Repository.ForcePrivate,
IsPrivate: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
DefaultBranch: form.DefaultBranch,
AutoInit: form.AutoInit,
IsTemplate: form.Template,
+10 -2
View File
@@ -127,14 +127,22 @@ func repoUnitPublicAccesses(ctx *context.Context) []*repoUnitPublicAccess {
func PublicAccess(ctx *context.Context) {
ctx.Data["PageIsSettingsPublicAccess"] = true
ctx.Data["RepoUnitPublicAccesses"] = repoUnitPublicAccesses(ctx)
ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate
if setting.Repository.ForcePrivate {
ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate || ctx.Repo.Repository.IsSystemRepo()
if ctx.Repo.Repository.IsSystemRepo() {
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.system_repo_private"), true)
} else if setting.Repository.ForcePrivate {
ctx.Flash.Error(ctx.Tr("form.repository_force_private"), true)
}
ctx.HTML(http.StatusOK, tplRepoSettingsPublicAccess)
}
func PublicAccessPost(ctx *context.Context) {
// System repos (dot-prefixed) cannot have public access on any unit.
if ctx.Repo.Repository.IsSystemRepo() {
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.system_repo_private"), true)
ctx.Redirect(ctx.Repo.RepoLink + "/settings/public_access")
return
}
accesses := repoUnitPublicAccesses(ctx)
for _, ua := range accesses {
formVal := ctx.FormString(ua.FormKey)
+7 -2
View File
@@ -54,7 +54,7 @@ const (
func SettingsCtxData(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.options")
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate || ctx.Repo.Repository.IsSystemRepo()
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
@@ -103,7 +103,7 @@ func Settings(ctx *context.Context) {
// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate || ctx.Repo.Repository.IsSystemRepo()
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
@@ -1008,6 +1008,11 @@ func handleSettingsPostVisibility(ctx *context.Context) {
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
// System repos (dot-prefixed) cannot be made public, regardless of user role.
if !private && repo.IsSystemRepo() {
ctx.JSONError(ctx.Tr("repo.settings.visibility.system_repo_private"))
return
}
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
ctx.JSONError(ctx.Tr("form.repository_force_private"))
+4
View File
@@ -123,6 +123,10 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili
}
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository, private bool) (err error) {
// System repos (dot-prefixed) are always private and cannot be made public.
if !private && repo.IsSystemRepo() {
return errors.New("system repositories (dot-prefixed) cannot be made public")
}
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = private
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {