Skip to content

Commit

Permalink
Merge remote-tracking branch 'giteaofficial/main'
Browse files Browse the repository at this point in the history
* giteaofficial/main:
  Fix counting and filtering on the dashboard page for issues (go-gitea#26657)
  add mfa doc (go-gitea#26654)
  [Refactor] getIssueStatsChunk to move inner function into own one (go-gitea#26671)
  Use line-height: normal by default (go-gitea#26635)
  Improve repo sub menu (go-gitea#26531)
  Fix organization list in dashboard (go-gitea#26650)
  • Loading branch information
zjjhot committed Aug 23, 2023
2 parents b29d031 + 5db21ce commit a34a70c
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 171 deletions.
35 changes: 35 additions & 0 deletions docs/content/usage/multi-factor-authentication.en-us.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
date: "2023-08-22T14:21:00+08:00"
title: "Usage: Multi-factor Authentication (MFA)"
slug: "multi-factor-authentication"
weight: 15
toc: false
draft: false
menu:
sidebar:
parent: "usage"
name: "Multi-factor Authentication (MFA)"
weight: 15
identifier: "multi-factor-authentication"
---

# Multi-factor Authentication (MFA)

Multi-factor Authentication (also referred to as MFA or 2FA) enhances security by requiring a time-sensitive set of credentials in addition to a password.
If a password were later to be compromised, logging into Gitea will not be possible without the additional credentials and the account would remain secure.
Gitea supports both TOTP (Time-based One-Time Password) tokens and FIDO-based hardware keys using the Webauthn API.

MFA can be configured within the "Security" tab of the user settings page.

## MFA Considerations

Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI.
This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled.
If SSH is not an option for Git operations, an access token can be generated within the "Applications" tab of the user settings page.
This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP.

> **Warning** - By its very nature, an access token sidesteps the security benefits of MFA.
> It must be kept secure and should only be used as a last resort.
The Gitea API supports providing the relevant TOTP password in the `X-Gitea-OTP` header, as described in [API Usage](development/api-usage.md).
This should be used instead of an access token where possible.
97 changes: 49 additions & 48 deletions models/issues/issue_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,68 +116,69 @@ func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) {
func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) {
stats := &IssueStats{}

countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
sess := db.GetEngine(db.DefaultContext).
Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
if len(opts.RepoIDs) > 1 {
sess.In("issue.repo_id", opts.RepoIDs)
} else if len(opts.RepoIDs) == 1 {
sess.And("issue.repo_id = ?", opts.RepoIDs[0])
}
sess := db.GetEngine(db.DefaultContext).
Join("INNER", "repository", "`issue`.repo_id = `repository`.id")

if len(issueIDs) > 0 {
sess.In("issue.id", issueIDs)
}
var err error
stats.OpenCount, err = applyIssuesOptions(sess, opts, issueIDs).
And("issue.is_closed = ?", false).
Count(new(Issue))
if err != nil {
return stats, err
}
stats.ClosedCount, err = applyIssuesOptions(sess, opts, issueIDs).
And("issue.is_closed = ?", true).
Count(new(Issue))
return stats, err
}

applyLabelsCondition(sess, opts)
func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int64) *xorm.Session {
if len(opts.RepoIDs) > 1 {
sess.In("issue.repo_id", opts.RepoIDs)
} else if len(opts.RepoIDs) == 1 {
sess.And("issue.repo_id = ?", opts.RepoIDs[0])
}

applyMilestoneCondition(sess, opts)
if len(issueIDs) > 0 {
sess.In("issue.id", issueIDs)
}

applyProjectCondition(sess, opts)
applyLabelsCondition(sess, opts)

if opts.AssigneeID > 0 {
applyAssigneeCondition(sess, opts.AssigneeID)
} else if opts.AssigneeID == db.NoConditionID {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
}
applyMilestoneCondition(sess, opts)

if opts.PosterID > 0 {
applyPosterCondition(sess, opts.PosterID)
}
applyProjectCondition(sess, opts)

if opts.MentionedID > 0 {
applyMentionedCondition(sess, opts.MentionedID)
}
if opts.AssigneeID > 0 {
applyAssigneeCondition(sess, opts.AssigneeID)
} else if opts.AssigneeID == db.NoConditionID {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
}

if opts.ReviewRequestedID > 0 {
applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
}
if opts.PosterID > 0 {
applyPosterCondition(sess, opts.PosterID)
}

if opts.ReviewedID > 0 {
applyReviewedCondition(sess, opts.ReviewedID)
}
if opts.MentionedID > 0 {
applyMentionedCondition(sess, opts.MentionedID)
}

switch opts.IsPull {
case util.OptionalBoolTrue:
sess.And("issue.is_pull=?", true)
case util.OptionalBoolFalse:
sess.And("issue.is_pull=?", false)
}
if opts.ReviewRequestedID > 0 {
applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
}

return sess
if opts.ReviewedID > 0 {
applyReviewedCondition(sess, opts.ReviewedID)
}

var err error
stats.OpenCount, err = countSession(opts, issueIDs).
And("issue.is_closed = ?", false).
Count(new(Issue))
if err != nil {
return stats, err
switch opts.IsPull {
case util.OptionalBoolTrue:
sess.And("issue.is_pull=?", true)
case util.OptionalBoolFalse:
sess.And("issue.is_pull=?", false)
}
stats.ClosedCount, err = countSession(opts, issueIDs).
And("issue.is_closed = ?", true).
Count(new(Issue))
return stats, err

return sess
}

// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
Expand Down
36 changes: 23 additions & 13 deletions models/repo/repo_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ type SearchRepoOptions struct {
// True -> include just collaborative
// False -> include just non-collaborative
Collaborate util.OptionalBool
// 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
Expand Down Expand Up @@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {

if opts.Collaborate != util.OptionalBoolFalse {
// A Collaboration is:
collaborateCond := builder.And(
// 1. Repository we don't own
builder.Neq{"owner_id": opts.OwnerID},
// 2. But we can see because of:
builder.Or(
// A. We have unit independent access
UserAccessRepoCond("`repository`.id", opts.OwnerID),
// B. We are in a team for
UserOrgTeamRepoCond("`repository`.id", opts.OwnerID),
// C. Public repositories in organizations that we are member of
userOrgPublicRepoCondPrivate(opts.OwnerID),
),
)

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))
}
Expand Down
41 changes: 38 additions & 3 deletions modules/indexer/issues/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

db_model "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/indexer/issues/bleve"
"code.gitea.io/gitea/modules/indexer/issues/db"
Expand Down Expand Up @@ -277,7 +278,7 @@ func IsAvailable(ctx context.Context) bool {
}

// SearchOptions indicates the options for searching issues
type SearchOptions internal.SearchOptions
type SearchOptions = internal.SearchOptions

const (
SortByCreatedDesc = internal.SortByCreatedDesc
Expand All @@ -291,7 +292,6 @@ const (
)

// SearchIssues search issues by options.
// It returns issue ids and a bool value indicates if the result is imprecise.
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
indexer := *globalIndexer.Load()

Expand All @@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
indexer = db.NewIndexer()
}

result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts))
result, err := indexer.Search(ctx, opts)
if err != nil {
return nil, 0, err
}
Expand All @@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err

return ret, result.Total, nil
}

// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) {
opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} })

