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
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:
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user