1
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-04-30 20:55:09 +00:00

Add anonymous access support for private repositories (backend) ()

Follow 

This PR add backend logic and test for "anonymous access", it shares the
same logic as "everyone access", so not too much change.

By the way, split `SettingsPost` into small functions to make it easier
to make frontend-related changes in the future.

Next PR will add frontend support for "anonymous access"
This commit is contained in:
wxiaoguang 2025-03-28 22:42:29 +08:00 committed by GitHub
parent 58d0a3f4c2
commit 0d2607a303
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1001 additions and 846 deletions
models
routers/web/repo/setting
services/context

View File

@ -378,6 +378,7 @@ func prepareMigrationTasks() []*migration {
newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner),
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
}
return preparedMigrations
}

View File

@ -0,0 +1,17 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_24 //nolint
import (
"code.gitea.io/gitea/models/perm"
"xorm.io/xorm"
)
func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error {
type RepoUnit struct { //revive:disable-line:exported
AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(&RepoUnit{})
}

View File

@ -25,7 +25,8 @@ type Permission struct {
units []*repo_model.RepoUnit
unitsMode map[unit.Type]perm_model.AccessMode
everyoneAccessMode map[unit.Type]perm_model.AccessMode
everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
}
// IsOwner returns true if current user is the owner of repository.
@ -39,7 +40,7 @@ func (p *Permission) IsAdmin() bool {
}
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
// It doesn't count the "everyone access mode".
// It doesn't count the "public(anonymous/everyone) access mode".
func (p *Permission) HasAnyUnitAccess() bool {
for _, v := range p.unitsMode {
if v >= perm_model.AccessModeRead {
@ -49,7 +50,12 @@ func (p *Permission) HasAnyUnitAccess() bool {
return p.AccessMode >= perm_model.AccessModeRead
}
func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool {
func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
for _, v := range p.anonymousAccessMode {
if v >= perm_model.AccessModeRead {
return true
}
}
for _, v := range p.everyoneAccessMode {
if v >= perm_model.AccessModeRead {
return true
@ -73,14 +79,16 @@ func (p *Permission) GetFirstUnitRepoID() int64 {
}
// UnitAccessMode returns current user access mode to the specify unit of the repository
// It also considers "everyone access mode"
// It also considers "public (anonymous/everyone) access mode"
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode {
// if the units map contains the access mode, use it, but admin/owner mode could override it
if m, ok := p.unitsMode[unitType]; ok {
return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m)
}
// if the units map does not contain the access mode, return the default access mode if the unit exists
unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType])
unitDefaultAccessMode := p.AccessMode
unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType])
unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType])
hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType })
return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone)
}
@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
format += "\n\tunitsMode[%-v]: %-v"
args = append(args, key.LogString(), value.LogString())
}
format += "\n\tanonymousAccessMode: %-v"
args = append(args, p.anonymousAccessMode)
format += "\n\teveryoneAccessMode: %-v"
args = append(args, p.everyoneAccessMode)
format += "\n\t]>"
return fmt.Sprintf(format, args...)
}
func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
if *modeMap == nil {
*modeMap = make(map[unit.Type]perm_model.AccessMode)
}
(*modeMap)[unitType] = accessMode
}
}
func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
// apply public (anonymous) access permissions
for _, u := range perm.units {
applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode)
}
if user == nil || user.ID <= 0 {
// for anonymous access, it could be:
// AccessMode is None or Read, units has repo units, unitModes is nil
return
}
// apply everyone access permissions
// apply public (everyone) access permissions
for _, u := range perm.units {
if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] {
if perm.everyoneAccessMode == nil {
perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode)
}
perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode
}
applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode)
}
if perm.unitsMode == nil {
@ -209,6 +228,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
break
}
}
for t := range perm.anonymousAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break
}
}
for t := range perm.everyoneAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break

View File