_, total, err := SearchIssues(ctx, opts)
return total, err
}

// CountIssuesByRepo counts issues by options and group by repo id.
// It's not a complete implementation, since it requires the caller should provide the repo ids.
// That means opts.RepoIDs must be specified, and opts.AllPublic must be false.
// It's good enough for the current usage, and it can be improved if needed.
// TODO: use "group by" of the indexer engines to implement it.
func CountIssuesByRepo(ctx context.Context, opts *SearchOptions) (map[int64]int64, error) {
if len(opts.RepoIDs) == 0 {
return nil, fmt.Errorf("opts.RepoIDs must be specified")
}
if opts.AllPublic {
return nil, fmt.Errorf("opts.AllPublic must be false")
}

repoIDs := container.SetOf(opts.RepoIDs...).Values()
ret := make(map[int64]int64, len(repoIDs))
// TODO: it could be faster if do it in parallel for some indexer engines. Improve it if users report it's slow.
for _, repoID := range repoIDs {
count, err := CountIssues(ctx, opts.Copy(func(o *internal.SearchOptions) { o.RepoIDs = []int64{repoID} }))
if err != nil {
return nil, err
}
ret[repoID] = count
}

return ret, nil
}
13 changes: 13 additions & 0 deletions modules/indexer/issues/internal/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ type SearchOptions struct {
SortBy SortBy // sort by field
}

// Copy returns a copy of the options.
// Be careful, it's not a deep copy, so `SearchOptions.RepoIDs = {...}` is OK while `SearchOptions.RepoIDs[0] = ...` is not.
func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOptions {
if o == nil {
return nil
}
v := *o
for _, e := range edit {
e(&v)
}
return &v
}

type SortBy string

const (
Expand Down
Loading

0 comments on commit a34a70c

Please sign in to comment.