mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-18 15:57:15 +01:00
## Overview This PR introduces granular permission controls for Gitea Actions tokens (`GITEA_TOKEN`), aligning Gitea's security model with GitHub Actions standards while maintaining compatibility with Gitea's unique repository unit system. It addresses the need for finer access control by allowing administrators and repository owners to define default token permissions, set maximum permission ceilings, and control cross-repository access within organizations. ## Key Features ### 1. Granular Token Permissions - **Standard Keyword Support**: Implements support for the `permissions:` keyword in workflow and job YAML files (e.g., `contents: read`, `issues: write`). - **Permission Modes**: - **Permissive**: Default write access for most units (backwards compatible). - **Restricted**: Default read-only access for `contents` and `packages`, with no access to other units. - ~~**Custom**: Allows defining specific default levels for each unit type (Code, Issues, PRs, Packages, etc.).~~**EDIT removed UI was confusing** - **Clamping Logic**: Workflow-defined permissions are automatically "clamped" by repository or organization-level maximum settings. Workflows cannot escalate their own permissions beyond these limits. ### 2. Organization & Repository Settings - **Settings UI**: Added new settings pages at both Organization and Repository levels to manage Actions token defaults and maximums. - **Inheritance**: Repositories can be configured to "Follow organization-level configuration," simplifying management across large organizations. - **Cross-Repository Access**: Added a policy to control whether Actions workflows can access other repositories or packages within the same organization. This can be set to "None," "All," or restricted to a "Selected" list of repositories. ### 3. Security Hardening - **Fork Pull Request Protection**: Tokens for workflows triggered by pull requests from forks are strictly enforced as read-only, regardless of repository settings. - ~~**Package Access**: Actions tokens can now only access packages explicitly linked to a repository, with cross-repo access governed by the organization's security policy.~~ **EDIT removed https://github.com/go-gitea/gitea/pull/36173#issuecomment-3873675346** - **Git Hook Integration**: Propagates Actions Task IDs to git hooks to ensure that pushes performed by Actions tokens respect the specific permissions granted at runtime. ### 4. Technical Implementation - **Permission Persistence**: Parsed permissions are calculated at job creation and stored in the `action_run_job` table. This ensures the token's authority is deterministic throughout the job's lifecycle. - **Parsing Priority**: Implemented a priority system in the YAML parser where the broad `contents` scope is applied first, allowing granular scopes like `code` or `releases` to override it for precise control. - **Re-runs**: Permissions are re-evaluated during a job re-run to incorporate any changes made to repository settings in the interim. ### How to Test 1. **Unit Tests**: Run `go test ./services/actions/...` and `go test ./models/repo/...` to verify parsing logic and permission clamping. 2. **Integration Tests**: Comprehensive tests have been added to `tests/integration/actions_job_token_test.go` covering: - Permissive vs. Restricted mode behavior. - YAML `permissions:` keyword evaluation. - Organization cross-repo access policies. - Resource access (Git, API, and Packages) under various permission configs. 3. **Manual Verification**: - Navigate to **Site/Org/Repo Settings -> Actions -> General**. - Change "Default Token Permissions" and verify that newly triggered workflows reflect these changes in their `GITEA_TOKEN` capabilities. - Attempt a cross-repo API call from an Action and verify the Org policy is enforced. ## Documentation Added a PR in gitea's docs for this : https://gitea.com/gitea/docs/pulls/318 ## UI: <img width="1366" height="619" alt="Screenshot 2026-01-24 174112" src="https://github.com/user-attachments/assets/bfa29c9a-4ea5-4346-9410-16d491ef3d44" /> <img width="1360" height="621" alt="Screenshot 2026-01-24 174048" src="https://github.com/user-attachments/assets/d5ec46c8-9a13-4874-a6a4-fb379936cef5" /> /fixes #24635 /claim #24635 --------- Signed-off-by: Excellencedev <ademiluyisuccessandexcellence@gmail.com> Signed-off-by: ChristopherHX <christopher.homberger@web.de> Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
789 lines
26 KiB
Go
789 lines
26 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/models/perm"
|
|
"code.gitea.io/gitea/models/unit"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// RepositoryListDefaultPageSize is the default number of repositories
|
|
// to load in memory when running administrative tasks on all (or almost
|
|
// all) of them.
|
|
// The number should be low enough to avoid filling up all RAM with
|
|
// repository data...
|
|
const RepositoryListDefaultPageSize = 64
|
|
|
|
// RepositoryList contains a list of repositories
|
|
type RepositoryList []*Repository
|
|
|
|
func (repos RepositoryList) Len() int {
|
|
return len(repos)
|
|
}
|
|
|
|
func (repos RepositoryList) Less(i, j int) bool {
|
|
return repos[i].FullName() < repos[j].FullName()
|
|
}
|
|
|
|
func (repos RepositoryList) Swap(i, j int) {
|
|
repos[i], repos[j] = repos[j], repos[i]
|
|
}
|
|
|
|
// ValuesRepository converts a repository map to a list
|
|
// FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18
|
|
func ValuesRepository(m map[int64]*Repository) []*Repository {
|
|
values := make([]*Repository, 0, len(m))
|
|
for _, v := range m {
|
|
values = append(values, v)
|
|
}
|
|
return values
|
|
}
|
|
|
|
// RepositoryListOfMap make list from values of map
|
|
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
|
|
return RepositoryList(ValuesRepository(repoMap))
|
|
}
|
|
|
|
func (repos RepositoryList) LoadUnits(ctx context.Context) error {
|
|
if len(repos) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Load units.
|
|
units := make([]*RepoUnit, 0, len(repos)*6)
|
|
if err := db.GetEngine(ctx).
|
|
In("repo_id", repos.IDs()).
|
|
Find(&units); err != nil {
|
|
return fmt.Errorf("find units: %w", err)
|
|
}
|
|
|
|
unitsMap := make(map[int64][]*RepoUnit, len(repos))
|
|
for _, unit := range units {
|
|
if !unit.Type.UnitGlobalDisabled() {
|
|
unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit)
|
|
}
|
|
}
|
|
|
|
for _, repo := range repos {
|
|
repo.Units = unitsMap[repo.ID]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (repos RepositoryList) IDs() []int64 {
|
|
repoIDs := make([]int64, len(repos))
|
|
for i := range repos {
|
|
repoIDs[i] = repos[i].ID
|
|
}
|
|
return repoIDs
|
|
}
|
|
|
|
func (repos RepositoryList) LoadOwners(ctx context.Context) error {
|
|
if len(repos) == 0 {
|
|
return nil
|
|
}
|
|
|
|
userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
|
|
return repo.OwnerID, true
|
|
})
|
|
|
|
// Load owners.
|
|
users := make(map[int64]*user_model.User, len(userIDs))
|
|
if err := db.GetEngine(ctx).
|
|
Where("id > 0").
|
|
In("id", userIDs).
|
|
Find(&users); err != nil {
|
|
return fmt.Errorf("find users: %w", err)
|
|
}
|
|
for i := range repos {
|
|
repos[i].Owner = users[repos[i].OwnerID]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error {
|
|
if len(repos) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Load primary language.
|
|
stats := make(LanguageStatList, 0, len(repos))
|
|
if err := db.GetEngine(ctx).
|
|
Where("`is_primary` = ? AND `language` != ?", true, "other").
|
|
In("`repo_id`", repos.IDs()).
|
|
Find(&stats); err != nil {
|
|
return fmt.Errorf("find primary languages: %w", err)
|
|
}
|
|
stats.LoadAttributes()
|
|
for i := range repos {
|
|
for _, st := range stats {
|
|
if st.RepoID == repos[i].ID {
|
|
repos[i].PrimaryLanguage = st
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadAttributes loads the attributes for the given RepositoryList
|
|
func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
|
|
if err := repos.LoadOwners(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return repos.LoadLanguageStats(ctx)
|
|
}
|
|
|
|
// SearchRepoOptions holds the search options
|
|
type SearchRepoOptions struct {
|
|
db.ListOptions
|
|
Actor *user_model.User
|
|
Keyword string
|
|
OwnerID int64
|
|
PriorityOwnerID int64
|
|
TeamID int64
|
|
OrderBy db.SearchOrderBy
|
|
Private bool // Include private repositories in results
|
|
StarredByID int64
|
|
WatchedByID int64
|
|
AllPublic bool // Include also all public repositories of users and public organisations
|
|
AllLimited bool // Include also all public repositories of limited organisations
|
|
// None -> include public and private
|
|
// True -> include just private
|
|
// False -> include just public
|
|
IsPrivate optional.Option[bool]
|
|
// None -> include collaborative AND non-collaborative
|
|
// True -> include just collaborative
|
|
// False -> include just non-collaborative
|
|
Collaborate optional.Option[bool]
|
|
// What type of unit the user can be collaborative in,
|
|
// it is ignored if Collaborate is False.
|
|
// TypeInvalid means any unit type.
|
|
UnitType unit.Type
|
|
// None -> include forks AND non-forks
|
|
// True -> include just forks
|
|
// False -> include just non-forks
|
|
Fork optional.Option[bool]
|
|
// If Fork option is True, you can use this option to limit the forks of a special repo by repo id.
|
|
ForkFrom int64
|
|
// None -> include templates AND non-templates
|
|
// True -> include just templates
|
|
// False -> include just non-templates
|
|
Template optional.Option[bool]
|
|
// None -> include mirrors AND non-mirrors
|
|
// True -> include just mirrors
|
|
// False -> include just non-mirrors
|
|
Mirror optional.Option[bool]
|
|
// None -> include archived AND non-archived
|
|
// True -> include just archived
|
|
// False -> include just non-archived
|
|
Archived optional.Option[bool]
|
|
// only search topic name
|
|
TopicOnly bool
|
|
// only search repositories with specified primary language
|
|
Language string
|
|
// include description in keyword search
|
|
IncludeDescription bool
|
|
// None -> include has milestones AND has no milestone
|
|
// True -> include just has milestones
|
|
// False -> include just has no milestone
|
|
HasMilestones optional.Option[bool]
|
|
// LowerNames represents valid lower names to restrict to
|
|
LowerNames []string
|
|
// When specified true, apply some filters over the conditions:
|
|
// - Don't show forks, when opts.Fork is OptionalBoolNone.
|
|
// - Do not display repositories that don't have a description, an icon and topics.
|
|
OnlyShowRelevant bool
|
|
}
|
|
|
|
// UserOwnedRepoCond returns user ownered repositories
|
|
func UserOwnedRepoCond(userID int64) builder.Cond {
|
|
return builder.Eq{
|
|
"repository.owner_id": userID,
|
|
}
|
|
}
|
|
|
|
// UserAssignedRepoCond return user as assignee repositories list
|
|
func UserAssignedRepoCond(id string, userID int64) builder.Cond {
|
|
return builder.And(
|
|
builder.Eq{
|
|
"repository.is_private": false,
|
|
},
|
|
builder.In(id,
|
|
builder.Select("issue.repo_id").From("issue_assignees").
|
|
InnerJoin("issue", "issue.id = issue_assignees.issue_id").
|
|
Where(builder.Eq{
|
|
"issue_assignees.assignee_id": userID,
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
|
|
// UserCreateIssueRepoCond return user created issues repositories list
|
|
func UserCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond {
|
|
return builder.And(
|
|
builder.Eq{
|
|
"repository.is_private": false,
|
|
},
|
|
builder.In(id,
|
|
builder.Select("issue.repo_id").From("issue").
|
|
Where(builder.Eq{
|
|
"issue.poster_id": userID,
|
|
"issue.is_pull": isPull,
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
|
|
// UserMentionedRepoCond return user metinoed repositories list
|
|
func UserMentionedRepoCond(id string, userID int64) builder.Cond {
|
|
return builder.And(
|
|
builder.Eq{
|
|
"repository.is_private": false,
|
|
},
|
|
builder.In(id,
|
|
builder.Select("issue.repo_id").From("issue_user").
|
|
InnerJoin("issue", "issue.id = issue_user.issue_id").
|
|
Where(builder.Eq{
|
|
"issue_user.is_mentioned": true,
|
|
"issue_user.uid": userID,
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
|
|
// UserAccessRepoCond returns a condition for selecting all repositories a user has unit independent access to
|
|
func UserAccessRepoCond(idStr string, userID int64) builder.Cond {
|
|
return builder.In(idStr, builder.Select("repo_id").
|
|
From("`access`").
|
|
Where(builder.And(
|
|
builder.Eq{"`access`.user_id": userID},
|
|
builder.Gt{"`access`.mode": int(perm.AccessModeNone)},
|
|
)),
|
|
)
|
|
}
|
|
|
|
// userCollaborationRepoCond returns a condition for selecting all repositories a user is collaborator in
|
|
func UserCollaborationRepoCond(idStr string, userID int64) builder.Cond {
|
|
return builder.In(idStr, builder.Select("repo_id").
|
|
From("`collaboration`").
|
|
Where(builder.And(
|
|
builder.Eq{"`collaboration`.user_id": userID},
|
|
)),
|
|
)
|
|
}
|
|
|
|
// UserOrgTeamRepoCond selects repos that the given user has access to through team membership
|
|
func UserOrgTeamRepoCond(idStr string, userID int64) builder.Cond {
|
|
return builder.In(idStr, userOrgTeamRepoBuilder(userID))
|
|
}
|
|
|
|
// userOrgTeamRepoBuilder returns repo ids where user's teams can access.
|
|
func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
|
|
return builder.Select("`team_repo`.repo_id").
|
|
From("team_repo").
|
|
Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id").
|
|
Where(builder.Eq{"`team_user`.uid": userID})
|
|
}
|
|
|
|
// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
|
|
func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
|
|
return userOrgTeamRepoBuilder(userID).
|
|
Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id").
|
|
Where(builder.Eq{"`team_unit`.`type`": unitType}).
|
|
And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)})
|
|
}
|
|
|
|
// userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit.
|
|
func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) builder.Cond {
|
|
return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType))
|
|
}
|
|
|
|
// UserOrgUnitRepoCond selects repos that the given user has access to through org and the special unit
|
|
func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond {
|
|
return builder.In(idStr,
|
|
userOrgTeamUnitRepoBuilder(userID, unitType).
|
|
And(builder.Eq{"`team_unit`.org_id": orgID}),
|
|
)
|
|
}
|
|
|
|
// userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations
|
|
func userOrgPublicRepoCond(userID int64) builder.Cond {
|
|
return builder.And(
|
|
builder.Eq{"`repository`.is_private": false},
|
|
builder.In("`repository`.owner_id",
|
|
builder.Select("`org_user`.org_id").
|
|
From("org_user").
|
|
Where(builder.Eq{"`org_user`.uid": userID}),
|
|
),
|
|
)
|
|
}
|
|
|
|
// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations
|
|
func userOrgPublicRepoCondPrivate(userID int64) builder.Cond {
|
|
return builder.And(
|
|
builder.Eq{"`repository`.is_private": false},
|
|
builder.In("`repository`.owner_id",
|
|
builder.Select("`org_user`.org_id").
|
|
From("org_user").
|
|
Join("INNER", "`user`", "`user`.id = `org_user`.org_id").
|
|
Where(builder.Eq{
|
|
"`org_user`.uid": userID,
|
|
"`user`.`type`": user_model.UserTypeOrganization,
|
|
"`user`.visibility": structs.VisibleTypePrivate,
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
|
|
// UserOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization
|
|
func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond {
|
|
return userOrgPublicRepoCond(userID).
|
|
And(builder.Eq{"`repository`.owner_id": orgID})
|
|
}
|
|
|
|
// SearchRepositoryCondition creates a query condition according search repository options
|
|
func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond {
|
|
cond := builder.NewCond()
|
|
|
|
if opts.Private {
|
|
if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID {
|
|
// OK we're in the context of a User
|
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
|
}
|
|
} else {
|
|
// Not looking at private organisations and users
|
|
// We should be able to see all non-private repositories that
|
|
// isn't in a private or limited organisation.
|
|
cond = cond.And(
|
|
builder.Eq{"is_private": false},
|
|
builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(
|
|
builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}),
|
|
)))
|
|
}
|
|
|
|
if opts.IsPrivate.Has() {
|
|
cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
|
|
}
|
|
|
|
if opts.Template.Has() {
|
|
cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
|
|
}
|
|
|
|
// Restrict to starred repositories
|
|
if opts.StarredByID > 0 {
|
|
cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID})))
|
|
}
|
|
|
|
// Restrict to watched repositories
|
|
if opts.WatchedByID > 0 {
|
|
cond = cond.And(builder.In("id", builder.Select("repo_id").From("watch").Where(builder.Eq{"user_id": opts.WatchedByID})))
|
|
}
|
|
|
|
// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
|
|
if opts.OwnerID > 0 {
|
|
accessCond := builder.NewCond()
|
|
if !opts.Collaborate.Value() {
|
|
accessCond = builder.Eq{"owner_id": opts.OwnerID}
|
|
}
|
|
|
|
if opts.Collaborate.ValueOrDefault(true) {
|
|
// A Collaboration is:
|
|
|
|
collaborateCond := builder.NewCond()
|
|
// 1. Repository we don't own
|
|
collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
|
|
// 2. But we can see because of:
|
|
{
|
|
userAccessCond := builder.NewCond()
|
|
// A. We have unit independent access
|
|
userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
|
|
// B. We are in a team for
|
|
if opts.UnitType == unit.TypeInvalid {
|
|
userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID))
|
|
} else {
|
|
userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType))
|
|
}
|
|
// C. Public repositories in organizations that we are member of
|
|
userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
|
|
collaborateCond = collaborateCond.And(userAccessCond)
|
|
}
|
|
if !opts.Private {
|
|
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
|
|
}
|
|
|
|
accessCond = accessCond.Or(collaborateCond)
|
|
}
|
|
|
|
if opts.AllPublic {
|
|
accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}))))
|
|
}
|
|
|
|
if opts.AllLimited {
|
|
accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited}))))
|
|
}
|
|
|
|
cond = cond.And(accessCond)
|
|
}
|
|
|
|
if opts.TeamID > 0 {
|
|
cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID})))
|
|
}
|
|
|
|
if opts.Keyword != "" {
|
|
// separate keyword
|
|
subQueryCond := builder.NewCond()
|
|
for v := range strings.SplitSeq(opts.Keyword, ",") {
|
|
if opts.TopicOnly {
|
|
subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)})
|
|
} else {
|
|
subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)})
|
|
}
|
|
}
|
|
subQuery := builder.Select("repo_topic.repo_id").From("repo_topic").
|
|
Join("INNER", "topic", "topic.id = repo_topic.topic_id").
|
|
Where(subQueryCond).
|
|
GroupBy("repo_topic.repo_id")
|
|
|
|
keywordCond := builder.In("id", subQuery)
|
|
if !opts.TopicOnly {
|
|
likes := builder.NewCond()
|
|
for v := range strings.SplitSeq(opts.Keyword, ",") {
|
|
likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
|
|
|
|
// If the string looks like "org/repo", match against that pattern too
|
|
if opts.TeamID == 0 && strings.Count(opts.Keyword, "/") == 1 {
|
|
pieces := strings.Split(opts.Keyword, "/")
|
|
ownerName := pieces[0]
|
|
repoName := pieces[1]
|
|
likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)}))
|
|
}
|
|
|
|
if opts.IncludeDescription {
|
|
likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)})
|
|
}
|
|
}
|
|
keywordCond = keywordCond.Or(likes)
|
|
}
|
|
cond = cond.And(keywordCond)
|
|
}
|
|
|
|
if opts.Language != "" {
|
|
cond = cond.And(builder.In("id", builder.
|
|
Select("repo_id").
|
|
From("language_stat").
|
|
Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
|
|
}
|
|
|
|
if opts.Fork.Has() || opts.OnlyShowRelevant {
|
|
if opts.OnlyShowRelevant && !opts.Fork.Has() {
|
|
cond = cond.And(builder.Eq{"is_fork": false})
|
|
} else {
|
|
cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
|
|
|
|
if opts.ForkFrom > 0 && opts.Fork.Value() {
|
|
cond = cond.And(builder.Eq{"fork_id": opts.ForkFrom})
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.Mirror.Has() {
|
|
cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
|
|
}
|
|
|
|
if opts.Actor != nil && opts.Actor.IsRestricted {
|
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
|
}
|
|
|
|
if opts.Archived.Has() {
|
|
cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
|
|
}
|
|
|
|
if opts.HasMilestones.Has() {
|
|
if opts.HasMilestones.Value() {
|
|
cond = cond.And(builder.Gt{"num_milestones": 0})
|
|
} else {
|
|
cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
|
|
}
|
|
}
|
|
|
|
if opts.OnlyShowRelevant {
|
|
// Only show a repo that has at least a topic, an icon, or a description
|
|
subQueryCond := builder.NewCond()
|
|
|
|
// Topic checking. Topics are present.
|
|
if setting.Database.Type.IsPostgreSQL() { // postgres stores the topics as json and not as text
|
|
subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"}))
|
|
} else {
|
|
subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
|
|
}
|
|
|
|
// Description checking. Description not empty
|
|
subQueryCond = subQueryCond.Or(builder.Neq{"description": ""})
|
|
|
|
// Repo has a avatar
|
|
subQueryCond = subQueryCond.Or(builder.Neq{"avatar": ""})
|
|
|
|
// Always hide repo's that are empty
|
|
subQueryCond = subQueryCond.And(builder.Eq{"is_empty": false})
|
|
|
|
cond = cond.And(subQueryCond)
|
|
}
|
|
|
|
return cond
|
|
}
|
|
|
|
// SearchRepository returns repositories based on search options,
|
|
// it returns results in given range and number of total results.
|
|
func SearchRepository(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
|
|
cond := SearchRepositoryCondition(opts)
|
|
return SearchRepositoryByCondition(ctx, opts, cond, true)
|
|
}
|
|
|
|
// CountRepository counts repositories based on search options,
|
|
func CountRepository(ctx context.Context, opts SearchRepoOptions) (int64, error) {
|
|
return db.GetEngine(ctx).Where(SearchRepositoryCondition(opts)).Count(new(Repository))
|
|
}
|
|
|
|
// SearchRepositoryByCondition search repositories by condition
|
|
func SearchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) {
|
|
sess, count, err := searchRepositoryByCondition(ctx, opts, cond)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
defaultSize := 50
|
|
if opts.PageSize > 0 {
|
|
defaultSize = opts.PageSize
|
|
}
|
|
repos := make(RepositoryList, 0, defaultSize)
|
|
if err := sess.Find(&repos); err != nil {
|
|
return nil, 0, fmt.Errorf("Repo: %w", err)
|
|
}
|
|
|
|
if opts.PageSize <= 0 {
|
|
count = int64(len(repos))
|
|
}
|
|
|
|
if loadAttributes {
|
|
if err := repos.LoadAttributes(ctx); err != nil {
|
|
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
|
|
}
|
|
}
|
|
|
|
return repos, count, nil
|
|
}
|
|
|
|
func searchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) {
|
|
page := opts.Page
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
|
|
orderBy := opts.OrderBy
|
|
if len(orderBy) == 0 {
|
|
orderBy = db.SearchOrderByAlphabetically
|
|
}
|
|
|
|
args := make([]any, 0)
|
|
if opts.PriorityOwnerID > 0 {
|
|
orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", orderBy))
|
|
args = append(args, opts.PriorityOwnerID)
|
|
} else if strings.Count(opts.Keyword, "/") == 1 {
|
|
// With "owner/repo" search times, prioritise results which match the owner field
|
|
orgName := strings.Split(opts.Keyword, "/")[0]
|
|
orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", orderBy))
|
|
args = append(args, orgName)
|
|
}
|
|
|
|
sess := db.GetEngine(ctx)
|
|
|
|
var count int64
|
|
if opts.PageSize > 0 {
|
|
var err error
|
|
count, err = sess.
|
|
Where(cond).
|
|
Count(new(Repository))
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("Count: %w", err)
|
|
}
|
|
}
|
|
|
|
sess = sess.Where(cond).OrderBy(orderBy.String(), args...)
|
|
if opts.PageSize > 0 {
|
|
sess = sess.Limit(opts.PageSize, (page-1)*opts.PageSize)
|
|
}
|
|
return sess, count, nil
|
|
}
|
|
|
|
// SearchRepositoryIDsByCondition search repository IDs by given condition.
|
|
func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]int64, error) {
|
|
repoIDs := make([]int64, 0, 10)
|
|
return repoIDs, db.GetEngine(ctx).
|
|
Table("repository").
|
|
Cols("id").
|
|
Where(cond).
|
|
Find(&repoIDs)
|
|
}
|
|
|
|
func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond {
|
|
return cond.Or(builder.And(
|
|
builder.Eq{"`repository`.is_private": false},
|
|
// Aren't in a private organisation or limited organisation if we're not logged in
|
|
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
|
|
builder.And(
|
|
builder.Eq{"type": user_model.UserTypeOrganization},
|
|
builder.In("visibility", orgVisibilityLimit)),
|
|
))))
|
|
}
|
|
|
|
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
|
|
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
|
|
cond := builder.NewCond()
|
|
|
|
if user == nil || !user.IsRestricted || user.ID <= 0 {
|
|
orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate}
|
|
if user == nil || user.ID <= 0 {
|
|
orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
|
|
}
|
|
// 1. Be able to see all non-private repositories
|
|
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
|
}
|
|
|
|
if user != nil {
|
|
// 2. Be able to see all repositories that we have unit independent access to
|
|
// 3. Be able to see all repositories through team membership(s)
|
|
if unitType == unit.TypeInvalid {
|
|
// Regardless of UnitType
|
|
cond = cond.Or(
|
|
UserAccessRepoCond("`repository`.id", user.ID),
|
|
UserOrgTeamRepoCond("`repository`.id", user.ID),
|
|
)
|
|
} else {
|
|
// For a specific UnitType
|
|
cond = cond.Or(
|
|
UserCollaborationRepoCond("`repository`.id", user.ID),
|
|
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
|
|
)
|
|
}
|
|
// 4. Repositories that we directly own
|
|
cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID})
|
|
if !user.IsRestricted {
|
|
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
|
cond = cond.Or(userOrgPublicRepoCond(user.ID))
|
|
} else if !setting.Service.RequireSignInViewStrict {
|
|
orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate, structs.VisibleTypeLimited}
|
|
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
|
}
|
|
}
|
|
|
|
return cond
|
|
}
|
|
|
|
// SearchRepositoryByName takes keyword and part of repository name to search,
|
|
// it returns results in given range and number of total results.
|
|
func SearchRepositoryByName(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
|
|
opts.IncludeDescription = false
|
|
return SearchRepository(ctx, opts)
|
|
}
|
|
|
|
// SearchRepositoryIDs takes keyword and part of repository name to search,
|
|
// it returns results in given range and number of total results.
|
|
func SearchRepositoryIDs(ctx context.Context, opts SearchRepoOptions) ([]int64, int64, error) {
|
|
opts.IncludeDescription = false
|
|
|
|
cond := SearchRepositoryCondition(opts)
|
|
|
|
sess, count, err := searchRepositoryByCondition(ctx, opts, cond)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
defaultSize := 50
|
|
if opts.PageSize > 0 {
|
|
defaultSize = opts.PageSize
|
|
}
|
|
|
|
ids := make([]int64, 0, defaultSize)
|
|
err = sess.Select("id").Table("repository").Find(&ids)
|
|
if opts.PageSize <= 0 {
|
|
count = int64(len(ids))
|
|
}
|
|
|
|
return ids, count, err
|
|
}
|
|
|
|
// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
|
|
func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder {
|
|
// NB: Please note this code needs to still work if user is nil
|
|
return builder.Select("id").From("repository").Where(AccessibleRepositoryCondition(user, unit.TypeInvalid))
|
|
}
|
|
|
|
// FindUserCodeAccessibleRepoIDs finds all at Code level accessible repositories' ID by the user's id
|
|
func FindUserCodeAccessibleRepoIDs(ctx context.Context, user *user_model.User) ([]int64, error) {
|
|
return SearchRepositoryIDsByCondition(ctx, AccessibleRepositoryCondition(user, unit.TypeCode))
|
|
}
|
|
|
|
// FindUserCodeAccessibleOwnerRepoIDs finds all repository IDs for the given owner whose code the user can see.
|
|
func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user *user_model.User) ([]int64, error) {
|
|
return SearchRepositoryIDsByCondition(ctx, builder.NewCond().And(
|
|
builder.Eq{"owner_id": ownerID},
|
|
AccessibleRepositoryCondition(user, unit.TypeCode),
|
|
))
|
|
}
|
|
|
|
// GetUserRepositories returns a list of repositories of given user.
|
|
func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
|
|
if len(opts.OrderBy) == 0 {
|
|
opts.OrderBy = "updated_unix DESC"
|
|
}
|
|
|
|
cond := builder.NewCond()
|
|
if opts.Actor == nil {
|
|
return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: Actor is needed but not given")
|
|
}
|
|
cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID})
|
|
if !opts.Private {
|
|
cond = cond.And(builder.Eq{"is_private": false})
|
|
}
|
|
|
|
if len(opts.LowerNames) > 0 {
|
|
cond = cond.And(builder.In("lower_name", opts.LowerNames))
|
|
}
|
|
|
|
sess := db.GetEngine(ctx)
|
|
|
|
count, err := sess.Where(cond).Count(new(Repository))
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("Count: %w", err)
|
|
}
|
|
|
|
sess = sess.Where(cond).OrderBy(opts.OrderBy.String())
|
|
repos := make(RepositoryList, 0, opts.PageSize)
|
|
return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos)
|
|
}
|
|
|
|
func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int64) (RepositoryList, error) {
|
|
if len(repoIDs) == 0 {
|
|
return RepositoryList{}, nil
|
|
}
|
|
repos := make(RepositoryList, 0, len(repoIDs))
|
|
return repos, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).In("id", repoIDs).Find(&repos)
|
|
}
|