@ -22,14 +22,21 @@ func TestHasAnyUnitAccess(t *testing.T) {
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
}
assert.False(t, perm.HasAnyUnitAccess())
assert.False(t, perm.HasAnyUnitAccessOrEveryoneAccess())
assert.False(t, perm.HasAnyUnitAccessOrPublicAccess())
perm = Permission{
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
}
assert.False(t, perm.HasAnyUnitAccess())
assert.True(t, perm.HasAnyUnitAccessOrEveryoneAccess())
assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
perm = Permission{
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
anonymousAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
}
assert.False(t, perm.HasAnyUnitAccess())
assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
perm = Permission{
AccessMode: perm_model.AccessModeRead,
@ -43,7 +50,7 @@ func TestHasAnyUnitAccess(t *testing.T) {
assert.True(t, perm.HasAnyUnitAccess())
}
func TestApplyEveryoneRepoPermission(t *testing.T) {
func TestApplyPublicAccessRepoPermission(t *testing.T) {
perm := Permission{
AccessMode: perm_model.AccessModeNone,
units: []*repo_model.RepoUnit{
@ -53,6 +60,15 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
finalProcessRepoUnitPermission(nil, &perm)
assert.False(t, perm.CanRead(unit.TypeWiki))
perm = Permission{
AccessMode: perm_model.AccessModeNone,
units: []*repo_model.RepoUnit{
{Type: unit.TypeWiki, AnonymousAccessMode: perm_model.AccessModeRead},
},
}
finalProcessRepoUnitPermission(nil, &perm)
assert.True(t, perm.CanRead(unit.TypeWiki))
perm = Permission{
AccessMode: perm_model.AccessModeNone,
units: []*repo_model.RepoUnit{

View File

@ -47,6 +47,7 @@ type RepoUnit struct { //revive:disable-line:exported
Type unit.Type `xorm:"INDEX(s)"`
Config convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
}

View File

@ -105,8 +105,6 @@ func Settings(ctx *context.Context) {
// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
@ -119,10 +117,55 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
repo := ctx.Repo.Repository
switch ctx.FormString("action") {
case "update":
handleSettingsPostUpdate(ctx)
case "mirror":
handleSettingsPostMirror(ctx)
case "mirror-sync":
handleSettingsPostMirrorSync(ctx)
case "push-mirror-sync":
handleSettingsPostPushMirrorSync(ctx)
case "push-mirror-update":
handleSettingsPostPushMirrorUpdate(ctx)
case "push-mirror-remove":
handleSettingsPostPushMirrorRemove(ctx)
case "push-mirror-add":
handleSettingsPostPushMirrorAdd(ctx)
case "advanced":
handleSettingsPostAdvanced(ctx)
case "signing":
handleSettingsPostSigning(ctx)
case "admin":
handleSettingsPostAdmin(ctx)
case "admin_index":
handleSettingsPostAdminIndex(ctx)
case "convert":
handleSettingsPostConvert(ctx)
case "convert_fork":
handleSettingsPostConvertFork(ctx)
case "transfer":
handleSettingsPostTransfer(ctx)
case "cancel_transfer":
handleSettingsPostCancelTransfer(ctx)
case "delete":
handleSettingsPostDelete(ctx)
case "delete-wiki":
handleSettingsPostDeleteWiki(ctx)
case "archive":
handleSettingsPostArchive(ctx)
case "unarchive":
handleSettingsPostUnarchive(ctx)
case "visibility":
handleSettingsPostVisibility(ctx)
default:
ctx.NotFound(nil)
}
}
func handleSettingsPostUpdate(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsOptions)
return
@ -185,8 +228,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
case "mirror":
func handleSettingsPostMirror(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
ctx.NotFound(nil)
return
@ -279,8 +325,10 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
case "mirror-sync":
func handleSettingsPostMirrorSync(ctx *context.Context) {
repo := ctx.Repo.Repository
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
ctx.NotFound(nil)
return
@ -290,8 +338,12 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
ctx.Redirect(repo.Link() + "/settings")
}
func handleSettingsPostPushMirrorSync(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
case "push-mirror-sync":
if !setting.Mirror.Enabled {
ctx.NotFound(nil)
return
@ -307,8 +359,12 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
ctx.Redirect(repo.Link() + "/settings")
}
func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
case "push-mirror-update":
if !setting.Mirror.Enabled || repo.IsArchived {
ctx.NotFound(nil)
return
@ -345,8 +401,12 @@ func SettingsPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
case "push-mirror-remove":
if !setting.Mirror.Enabled || repo.IsArchived {
ctx.NotFound(nil)
return
@ -374,8 +434,12 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
case "push-mirror-add":
if setting.Mirror.DisableNewPush || repo.IsArchived {
ctx.NotFound(nil)
return
@ -438,8 +502,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
case "advanced":
func handleSettingsPostAdvanced(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
var repoChanged bool
var units []repo_model.RepoUnit
var deleteUnitTypes []unit_model.Type
@ -627,8 +694,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "signing":
func handleSettingsPostSigning(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
changed := false
trustModel := repo_model.ToTrustModel(form.TrustModel)
if trustModel != repo.TrustModel {
@ -646,8 +716,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "admin":
func handleSettingsPostAdmin(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Doer.IsAdmin {
ctx.HTTPError(http.StatusForbidden)
return
@ -666,8 +739,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "admin_index":
func handleSettingsPostAdminIndex(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Doer.IsAdmin {
ctx.HTTPError(http.StatusForbidden)
return
@ -694,8 +770,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "convert":
func handleSettingsPostConvert(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
return
@ -721,8 +800,11 @@ func SettingsPost(ctx *context.Context) {
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
ctx.Redirect(repo.Link())
}
case "convert_fork":
func handleSettingsPostConvertFork(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
return
@ -758,8 +840,11 @@ func SettingsPost(ctx *context.Context) {
log.Trace("Repository converted from fork to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
ctx.Redirect(repo.Link())
}
case "transfer":
func handleSettingsPostTransfer(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
return
@ -816,8 +901,10 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
}
ctx.Redirect(repo.Link() + "/settings")
}
case "cancel_transfer":
func handleSettingsPostCancelTransfer(ctx *context.Context) {
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
return
@ -842,8 +929,11 @@ func SettingsPost(ctx *context.Context) {
log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
ctx.Redirect(repo.Link() + "/settings")
}
case "delete":
func handleSettingsPostDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
return
@ -866,8 +956,11 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
ctx.Redirect(ctx.Repo.Owner.DashboardLink())
}
case "delete-wiki":
func handleSettingsPostDeleteWiki(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
return
@ -885,8 +978,10 @@ func SettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "archive":
func handleSettingsPostArchive(ctx *context.Context) {
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusForbidden)
return
@ -916,8 +1011,10 @@ func SettingsPost(ctx *context.Context) {
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "unarchive":
func handleSettingsPostUnarchive(ctx *context.Context) {
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusForbidden)
return
@ -943,8 +1040,11 @@ func SettingsPost(ctx *context.Context) {
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
case "visibility":
func handleSettingsPostVisibility(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if repo.IsFork {
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
@ -976,10 +1076,6 @@ func SettingsPost(ctx *context.Context) {
log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
default:
ctx.NotFound(nil)
}
}
func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {

View File

@ -346,7 +346,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
return
}
if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) {
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {
if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
return