Compare commits

...

6 Commits

Author SHA1 Message Date
Giteabot
a016d678db fix(workflows): branch protection status checks fail when workflow uses on: paths filter (#38237) (#38302) 2026-07-01 20:10:11 +00:00
Giteabot
b6e409badd fix(oauth2): persist linkAccountData during auto-link 2FA flow (#38274) (#38295)
Backport #38274 by @afahey03

Fixes HTTP 500 when OIDC auto account linking (`ACCOUNT_LINKING=auto`)
requires local 2FA. `oauth2LinkAccount` set `linkAccount` in the session
before redirecting to 2FA but did not persist `linkAccountData`, so
`TwoFactorPost` failed with `not in LinkAccount session`. The manual
linking flow already stored both, this aligns auto-link with that
behavior.

Created the test, `TestOAuth2AutoLinkWithTwoFactor`, which verifies that
automatic account linking completes after the user passes local 2FA when
an OIDC identity matches an existing account.

DISCLAIMER: I used AI to create the test

Closes #38171

Co-authored-by: Aidan Fahey <afahey2003@yahoo.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-01 13:18:37 +02:00
Giteabot
2734504cfc fix(actions): allow Actions bot to push to protected branches (#38284) (#38293)
Backport #38284 by @bircni

Fixes #38278

## Problem

When branch protection matches the branch an Actions workflow pushes to,
the runner's `git push` is rejected — even though the workflow token has
`contents: write` and the same push performed with a PAT (write access)
succeeds. Disabling protection or changing the pattern so it no longer
matches makes the push work.

## Root cause

In `preReceiveBranch` (`routers/private/hook_pre_receive.go`), the "can
the doer push to this protected branch" check resolves the pusher with
`user_model.GetUserByID(ctx, ctx.opts.UserID)`. For an Actions push the
user ID is `-2` (the virtual `ActionsUserID`), which has no database
row, so the lookup fails. Even past that, `CanUserPush` →
`HasAccessUnit`/whitelist membership cannot evaluate a virtual user and
returns `false`. As a result the Actions bot was rejected on every
matching protected branch, despite the earlier `assertCanWriteRef`
already confirming the token's code-write via
`GetActionsUserRepoPermission`.

This was inconsistent: a PAT with identical write access passed the
exact same check.

## Fix

Evaluate the Actions bot against its already-computed token permission
instead of a user lookup, mirroring the existing
`IsUserMergeWhitelisted` pattern:

- Add `CanActionsUserPush` / `CanActionsUserForcePush` on
`ProtectedBranch`, which take the precomputed `access_model.Permission`.
- Allow the push when push is enabled, **no** push whitelist is
enforced, and the token has code-write.
- Keep the bot blocked when a whitelist is enforced — it cannot be added
to one, so it must use a pull request. This preserves the whitelist as a
real security boundary.

Force-push, signed-commit and protected-file-path checks are untouched.

Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-01 11:46:17 +02:00
Giteabot
eee7967b81 fix(actions): include all aggregable run statuses in status filter (#38280) (#38287)
Backport #38280 by @bircni

The **Status** filter dropdown on the repository Actions run list does
not let you filter for **Blocked** runs (nor **Cancelled** or
**Skipped**). These statuses are missing from the dropdown even though a
run can legitimately end up in any of them.

A run's status is computed by `aggregateJobStatus`, which can return
`Blocked`, `Cancelled` and `Skipped`. Because the filter dropdown only
offered Success, Failure, Waiting, Running and Cancelling, runs in those
other states existed but were impossible to filter for.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-01 10:12:11 +02:00
Giteabot
1df8f91691 fix(archiver): use serializable repo-archive queue payload (#38273) (#38283)
Backport #38273 by @Vinod-OAI

After upgrading from 1.25.x to 1.26.x, `repo-archive` workers can fail
to unmarshal queued items:

```
Failed to unmarshal item from queue "repo-archive":
json: unable to unmarshal into Go convert.Conversion within "/Repo/Units/0/Config":
cannot derive concrete type for nil interface with finite type set
```

`ArchiveRequest` started embedding `*repo_model.Repository` in 1.26,
which does not round-trip through the JSON queue.

This change stores a minimal `archiveQueueItem` (`RepoID`, `Type`,
`CommitID`, `Paths`) in `repo-archive` and loads the repository in the
worker. `UnmarshalJSON` accepts legacy payloads that used `RepoID` or
embedded `Repo.id`.

Fixes #38272

<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch
names`.
- Read the contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs

Describe your change below and link any issue it fixes.
-->

Co-authored-by: Vinod-OAI <venkat.vinod@observe.ai>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-30 19:17:29 +02:00
bircni
4654e7eccd docs: Changelog for 1.27.0-rc0 (#38247)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-06-29 21:06:07 +02:00
20 changed files with 771 additions and 150 deletions

View File

@@ -4,6 +4,274 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.27.0-rc0](https://github.com/go-gitea/gitea/releases/tag/v1.27.0-rc0) - 2026-06-28
* BREAKING
* Feat(actions)!: improve support for reusable workflows (#37478)
* Use Content-Security-Policy: script nonce (#37232)
* SECURITY
* Fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 [security] (#37786)
* Fix(oauth): restrict introspection to the token's client (#38042)
* Fix(api): don't expose private org membership via public_members (#38145)
* Fix(actions): deny fork-PR cross-repo access via collaborative owner (#38214)
* Fix(migrations): prevent path traversal in repository restore (#38215)
* FEATURES
* Feat(actions): add workflow status badge modal (#38196)
* Feat(actions): support owner-level and global scoped workflows (#38154)
* Feat(api): support ref suffixes in compare (#38148)
* Feat(actions): implement `jobs.<job_id>.continue-on-error` (#38100)
* Feat(actions): show run status on browser tab favicon (#38071)
* Feat(api): add token introspection and self-deletion endpoint (#37995)
* Feat(api): add q parameter to list branches API for server-side filtering (#37982)
* Feat(repo): split repository creation limit into user and org scopes (#37872)
* Feat(actions): bulk delete, disable and enable runners in admin UI (#37869)
* Feat(actions): List workflows that were executed once but got removed from the default branch (#37835)
* Feat(org): add team visibility so org members can discover teams (#37680)
* Feat: add raw diff/patch endpoint for repository comparisons (#37632)
* Feat: Add avatar stacks (#37594)
* Feat(actions): add job summaries (GITHUB_STEP_SUMMARY) (#37500)
* Feat(web): Add Jupyter Notebook (.ipynb) Rendering Support (#37433)
* Support for Custom URI Schemes in OAuth2 Redirect URIs (#37356)
* Feat(orgs): Add search bar for organization members tab page (#37347)
* Feat(api): Add assignees APIs (#37330)
* Feat(api): Add GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs (#37196)
* Serve OpenAPI 3.0 spec at /openapi.v1.json (#37038)
* Add project column picker to issue and pull request sidebar (#37037)
* Allow multiple projects per issue and pull requests (#36784)
* Feat(ui): add "follow rename" to file commit history list (#34994)
* Feat(ssh): auto generate additional ssh keys (#33974)
* ENHANCEMENTS
* Enhance: allow builtin default git config options to be overridden (#38172)
* Enhance: allow MathML core elements (#38034)
* Enhance(markup): improve issue title rendering (#37908)
* Enhance(actions): set descriptive browser tab title on run view (#37870)
* Enhance: Migrate remaining gopkg.in/yaml.v3 usages to go.yaml.in/yaml/v4 (#37866)
* Enhance(actions): show workflow name from YAML instead of filename (#37833)
* Feat(actions): add before/after to PR synchronize event payload (#37827)
* Enhance(actions): add branch filters to run list (#37826)
* Enhance(actions): Make Summary UI more beautiful with more infos (#37824)
* Feat: add copy button to action step header, improve other copy buttons (#37744)
* Fix(icon): use repo-forked icon to display forks count (#37731)
* Feat(api): add sort and order query parameters to job list endpoints (#37672)
* Feat(api): add last_sync to repository API (#37566)
* Enhance: Adjust Workflow Graph styling (#37497)
* Improve code editor text selection and clean up lint enablement (#37474)
* Add mirror auth updates to repo edit API and settings (#37468)
* Replace `olivere/elastic` with REST API client, add OpenSearch support (#37411)
* Feat: Add default PR branch update style setting (#37410)
* Fix inconsistent disabled styling on logged-out repo header buttons (#37406)
* Allow fast-forward-only merge when signed commits are required (#37335)
* Enhance styling in actions page (#37323)
* Fix: improve actions status icons and texts (#37206)
* Make Markdown fenced code block work with more syntaxes (#37154)
* Fix: Sort action run jobs by JobID and Name with matrix examples (#37046)
* Add API endpoint to reply to pull request review comments (#36683)
* PERFORMANCE
* Perf(web): sort the action_run query by a repo-scoped index when possible (#38155)
* Perf: Various performance regression fixes (#38078)
* Perf: extend action `c_u` index to include `created_unix` for faster dashboard feeds (#38076)
* Batch-load related data in actions run, job, and task API endpoints (#37032)
* BUGFIXES
* Fix: update npm dependencies, fix misc issues (#38257)
* Fix(api): respect since/until when counting commits for X-Total-Count (#38204)
* Fix: codemirror regressions (#38248)
* Fix(api): support HEAD requests on all API GET endpoints (#38245)
* Fix(actions): Cleanup workflow status badge code (#38241)
* Fix(web): Correctly align the "disabled" label on larger workflow names (#38240)
* Fix(actions): don't swallow HTML entities into linkified URLs (#38239)
* Fix(packages): accept npm "repository" and "bin" in string form (#38236)
* Fix(actions): fix 500 error when canceling a canceling task (#38223)
* Fix(deps): update module golang.org/x/image to v0.43.0 [security] (#38219)
* Fix(mssql): convert legacy DATETIME columns to DATETIME2 (#38216)
* Fix(api): deny private org member enumeration via /members (#38213)
* Fix(actions): ensure all waiting jobs get runners in large workflows (#38200)
* Fix(deps): update go dependencies (#38194)
* Fix(deps): update npm dependencies (#38193)
* Fix(cli): default must-change-password to false for bot users (#38175)
* Fix(actions): show run index in run view and fix summary graph height (#38165)
* Fix: csp (#38162)
* Fix(deps): update npm dependencies (#38123)
* Fix(mssql): expand legacy issue and comment long-text columns (#38120)
* Fix(packages): validate debian distribution and component names (#38116)
* Fix(packages): validate module version in goproxy ParsePackage (#38104)
* Fix(deps): update dependency esbuild to v0.28.1 [security] (#38097)
* Fix: git push hook post receive (#38089)
* Fix(ui): prevent commit status popup overflowing its row (#38081)
* Fix: validate gem name in rubygems parseMetadataFile (#38061)
* Fix: commit display name (#38057)
* Fix: csp regressions (#38047)
* Fix: api error message (#38031)
* Fix(deps): update npm dependencies (#38029)
* Fix: pgsql lint (#38022)
* Fix(indexer): fix assignee filters in issue search (#38021)
* Fix: various dropdown problems (#38020)
* Fix: refactor git error handling and make archive streaming handle non-existing commit id (#38007)
* Fix: raise git required version to 2.13 (#37996)
* Fix: remove "no-transfrom" from the cache-control header (#37985)
* Fix(deps): update module github.com/google/go-github/v87 to v88 (#37971)
* Fix: use committer time where ever possible as default (#37969)
* Fix(deps): update npm dependencies, remove nolyfill (#37968)
* Fix(deps): update go dependencies (#37967)
* Fix(pull): preserve squash message trailers and additional commit messages (#37954)
* Fix(deps): update module golang.org/x/image to v0.41.0 [security] (#37904)
* Fix: support ##[command] log prefix in action run UI (#37882)
* Fix(deps): update module github.com/google/go-github/v86 to v87 (#37845)
* Fix(deps): update npm dependencies (#37844)
* Fix(deps): update go dependencies (#37841)
* Fix(frontend): resolve Vite assets by manifest source path (#37836)
* Fix(locales): Replace hardcoded strings (#37788)
* Fix(packages): render markdown links relative to linked repo (#37676)
* Fix: persist mirror repository metadata (#37519)
* Fix cmd tests by mocking builtin paths (#37369)
* Add `form-fetch-action` to some forms, fix "fetch action" resp bug (#37305)
* Feat: execute post run cleanup when workflow is cancelled (#37275)
* Fix `relative-time` error and improve global error handler (#37241)
* Refactor flash message and remove SanitizeHTML template func (#37179)
* TESTING
* Test: speed up two tests (#37905)
* Test: Fix random failure test (#37887)
* Test: fix flaky `issue-comment` close test (#37880)
* Test: enable WAL for sqlite integration tests (#37861)
* Test: fix flaky `TestResourceIndex` and reduce its runtime (#37847)
* Test: run `TestAPIRepoMigrate` offline via a local clone source (#37817)
* Ci: shard tests and reduce redundant work (#37618)
* Test(e2e): run playwright via container (#37300)
* Remove external service dependencies in migration tests (#36866)
* BUILD
* Fix(actions): authenticate snapcraft before nightly remote build (#38252)
* Ci: cap Elasticsearch heap in db-tests (#37816)
* Build(snap): publish nightly version to snapcraft via actions (#37814)
* Ci: split pgsql shards into plain jobs, dedupe setup actions (#37802)
* Ci: narrow files-changed frontend filter (#37749)
* Ci: add `zizmor` to `lint-actions` (#37720)
* Chore: clean up "contrib" dir (#37690)
* Fix: snap build (main branch) (#37685)
* Ci: Also lint json5 files (#37659)
* Feat(editor): broaden language detection in web code editor (#37619)
* Build: update pnpm to v11 (#37591)
* Refactor(deps): migrate from `nektos/act` fork to `gitea/runner` (#37557)
* Refactor: lint bare `fill`/`stroke` colors, add vars for git graph color series (#37543)
* Update go js py dependencies (#37525)
* Ci: lint PR titles with commitlint (#37498)
* Chore: upgrade Go version in devcontainer image to 1.26 (#37374)
* Update GitHub Actions to latest major versions (#37313)
* Update go js dependencies (#37312)
* Fail vite build on rolldown warnings via NODE_ENV=test (#37270)
* Remove htmx (#37224)
* Replace custom Go formatter with `golangci-lint fmt` (#37194)
* Refactor htmx and fetch-action related code (#37186)
* Integrate renovate bot for all dependency updates (#37050)
* Build(sign): move to sigstore (#38250)
* DOCS
* Docs: update changelog for 1.26.3 & 1.26.4 (#38178)
* Docs: fix duplicated word in foreachref doc comment (#38161)
* Docs: Clarify criteria for becoming a merger (#38113)
* Docs: Publish TOC Election Result 2026 (#38111)
* Docs: mark openapi3 as autogenerated in attributes (#37963)
* Docs: add development setup guide (#37960)
* MISC
* Revert(sign): restore gpg (#38251)
* Refactor: replace legacy `delete-button` with `link-action` (#38143)
* Refactor(actions): read runner capabilities from proto field (#38068)
* Refactor(api): clarify APIError message usage and fix legacy lint error (#38012)
* Refactor: Use db.Get[] instead of db.GetEngine(ctx).Get(bean) to avoid zero value fetching wrong database record (#37977)
* Fix(deps): update go dependencies (#37851)
* Ci: Fix sync PR labels from the conventional-commit title (#37784) (#37825)
* Ci: tweak `files-changed`, add `free-disk-space` (#37819)
* Fix(deps): update module golang.org/x/crypto to v0.52.0 [security] (#37806)
* Test(e2e): add comment, release, star, PR and fork tests (#37800)
* Chore: simplify issue and pull request templates (#37799)
* Chore: Update giteabot to fix failure when backport (#37789)
* Fix(api): handle partial failures in push mirror synchronization gracefully (#37782)
* Fix(deps): update module gitlab.com/gitlab-org/api/client-go/v2 to v2.26.0 (#37771)
* Ci: split giteabot workflow (#37770)
* Fix(deps): update npm dependencies (#37768)
* Refactor(waitgroup): replace Add/Done goroutines with WaitGroup.Go (#37764)
* Fix(deps): update module google.golang.org/grpc to v1.81.1 (#37762)
* Ci: fix cache-related issues (#37761)
* Chore: fix tests (#37760)
* Fix(deps): update module github.com/google/go-github/v85 to v86 (#37754)
* Fix(deps): update npm dependencies (#37753)
* Fix(deps): update go dependencies (#37752)
* Chore(deps): update action dependencies (#37751)
* Fix(markup): wrap indented code blocks for the code-copy button (#37748)
* Chore(db): introduce db.Session and db.EngineMigration interfaces (#37746)
* Feat(web): also display PR counts in repo list (#37739)
* Refactor(glob): use strings.Builder for regexp compilation (#37730)
* Chore(doctor): remove four obsolete doctor check implementations (#37728)
* Refactor(org): simplify owner-team org repo creation logic (#37727)
* Refactor: move `workflowpattern` into `modules/actions` (#37717)
* Chore: clean up tests (#37715)
* Style: misc UI fixes (#37691)
* Ci: add shellcheck linter (#37682)
* Fix: catch and fix more lint problems (#37674)
* Fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test (#37662)
* Fix(deps): update npm dependencies (#37647)
* Ci(renovate): update Go import paths on major bumps (#37641)
* Fix(deps): update go dependencies (major) (#37639)
* Chore(deps): update action dependencies (major) (#37638)
* Fix(deps): update module code.gitea.io/sdk/gitea to v0.25.0 (#37637)
* Fix(deps): update npm dependencies (#37636)
* Refactor(log): replace log.Critical with log.Error (#37624)
* Build(deps): bump fast-uri from 3.1.0 to 3.1.2 (#37616)
* Feat(oauth): Support AWS Cognito OAuth2 provider (#37607)
* Chore(deps): update action dependencies (#37603)
* Ci: allow `chore` type in PR title lint (#37575)
* Refactor: only reset a database table when the table's data was changed (#37573)
* Ci: increase renovate frequency and fix RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS (#37565)
* Refactor: use modernc sqlite driver as default (#37562)
* Docs: fix 4 typos in CHANGELOG.md (#37549)
* Fix(deps): update go dependencies (#37541)
* Chore(deps): update action dependencies (#37540)
* Refactor pull request view (6) (#37522)
* Fix: redirect early CLI console logger to stderr (#37507)
* Refactor "flex-list" to "flex-divided-list" (#37505)
* Refactor compare diff/pull page (1) (#37481)
* Refactor pull request view (4) (#37451)
* Update 1.26.1 changelog in main (#37442)
* Refactor: use named `Permission` field in `Repository` struct instead of anonymous embedding (#37441)
* Refactor: serve site manifest via `/assets/site-manifest.json` endpoint (#37405)
* Remove IsValidExternalURL/IsAPIURL and use IsValidURL at call sites (#37364)
* Update `Block a user` form (#37359)
* Move review request functions to a standalone file (#37358)
* Feat(security): set X-Content-Type-Options: nosniff by default (#37354)
* Enable strict TypeScript, add `errorMessage` helper (#37292)
* Refactor frontend `tw-justify-between` layouts to `flex-left-right` (#37291)
* Update Nix flake (#37284)
* Fix Repository transferring page (#37277)
* Remove `SubmitEvent` polyfill (#37276)
* Remove dead code identified by `deadcode` tool (#37271)
* Upgrade go-git to v5.18.0 (#37268)
* Don't add useless labels which will bother changelog generation (#37267)
* Move heatmap to first-party code (#37262)
* Tests/integration: simplify code (#37249)
* Add pagination and search box to org teams list (#37245)
* Remove error returns from crypto random helpers and callers (#37240)
* Add `ExternalIDClaim` option for OAuth2 OIDC auth source (#37229)
* Refactor: simplify ParseCatFileTreeLine and catBatchParseTreeEntries (#37210)
* Refactor "htmx" to "fetch action" (#37208)
* Update go js py dependencies (#37204)
* Add comment for the design of "user activity time" (#37195)
* Remove outdated RunUser logic (#37180)
* Models/fixtures: add "DO NOT add more test data" comment to all yml fixture files (#37150)
* Update javascript dependencies (#37142)
* Update go dependencies (#37141)
* Frontport changelog of v1.26.0-rc0 (#37138)
* Introduce `ActionRunAttempt` to represent each execution of a run (#37119)
* Workflow Artifact Info Hover (#37100)
* Extend issue context popup beyond markdown content (#36908)
* Add bulk repository deletion for organizations (#36763)
* Feat: Add bypass allowlist for branch protection (#36514)
## [1.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21
* SECURITY

View File

@@ -135,8 +135,8 @@ type StatusInfo struct {
// GetStatusInfoList returns a slice of StatusInfo
func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
// same as those in aggregateJobStatus
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning, StatusCancelling}
// same as those in aggregateJobStatus (StatusUnknown excluded; it's the "shouldn't happen" fallback)
allStatus := []Status{StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped, StatusWaiting, StatusRunning, StatusBlocked, StatusCancelling}
statusInfoList := make([]StatusInfo, 0, len(allStatus))
for _, s := range allStatus {
statusInfoList = append(statusInfoList, StatusInfo{

View File

@@ -73,8 +73,11 @@ func TestGetStatusInfoList(t *testing.T) {
assert.Equal(t, []StatusInfo{
{Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"},
{Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"},
{Status: int(StatusCancelled), StatusName: StatusCancelled.String(), DisplayedStatus: "actions.status.cancelled"},
{Status: int(StatusSkipped), StatusName: StatusSkipped.String(), DisplayedStatus: "actions.status.skipped"},
{Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"},
{Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"},
{Status: int(StatusBlocked), StatusName: StatusBlocked.String(), DisplayedStatus: "actions.status.blocked"},
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
}, statusInfoList)
}

View File

@@ -55,29 +55,34 @@ func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, er
return parsed, nil
}
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event, returning those whose `on:` matches.
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event.
// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered).
func MatchScopedWorkflows(
parsed []*ParsedScopedWorkflow,
consumerGitRepo *git.Repository,
consumerCommit *git.Commit,
triggedEvent webhook_module.HookEventType,
payload api.Payloader,
) []*DetectedWorkflow {
workflows := make([]*DetectedWorkflow, 0, len(parsed))
) (matched, filtered []*DetectedWorkflow) {
for _, p := range parsed {
for _, evt := range p.Events {
if evt.IsSchedule() {
// schedule is a non-target for scoped workflows
continue
}
if detectMatched(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
workflows = append(workflows, &DetectedWorkflow{
EntryName: p.EntryName,
TriggerEvent: evt,
Content: p.Content,
})
dwf := &DetectedWorkflow{
EntryName: p.EntryName,
TriggerEvent: evt,
Content: p.Content,
}
switch detectWorkflowMatch(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
case detectMatched:
matched = append(matched, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
}
}
}
return workflows
return matched, filtered
}

View File

@@ -30,6 +30,14 @@ type DetectedWorkflow struct {
Content []byte
}
type detectResult int
const (
detectMatched detectResult = iota // event matched; run normally
detectNotApplicable // event/type doesn't apply; create nothing
detectFilteredOut // matched but excluded by a branch/paths filter; emits a skipped commit status
)
func init() {
model.OnDecodeNodeError = func(node yaml.Node, out any, err error) {
// Log the error instead of panic or fatal.
@@ -172,18 +180,16 @@ func DetectWorkflows(
triggedEvent webhook_module.HookEventType,
payload api.Payloader,
detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
) (workflows, schedules, filtered []*DetectedWorkflow, err error) {
_, entries, err := ListWorkflows(commit)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
workflows := make([]*DetectedWorkflow, 0, len(entries))
schedules := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(entry)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// one workflow may have multiple events
@@ -203,18 +209,24 @@ func DetectWorkflows(
}
schedules = append(schedules, dwf)
}
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
} else {
dwf := &DetectedWorkflow{
EntryName: entry.Name(),
TriggerEvent: evt,
Content: content,
}
workflows = append(workflows, dwf)
switch detectWorkflowMatch(gitRepo, commit, triggedEvent, payload, evt) {
case detectMatched:
workflows = append(workflows, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
}
}
}
}
return workflows, schedules, nil
return workflows, schedules, filtered, nil
}
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
@@ -252,9 +264,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D
return wfs, nil
}
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
func detectWorkflowMatch(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) detectResult {
if !canGithubEventMatch(evt.Name, triggedEvent) {
return false
return detectNotApplicable
}
switch triggedEvent {
@@ -268,7 +280,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
}
// no special filter parameters for these events, just return true if name matched
return true
return detectMatched
case // push
webhook_module.HookEventPush:
@@ -279,14 +291,20 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
webhook_module.HookEventIssueAssign,
webhook_module.HookEventIssueLabel,
webhook_module.HookEventIssueMilestone:
return matchIssuesEvent(payload.(*api.IssuePayload), evt)
if matchIssuesEvent(payload.(*api.IssuePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // issue_comment
webhook_module.HookEventIssueComment,
// `pull_request_comment` is same as `issue_comment`
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
webhook_module.HookEventPullRequestComment:
return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt)
if matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // pull_request
webhook_module.HookEventPullRequest,
@@ -300,34 +318,49 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
case // pull_request_review
webhook_module.HookEventPullRequestReviewApproved,
webhook_module.HookEventPullRequestReviewRejected:
return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt)
if matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // pull_request_review_comment
webhook_module.HookEventPullRequestReviewComment:
return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt)
if matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // release
webhook_module.HookEventRelease:
return matchReleaseEvent(payload.(*api.ReleasePayload), evt)
if matchReleaseEvent(payload.(*api.ReleasePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // registry_package
webhook_module.HookEventPackage:
return matchPackageEvent(payload.(*api.PackagePayload), evt)
if matchPackageEvent(payload.(*api.PackagePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // workflow_run
webhook_module.HookEventWorkflowRun:
return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt)
if matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) {
return detectMatched
}
return detectNotApplicable
default:
log.Warn("unsupported event %q", triggedEvent)
return false
return detectNotApplicable
}
}
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool {
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) detectResult {
// with no special filter parameters
if len(evt.Acts()) == 0 {
return true
return detectMatched
}
matchTimes := 0
@@ -393,14 +426,14 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
case "paths-ignore":
if refName.IsTag() {
@@ -410,14 +443,14 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
default:
log.Warn("push event unsupported condition %q", cond)
@@ -427,7 +460,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
if hasBranchFilter && hasTagFilter {
matchTimes++
}
return matchTimes == len(evt.Acts())
if matchTimes == len(evt.Acts()) {
return detectMatched
}
return detectFilteredOut
}
func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
@@ -478,7 +514,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
return matchTimes == len(evt.Acts())
}
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) detectResult {
acts := evt.Acts()
activityTypeMatched := false
matchTimes := 0
@@ -525,7 +561,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha)
if err != nil {
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
return false
return detectNotApplicable
}
}
@@ -557,33 +593,39 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
case "paths-ignore":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
default:
log.Warn("pull request event unsupported condition %q", cond)
}
}
return activityTypeMatched && matchTimes == len(evt.Acts())
if !activityTypeMatched {
return detectNotApplicable
}
if matchTimes != len(evt.Acts()) {
return detectFilteredOut
}
return detectMatched
}
func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {

View File

@@ -101,49 +101,49 @@ func TestDetectMatched(t *testing.T) {
triggedEvent webhook_module.HookEventType
payload api.Payloader
yamlOn string
expected bool
expected detectResult
}{
{
desc: "HookEventCreate(create) matches GithubEventCreate(create)",
triggedEvent: webhook_module.HookEventCreate,
payload: nil,
yamlOn: "on: create",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
triggedEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueOpened},
yamlOn: "on: issues",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
triggedEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
yamlOn: "on: issues",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
triggedEvent: webhook_module.HookEventPullRequestSync,
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
yamlOn: "on: pull_request",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on: pull_request",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
yamlOn: "on: pull_request",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
@@ -155,56 +155,56 @@ func TestDetectMatched(t *testing.T) {
},
},
yamlOn: "on:\n pull_request:\n branches: [main]",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on:\n pull_request:\n types: [labeled]",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
triggedEvent: webhook_module.HookEventPullRequestReviewComment,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
triggedEvent: webhook_module.HookEventPullRequestReviewRejected,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
triggedEvent: webhook_module.HookEventRelease,
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
yamlOn: "on:\n release:\n types: [published]",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
triggedEvent: webhook_module.HookEventPackage,
payload: &api.PackagePayload{Action: api.HookPackageCreated},
yamlOn: "on:\n registry_package:\n types: [updated]",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
triggedEvent: webhook_module.HookEventWiki,
payload: nil,
yamlOn: "on: gollum",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
triggedEvent: webhook_module.HookEventSchedule,
payload: nil,
yamlOn: "on: schedule",
expected: true,
expected: detectMatched,
},
{
desc: "push to tag matches workflow with paths condition (should skip paths check)",
@@ -222,7 +222,19 @@ func TestDetectMatched(t *testing.T) {
},
commit: nil,
yamlOn: "on:\n push:\n paths:\n - src/**",
expected: true,
expected: detectMatched,
},
{
desc: "push branch filter excludes -> filtered out",
triggedEvent: webhook_module.HookEventPush,
payload: &api.PushPayload{
Ref: "refs/heads/feature/x",
Before: "0000000",
Commits: []*api.PayloadCommit{{ID: "abc", Added: []string{"a.go"}, Message: "x"}},
},
commit: nil,
yamlOn: "on:\n push:\n branches: [main]",
expected: detectFilteredOut,
},
}
@@ -231,7 +243,7 @@ func TestDetectMatched(t *testing.T) {
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
assert.NoError(t, err)
assert.Len(t, evts, 1)
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
assert.Equal(t, tc.expected, detectWorkflowMatch(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
})
}
}

View File

@@ -147,6 +147,9 @@ func MustInitSessioner() func(next http.Handler) http.Handler {
Secure: setting.SessionConfig.Secure,
SameSite: setting.SessionConfig.SameSite,
Domain: setting.SessionConfig.Domain,
// in the future, if websocket is used, the websocket handler should manage its own session sync (release)
IgnoreReleaseForWebSocket: true,
})
if err != nil {
log.Fatal("common.Sessioner failed: %v", err)

View File

@@ -31,9 +31,7 @@ import (
type preReceiveContext struct {
*gitea_context.PrivateContext
// loadedPusher indicates that where the following information are loaded
loadedPusher bool
user *user_model.User // it's the org user if a DeployKey is used
user *user_model.User // the "pusher", it's the org user if a DeployKey is used
userPerm access_model.Permission
deployKeyAccessMode perm_model.AccessMode
@@ -53,10 +51,7 @@ type preReceiveContext struct {
func (ctx *preReceiveContext) canWriteCodeUnit() bool {
if ctx.canWriteCodeUnitCached == nil {
var canWrite bool
if ctx.loadPusherAndPermission() {
canWrite = ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
}
canWrite := ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
ctx.canWriteCodeUnitCached = &canWrite
}
return *ctx.canWriteCodeUnitCached
@@ -91,9 +86,6 @@ func (ctx *preReceiveContext) assertCanWriteRef(refFullName git.RefName) bool {
// CanCreatePullRequest returns true if pusher can create pull requests
func (ctx *preReceiveContext) CanCreatePullRequest() bool {
if !ctx.checkedCanCreatePullRequest {
if !ctx.loadPusherAndPermission() {
return false
}
ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
ctx.checkedCanCreatePullRequest = true
}
@@ -124,6 +116,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
opts: opts,
}
if !ourCtx.loadPusherAndPermission() {
return // if error occurs, loadPusherAndPermission had written the error response
}
// Iterate across the provided old commit IDs
for i := range opts.OldCommitIDs {
oldCommitID := opts.OldCommitIDs[i]
@@ -281,18 +277,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
}
} else {
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
}
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user)
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.user)
} else {
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.user)
}
}
@@ -354,12 +342,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
return
}
// although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below
if !ctx.loadPusherAndPermission() {
// if error occurs, loadPusherAndPermission had written the error response
return
}
// Now check if the user is allowed to merge PRs for this repository
// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user)
@@ -499,10 +481,6 @@ func generateGitEnv(opts *private.HookOptions) (env []string) {
// loadPusherAndPermission returns false if an error occurs, and it writes the error response
func (ctx *preReceiveContext) loadPusherAndPermission() bool {
if ctx.loadedPusher {
return true
}
if ctx.opts.UserID == user_model.ActionsUserID {
taskID := ctx.opts.ActionsTaskID
ctx.user = user_model.NewActionsUserWithTaskID(taskID)
@@ -555,7 +533,5 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
}
ctx.deployKeyAccessMode = deployKey.Mode
}
ctx.loadedPusher = true
return true
}

View File

@@ -52,7 +52,6 @@ func TestPreReceiveCanWriteCodePerBranch(t *testing.T) {
mockCtx, _ := contexttest.MockPrivateContext(t, "/")
ctx := &preReceiveContext{
PrivateContext: mockCtx,
loadedPusher: true,
user: maintainer,
userPerm: headPerm,
}

View File

@@ -118,7 +118,7 @@ func autoSignIn(ctx *context.Context) (bool, error) {
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
if err := updateSession(ctx, nil, map[string]any{
if err := regenerateSession(ctx, nil, map[string]any{
session.KeyUID: u.ID,
session.KeyUname: u.Name,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
@@ -357,7 +357,7 @@ func SignInPost(ctx *context.Context) {
// User will need to use WebAuthn, save data
updates["totpEnrolled"] = u.ID
}
if err := updateSession(ctx, nil, updates); err != nil {
if err := regenerateSession(ctx, nil, updates); err != nil {
ctx.ServerError("UserSignIn: Unable to update session", err)
return
}
@@ -398,7 +398,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
return
}
if err := updateSession(ctx, []string{
if err := regenerateSession(ctx, []string{
// Delete the openid, 2fa and link_account data
"openid_verified_uri",
"openid_signin_remember",
@@ -884,7 +884,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
log.Trace("User activated: %s", user.Name)
if err := updateSession(ctx, nil, map[string]any{
if err := regenerateSession(ctx, nil, map[string]any{
"uid": user.ID,
"uname": user.Name,
}); err != nil {
@@ -936,7 +936,7 @@ func ActivateEmail(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
func updateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
func regenerateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
return fmt.Errorf("regenerate session: %w", err)
}

View File

@@ -164,7 +164,12 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData
return
}
if err := updateSession(ctx, nil, map[string]any{
if err := Oauth2SetLinkAccountData(ctx, *linkAccountData); err != nil {
ctx.ServerError("Oauth2SetLinkAccountData", err)
return
}
if err := regenerateSession(ctx, nil, map[string]any{
// User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID,
"twofaRemember": remember,

View File

@@ -285,9 +285,7 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
}
func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error {
return updateSession(ctx, nil, map[string]any{
"linkAccountData": linkAccountData,
})
return ctx.Session.Set("linkAccountData", linkAccountData)
}
func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.User) {
@@ -409,7 +407,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
return
}
if err := updateSession(ctx, nil, map[string]any{
if err := regenerateSession(ctx, nil, map[string]any{
session.KeyUID: u.ID,
session.KeyUname: u.Name,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
@@ -434,7 +432,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
}
}
if err := updateSession(ctx, nil, map[string]any{
if err := regenerateSession(ctx, nil, map[string]any{
// User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID,
"twofaRemember": false,

View File

@@ -213,7 +213,7 @@ func signInOpenIDVerify(ctx *context.Context) {
if u != nil {
nickname = u.LowerName
}
if err := updateSession(ctx, nil, map[string]any{
if err := regenerateSession(ctx, nil, map[string]any{
"openid_verified_uri": id,
"openid_determined_email": email,
"openid_determined_username": nickname,

View File

@@ -14,8 +14,10 @@ import (
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/commitstatus"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
webhook_module "gitea.dev/modules/webhook"
commitstatus_service "gitea.dev/services/repository/commitstatus"
@@ -147,21 +149,78 @@ func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event,
// scopedPrefix is computed once per run by the caller. The settings page derives the same string to preview expected checks.
ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, job.Name, event)
}
targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID)
return createWorkflowCommitStatus(ctx, repo, commitID, ctxName, run.WorkflowID, toCommitStatus(job.Status), targetURL, toCommitStatusDescription(job))
}
// CreateSkippedCommitStatusForFilteredWorkflow posts a skipped commit status for each job of a
// workflow that matched the triggering event but was excluded by a branch/paths filter.
// This lets a required status check tied to that context be satisfied without the workflow running.
// No ActionRun is created, so the status has no target URL (there is no run/job to link to).
// A non-empty scopedPrefix prefixes each context with its source repo, matching scoped runs.
func CreateSkippedCommitStatusForFilteredWorkflow(ctx context.Context, repo *repo_model.Repository, event webhook_module.HookEventType, triggerEvent, workflowID string, content []byte, payload api.Payloader, scopedPrefix string) error {
// Derive the status event name and target commit from the payload.
// TODO: this mirrors getCommitStatusEventNameAndCommitID, which derives the same from a persisted run. Should merge the logic if possible.
var statusEvent, commitID string
switch event {
case webhook_module.HookEventPush:
if p, ok := payload.(*api.PushPayload); ok && p.HeadCommit != nil {
statusEvent, commitID = "push", p.HeadCommit.ID
}
case webhook_module.HookEventPullRequest,
webhook_module.HookEventPullRequestSync,
webhook_module.HookEventPullRequestAssign,
webhook_module.HookEventPullRequestLabel,
webhook_module.HookEventPullRequestReviewRequest,
webhook_module.HookEventPullRequestMilestone:
if p, ok := payload.(*api.PullRequestPayload); ok && p.PullRequest != nil && p.PullRequest.Head != nil {
statusEvent, commitID = "pull_request", p.PullRequest.Head.Sha
if triggerEvent == actions_module.GithubEventPullRequestTarget {
statusEvent = "pull_request_target"
}
}
}
if statusEvent == "" || commitID == "" {
return nil // unsupported event or missing commit id, nothing to post
}
workflows, err := jobparser.Parse(content)
if err != nil {
return fmt.Errorf("jobparser.Parse: %w", err)
}
displayName := actions_module.WorkflowDisplayName(workflowID, content)
for _, sw := range workflows {
_, job := sw.Job()
if job == nil {
continue
}
jobName := util.EllipsisDisplayString(job.Name, 255) // run creation truncates job names the same way
ctxName := actions_module.WorkflowStatusContextName(displayName, jobName, statusEvent)
if scopedPrefix != "" {
ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, jobName, statusEvent)
}
// "Skipped" mirrors toCommitStatusDescription for StatusSkipped.
if err := createWorkflowCommitStatus(ctx, repo, commitID, ctxName, workflowID, commitstatus.CommitStatusSkipped, "", "Skipped"); err != nil {
return err
}
}
return nil
}
// createWorkflowCommitStatus posts the commit status for one workflow-job context.
func createWorkflowCommitStatus(ctx context.Context, repo *repo_model.Repository, commitID, ctxName, workflowID string, state commitstatus.CommitStatusState, targetURL, description string) error {
// Mix the workflow file path into the hash so two workflow files that
// share the same `name:` and job name produce distinct commit statuses
// even though they render identically — matching GitHub's behavior
// (issue #35699).
ctxHash := git_model.HashCommitStatusContext(ctxName + "\x00" + run.WorkflowID)
ctxHash := git_model.HashCommitStatusContext(ctxName + "\x00" + workflowID)
// Pre-fix rows were hashed from Context alone. If a pre-existing row with
// the legacy hash is still the "latest" for this SHA, reuse that hash so
// the new row supersedes it; otherwise the old pending status would stay
// stuck forever (it lives in its own dedupe group). Only relevant for
// in-flight workflows at upgrade time.
legacyHash := git_model.HashCommitStatusContext(ctxName)
state := toCommitStatus(job.Status)
targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID)
description := toCommitStatusDescription(job)
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
if err != nil {

View File

@@ -183,8 +183,9 @@ func notify(ctx context.Context, input *notifyInput) error {
}
var detectedWorkflows []*actions_module.DetectedWorkflow
var filteredWorkflows []*actions_module.DetectedWorkflow
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
workflows, schedules, filtered, err := actions_module.DetectWorkflows(gitRepo, commit,
input.Event,
input.Payload,
shouldDetectSchedules,
@@ -212,6 +213,17 @@ func notify(ctx context.Context, input *notifyInput) error {
}
}
for _, wf := range filtered {
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName)
continue
}
if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget {
filteredWorkflows = append(filteredWorkflows, wf)
}
}
if input.PullRequest != nil {
// detect pull_request_target workflows
baseRef := git.BranchPrefix + input.PullRequest.BaseBranch
@@ -219,7 +231,7 @@ func notify(ctx context.Context, input *notifyInput) error {
if err != nil {
return fmt.Errorf("gitRepo.GetCommit: %w", err)
}
baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
baseWorkflows, _, baseFiltered, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
if err != nil {
return fmt.Errorf("DetectWorkflows: %w", err)
}
@@ -227,11 +239,24 @@ func notify(ctx context.Context, input *notifyInput) error {
log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RelativePath(), baseCommit.ID)
} else {
for _, wf := range baseWorkflows {
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName)
continue
}
if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
detectedWorkflows = append(detectedWorkflows, wf)
}
}
}
for _, wf := range baseFiltered {
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName)
continue
}
if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
filteredWorkflows = append(filteredWorkflows, wf)
}
}
}
if shouldDetectSchedules {
@@ -244,6 +269,8 @@ func notify(ctx context.Context, input *notifyInput) error {
return err
}
handleFilteredWorkflows(ctx, input, filteredWorkflows)
return detectAndHandleScopedWorkflows(ctx, input, ref, gitRepo, commit)
}
@@ -369,6 +396,16 @@ func buildApproveAndInsertRun(
return nil
}
// handleFilteredWorkflows posts a skipped commit status for each workflow that matched the event but was excluded by a branch/paths filter.
func handleFilteredWorkflows(ctx context.Context, input *notifyInput, filteredWorkflows []*actions_module.DetectedWorkflow) {
for _, dwf := range filteredWorkflows {
if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, ""); err != nil {
log.Error("repo %s: skipped commit status for workflow %s: %v", input.Repo.RelativePath(), dwf.EntryName, err)
continue
}
}
}
func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
return newNotifyInput(issue.Repo, issue.Poster, event)
}
@@ -640,7 +677,7 @@ func detectAndHandleScopedWorkflows(
continue
}
sourceCommitSHA, detected, err := detectScopedWorkflowsForSource(ctx, input, consumerGitRepo, consumerCommit, sourceRepo)
sourceCommitSHA, detected, filtered, err := detectScopedWorkflowsForSource(ctx, input, consumerGitRepo, consumerCommit, sourceRepo)
if err != nil {
log.Error("scoped workflows: source %d for consumer %s: %v", sourceRepoID, input.Repo.RelativePath(), err)
continue
@@ -658,23 +695,40 @@ func detectAndHandleScopedWorkflows(
continue
}
}
// A filtered-out scoped workflow posts a skipped commit status.
if len(filtered) > 0 {
scopedPrefix := actions_model.ScopedStatusContextPrefix(ctx, sourceRepo.ID)
for _, dwf := range filtered {
if actions_model.ScopedWorkflowOptedOut(actionsConfig, sources, sourceRepo.ID, dwf.EntryName) {
continue
}
if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, scopedPrefix); err != nil {
log.Error("scoped workflows: skipped commit status for source %s workflow %s: %v", sourceRepo.RelativePath(), dwf.EntryName, err)
continue
}
}
}
}
return nil
}
// detectScopedWorkflowsForSource detects the scoped workflows from the source repo at its default branch
// detectScopedWorkflowsForSource detects the scoped workflows from the source repo at its default branch.
// detected are the workflows to run; filtered matched the event but were excluded by a branch/paths
// filter and post a skipped commit status.
func detectScopedWorkflowsForSource(
ctx context.Context,
input *notifyInput,
consumerGitRepo *git.Repository,
consumerCommit *git.Commit,
sourceRepo *repo_model.Repository,
) (sourceCommitSHA string, detected []*actions_module.DetectedWorkflow, err error) {
) (sourceCommitSHA string, detected, filtered []*actions_module.DetectedWorkflow, err error) {
// scoped workflow content is always taken from the source repo's default branch; the parse is cached per (source, default-branch SHA) and reused across consuming repos/events
sourceCommitSHA, parsed, err := LoadParsedScopedWorkflows(ctx, sourceRepo)
if err != nil {
return "", nil, err
return "", nil, nil, err
}
return sourceCommitSHA, actions_module.MatchScopedWorkflows(parsed, consumerGitRepo, consumerCommit, input.Event, input.Payload), nil
detected, filtered = actions_module.MatchScopedWorkflows(parsed, consumerGitRepo, consumerCommit, input.Event, input.Payload)
return sourceCommitSHA, detected, filtered, nil
}

View File

@@ -42,6 +42,38 @@ type ArchiveRequest struct {
archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id"
}
type archiveQueueItem struct {
RepoID int64 `json:"RepoID"`
Type repo_model.ArchiveType `json:"Type"`
CommitID string `json:"CommitID"`
Paths []string `json:"Paths,omitempty"`
ArchiveRefShortName string `json:"ArchiveRefShortName,omitempty"`
}
func (aReq *ArchiveRequest) toQueueItem() *archiveQueueItem {
return &archiveQueueItem{
RepoID: aReq.Repo.ID,
Type: aReq.Type,
CommitID: aReq.CommitID,
Paths: aReq.Paths,
ArchiveRefShortName: aReq.archiveRefShortName,
}
}
func (item *archiveQueueItem) toArchiveRequest(ctx context.Context) (*ArchiveRequest, error) {
repo, err := repo_model.GetRepositoryByID(ctx, item.RepoID)
if err != nil {
return nil, err
}
return &ArchiveRequest{
Repo: repo,
Type: item.Type,
CommitID: item.CommitID,
Paths: item.Paths,
archiveRefShortName: item.ArchiveRefShortName,
}, nil
}
// NewRequest creates an archival request, based on the URI. The
// resulting ArchiveRequest is suitable for being passed to Await()
// if it's determined that the request still needs to be satisfied.
@@ -227,13 +259,18 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
return archiver, nil
}
var archiverQueue *queue.WorkerPoolQueue[*ArchiveRequest]
var archiverQueue *queue.WorkerPoolQueue[*archiveQueueItem]
// Init initializes archiver
func Init(ctx context.Context) error {
handler := func(items ...*ArchiveRequest) []*ArchiveRequest {
for _, archiveReq := range items {
log.Trace("ArchiverData Process: %#v", archiveReq)
handler := func(items ...*archiveQueueItem) []*archiveQueueItem {
for _, item := range items {
log.Trace("ArchiverData Process: %#v", item)
archiveReq, err := item.toArchiveRequest(ctx)
if err != nil {
log.Error("Archive repo %d: %v", item.RepoID, err)
continue
}
if archiver, err := doArchive(ctx, archiveReq); err != nil {
log.Error("Archive %v failed: %v", archiveReq, err)
} else {
@@ -254,14 +291,15 @@ func Init(ctx context.Context) error {
// StartArchive push the archive request to the queue
func StartArchive(request *ArchiveRequest) error {
has, err := archiverQueue.Has(request)
item := request.toQueueItem()
has, err := archiverQueue.Has(item)
if err != nil {
return err
}
if has {
return nil
}
return archiverQueue.Push(request)
return archiverQueue.Push(item)
}
func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error {

View File

@@ -7,7 +7,9 @@ import (
"testing"
"time"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"gitea.dev/services/contexttest"
@@ -21,6 +23,22 @@ func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func TestArchiveQueueItemJSON(t *testing.T) {
orig := &archiveQueueItem{
RepoID: 7,
Type: repo_model.ArchiveZip,
CommitID: "abc123",
Paths: []string{"agents"},
ArchiveRefShortName: "main",
}
bs, err := json.Marshal(orig)
require.NoError(t, err)
var decoded archiveQueueItem
require.NoError(t, json.Unmarshal(bs, &decoded))
assert.Equal(t, *orig, decoded)
}
func TestArchive_Basic(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@@ -344,6 +344,59 @@ jobs:
})
})
t.Run("Filtered required scoped check passes as skipped and allows merge", func(t *testing.T) {
// A required scoped workflow excluded by a paths filter posts a skipped (success) commit status,
// so the required check is satisfied and the PR can merge.
const scopedFilteredPRWorkflow = `name: Scoped Filtered PR
on:
pull_request:
paths:
- src/**
jobs:
scoped-filtered-job:
runs-on: ubuntu-latest
steps:
- run: echo scoped-filtered
`
source := createTestRepo(t, "sw-filtered-source", false)
createRepoWorkflowFile(t, user2, user2Token, source, ".gitea/scoped_workflows/pr.yaml", scopedFilteredPRWorkflow)
registerUserScopedSource(t, source, "pr.yaml") // required
consumer := createTestRepo(t, "sw-filtered-consumer", false)
// Protect the default branch (its own status check stays off, so only the required scoped check gates the merge).
user2Session.MakeRequest(t, NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", consumer.OwnerName, consumer.Name), map[string]string{
"rule_name": consumer.DefaultBranch,
"enable_push": "true",
"block_admin_merge_override": "true", // otherwise the repo owner bypasses the status check
}), http.StatusSeeOther)
// Open a PR that changes a file NOT matching the workflow's `paths: [src/**]`, so it is filtered out.
prFile := &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: consumer.DefaultBranch, NewBranchName: "filtered-pr", Message: "pr change",
Author: api.Identity{Name: user2.Name, Email: user2.Email},
Committer: api.Identity{Name: user2.Name, Email: user2.Email},
Dates: api.CommitDateOptions{Author: time.Now(), Committer: time.Now()},
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("pr change")),
}
createWorkflowFile(t, user2Token, consumer.OwnerName, consumer.Name, "docs.txt", prFile)
apiCtx := NewAPITestContext(t, user2.Name, consumer.Name, auth_model.AccessTokenScopeWriteRepository)
pr, err := doAPICreatePullRequest(apiCtx, consumer.OwnerName, consumer.Name, consumer.DefaultBranch, "filtered-pr")(t)
require.NoError(t, err)
// Filtered: no scoped run is created, but a skipped commit status is posted on the PR head.
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: consumer.ID, IsScopedRun: true}), "filtered scoped workflow creates no run")
assertSkippedCommitStatusExists(t, consumer.ID, pr.Head.Sha, "pull_request")
// The skipped (success) status satisfies the required scoped check (prefixed with the source repo), so the merge is allowed.
assert.NoError(t, queue.GetManager().FlushAll(t.Context(), 5*time.Second))
mergeReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", consumer.OwnerName, consumer.Name, pr.Index),
&forms.MergePullRequestForm{Do: string(repo_model.MergeStyleMerge), MergeMessageField: "merge"}).AddTokenAuth(user2Token)
user2Session.MakeRequest(t, mergeReq, http.StatusOK)
})
t.Run("Settings page required patterns", func(t *testing.T) {
source := createTestRepo(t, "sw-settings-source", false)
createRepoWorkflowFile(t, user2, user2Token, source, ".gitea/scoped_workflows/push.yaml", scopedPushWorkflow)

View File

@@ -215,8 +215,9 @@ jobs:
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
// the new pull request cannot trigger actions, so there is still only 1 record
// the new pull request is filtered by paths, so no run is created; a skipped commit status is posted instead
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
assertSkippedCommitStatusExists(t, baseRepo.ID, addFileToForkedResp.Commit.SHA, "pull_request_target")
})
}
@@ -338,6 +339,9 @@ jobs:
})
assert.NoError(t, err)
assert.NotEmpty(t, addFileToBranchResp)
// the push to test-skip-ci is filtered by branches, so no run is created; a skipped commit status is posted instead
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
assertSkippedCommitStatusExists(t, repo.ID, addFileToBranchResp.Commit.SHA, "push")
resp := testPullCreate(t, session, "user2", "skip-ci", true, "master", "test-skip-ci", "[skip ci] test-skip-ci")
@@ -345,7 +349,7 @@ jobs:
url := test.RedirectURL(resp)
assert.Regexp(t, "^/user2/skip-ci/pulls/[0-9]*$", url)
// the pr title contains a configured skip-ci string, so there is still only 1 record
// the pr title contains a configured skip-ci string, so no run and no skipped status are created
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
})
}
@@ -1879,3 +1883,16 @@ jobs:
runner.fetchNoTask(t)
})
}
// assertSkippedCommitStatusExists asserts that a filtered-out workflow posted a skipped commit status on sha
func assertSkippedCommitStatusExists(t *testing.T, repoID int64, sha, eventSuffix string) {
t.Helper()
statuses, err := git_model.GetLatestCommitStatus(t.Context(), repoID, sha, db.ListOptionsAll)
require.NoError(t, err)
for _, s := range statuses {
if s.State == commitstatus.CommitStatusSkipped && strings.Contains(s.Context, "("+eventSuffix+")") {
return
}
}
assert.Failf(t, "missing skipped commit status", "no skipped commit status with event %q on %s (found %d statuses)", eventSuffix, sha, len(statuses))
}

View File

@@ -22,6 +22,7 @@ import (
"gitea.dev/services/auth/source/oauth2"
"gitea.dev/tests"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
@@ -489,3 +490,73 @@ func TestOAuth2GroupClaimsManualLinking(t *testing.T) {
})
}
}
// TestOAuth2AutoLinkWithTwoFactor verifies that automatic account linking completes
// after the user passes local 2FA when an OIDC identity matches an existing account.
func TestOAuth2AutoLinkWithTwoFactor(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&setting.OAuth2Client.AccountLinking, setting.OAuth2AccountLinkingAuto)()
defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameEmail)()
const (
sourceName = "test-oauth-auto-link-2fa"
sub = "oidc-auto-link-2fa-sub"
email = "oidc-auto-link-2fa@example.com"
userName = "oidc-auto-link-2fa"
)
srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: sub, Email: email, Name: "OIDC Auto Link 2FA"})
addOAuth2Source(t, sourceName, oauth2.Source{
Provider: "openidConnect",
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
OpenIDConnectAutoDiscoveryURL: srv.URL + "/.well-known/openid-configuration",
})
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), sourceName)
require.NoError(t, err)
localUser := &user_model.User{Name: userName, Email: email}
require.NoError(t, user_model.CreateUser(t.Context(), localUser, &user_model.Meta{}))
otpKey, err := totp.Generate(totp.GenerateOpts{
SecretSize: 40,
Issuer: "gitea-test",
AccountName: localUser.Name,
})
require.NoError(t, err)
tfa := &auth_model.TwoFactor{UID: localUser.ID}
require.NoError(t, tfa.SetSecret(otpKey.Secret()))
require.NoError(t, auth_model.NewTwoFactor(t.Context(), tfa))
unittest.AssertNotExistsBean(t, &user_model.ExternalLoginUser{ExternalID: sub, LoginSourceID: authSource.ID}, unittest.OrderBy("external_id ASC"))
session := emptyTestSession(t)
resp := session.MakeRequest(t, NewRequest(t, "GET", "/user/oauth2/"+sourceName), http.StatusTemporaryRedirect)
location := resp.Header().Get("Location")
u, err := url.Parse(location)
require.NoError(t, err)
state := u.Query().Get("state")
require.NotEmpty(t, state)
callbackURL := fmt.Sprintf("/user/oauth2/%s/callback?code=test-code&state=%s", sourceName, url.QueryEscape(state))
resp = session.MakeRequest(t, NewRequest(t, "GET", callbackURL), http.StatusSeeOther)
assert.Contains(t, resp.Header().Get("Location"), "/user/two_factor")
session.MakeRequest(t, NewRequest(t, "GET", "/user/two_factor"), http.StatusOK)
passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
require.NoError(t, err)
req := NewRequestWithValues(t, "POST", "/user/two_factor", map[string]string{
"passcode": passcode,
})
session.MakeRequest(t, req, http.StatusSeeOther)
externalLink := unittest.AssertExistsAndLoadBean(t, &user_model.ExternalLoginUser{ExternalID: sub, LoginSourceID: authSource.ID}, unittest.OrderBy("external_id ASC"))
assert.Equal(t, localUser.ID, externalLink.UserID)
session.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK)
}