From c0fbdbb9be00f3b1233d6be306fe6c94fd6a9bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 15:41:23 -0400 Subject: [PATCH] inject group-related values into web context --- services/context/context.go | 16 +- services/context/group.go | 323 ++++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 services/context/group.go diff --git a/services/context/context.go b/services/context/context.go index a6a861ecaa..1f28ae1c94 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -58,9 +58,10 @@ type Context struct { ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer - Repo *Repository - Org *Organization - Package *Package + RepoGroup *RepoGroup + Repo *Repository + Org *Organization + Package *Package } type TemplateContext map[string]any @@ -128,10 +129,11 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { Render: render, Session: session, - Cache: cache.GetCache(), - Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), - Repo: &Repository{}, - Org: &Organization{}, + Cache: cache.GetCache(), + Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), + Repo: &Repository{}, + Org: &Organization{}, + RepoGroup: &RepoGroup{}, } ctx.TemplateContext = NewTemplateContextForWeb(ctx, ctx.Base.Req, ctx.Base.Locale) ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} diff --git a/services/context/group.go b/services/context/group.go new file mode 100644 index 0000000000..545465083e --- /dev/null +++ b/services/context/group.go @@ -0,0 +1,323 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "context" + "strings" + + group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + shared_group "code.gitea.io/gitea/models/shared/group" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" +) + +type RepoGroup struct { + IsOwner bool + IsMember bool + IsGroupAdmin bool + Group *group_model.Group + GroupLink string + OrgGroupLink string + CanCreateRepoOrGroup bool + Team *organization.Team + Teams []*organization.Team + GroupTeam *group_model.RepoGroupTeam +} + +func (g *RepoGroup) CanWriteUnit(ctx *Context, unitType unit.Type) bool { + return g.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite +} + +func (g *RepoGroup) CanReadUnit(ctx *Context, unitType unit.Type) bool { + return g.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead +} + +func (g *RepoGroup) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode { + if doer != nil { + teams, err := organization.GetUserGroupTeams(ctx, g.Group.ID, doer.ID) + if err != nil { + log.Error("GetUserOrgTeams: %v", err) + return perm.AccessModeNone + } + + if err := teams.LoadUnits(ctx); err != nil { + log.Error("LoadUnits: %v", err) + return perm.AccessModeNone + } + + if len(teams) > 0 { + return teams.UnitMaxAccess(unitType) + } + } + + if g.Group.Visibility.IsPublic() { + return perm.AccessModeRead + } + + return perm.AccessModeNone +} + +func GetGroupByParams(ctx *Context) (err error) { + groupID := ctx.PathParamInt64("group_id") + + ctx.RepoGroup.Group, err = group_model.GetGroupByID(ctx, groupID) + if err != nil { + if group_model.IsErrGroupNotExist(err) { + ctx.NotFound(err) + } else { + ctx.ServerError("GetUserByName", err) + } + return err + } + if err = ctx.RepoGroup.Group.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + } + return err +} + +type GroupAssignmentOptions struct { + RequireMember bool + RequireOwner bool + RequireGroupAdmin bool +} + +func groupAssignment(ctx *Context) (bool, error) { + var err error + if ctx.RepoGroup.Group == nil { + err = GetGroupByParams(ctx) + } + if ctx.Written() { + return false, err + } + group := ctx.RepoGroup.Group + canAccess, err := group.CanAccess(ctx, ctx.Doer) + if err != nil { + ctx.ServerError("error checking group access", err) + return false, err + } + if group.Owner == nil { + err = group.LoadOwner(ctx) + if err != nil { + ctx.ServerError("LoadOwner", err) + return false, err + } + } + ownerAsOrg := (*organization.Organization)(group.Owner) + var orgWideAdmin, orgWideOwner, isOwnedBy bool + + if ctx.IsSigned { + if orgWideAdmin, err = ownerAsOrg.IsOrgAdmin(ctx, ctx.Doer.ID); err != nil { + ctx.ServerError("IsOrgAdmin", err) + return false, err + } + if orgWideOwner, err = ownerAsOrg.IsOwnedBy(ctx, ctx.Doer.ID); err != nil { + ctx.ServerError("IsOwnedBy", err) + } + } + if orgWideOwner { + ctx.RepoGroup.IsOwner = true + } + if orgWideAdmin { + ctx.RepoGroup.IsGroupAdmin = true + } + + if ctx.IsSigned && ctx.Doer.IsAdmin { + ctx.RepoGroup.IsOwner = true + ctx.RepoGroup.IsMember = true + ctx.RepoGroup.IsGroupAdmin = true + ctx.RepoGroup.CanCreateRepoOrGroup = true + } else if ctx.IsSigned { + isOwnedBy, err = group.IsOwnedBy(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("IsOwnedBy", err) + return false, err + } + ctx.RepoGroup.IsOwner = ctx.RepoGroup.IsOwner || isOwnedBy + + if ctx.RepoGroup.IsOwner { + ctx.RepoGroup.IsMember = true + ctx.RepoGroup.IsGroupAdmin = true + ctx.RepoGroup.CanCreateRepoOrGroup = true + } else { + ctx.RepoGroup.IsMember, err = shared_group.IsGroupMember(ctx, group.ID, ctx.Doer) + if err != nil { + ctx.ServerError("IsOrgMember", err) + return false, err + } + ctx.RepoGroup.CanCreateRepoOrGroup, err = group.CanCreateIn(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("CanCreateIn", err) + return false, err + } + } + } else { + ctx.Data["SignedUser"] = &user_model.User{} + } + ctx.RepoGroup.GroupLink = group.GroupLink() + ctx.RepoGroup.OrgGroupLink = group.OrgGroupLink() + + if ctx.RepoGroup.IsMember { + shouldSeeAllTeams := false + if ctx.RepoGroup.IsOwner { + shouldSeeAllTeams = true + } else { + teams, err := organization.GetUserGroupTeams(ctx, group.ID, ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetUserTeams", err) + return false, err + } + for _, team := range teams { + if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin { + shouldSeeAllTeams = true + break + } + } + } + if shouldSeeAllTeams { + ctx.RepoGroup.Teams, err = shared_group.GetGroupTeams(ctx, group.ID) + if err != nil { + ctx.ServerError("LoadTeams", err) + return false, err + } + } else { + ctx.RepoGroup.Teams, err = organization.GetUserGroupTeams(ctx, group.ID, ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetUserTeams", err) + return false, err + } + } + ctx.Data["NumTeams"] = len(ctx.RepoGroup.Teams) + } + + teamName := ctx.PathParam("team") + if len(teamName) > 0 { + teamExists := false + for _, team := range ctx.RepoGroup.Teams { + if strings.EqualFold(team.LowerName, strings.ToLower(teamName)) { + teamExists = true + var groupTeam *group_model.RepoGroupTeam + groupTeam, err = group_model.FindGroupTeamByTeamID(ctx, group.ID, team.ID) + if err != nil { + ctx.ServerError("FindGroupTeamByTeamID", err) + return false, err + } + ctx.RepoGroup.GroupTeam = groupTeam + ctx.RepoGroup.Team = team + ctx.RepoGroup.IsMember = true + ctx.Data["Team"] = ctx.RepoGroup.Team + break + } + } + + if !teamExists { + ctx.NotFound(err) + return false, err + } + + ctx.Data["IsTeamMember"] = ctx.RepoGroup.IsMember + + ctx.RepoGroup.IsGroupAdmin = ctx.RepoGroup.Team.IsOwnerTeam() || ctx.RepoGroup.Team.AccessMode >= perm.AccessModeAdmin + } else { + for _, team := range ctx.RepoGroup.Teams { + if team.AccessMode >= perm.AccessModeAdmin { + ctx.RepoGroup.IsGroupAdmin = true + break + } + } + } + if ctx.IsSigned { + isAdmin, err := group.IsAdminOf(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("IsAdminOf", err) + return false, err + } + ctx.RepoGroup.IsGroupAdmin = ctx.RepoGroup.IsGroupAdmin || isAdmin + } + return canAccess, nil +} + +func GroupAssignment(args GroupAssignmentOptions) func(ctx *Context) { + return func(ctx *Context) { + var err error + + ca, err := groupAssignment(ctx) + + if ctx.Written() || err != nil { + return + } + + group := ctx.RepoGroup.Group + if ctx.RepoGroup.Group.Visibility != structs.VisibleTypePublic && !ctx.IsSigned { + ctx.NotFound(err) + return + } + + if ctx.RepoGroup.Group.Visibility == structs.VisibleTypePrivate { + args.RequireMember = true + } else if ctx.IsSigned && (!ca && ctx.RepoGroup.Group.Visibility != structs.VisibleTypePublic) { + ctx.NotFound(err) + return + } + + if (args.RequireMember && !ctx.RepoGroup.IsMember) || + (args.RequireOwner && !ctx.RepoGroup.IsOwner) { + ctx.NotFound(err) + return + } + + ctx.Data["EnableFeed"] = setting.Other.EnableFeed + ctx.Data["FeedURL"] = ctx.RepoGroup.Group.GroupLink() + ctx.Data["IsGroupOwner"] = ctx.RepoGroup.IsOwner + ctx.Data["IsGroupMember"] = ctx.RepoGroup.IsMember + ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + ctx.Data["IsPublicMember"] = func(uid int64) bool { + is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid) + return is + } + ctx.Data["CanReadProjects"] = ctx.RepoGroup.CanReadUnit(ctx, unit.TypeProjects) + ctx.Data["CanCreateOrgRepo"] = ctx.RepoGroup.CanCreateRepoOrGroup + + ctx.Data["IsGroupAdmin"] = ctx.RepoGroup.IsGroupAdmin + if args.RequireGroupAdmin && !ctx.RepoGroup.IsGroupAdmin { + ctx.NotFound(err) + return + } + + if len(ctx.RepoGroup.Group.Description) != 0 { + ctx.Data["RenderedDescription"], err = markdown.RenderString(markup.NewRenderContext(ctx), ctx.RepoGroup.Group.Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + } + ctx.Data["Group"] = ctx.RepoGroup.Group + ctx.Data["ContextGroup"] = ctx.RepoGroup + ctx.Data["Doer"] = ctx.Doer + ctx.Data["GroupLink"] = ctx.RepoGroup.Group.GroupLink() + ctx.Data["OrgGroupLink"] = ctx.RepoGroup.OrgGroupLink + ctx.Data["Breadcrumbs"], err = group_model.GetParentGroupChain(ctx, group.ID) + if err != nil { + ctx.ServerError("GetParentGroupChain", err) + return + } + } +} + +func groupIsCurrent(ctx *Context) func(groupID int64) bool { + return func(groupID int64) bool { + if ctx.RepoGroup.Group == nil { + return false + } + return ctx.RepoGroup.Group.ID == groupID + } +}