Compare commits

...

37 Commits

Author SHA1 Message Date
jmiller 39150c6968 Merge pull request 'fix(ci): PR RC workflow YAML fix' (#170) from feat/test-rc-workflow into main 2026-05-25 05:01:00 +00:00
moko-deploy 417bda1735 chore(ci): update RC stream for PR #170 2026-05-25 04:59:38 +00:00
Jonathan Miller f09aadf60c fix(ci): use env var for API_BASE in RC release step
Branch Policy Check / Verify merge target (pull_request) Failing after 1s
PR RC Release / Build RC Release (pull_request) Successful in 19s
The Python heredoc couldn't access shell-local API variable.
Move it to step-level env so os.environ sees it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:59:15 -05:00
Jonathan Miller fa54fe1ffc fix(ci): rewrite PR RC workflow — move XML generation to Python
Branch Policy Check / Verify merge target (pull_request) Failing after 1s
PR RC Release / Build RC Release (pull_request) Failing after 16s
The previous workflow had raw XML in bash heredocs that broke the
YAML parser during workflow discovery, causing Gitea to silently
skip the entire workflow.

Fix: all XML generation and API calls now use Python heredocs
(<<'PYEOF') which don't contain characters that confuse the YAML
parser. All github context values passed via env vars instead of
inline expressions in run blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:54:54 -05:00
Jonathan Miller 49fb7bb9a4 test: verify PR RC workflow triggers
Branch Policy Check / Verify merge target (pull_request) Failing after 0s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:51:31 -05:00
Jonathan Miller 5dd98c04d8 fix(ci): rewrite PR RC workflow to bypass Gitea branches filter bug
Gitea Actions doesn't reliably evaluate the branches: filter on
pull_request events, causing the workflow to never trigger. Replaced
with a step-level guard that checks github.base_ref at runtime.

Also fixed:
- XML insertion using sed 'e' command instead of shell interpolation
- RC entry removal using Python regex for reliability
- Simplified API URL construction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:51:18 -05:00
jmiller ea3957597c Merge pull request 'rc(v04.01.00): release candidate' (#168) from rc/04.01.00 into main 2026-05-25 04:27:24 +00:00
Jonathan Miller 45e08616ac fix(ci): remove draft check from PR RC workflow
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Gitea Actions may not evaluate github.event.pull_request.draft
correctly, preventing the workflow from triggering entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:26:16 -05:00
Jonathan Miller 4dafcc5429 fix(ci): remove draft check from PR RC workflow
Gitea Actions may not evaluate github.event.pull_request.draft
correctly, preventing the workflow from triggering entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:26:13 -05:00
Jonathan Miller 3159d53322 chore: bump version to 04.01.00 for next release cycle
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:21:20 -05:00
Jonathan Miller fff64e6e7c fix(ci): fix RC workflow API URL, update CHANGELOG for v1.26.1-moko.04.00.00
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Fix missing https:// protocol in pr-rc-release.yml API URL
- Update comment to remove stale fan-out reference
- Add comprehensive CHANGELOG entry for v1.26.1-moko.04.00.00 release

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:17:31 -05:00
Jonathan Miller 1d4340a142 feat(ci): auto-build RC release on PR to main
When a PR is opened or updated against main:
- Determines RC version from updates.xml base + PR number
- Creates/updates a prerelease on Gitea tagged as v1.26.1-moko.{version}-rc.{PR#}
- Updates updates.xml with RC channel entry pointing to the PR

Admins on the RC update channel will see the PR build as an available
update, matching the Joomla update server pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:09:49 -05:00
Jonathan Miller 60670d066b feat: Joomla-style updates.xml with channel selection for update checker
Replace JSON API-based update checking with Joomla-style updates.xml
that supports multiple update streams (stable, dev, security).

Changes:
- Add updates.xml at repo root with stable/dev/security channels
  following the same XML structure as MokoOnyx and other Joomla repos
- Rewrite updatechecker module to parse XML with channel filtering
- Add CHANNEL setting to [update_checker] config (default: stable)
- Show channel name and docker pull command in admin dashboard banner

Config example:
  [update_checker]
  ENABLED = true
  CHANNEL = stable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 22:56:04 -05:00
Jonathan Miller e8b2a485fc fix: remove unused setting import in action.go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 17:47:58 -05:00
Jonathan Miller cd496f159d fix: restore Permission field access in context middleware functions
The upstream reading permission fix (#37781) refactored Repository
to have direct IsAdmin/CanWrite/CanRead methods, but our fork's
Repository struct still uses the Permission field for these.
Keep the new CheckTokenScopes function but use ctx.Repo.Permission.*
for the middleware functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 17:42:17 -05:00
Claude afbff02d81 chore: go mod tidy for security dependency updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 22:07:57 +00:00
Claude 8355b39ad4 chore: update go.sum for golang.org/x/net v0.55.0 transitive deps
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 19:52:16 +00:00
Jonathan Miller c2c9e053ff chore: update version format to 3-part moko semver
Update deploy workflow description to reflect new versioning scheme:
v{upstream}-moko.{major}.{minor}.{patch} (e.g. v1.26.1-moko.04.00.00)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 13:29:30 -05:00
jmiller 451b3022bd Merge pull request 'fix: .mod lexer panic, pnpm lockfile, branding updates' (#166) from fix/security-backports into main 2026-05-24 09:14:24 +00:00
Claude 2153d7c916 chore: regenerate pnpm-lock.yaml for mermaid v11.15.0 security update
Branch Policy Check / Verify merge target (pull_request) Failing after 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 09:11:14 +00:00
Jonathan Miller 63c801d595 fix: remove invalid .mod lexer mapping, update upstream references to MokoGitea
- Remove .mod -> AMPL mapping from conflictingExtLangMap (AMPL lexer
  doesn't exist in chroma v2.23.1, causing a panic when viewing .mod
  files). Upstream doesn't have this mapping either.
- Update 500 error page issue link to MokoGitea repo
- Update home page install/license links to MokoGitea repo
- Update theme settings link to MokoGitea repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 04:09:42 -05:00
jmiller 0270be743f Merge pull request 'fix(security): backport 12 upstream security fixes from v1.26.2' (#165) from fix/security-backports into main 2026-05-24 08:53:19 +00:00
Lunny Xiao dbf70a7def fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829)
Branch Policy Check / Verify merge target (pull_request) Failing after 1s
Backport #37813

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-24 03:52:01 -05:00
Nicolas 43b5a54ffa fix(actions): make artifact signature payloads unambiguous (#37707) (#37795)
This PR hardens artifact URL signing by encoding signature inputs in an
unambiguous binary payload before computing the HMAC.

What it changes:

- replace direct concatenation-style signing inputs with explicit
payload builders
- encode string fields with a length prefix before appending their bytes
- encode integer fields as fixed-width binary values instead of decimal
text
- apply the same hardening to both:
  - Actions Artifact V4 signing in `routers/api/actions/artifactsv4.go`
  - artifact download signing in `routers/api/v1/repo/action.go`
- add regression tests that verify distinct field combinations produce
distinct payloads and signatures

Why:

The previous signing logic built HMAC inputs by appending multiple
fields without a strongly structured representation. That kind of
construction can create ambiguity at field boundaries, where different
parameter combinations may serialize into the same byte stream for
signing.

This change removes that ambiguity by constructing a deterministic
payload format with explicit boundaries between fields.

Backport #37707

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-24 03:49:09 -05:00
Giteabot b182855fc5 fix(permissions): Fix reading permission (#37769) (#37781) 2026-05-24 03:39:48 -05:00
Lunny Xiao ca6c8c958c fix: Unify public-only token filtering in API queries and repo access checks (#37118) (#37773)
backport #37118 

This PR closes remaining `public-only` token gaps in the API by making
the restriction apply consistently across repository, organization,
activity, notification, and authenticated `/api/v1/user/...` routes.

Previously, `public-only` tokens were still able to:
- receive private results from some list/search/self endpoints,
- access repository data through ID-based lookups,
- and reach several authenticated self routes that should remain
unavailable for public-only access.

This change treats `public-only` as a cross-cutting visibility boundary:
- list/search endpoints now filter private resources consistently,
- repository lookups enforce the same restriction even when addressed
indirectly,
- and self routes that inherently expose or mutate private account state
now reject `public-only` tokens.

---
Generated by a coding agent with Codex 5.2

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-24 03:38:02 -05:00
Giteabot e9efbbc93b fix: Add missed token scope checking (#37735) (#37757)
Backport #37735 by @lunny

Follow #37698

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-05-24 03:37:58 -05:00
Lunny Xiao 2482a3726e fix(oauth): bind token exchanges to the original client request (#37704) (#37740)
Backport #37704 

This PR hardens OAuth token exchange validation by binding exchanged
credentials to the client and redirect URI that originally obtained
them.

What it changes:

- reject refresh token exchanges when the refresh token belongs to a
different OAuth application
- reject authorization code exchanges when the `redirect_uri` in the
token request differs from the `redirect_uri` stored with the
authorization code
- add integration coverage for:
  - authorization code exchange with a mismatched redirect URI
- refresh token reuse across two different dynamically created OAuth
applications

Why:

OAuth authorization codes and refresh tokens must remain bound to the
client context that originally received them. Without those checks:
- a valid authorization code can be redeemed against a different
registered redirect URI of the same client
- a refresh token can be replayed by a different OAuth client

---------

Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-24 03:37:57 -05:00
Lunny Xiao 2f381dc16c fix(oauth): strengthen PKCE validation and refresh token replay protection (#37706) (#37738)
Backport #37706

This PR tightens several OAuth validation paths related to PKCE
handling, redirect URI normalization, and refresh-token replay safety.

What it changes:

- switch redirect URI comparison to ASCII-only normalization for
exact-match checks, avoiding Unicode case-folding surprises
- harden PKCE verification by:
  - allowing PKCE omission only when no challenge data was stored
  - rejecting exchanges with a missing verifier when PKCE was used
- rejecting malformed challenge state where a challenge exists without a
valid method
  - comparing derived challenges with constant-time string matching
- make refresh-token invalidation counter updates conditional on the
previously observed counter value, so stale refresh state cannot be
accepted after the grant changes

Why:

These checks close gaps where:
- redirect URI comparisons could rely on broader Unicode normalization
than intended
- malformed or incomplete PKCE state could be treated too permissively
- concurrent or stale refresh-token use could advance the same grant
more than once

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-24 03:37:42 -05:00
Giteabot d2cdd9b1d6 fix(web): enforce token scopes on raw, media, and attachment downloads (#37698) (#37733) 2026-05-24 03:36:36 -05:00
Giteabot 6e0236d433 fix(security): enforce wiki git writes and LFS token access at request time (#37695) (#37714)
Backport #37695 by @lunny

This PR fixes two permission-checking gaps in Git and LFS request
handling.

## What it changes

- keep wiki Git HTTP pushes on the normal write-permission path, even
when proc-receive support is enabled
- revalidate LFS bearer token requests against the current user state
and current repository permissions before allowing access
- add regression coverage for unauthorized wiki HTTP pushes
- add LFS tests for blocked users, revoked repository access, read-only
upload attempts, and valid write access

## Why

- wiki repositories should not inherit the relaxed refs/for handling
used for normal code repositories
- LFS authorization tokens should not remain usable after a user is
disabled or loses repository access

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-24 03:35:22 -05:00
Giteabot ce83900967 feat(api): encrypt AWS creds (#37679) (#37713)
Backport #37679 by @Exgene

## Description

As mentioned in #37654 `AWSAccessKeyID` and `AWSSecretAccessKey` are not
encrypted and stored as is.

## Update

Follow the existing `AuthToken` flow of setting the `Encrypted` fields,
`Decrypting` them later and `Clearing` them at the end.

Closes #37654

Signed-off-by: Kausthubh J Rao <105716675+Exgene@users.noreply.github.com>
Co-authored-by: Kausthubh J Rao <105716675+Exgene@users.noreply.github.com>
Co-authored-by: Lauris B <lauris@nix.lv>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-24 03:35:21 -05:00
silverwind ee6405f4fd fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test (#37665)
Backport of #37662.

---
This PR was written with the help of Claude Opus 4.7

---------

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-24 03:35:04 -05:00
Giteabot 738117248d fix(packages): Add label for private and internal package and fix composor package source permission check (#37610) (#37643)
Backport #37610 by @lunny

- Add permission checks for Composer package source links

- Add private/internal visibility labels for packages, similar to
repository visibility labels

<img width="969" height="571" alt="image"
src="https://github.com/user-attachments/assets/8a8ec3a0-bfbd-4dd6-b45b-58eda5db1a2d"
/>

- Add a link to change package visibility

<img width="1309" height="208" alt="image"
src="https://github.com/user-attachments/assets/3fa82b23-4c63-4a5e-b3f0-d37a103231ee"
/>

- Update link package descriptions

<img width="1308" height="265" alt="image"
src="https://github.com/user-attachments/assets/2c80b50e-5ffe-4d96-aedd-aa15964c4e05"
/>

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-24 03:34:47 -05:00
jmiller d2d652e5b7 Merge pull request 'feat(ci): add upstream bug sync workflow' (#156) from feat/upstream-bug-sync into main 2026-05-24 08:29:33 +00:00
Jonathan Miller 0166a6d02a feat(ci): add upstream bug sync workflow
Branch Policy Check / Verify merge target (pull_request) Failing after 1s
Adds a scheduled workflow that runs daily at 08:00 UTC to automatically
detect new bug fixes merged into upstream Gitea's release/v1.26 branch
and create corresponding issues in the MokoGitea tracker.

The workflow:
- Queries GitHub Search API for recently merged fix/security PRs
- Cross-references against existing MokoGitea issues to avoid duplicates
- Creates labeled issues (type: bug, upstream, priority, security)
- Supports manual dispatch with configurable lookback period

Requires secrets: GH_TOKEN (GitHub), GITEA_TOKEN (MokoGitea)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:28:47 -05:00
jmiller 0f543903fb Merge pull request 'fix: backport upstream v1.26.2 critical fixes' (#139) from fix/upstream-v1.26.2-backports into main 2026-05-24 04:30:33 +00:00
74 changed files with 2754 additions and 472 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v1.26.1-moko.2)'
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
required: true
default: 'latest'
environment:
+170
View File
@@ -0,0 +1,170 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Auto-build RC release on PR to main, update RC update stream
name: "PR RC Release"
on:
pull_request:
types: [opened, synchronize]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
REGISTRY: git.mokoconsulting.tech
IMAGE: mokoconsulting/mokogitea
permissions:
contents: write
jobs:
rc-release:
name: Build RC Release
runs-on: ubuntu-latest
steps:
- name: Check target branch
id: guard
env:
BASE_BRANCH: ${{ github.base_ref }}
run: |
echo "PR target: ${BASE_BRANCH}"
if [ "$BASE_BRANCH" != "main" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Skipping RC — only for PRs targeting main"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout PR branch
if: steps.guard.outputs.skip != 'true'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Determine RC version
if: steps.guard.outputs.skip != 'true'
id: version
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
BASE_VERSION=$(sed -n 's/.*<version>\(.*\)<\/version>.*/\1/p' updates.xml | head -1)
[ -z "$BASE_VERSION" ] && BASE_VERSION="04.00.00"
RC_VERSION="${BASE_VERSION}-rc.${PR_NUMBER}"
RC_TAG="v1.26.1-moko.${RC_VERSION}"
echo "version=$RC_VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$RC_TAG" >> "$GITHUB_OUTPUT"
echo "RC version: $RC_VERSION (tag: $RC_TAG)"
- name: Update updates.xml RC channel
if: steps.guard.outputs.skip != 'true'
env:
RC_VERSION: ${{ steps.version.outputs.version }}
RC_TAG: ${{ steps.version.outputs.tag }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
DOCKER_TAG="${REGISTRY}/${IMAGE}:${RC_TAG}"
python3 << 'PYEOF'
import os, re
rc_version = os.environ["RC_VERSION"]
rc_tag = os.environ["RC_TAG"]
pr_url = os.environ["PR_URL"]
pr_num = os.environ["PR_NUM"]
docker_tag = os.environ["REGISTRY"] + "/" + os.environ["IMAGE"] + ":" + rc_tag
entry = f""" <update>
<name>MokoGitea</name>
<description>MokoGitea RC from PR #{pr_num}</description>
<element>mokogitea</element>
<type>application</type>
<version>{rc_version}</version>
<client>server</client>
<tags><tag>rc</tag></tags>
<infourl title="MokoGitea RC">{pr_url}</infourl>
<downloads>
<downloadurl type="full" format="docker">{docker_tag}</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\\.25\\.)|(1\\.26\\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>"""
content = open("updates.xml").read()
# Remove existing RC entry
content = re.sub(
r"\s*<update>[\s\S]*?<tag>rc</tag>[\s\S]*?</update>",
"",
content,
)
# Insert before </updates>
content = content.replace("</updates>", entry + "\n</updates>")
open("updates.xml", "w").write(content)
print(f"Updated updates.xml with RC entry: {rc_version}")
PYEOF
- name: Create RC release
if: steps.guard.outputs.skip != 'true'
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
RC_TAG: ${{ steps.version.outputs.tag }}
RC_VERSION: ${{ steps.version.outputs.version }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
API_BASE: https://${{ env.REGISTRY }}/api/v1/repos/${{ github.repository }}
run: |
# Delete existing RC release/tag if present
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/releases/tags/${RC_TAG}" 2>/dev/null || true
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/tags/${RC_TAG}" 2>/dev/null || true
# Create prerelease
python3 << PYEOF
import json, os, urllib.request
api = os.environ["API_BASE"]
token = os.environ["GITEA_TOKEN"]
payload = json.dumps({
"tag_name": os.environ["RC_TAG"],
"target_commitish": os.environ["HEAD_SHA"],
"name": f"RC: {os.environ['PR_TITLE']}",
"body": f"Release candidate from PR #{os.environ['PR_NUMBER']}\n\nPR: {os.environ['PR_URL']}\nDocker: docker pull {os.environ['REGISTRY']}/{os.environ['IMAGE']}:{os.environ['RC_TAG']}",
"draft": False,
"prerelease": True,
}).encode()
req = urllib.request.Request(
f"{api}/releases",
data=payload,
headers={
"Authorization": f"token {token}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(f"Created RC release: {result.get('tag_name')}")
PYEOF
- name: Commit updates.xml
if: steps.guard.outputs.skip != 'true'
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
git config user.name "MokoGitea Bot"
git config user.email "deploy@mokoconsulting.tech"
git add updates.xml
if git diff --cached --quiet; then
echo "No changes to updates.xml"
else
git commit -m "chore(ci): update RC stream for PR #${PR_NUM}"
git push origin "HEAD:${HEAD_REF}" || echo "Push failed"
fi
+167
View File
@@ -0,0 +1,167 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Sync upstream Gitea bug fixes into MokoGitea issue tracker
name: Upstream Bug Sync
on:
schedule:
- cron: '0 8 * * *' # daily at 08:00 UTC
workflow_dispatch:
inputs:
days_back:
description: 'How many days back to scan (default: 7)'
required: false
default: '7'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Sync upstream bugs
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
MOKOGITEA_URL: https://git.mokoconsulting.tech
MOKOGITEA_REPO: MokoConsulting/MokoGitea
UPSTREAM_BRANCH: release/v1.26
DAYS_BACK: ${{ github.event.inputs.days_back || '7' }}
run: |
python3 << 'PYEOF'
import json, os, re, sys, urllib.parse, urllib.request
from datetime import datetime, timedelta, timezone
GH_TOKEN = os.environ["GH_TOKEN"]
MOKO_TOKEN = os.environ["MOKOGITEA_TOKEN"]
MOKO_URL = os.environ["MOKOGITEA_URL"]
MOKO_REPO = os.environ["MOKOGITEA_REPO"]
BRANCH = os.environ["UPSTREAM_BRANCH"]
DAYS = int(os.environ.get("DAYS_BACK", "7"))
# Label IDs in MokoGitea
LABELS = {
"type_bug": 5757, "upstream": 5758, "security": 5032,
"critical": 5018, "high": 5019, "medium": 5020, "low": 5021,
}
def gh_get(url):
req = urllib.request.Request(url, headers={
"Authorization": f"token {GH_TOKEN}",
"Accept": "application/vnd.github.v3+json",
})
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
def moko_get(path):
req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", headers={
"Authorization": f"token {MOKO_TOKEN}",
})
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
def moko_post(path, data):
payload = json.dumps(data).encode()
req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}",
data=payload, method="POST", headers={
"Authorization": f"token {MOKO_TOKEN}",
"Content-Type": "application/json",
})
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
# ── Step 1: Find recently merged upstream PRs ──
since = (datetime.now(timezone.utc) - timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ")
query = f"repo:go-gitea/gitea is:pr is:merged base:{BRANCH} merged:>={since}"
encoded = urllib.parse.quote(query)
print(f"Scanning: {query}")
result = gh_get(f"https://api.github.com/search/issues?q={encoded}&per_page=100&sort=updated&order=desc")
total = result["total_count"]
print(f"Found {total} merged PRs in the last {DAYS} days")
if total == 0:
print("Nothing to sync.")
sys.exit(0)
# ── Step 2: Filter for bug/security fixes ──
bugs = []
for pr in result["items"]:
title = pr["title"]
label_names = [l["name"].lower() for l in pr.get("labels", [])]
is_fix = title.lower().startswith("fix")
is_security = any("security" in l for l in label_names) or "[security]" in title.lower()
is_bug = any("bug" in l for l in label_names)
if not (is_fix or is_security or is_bug):
continue
refs = re.findall(r"#(\d+)", title)
severity = "critical" if is_security and "[security]" in title.lower() else \
"high" if is_security else "medium"
bugs.append({
"number": pr["number"], "title": title, "url": pr["html_url"],
"severity": severity, "is_security": is_security, "refs": refs,
"merged": pr.get("pull_request", {}).get("merged_at", "")[:10],
})
print(f"Filtered to {len(bugs)} bug/security fixes")
if not bugs:
sys.exit(0)
# ── Step 3: Collect already-tracked PR numbers ──
tracked = set()
for state in ["open", "closed"]:
try:
issues = moko_get(f"repos/{MOKO_REPO}/issues?state={state}&type=issues&limit=50&labels=upstream")
for iss in issues:
text = (iss.get("body") or "") + " " + (iss.get("title") or "")
tracked.update(re.findall(r"(?:#|/pull/)(\d{4,})", text))
except Exception:
pass
print(f"Already tracked: {len(tracked)} upstream PRs")
# ── Step 4: Create issues for new bugs ──
created = skipped = errors = 0
for bug in bugs:
if any(r in tracked for r in bug["refs"]):
print(f" SKIP #{bug['number']}: {bug['title'][:55]} (tracked)")
skipped += 1
continue
labels = [LABELS["type_bug"], LABELS["upstream"], LABELS[bug["severity"]]]
if bug["is_security"]:
labels.append(LABELS["security"])
body = (
f"## Summary\n\n"
f"Upstream bug fix merged into `{BRANCH}`.\n\n"
f"## Upstream Reference\n\n"
f"- PR: {bug['url']}\n"
f"- Merged: {bug['merged']}\n"
f"- Branch: {BRANCH}\n\n"
f"## Severity: {bug['severity'].title()}"
f"{' (security)' if bug['is_security'] else ''}\n\n"
f"## Action\n\n"
f"Cherry-pick from upstream `{BRANCH}` branch.\n\n"
f"---\n"
f"*Auto-created by upstream-bug-sync workflow*\n"
f"*Authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>*"
)
try:
iss = moko_post(f"repos/{MOKO_REPO}/issues", {
"title": bug["title"], "body": body, "labels": labels,
})
print(f" CREATED #{iss['number']}: {bug['title'][:55]}")
created += 1
except Exception as e:
print(f" ERROR #{bug['number']}: {e}")
errors += 1
print(f"\n=== Done: {created} created, {skipped} skipped, {errors} errors ===")
PYEOF
+38
View File
@@ -4,6 +4,43 @@ 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).
## [v1.26.1-moko.04.00.00] - 2026-05-24
* SECURITY
* Backport 12 upstream v1.26.2 security fixes:
* golang.org/x/net v0.55.0 security update (#140)
* Token scope enforcement on raw/media/attachment downloads (#141)
* OAuth PKCE hardening and refresh token replay protection (#142)
* Wiki git write and LFS token access enforcement (#143)
* Public-only token filtering in API queries (#144)
* Reading permission fix (#145)
* Artifact signature payload hardening (#146)
* AWS credentials encryption (#161)
* Mermaid v11.15.0 security update (#162)
* Composer package permission check (#164)
* BUGFIXES
* fix(actions): nil pointer dereference in concurrency during PR creation (#136)
* fix(ui): actions runs list broken row layout — CSS class mismatch (#138)
* fix: scheduled action panic with null event payload (upstream #37459)
* fix: treat email addresses case-insensitively (upstream #37600)
* fix: .mod lexer panic — removed invalid AMPL mapping
* fix: remove unused setting import in action.go
* fix: restore Permission field access in context middleware
* FEATURES
* Joomla-style updates.xml with channel selection (stable/dev/security/rc)
* Update checker reads from updates.xml with configurable CHANNEL setting
* Admin dashboard shows update banner with channel name and docker pull command
* Upstream bug sync workflow — daily automated issue creation from release/v1.26
* PR RC release workflow — auto-build RC on PR to main
* INFRASTRUCTURE
* New 3-part versioning: v{upstream}-moko.{major}.{minor}.{patch}
* Branding updates: error pages, home page, settings link to MokoGitea
* Deploy workflow updated for new version format
* PROCESS
* Created `type: bug` and `upstream` labels for automated issue tracking
* Deduplicated 19 duplicate feature request issues
* Closed 24 upstream bug/security issues after backporting
## [MokoGitea Unreleased]
* FEATURES
@@ -5815,3 +5852,4 @@ Key highlights of this release encompass significant changes categorized under `
## Archived releases
* [CHANGELOG-archived.md](CHANGELOG-archived.md)
# PR RC Workflow Test
+17 -10
View File
@@ -46,6 +46,7 @@ require (
github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.9.0
github.com/getkin/kin-openapi v0.138.0
github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
@@ -86,7 +87,6 @@ require (
github.com/msteinert/pam/v2 v2.1.0
github.com/nektos/act v0.2.63
github.com/niklasfasching/go-org v1.9.1
github.com/olivere/elastic/v7 v7.0.32
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pquerna/otp v1.5.0
@@ -110,17 +110,18 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go v1.46.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/crypto v0.49.0
golang.org/x/image v0.38.0
golang.org/x/net v0.52.0
golang.org/x/crypto v0.52.0
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.42.0
golang.org/x/text v0.35.0
golang.org/x/sys v0.45.0
golang.org/x/text v0.37.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.20.4
mvdan.cc/xurls/v2 v2.6.0
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
xorm.io/builder v0.3.13
@@ -187,13 +188,14 @@ require (
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/getkin/kin-openapi v0.138.0 // indirect
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
@@ -234,16 +236,20 @@ require (
github.com/minio/minlz v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.12 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
@@ -259,11 +265,13 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/smartystreets/assertions v1.1.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
@@ -280,9 +288,9 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
@@ -292,7 +300,6 @@ require (
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.20.4 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
+50 -20
View File
@@ -269,8 +269,6 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -314,6 +312,10 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
@@ -551,6 +553,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -569,6 +573,10 @@ github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsR
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M=
github.com/oasdiff/yaml3 v0.0.12/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
@@ -577,8 +585,6 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -595,6 +601,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
@@ -704,6 +712,8 @@ github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -716,6 +726,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -787,12 +799,12 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -802,8 +814,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -821,8 +833,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -870,8 +882,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -882,8 +894,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -894,8 +906,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -908,8 +920,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -950,8 +962,20 @@ lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
@@ -960,12 +984,18 @@ modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
+6
View File
@@ -436,6 +436,12 @@ type GetFeedsOptions struct {
DontCount bool // do counting in GetFeeds
}
func (opts *GetFeedsOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
// ActivityReadable return whether doer can read activities of user
func ActivityReadable(user, doer *user_model.User) bool {
return !user.KeepActivityPrivate ||
+7
View File
@@ -137,6 +137,11 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err)
}
}
if opts.AWSSecretAccessKeyEncrypted != "" {
if opts.AWSSecretAccessKey, err = secret.DecryptSecret(setting.SecretKey, opts.AWSSecretAccessKeyEncrypted); err != nil {
log.Error("Unable to decrypt AWSSecretAccessKey, maybe SECRET_KEY is wrong: %v", err)
}
}
return &opts, nil
}
@@ -201,6 +206,8 @@ func FinishMigrateTask(ctx context.Context, task *Task) error {
conf.AuthPasswordEncrypted = ""
conf.AuthTokenEncrypted = ""
conf.CloneAddrEncrypted = ""
conf.AWSSecretAccessKey = ""
conf.AWSSecretAccessKeyEncrypted = ""
confBytes, err := json.Marshal(conf)
if err != nil {
return err
+66 -38
View File
@@ -5,9 +5,8 @@ package auth
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/base32"
"encoding/base64"
"errors"
"fmt"
"net"
@@ -24,6 +23,7 @@ import (
uuid "github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
"xorm.io/builder"
"xorm.io/xorm"
)
@@ -31,7 +31,10 @@ import (
// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
const oauth2AuthorizationCodeValidity = 10 * time.Minute
var ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
var (
ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
ErrOAuth2GrantStaleCounter = errors.New("oauth2 grant state changed during token refresh")
)
// OAuth2Application represents an OAuth2 client (RFC 6749)
type OAuth2Application struct {
@@ -151,30 +154,40 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
// https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1
contains := func(s string) bool {
s = strings.TrimSuffix(strings.ToLower(s), "/")
for _, u := range app.RedirectURIs {
if strings.TrimSuffix(strings.ToLower(u), "/") == s {
redirectCandidates := []string{redirectURI}
if !app.ConfidentialClient {
loopbackRedirect, ok := normalizePublicClientRedirectURI(redirectURI)
if ok {
redirectCandidates = append(redirectCandidates, loopbackRedirect)
}
}
for _, candidate := range redirectCandidates {
normalizedCandidate := normalizeRedirectURIForComparison(candidate)
for _, registeredURI := range app.RedirectURIs {
if normalizeRedirectURIForComparison(registeredURI) == normalizedCandidate {
return true
}
}
return false
}
if !app.ConfidentialClient {
uri, err := url.Parse(redirectURI)
// ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
if err == nil && uri.Scheme == "http" && uri.Port() != "" {
ip := net.ParseIP(uri.Hostname())
if ip != nil && ip.IsLoopback() {
// strip port
uri.Host = uri.Hostname()
if contains(uri.String()) {
return true
}
}
}
return false
}
func normalizeRedirectURIForComparison(redirectURI string) string {
return strings.TrimSuffix(util.ToLowerASCII(redirectURI), "/")
}
func normalizePublicClientRedirectURI(redirectURI string) (string, bool) {
parsedURI, err := url.Parse(redirectURI)
if err != nil || parsedURI.Scheme != "http" || parsedURI.Port() == "" {
return "", false
}
return contains(redirectURI)
if ip := net.ParseIP(parsedURI.Hostname()); ip == nil || !ip.IsLoopback() {
return "", false
}
parsedURI.Host = parsedURI.Hostname()
return parsedURI.String(), true
}
// Base32 characters, but lowercased.
@@ -424,22 +437,34 @@ func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
return nil
}
func (code *OAuth2AuthorizationCode) requiresCodeVerifier() bool {
return code.CodeChallengeMethod != "" || code.CodeChallenge != ""
}
func deriveCodeChallenge(method, verifier string) (string, bool) {
switch method {
case "S256":
return oauth2.S256ChallengeFromVerifier(verifier), true
case "plain":
return verifier, true
default:
return "", false
}
}
// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool {
switch code.CodeChallengeMethod {
case "S256":
// base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6
h := sha256.Sum256([]byte(verifier))
hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:])
return hashedVerifier == code.CodeChallenge
case "plain":
return verifier == code.CodeChallenge
case "":
if !code.requiresCodeVerifier() {
return true
default:
// unsupported method -> return false
}
if verifier == "" || code.CodeChallengeMethod == "" {
return false
}
expectedChallenge, ok := deriveCodeChallenge(code.CodeChallengeMethod, verifier)
if !ok {
return false
}
return subtle.ConstantTimeCompare([]byte(expectedChallenge), []byte(code.CodeChallenge)) == 1
}
// GetOAuth2AuthorizationByCode returns an authorization by its code
@@ -504,15 +529,18 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi
// IncreaseCounter increases the counter and updates the grant
func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
_, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant))
affected, err := db.GetEngine(ctx).
Where("id = ?", grant.ID).
And("counter = ?", grant.Counter).
Incr("counter").
Update(new(OAuth2Grant))
if err != nil {
return err
}
updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID)
if err != nil {
return err
if affected == 0 {
return ErrOAuth2GrantStaleCounter
}
grant.Counter = updatedGrant.Counter
grant.Counter++
return nil
}
+80 -25
View File
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
func TestOAuth2AuthorizationCode(t *testing.T) {
@@ -116,6 +117,47 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) {
assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other"))
}
func TestOAuth2Application_ContainsRedirectURI_ASCIIOnlyNormalization(t *testing.T) {
testCases := []struct {
name string
registered []string
redirectURI string
allowed bool
}{
{
name: "exact-match",
registered: []string{"https://signin.example.test/callback"},
redirectURI: "https://signin.example.test/callback",
allowed: true,
},
{
name: "ascii-case-insensitive",
registered: []string{"https://signin.example.test/callback"},
redirectURI: "https://signIN.example.test/callback",
allowed: true,
},
{
name: "non-ascii-not-folded",
registered: []string{"https://signin.example.test/callback"},
redirectURI: "https://signİn.example.test/callback",
allowed: false,
},
{
name: "loopback-strips-port",
registered: []string{"http://127.0.0.1/callback"},
redirectURI: "http://127.0.0.1:12345/callback",
allowed: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
app := &auth_model.OAuth2Application{RedirectURIs: tc.registered}
assert.Equal(t, tc.allowed, app.ContainsRedirectURI(tc.redirectURI))
})
}
}
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
@@ -193,6 +235,16 @@ func TestOAuth2Grant_IncreaseCounter(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2})
}
func TestOAuth2Grant_IncreaseCounterRejectsStaleCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1})
stale := *grant
assert.NoError(t, grant.IncreaseCounter(t.Context()))
err := stale.IncreaseCounter(t.Context())
assert.ErrorIs(t, err, auth_model.ErrOAuth2GrantStaleCounter)
}
func TestOAuth2Grant_ScopeContains(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Scope: "openid profile"})
@@ -237,35 +289,38 @@ func TestRevokeOAuth2Grant(t *testing.T) {
//////////////////// Authorization Code
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
// test plain
code := &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "plain",
CodeChallenge: "test123",
}
assert.True(t, code.ValidateCodeChallenge("test123"))
assert.False(t, code.ValidateCodeChallenge("ierwgjoergjio"))
s256Verifier := "s256-verifier"
s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier)
missingVerifierChallenge := oauth2.S256ChallengeFromVerifier("verifier-not-supplied")
// test S256
code = &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "S256",
CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg",
testCases := []struct {
name string
method string
challenge string
verifier string
valid bool
}{
{"plain-success", "plain", "plain-secret", "plain-secret", true},
{"plain-failure", "plain", "plain-secret", "ierwgjoergjio", false},
{"s256-success", "S256", s256Challenge, s256Verifier, true},
{"s256-failure", "S256", s256Challenge, "wiogjerogorewngoenrgoiuenorg", false},
{"unsupported-method", "monkey", "foiwgjioriogeiogjerger", "foiwgjioriogeiogjerger", false},
{"no-pkce-configured", "", "", "", true},
{"s256-missing-verifier", "S256", missingVerifierChallenge, "", false},
{"plain-missing-verifier", "plain", "plain-secret", "", false},
{"missing-method-with-challenge", "", "foierjiogerogerg", "", false},
{"missing-method-rejects-even-matching-verifier", "", "foierjiogerogerg", "foierjiogerogerg", false},
}
assert.True(t, code.ValidateCodeChallenge("N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt"))
assert.False(t, code.ValidateCodeChallenge("wiogjerogorewngoenrgoiuenorg"))
// test unknown
code = &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "monkey",
CodeChallenge: "foiwgjioriogeiogjerger",
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
code := &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: tc.method,
CodeChallenge: tc.challenge,
}
assert.Equal(t, tc.valid, code.ValidateCodeChallenge(tc.verifier))
})
}
assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger"))
// test no code challenge
code = &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "",
CodeChallenge: "foierjiogerogerg",
}
assert.True(t, code.ValidateCodeChallenge(""))
}
func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
+6
View File
@@ -54,6 +54,12 @@ type FindOrgOptions struct {
IncludeVisibility structs.VisibleType
}
func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludeVisibility = structs.VisibleTypePublic
}
}
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
cond := builder.Eq{"uid": userID}
if !includePrivate {
+7
View File
@@ -212,6 +212,13 @@ type SearchRepoOptions struct {
OnlyShowRelevant bool
}
func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Private = false
opts.AllLimited = false
}
}
// UserOwnedRepoCond returns user ownered repositories
func UserOwnedRepoCond(userID int64) builder.Cond {
return builder.Eq{
+12
View File
@@ -24,6 +24,12 @@ type StarredReposOptions struct {
IncludePrivate bool
}
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
func (opts *StarredReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"star.uid": opts.StarrerID,
@@ -62,6 +68,12 @@ type WatchedReposOptions struct {
IncludePrivate bool
}
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
func (opts *WatchedReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"watch.user_id": opts.WatcherID,
+6
View File
@@ -59,6 +59,12 @@ type SearchUserOptions struct {
IncludeReserved bool
}
func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
}
}
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
var cond builder.Cond
cond = builder.In("type", opts.Types)
+7
View File
@@ -307,6 +307,13 @@ func (u *User) DashboardLink() string {
return setting.AppSubURL + "/"
}
func (u *User) SettingsLink() string {
if u.IsOrganization() {
return u.OrganisationLink() + "/settings"
}
return setting.AppSubURL + "/user/settings"
}
// HomeLink returns the user or organization home page link.
func (u *User) HomeLink() string {
return setting.AppSubURL + "/" + url.PathEscape(u.Name)
+20
View File
@@ -4,6 +4,10 @@
package actions
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"io"
"net/http"
"strings"
@@ -15,6 +19,22 @@ import (
"code.gitea.io/gitea/services/context"
)
type tagType string
// BuildSignature builds a hmac signature for the input values.
// "tag" is an internal pre-defined static string to distinguish the signatures for different purpose.
func BuildSignature(tag tagType, vals ...string) []byte {
m := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
_, _ = io.WriteString(m, string(tag))
var buf8 [8]byte
for _, v := range vals {
binary.LittleEndian.PutUint64(buf8[:], uint64(len(v)))
_, _ = m.Write(buf8[:])
_, _ = io.WriteString(m, v)
}
return m.Sum(nil)
}
// IsArtifactV4 detects whether the artifact is likely from v4.
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildSignature(t *testing.T) {
a := BuildSignature("v0", "x")
b := BuildSignature("v0", "x")
assert.Equal(t, a, b)
a = BuildSignature("v0", "x", "yz")
b = BuildSignature("v0", "xy", "z")
assert.NotEqual(t, a, b)
a = BuildSignature("v1", "x")
b = BuildSignature("v2", "x")
assert.NotEqual(t, a, b)
a = BuildSignature("v0", "x")
b = BuildSignature("v0x")
assert.NotEqual(t, a, b)
a = BuildSignature("v0", "", "x")
b = BuildSignature("v0", "x", "")
assert.NotEqual(t, a, b)
a = BuildSignature("v0")
b = BuildSignature("v0")
assert.Equal(t, a, b)
}
-1
View File
@@ -58,7 +58,6 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
".inc": "PHP", // ObjectPascal, POVRay, SourcePawn, PHTML
".m": "Objective-C", // Matlab, Mathematica, Mason
".mc": "Mason", // MonkeyC
".mod": "AMPL", // Modula-2
".network": "SYSTEMD", // INI
".php": "PHP", // PHTML
".php3": "PHP", // PHTML
+3 -1
View File
@@ -40,5 +40,7 @@ type MigrateOptions struct {
MirrorInterval string `json:"mirror_interval"`
AWSAccessKeyID string
AWSSecretAccessKey string
AWSSecretAccessKey string `json:",omitempty"`
AWSSecretAccessKeyEncrypted string `json:"aws_secret_access_key_encrypted,omitempty"`
}
+4 -1
View File
@@ -32,9 +32,11 @@ var (
UpdateChecker = struct {
Enabled bool
Endpoint string
Channel string // stable, dev, security
}{
Enabled: true,
Endpoint: "https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoGitea/releases/latest",
Endpoint: "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/updates.xml",
Channel: "stable",
}
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
@@ -176,6 +178,7 @@ func loadUpdateCheckerFrom(cfg ConfigProvider) {
sec := cfg.Section("update_checker")
UpdateChecker.Enabled = sec.Key("ENABLED").MustBool(true)
UpdateChecker.Endpoint = sec.Key("ENDPOINT").MustString(UpdateChecker.Endpoint)
UpdateChecker.Channel = sec.Key("CHANNEL").MustString(UpdateChecker.Channel)
}
func loadRunModeFrom(rootCfg ConfigProvider) {
+76 -20
View File
@@ -4,7 +4,7 @@
package updatechecker
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
@@ -21,6 +21,8 @@ type UpdateInfo struct {
UpdateAvailable bool
LatestVersion string
ReleaseURL string
DockerImage string
Channel string
CheckedAt time.Time
}
@@ -29,20 +31,53 @@ var (
mu sync.RWMutex
)
// giteaRelease is the subset of Gitea's release API response we need.
type giteaRelease struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Draft bool `json:"draft"`
// xmlUpdates mirrors the updates.xml structure (Joomla-style).
type xmlUpdates struct {
XMLName xml.Name `xml:"updates"`
Updates []xmlUpdate `xml:"update"`
}
// CheckForUpdate fetches the latest release from the configured endpoint
// and compares it to the running version.
type xmlUpdate struct {
Name string `xml:"name"`
Version string `xml:"version"`
Tags xmlTags `xml:"tags"`
InfoURL xmlInfoURL `xml:"infourl"`
Downloads xmlDownloads `xml:"downloads"`
Maintainer string `xml:"maintainer"`
Description string `xml:"description"`
}
type xmlTags struct {
Tag string `xml:"tag"`
}
type xmlInfoURL struct {
Title string `xml:"title,attr"`
URL string `xml:",chardata"`
}
type xmlDownloads struct {
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
}
type xmlDownloadURL struct {
Type string `xml:"type,attr"`
Format string `xml:"format,attr"`
URL string `xml:",chardata"`
}
// CheckForUpdate fetches updates.xml from the configured endpoint,
// filters by the selected channel, and compares to the running version.
func CheckForUpdate() error {
if !setting.UpdateChecker.Enabled || setting.UpdateChecker.Endpoint == "" {
return nil
}
channel := setting.UpdateChecker.Channel
if channel == "" {
channel = "stable"
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(setting.UpdateChecker.Endpoint)
if err != nil {
@@ -59,37 +94,58 @@ func CheckForUpdate() error {
return fmt.Errorf("reading update response: %w", err)
}
var release giteaRelease
if err := json.Unmarshal(body, &release); err != nil {
return fmt.Errorf("parsing update response: %w", err)
var updates xmlUpdates
if err := xml.Unmarshal(body, &updates); err != nil {
return fmt.Errorf("parsing updates.xml: %w", err)
}
if release.Draft || release.TagName == "" {
// Find the entry matching the selected channel
var matched *xmlUpdate
for i := range updates.Updates {
if strings.EqualFold(updates.Updates[i].Tags.Tag, channel) {
matched = &updates.Updates[i]
break
}
}
if matched == nil {
log.Debug("No update entry found for channel %q", channel)
return nil
}
latestVersion := strings.TrimPrefix(release.TagName, "v")
latestVersion := matched.Version
currentVersion := setting.AppVer
// Extract docker image URL if available
dockerImage := ""
for _, dl := range matched.Downloads.DownloadURL {
if dl.Format == "docker" {
dockerImage = strings.TrimSpace(dl.URL)
break
}
}
info := &UpdateInfo{
LatestVersion: latestVersion,
ReleaseURL: release.HTMLURL,
ReleaseURL: strings.TrimSpace(matched.InfoURL.URL),
DockerImage: dockerImage,
Channel: channel,
CheckedAt: time.Now(),
}
// Simple comparison: if latest != current, update is available.
// This handles both upgrades and the case where versions differ
// in any way (patch, upstream bump, etc.)
info.UpdateAvailable = latestVersion != "" && !strings.HasPrefix(currentVersion, latestVersion)
// Update is available if the latest version string is not a prefix of the current version.
// e.g., current "1.26.1+305-gabcdef" does not start with "04.00.00"
// This handles both moko semver and git-describe suffixed versions.
info.UpdateAvailable = latestVersion != "" && !strings.Contains(currentVersion, latestVersion)
mu.Lock()
cachedInfo = info
mu.Unlock()
if info.UpdateAvailable {
log.Info("MokoGitea update available: %s (current: %s)", latestVersion, currentVersion)
log.Info("MokoGitea update available: %s [%s] (current: %s)", latestVersion, channel, currentVersion)
} else {
log.Debug("MokoGitea is up to date: %s", currentVersion)
log.Debug("MokoGitea is up to date: %s [%s]", currentVersion, channel)
}
return nil
+7 -1
View File
@@ -3626,7 +3626,13 @@
"packages.terraform.delete.latest": "The latest version of a Terraform state cannot be deleted.",
"packages.vagrant.install": "To add a Vagrant box, run the following command:",
"packages.settings.link": "Link this package to a repository",
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list. Only repositories under the same owner can be linked. Leaving the field empty will remove the link.",
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list.",
"packages.settings.link.notice1": "Only repositories under the same owner can be linked.",
"packages.settings.link.notice2": "Linking a repository does not change the package visibility.",
"packages.settings.link.notice3": "Leaving the field empty will remove the link.",
"packages.settings.visibility": "Package visibility",
"packages.settings.visibility.inherit": "Package visibility is inherited from the owner and cannot be changed independently here. To change it, update the visibility settings of the user or organization that owns this package.",
"packages.settings.visibility.button": "Change owner visibility",
"packages.settings.link.select": "Select Repository",
"packages.settings.link.button": "Update Repository Link",
"packages.settings.link.success": "Repository link was successfully updated.",
+1 -1
View File
@@ -54,7 +54,7 @@
"jquery": "4.0.0",
"js-yaml": "4.1.1",
"katex": "0.16.44",
"mermaid": "11.14.0",
"mermaid": "11.15.0",
"online-3d-viewer": "0.18.0",
"pdfobject": "2.3.1",
"perfect-debounce": "2.1.0",
+532 -116
View File
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -162,13 +162,7 @@ func ArtifactsV4Routes(prefix string) *web.Router {
}
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endpoint))
mac.Write([]byte(expires))
mac.Write([]byte(artifactName))
_, _ = fmt.Fprint(mac, taskID)
_, _ = fmt.Fprint(mac, artifactID)
return mac.Sum(nil)
return actions.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10))
}
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
+13 -5
View File
@@ -9,7 +9,10 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/log"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/services/context"
)
// ServiceIndexResponse contains registry endpoints
@@ -91,7 +94,7 @@ type Source struct {
Reference string `json:"reference"`
}
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
func createPackageMetadataResponse(ctx *context.Context, registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
versions := make([]*PackageVersionMetadata, 0, len(pds))
for _, pd := range pds {
@@ -116,10 +119,15 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
},
}
if pd.Repository != nil {
pkg.Source = Source{
URL: pd.Repository.HTMLURL(),
Type: "git",
Reference: pd.Version.Version,
permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", pd.Repository.ID, err)
} else if permission.HasAnyUnitAccessOrPublicAccess() {
pkg.Source = Source{
URL: pd.Repository.HTMLURL(),
Type: "git",
Reference: pd.Version.Version,
}
}
}
@@ -146,6 +146,7 @@ func PackageMetadata(ctx *context.Context) {
}
resp := createPackageMetadataResponse(
ctx,
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
pds,
)
+85 -62
View File
@@ -212,6 +212,11 @@ func repoAssignment() func(ctx *context.APIContext) {
ctx.APIErrorNotFound()
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
}
}
@@ -249,51 +254,66 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
// public Only permission check
switch {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
for _, category := range requiredScopeCategories {
switch category {
case auth_model.AccessTokenScopeCategoryRepository:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.AccessTokenScopeCategoryIssue:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.AccessTokenScopeCategoryOrganization:
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
if orgPrivate || userOrgPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.AccessTokenScopeCategoryUser:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.AccessTokenScopeCategoryActivityPub:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.AccessTokenScopeCategoryNotification:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.AccessTokenScopeCategoryPackage:
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
}
}
}
}
func rejectPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
}
}
func contextAuthenticatedUser() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
ctx.ContextUser = ctx.Doer
}
}
// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
@@ -957,6 +977,8 @@ func Routes() *web.Router {
})
// Notifications (requires 'notifications' scope)
// The notifications API is not available for public-only tokens because a user's notifications mix
// public and private repository events in the same mailbox.
m.Group("/notifications", func() {
m.Combo("").
Get(reqToken(), notify.ListNotifications).
@@ -965,7 +987,7 @@ func Routes() *web.Router {
m.Combo("/threads/{id}").
Get(reqToken(), notify.GetThread).
Patch(reqToken(), notify.ReadThread)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
// Users (requires user scope)
m.Group("/users", func() {
@@ -1013,8 +1035,9 @@ func Routes() *web.Router {
m.Group("/settings", func() {
m.Get("", user.GetUserSettings)
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
}, reqToken())
m.Combo("/emails").
}, rejectPublicOnly())
// Email addresses are always private account data.
m.Combo("/emails", rejectPublicOnly()).
Get(user.ListEmails).
Post(bind(api.CreateEmailOption{}), user.AddEmail).
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
@@ -1046,7 +1069,7 @@ func Routes() *web.Router {
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
})
}, rejectPublicOnly())
m.Get("/followers", user.ListMyFollowers)
m.Group("/following", func() {
@@ -1064,7 +1087,7 @@ func Routes() *web.Router {
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
m.Combo("/{id}").Get(user.GetPublicKey).
Delete(user.DeletePublicKey)
})
}, rejectPublicOnly())
// (admin:application scope)
m.Group("/applications", func() {
@@ -1075,7 +1098,7 @@ func Routes() *web.Router {
Delete(user.DeleteOauth2Application).
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
Get(user.GetOauth2Application)
})
}, rejectPublicOnly())
// (admin:gpg_key scope)
m.Group("/gpg_keys", func() {
@@ -1083,13 +1106,13 @@ func Routes() *web.Router {
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
m.Combo("/{id}").Get(user.GetGPGKey).
Delete(user.DeleteGPGKey)
})
m.Get("/gpg_key_token", user.GetVerificationToken)
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
}, rejectPublicOnly())
m.Get("/gpg_key_token", rejectPublicOnly(), user.GetVerificationToken)
m.Post("/gpg_key_verify", rejectPublicOnly(), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
// (repo scope)
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create)
Post(rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.Create)
// (repo scope)
m.Group("/starred", func() {
@@ -1100,22 +1123,22 @@ func Routes() *web.Router {
m.Delete("", user.Unstar)
}, repoAssignment(), checkTokenPublicOnly())
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes)
m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches)
m.Get("/subscriptions", user.GetMyWatchedRepos)
m.Get("/teams", org.ListUserTeams)
m.Get("/teams", rejectPublicOnly(), org.ListUserTeams)
m.Group("/hooks", func() {
m.Combo("").Get(user.ListHooks).
Post(bind(api.CreateHookOption{}), user.CreateHook)
m.Combo("/{id}").Get(user.GetHook).
Patch(bind(api.EditHookOption{}), user.EditHook).
Delete(user.DeleteHook)
}, reqWebhooksEnabled())
}, reqWebhooksEnabled(), rejectPublicOnly())
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
})
}, rejectPublicOnly())
m.Group("/blocks", func() {
m.Get("", user.ListBlocks)
@@ -1124,8 +1147,8 @@ func Routes() *web.Router {
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
}, rejectPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
@@ -1431,10 +1454,10 @@ func Routes() *web.Router {
}, reqAdmin())
}, reqAnyRepoReader())
// MokoGitea badge engine
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
@@ -1648,7 +1671,7 @@ func Routes() *web.Router {
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
+8 -3
View File
@@ -40,6 +40,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
UserID: u.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
}
opts.ApplyPublicOnly(ctx.PublicOnly)
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
@@ -199,7 +200,7 @@ func GetAll(ctx *context.APIContext) {
// "$ref": "#/responses/OrganizationList"
vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned && !ctx.PublicOnly {
if ctx.IsSigned {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
@@ -208,13 +209,16 @@ func GetAll(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
searchOpts := user_model.SearchUserOptions{
Actor: ctx.Doer,
ListOptions: listOptions,
Types: []user_model.UserType{user_model.UserTypeOrganization},
OrderBy: db.SearchOrderByAlphabetically,
Visible: vMode,
})
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -494,6 +498,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
+1 -7
View File
@@ -6,7 +6,6 @@ package repo
import (
go_context "context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
@@ -24,7 +23,6 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -1939,11 +1937,7 @@ func DeleteArtifact(ctx *context.APIContext) {
}
func buildSignature(endp string, expires, artifactID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endp))
fmt.Fprint(mac, expires)
fmt.Fprint(mac, artifactID)
return mac.Sum(nil)
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
}
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
+2 -1
View File
@@ -47,9 +47,10 @@ func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPubl
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = !ctx.PublicOnly
opts.Private = true
opts.AllLimited = true
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
+6 -3
View File
@@ -134,9 +134,6 @@ func Search(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
if ctx.PublicOnly {
private = false
}
opts := repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
@@ -152,6 +149,7 @@ func Search(ctx *context.APIContext) {
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("template") != "" {
opts.Template = optional.Some(ctx.FormBool("template"))
@@ -573,6 +571,10 @@ func GetByID(ctx *context.APIContext) {
}
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
@@ -1321,6 +1323,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
+7 -4
View File
@@ -19,12 +19,15 @@ import (
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx)
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
searchOpts := repo_model.SearchRepoOptions{
Actor: u,
Private: private,
ListOptions: opts,
OrderBy: "id ASC",
})
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -79,8 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
private := ctx.IsSigned
listUserRepos(ctx, ctx.ContextUser, private)
listUserRepos(ctx, ctx.ContextUser, ctx.IsSigned)
}
// ListMyRepos - list the repositories you own or have access to.
@@ -110,6 +112,7 @@ func ListMyRepos(ctx *context.APIContext) {
Private: ctx.IsSigned,
IncludeDescription: true,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
+5 -2
View File
@@ -20,11 +20,14 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has
// starred
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
opts := &repo_model.StarredReposOptions{
ListOptions: utils.GetListOptions(ctx),
StarrerID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
if err != nil {
return nil, err
}
+5 -8
View File
@@ -9,7 +9,6 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -69,19 +68,16 @@ func Search(ctx *context.APIContext) {
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
var visible []structs.VisibleType
if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic}
}
users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
opts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Types: []user_model.UserType{user_model.UserTypeIndividual},
SearchByEmail: true,
Visible: visible,
ListOptions: listOptions,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
users, maxResults, err = user_model.SearchUsers(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]any{
"ok": false,
@@ -214,6 +210,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
+5 -2
View File
@@ -18,11 +18,14 @@ import (
// getWatchedRepos returns the repos that the user with the specified userID is watching
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
opts := &repo_model.WatchedReposOptions{
ListOptions: utils.GetListOptions(ctx),
WatcherID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
if err != nil {
return nil, 0, err
}
+2
View File
@@ -142,6 +142,8 @@ func Dashboard(ctx *context.Context) {
ctx.Data["NeedUpdate"] = info.UpdateAvailable
ctx.Data["LatestVersion"] = info.LatestVersion
ctx.Data["ReleaseURL"] = info.ReleaseURL
ctx.Data["UpdateChannel"] = info.Channel
ctx.Data["DockerImage"] = info.DockerImage
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
+14
View File
@@ -561,6 +561,13 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
})
return
}
if grant.ApplicationID != app.ID {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "refresh token belongs to a different client",
})
return
}
// check if token got already used
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
@@ -630,6 +637,13 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
})
return
}
if authorizationCode.RedirectURI != "" && form.RedirectURI != authorizationCode.RedirectURI {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "redirect_uri differs from the original authorization request",
})
return
}
// check if granted for this application
if authorizationCode.Grant.ApplicationID != app.ID {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+25 -3
View File
@@ -6,6 +6,7 @@ package repo
import (
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
@@ -21,6 +22,17 @@ import (
repo_service "code.gitea.io/gitea/services/repository"
)
func attachmentReadScope(unitType unit.Type) (auth_model.AccessTokenScope, bool) {
switch unitType {
case unit.TypeIssues, unit.TypePullRequests:
return auth_model.AccessTokenScopeReadIssue, true
case unit.TypeReleases:
return auth_model.AccessTokenScopeReadRepository, true
default:
return "", false
}
}
// UploadIssueAttachment response for Issue/PR attachments
func UploadIssueAttachment(ctx *context.Context) {
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue)
@@ -150,9 +162,12 @@ func ServeAttachment(ctx *context.Context, uuid string) {
return
}
} else { // If we have the linked type, we need to check access
var perm access_model.Permission
if ctx.Repo.Repository == nil {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
var (
perm access_model.Permission
repo = ctx.Repo.Repository
)
if repo == nil {
repo, err = repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
@@ -170,6 +185,13 @@ func ServeAttachment(ctx *context.Context, uuid string) {
ctx.HTTPError(http.StatusNotFound)
return
}
if requiredScope, ok := attachmentReadScope(unitType); ok {
context.CheckTokenScopes(ctx, repo, requiredScope)
if ctx.Written() {
return
}
}
}
if err := attach.IncreaseDownloadCount(ctx); err != nil {
+22
View File
@@ -7,6 +7,7 @@ package repo
import (
"time"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@@ -18,6 +19,11 @@ import (
"code.gitea.io/gitea/services/context"
)
func checkDownloadTokenScope(ctx *context.Context) bool {
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
return !ctx.Written()
}
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Time) error {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
@@ -88,6 +94,10 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
// SingleDownload download a file by repos path
func SingleDownload(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, lastModified := getBlobForEntry(ctx)
if blob == nil {
return
@@ -100,6 +110,10 @@ func SingleDownload(ctx *context.Context) {
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
func SingleDownloadOrLFS(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, lastModified := getBlobForEntry(ctx)
if blob == nil {
return
@@ -112,6 +126,10 @@ func SingleDownloadOrLFS(ctx *context.Context) {
// DownloadByID download a file by sha1 ID
func DownloadByID(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
@@ -128,6 +146,10 @@ func DownloadByID(ctx *context.Context) {
// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
func DownloadByIDOrLFS(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
+2 -2
View File
@@ -180,8 +180,8 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
}
if repoExist {
// Because of special ref "refs/for" (agit) , need delay write permission check
if git.DefaultFeatures().SupportProcReceive {
// Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks.
if git.DefaultFeatures().SupportProcReceive && !isWiki {
accessMode = perm.AccessModeRead
}
+8
View File
@@ -364,6 +364,10 @@ func RedirectDownload(ctx *context.Context) {
// Download an archive of a repository
func Download(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
@@ -389,6 +393,10 @@ func Download(ctx *context.Context) {
// a request that's already in-progress, but the archiver service will just
// kind of drop it on the floor if this is the case.
func InitiateDownload(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
paths := ctx.FormStrings("path")
if setting.Repository.StreamArchives || len(paths) > 0 {
ctx.JSON(http.StatusOK, map[string]any{
+7
View File
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
@@ -47,6 +48,12 @@ type APIContext struct {
PublicOnly bool // Whether the request is for a public endpoint
}
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
}
func init() {
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(apiContextKey).(*APIContext)
+35 -34
View File
@@ -12,6 +12,39 @@ import (
"code.gitea.io/gitea/models/unit"
)
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
if ctx.Data["IsApiToken"] != true {
return
}
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if !ok {
return
}
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.ServerError("PublicOnly", err)
return
}
if publicOnly && repo != nil && repo.IsPrivate {
ctx.HTTPError(http.StatusForbidden)
return
}
scopeMatched, err := scope.HasAnyScope(scopes...)
if err != nil {
ctx.ServerError("HasAnyScope", err)
return
}
if !scopeMatched {
ctx.HTTPError(http.StatusForbidden)
}
}
// RequireRepoAdmin returns a middleware for requiring repository admin permission
func RequireRepoAdmin() func(ctx *Context) {
return func(ctx *Context) {
@@ -57,39 +90,7 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
}
}
// CheckRepoScopedToken check whether personal access token has repo scope
// CheckRepoScopedToken checks whether the authenticated API token has repo scope.
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
return
}
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ok { // it's a personal access token but not oauth2 token
var scopeMatched bool
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if publicOnly && repo.IsPrivate {
ctx.HTTPError(http.StatusForbidden)
return
}
scopeMatched, err = scope.HasScope(requiredScopes...)
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if !scopeMatched {
ctx.HTTPError(http.StatusForbidden)
return
}
}
CheckTokenScopes(ctx, repo, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
}
+13
View File
@@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"github.com/golang-jwt/jwt/v5"
@@ -605,6 +606,18 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
return nil, err
}
if !u.IsActive || u.ProhibitLogin {
return nil, util.NewPermissionDeniedErrorf("not allowed to access any repository")
}
perm, err := access_model.GetDoerRepoPermission(ctx, target, u)
if err != nil {
log.Error("Unable to GetDoerRepoPermission for user[%d] repo[%d]: %v", claims.UserID, target.ID, err)
return nil, err
}
if !perm.CanAccess(mode, unit.TypeCode) {
return nil, util.NewPermissionDeniedErrorf("no permission to access the repository")
}
return u, nil
}
+58 -5
View File
@@ -7,9 +7,11 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/models/db"
perm_model "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
@@ -22,11 +24,15 @@ func TestMain(m *testing.M) {
func TestAuthenticate(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx, _ := contexttest.MockContext(t, "/")
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
token2, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: "download", UserID: 2, RepoID: 1})
_, token2, _ = strings.Cut(token2, " ")
ctx, _ := contexttest.MockContext(t, "/")
getUserToken := func(op string, userID int64, repo *repo_model.Repository) string {
s, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: op, UserID: userID, RepoID: repo.ID})
_, token, _ := strings.Cut(s, " ")
return token
}
t.Run("handleLFSToken", func(t *testing.T) {
u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead)
@@ -37,15 +43,62 @@ func TestAuthenticate(t *testing.T) {
require.Error(t, err)
assert.Nil(t, u)
u, err = handleLFSToken(ctx, token2, repo1, perm_model.AccessModeRead)
u, err = handleLFSToken(ctx, getUserToken("download", 2, repo1), repo1, perm_model.AccessModeRead)
require.NoError(t, err)
assert.EqualValues(t, 2, u.ID)
})
t.Run("authenticate", func(t *testing.T) {
const prefixBearer = "Bearer "
token := getUserToken("download", 2, repo1)
assert.False(t, authenticate(ctx, repo1, "", true, false))
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false))
assert.True(t, authenticate(ctx, repo1, prefixBearer+token, true, false))
})
handleLFSTokenTestPerm := func(op string, userID int64, repo *repo_model.Repository, accessMode perm_model.AccessMode) error {
token := getUserToken(op, userID, repo)
u, err := handleLFSToken(ctx, token, repo, accessMode)
if err == nil {
assert.Equal(t, userID, u.ID)
}
return err
}
t.Run("handleLFSToken blocks prohibited users", func(t *testing.T) {
user37 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 37})
// prohibited user
assert.True(t, user37.ProhibitLogin)
err := handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead)
assert.ErrorContains(t, err, "not allowed to access any repository")
// normal user
_, _ = db.GetEngine(t.Context()).ID(37).Cols("prohibit_login").Update(&user_model.User{ProhibitLogin: false})
err = handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead)
assert.NoError(t, err)
// inactive user
_, _ = db.GetEngine(t.Context()).ID(37).Cols("is_active").Update(&user_model.User{IsActive: false})
err = handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead)
assert.ErrorContains(t, err, "not allowed to access any repository")
})
t.Run("handleLFSToken blocks users without repo access", func(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
err := handleLFSTokenTestPerm("download", 10, repo2, perm_model.AccessModeRead)
assert.ErrorContains(t, err, "no permission to access the repository")
})
t.Run("handleLFSToken requires write access for uploads", func(t *testing.T) {
err := handleLFSTokenTestPerm("download", 10, repo1, perm_model.AccessModeRead)
assert.NoError(t, err)
err = handleLFSTokenTestPerm("upload", 10, repo1, perm_model.AccessModeWrite)
assert.ErrorContains(t, err, "no permission to access the repository")
})
t.Run("handleLFSToken allows writes for authorized users", func(t *testing.T) {
err := handleLFSTokenTestPerm("upload", 2, repo1, perm_model.AccessModeWrite)
assert.NoError(t, err)
})
}
+5
View File
@@ -85,6 +85,11 @@ func CreateMigrateTask(ctx context.Context, doer, u *user_model.User, opts base.
return nil, err
}
opts.AuthToken = ""
opts.AWSSecretAccessKeyEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.AWSSecretAccessKey)
if err != nil {
return nil, err
}
opts.AWSSecretAccessKey = ""
bs, err := json.Marshal(&opts)
if err != nil {
return nil, err
+2 -1
View File
@@ -3,8 +3,9 @@
{{if .NeedUpdate}}
<div class="ui positive message">
<div class="header">{{svg "octicon-info"}} MokoGitea Update Available</div>
<p>A new version <strong>{{.LatestVersion}}</strong> is available.
<p>A new version <strong>{{.LatestVersion}}</strong> is available{{if .UpdateChannel}} ({{.UpdateChannel}} channel){{end}}.
{{if .ReleaseURL}}<a href="{{.ReleaseURL}}" target="_blank" rel="noopener noreferrer">View release notes</a>{{end}}</p>
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
</div>
{{end}}
<h4 class="ui top attached header">
+2 -2
View File
@@ -17,7 +17,7 @@
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
</h1>
<p class="large tw-text-balance">
{{ctx.Locale.Tr "startpage.install_desc" "{{HelpURL}}/installation/install-from-binary" "https://github.com/go-gitea/gitea/tree/master/docker" "{{HelpURL}}/installation/install-from-package"}}
{{ctx.Locale.Tr "startpage.install_desc" "{{HelpURL}}/installation/install-from-binary" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/main/docker" "{{HelpURL}}/installation/install-from-package"}}
</p>
</div>
<div class="eight wide center column">
@@ -43,7 +43,7 @@
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
</h1>
<p class="large tw-text-balance">
{{ctx.Locale.Tr "startpage.license_desc" "https://code.gitea.io/gitea" "code.gitea.io/gitea" "https://github.com/go-gitea/gitea"}}
{{ctx.Locale.Tr "startpage.license_desc" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea" "MokoConsulting/MokoGitea" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea"}}
</p>
</div>
</div>
+17 -1
View File
@@ -10,12 +10,28 @@
{{template "user/overview/header" .}}
{{end}}
{{template "base/alert" .}}
<p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
<p>
<a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a>
<span class="label-list">
{{template "package/shared/visibility_badge" dict "Package" .PackageDescriptor.Package "Owner" .PackageDescriptor.Owner}}
</span>
/ <strong>{{ctx.Locale.Tr "repo.settings"}}</strong>
</p>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "packages.settings.visibility"}}
</h4>
<div class="ui attached segment">
<p>{{ctx.Locale.Tr "packages.settings.visibility.inherit"}}</p>
<a class="ui basic button" href="{{.ContextUser.SettingsLink}}">{{ctx.Locale.Tr "packages.settings.visibility.button"}}</a>
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "packages.settings.link"}}
</h4>
<div class="ui attached segment">
<p>{{ctx.Locale.Tr "packages.settings.link.description"}}</p>
<p>- {{ctx.Locale.Tr "packages.settings.link.notice1"}}</p>
<p>- {{ctx.Locale.Tr "packages.settings.link.notice2"}}</p>
<p>- {{ctx.Locale.Tr "packages.settings.link.notice3"}}</p>
<form class="ui form form-fetch-action ignore-dirty flex-text-block" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="link">
<div data-global-init="initSearchRepoBox" class="ui search" data-uid="{{.PackageDescriptor.Owner.ID}}">
@@ -18,7 +18,12 @@
{{range .VersionsToRemove}}
<tr>
<td>{{.Package.Type.Name}}</td>
<td>{{.Package.Name}}</td>
<td>
{{.Package.Name}}
<span class="label-list">
{{template "package/shared/visibility_badge" dict "Package" .Package "Owner" .Owner}}
</span>
</td>
<td><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
<td>{{FileSize .CalculateBlobSize}}</td>
+4 -1
View File
@@ -21,7 +21,10 @@
<div class="item-main">
<div class="item-title">
<a href="{{.VersionWebLink}}">{{.Package.Name}}</a>
<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
<span class="label-list">
{{template "package/shared/visibility_badge" dict "Package" .Package "Owner" .Owner}}
<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
</span>
</div>
<div class="item-body">
{{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}}
+7 -1
View File
@@ -1,4 +1,10 @@
<p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "packages.versions"}}</strong></p>
<p>
<a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a>
<span class="label-list">
{{template "package/shared/visibility_badge" dict "Package" .PackageDescriptor.Package "Owner" .PackageDescriptor.Owner}}
</span>
/ <strong>{{ctx.Locale.Tr "packages.versions"}}</strong>
</p>
<form class="ui form ignore-dirty">
<div class="ui small fluid action input">
{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
+6 -1
View File
@@ -1,6 +1,11 @@
<div class="issue-title-header">
{{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}}
<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
<div class="tw-flex tw-flex-wrap tw-items-center tw-gap-2">
<h1 class="tw-mb-0">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
<span class="label-list">
{{template "package/shared/visibility_badge" dict "Package" .PackageDescriptor.Package "Owner" .PackageDescriptor.Owner}}
</span>
</div>
<div>
{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
{{if .HasRepositoryAccess}}
@@ -0,0 +1,7 @@
{{if .Package.IsInternal}}
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
{{else if .Owner.Visibility.IsPrivate}}
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
{{else if .Owner.Visibility.IsLimited}}
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
{{end}}
+1 -1
View File
@@ -43,7 +43,7 @@
{{end}}
<div class="tw-mt-8 tw-text-center">
{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" "https://github.com/go-gitea/gitea/issues"}}</p>{{end}}
{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/issues"}}</p>{{end}}
</div>
</div>
</div>
+1 -1
View File
@@ -9,7 +9,7 @@
<form class="ui form" action="{{.Link}}/theme" method="post">
<div class="field">
{{ctx.Locale.Tr "settings.theme_desc"}}
<a class="muted" target="_blank" href="https://github.com/go-gitea/gitea/blob/main/web_src/css/themes/" data-tooltip-content="{{ctx.Locale.Tr "settings.theme_colorblindness_prompt"}}">
<a class="muted" target="_blank" href="https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/main/web_src/css/themes/" data-tooltip-content="{{ctx.Locale.Tr "settings.theme_colorblindness_prompt"}}">
{{svg "octicon-question"}} {{ctx.Locale.Tr "settings.theme_colorblindness_help"}}
</a>
</div>
+22
View File
@@ -0,0 +1,22 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {apiCreateRepo, apiHeaders, assertNoJsError, baseUrl, randomString} from './utils.ts';
test('mermaid diagram in issue', async ({page, request}) => {
const repoName = `e2e-mermaid-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n';
const response = await request.post(`${baseUrl()}/api/v1/repos/${owner}/${repoName}/issues`, {
headers: apiHeaders(),
data: {title: 'mermaid test', body},
});
expect(response.ok(), `create issue failed: ${response.status()}`).toBe(true);
const {number} = await response.json();
await page.goto(`/${owner}/${repoName}/issues/${number}`);
const svg = page.frameLocator('iframe.markup-content-iframe').locator('svg');
await expect(svg).toContainText(/Alpha[\s\S]*Beta[\s\S]*Gamma/);
await assertNoJsError(page);
});
@@ -8,6 +8,7 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -176,3 +177,19 @@ func TestAPIRepoValidateIssueConfig(t *testing.T) {
assert.NotEmpty(t, issueConfigValidation.Message)
})
}
func TestAPIRepoIssueConfigRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
for _, path := range []string{
fmt.Sprintf("/api/v1/repos/%s/issue_config", repo.FullName()),
fmt.Sprintf("/api/v1/repos/%s/issue_config/validate", repo.FullName()),
} {
req := NewRequest(t, "GET", path).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
}
@@ -8,10 +8,12 @@ import (
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
@@ -49,3 +51,19 @@ about: bar
assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
})
}
func TestAPIIssueTemplateRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
issueTemplatesURL := "/api/v1/repos/" + repo.FullName() + "/issue_templates"
languagesURL := "/api/v1/repos/" + repo.FullName() + "/languages"
req := NewRequest(t, "GET", issueTemplatesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", languagesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
+1 -1
View File
@@ -108,7 +108,7 @@ func testAPIListIssuesPublicOnly(t *testing.T) {
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusForbidden)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPICreateIssue(t *testing.T) {
@@ -209,3 +209,23 @@ func TestAPINotificationPUT(t *testing.T) {
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
}
func TestAPINotificationPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
token := getUserToken(t, user2.Name, auth_model.AccessTokenScopeReadNotification, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/notifications").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", "/api/v1/notifications/new").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d", thread5.ID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
@@ -27,6 +27,8 @@ func TestPackageComposer(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
otherUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
vendorName := "gitea"
projectName := "composer-package"
@@ -243,5 +245,85 @@ func TestPackageComposer(t *testing.T) {
assert.Equal(t, repo1.HTMLURL(), pkgs[0].Source.URL)
assert.Equal(t, "git", pkgs[0].Source.Type)
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
// Private repository links remain visible to callers who can access the repository.
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
err = packages.SetRepositoryLink(t.Context(), userPkgs[0].ID, repo2.ID)
assert.NoError(t, err)
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
assert.Equal(t, repo2.HTMLURL(), pkgs[0].Source.URL)
assert.Equal(t, "git", pkgs[0].Source.Type)
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
// Callers without repository access still get the package metadata, but not the private source URL.
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
AddBasicAuth(otherUser.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
assert.Empty(t, pkgs[0].Source.URL)
assert.Empty(t, pkgs[0].Source.Type)
assert.Empty(t, pkgs[0].Source.Reference)
})
t.Run("WebVisibilityBadge", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
listReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", user.Name)).
AddBasicAuth(user.Name)
listResp := MakeRequest(t, listReq, http.StatusOK)
listDoc := NewHTMLParser(t, listResp.Body)
assert.Equal(t, 0, listDoc.Find(".flex-item-title .ui.basic.label").Length())
viewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion))).
AddBasicAuth(user.Name)
viewResp := MakeRequest(t, viewReq, http.StatusOK)
viewDoc := NewHTMLParser(t, viewResp.Body)
assert.Equal(t, 0, viewDoc.Find(".issue-title-header .ui.basic.label").Length())
privatePackageName := privateUser.Name + "/private-composer-package"
privatePackageVersion := "1.0.0"
privateContent := test.WriteZipArchive(map[string]string{
"composer.json": `{
"name": "` + privatePackageName + `",
"description": "Private Package",
"type": "` + packageType + `",
"license": "` + packageLicense + `",
"authors": [
{
"name": "` + packageAuthor + `"
}
]
}`,
}).Bytes()
privateUploadURL := fmt.Sprintf("%sapi/packages/%s/composer?version=%s", setting.AppURL, privateUser.Name, privatePackageVersion)
uploadReq := NewRequestWithBody(t, "PUT", privateUploadURL, bytes.NewReader(privateContent)).
AddBasicAuth(privateUser.Name)
MakeRequest(t, uploadReq, http.StatusCreated)
privateSession := loginUser(t, privateUser.Name)
privateListReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", privateUser.Name))
privateListResp := privateSession.MakeRequest(t, privateListReq, http.StatusOK)
privateListDoc := NewHTMLParser(t, privateListResp.Body)
assert.Equal(t, 1, privateListDoc.Find(".flex-item-title .ui.basic.label").Length())
assert.Equal(t, "Private", privateListDoc.Find(".flex-item-title .ui.basic.label").First().Text())
privateViewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", privateUser.Name, neturl.PathEscape(privatePackageName), neturl.PathEscape(privatePackageVersion)))
privateViewResp := privateSession.MakeRequest(t, privateViewReq, http.StatusOK)
privateViewDoc := NewHTMLParser(t, privateViewResp.Body)
assert.Equal(t, 1, privateViewDoc.Find(".issue-title-header .ui.basic.label").Length())
assert.Equal(t, "Private", privateViewDoc.Find(".issue-title-header .ui.basic.label").First().Text())
})
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUserReposPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/repos").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
req = NewRequest(t, "GET", "/api/v1/users/user2/repos").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
func repoNames(repos []api.Repository) []string {
names := make([]string, 0, len(repos))
for _, repo := range repos {
names = append(names, repo.FullName)
}
return names
}
func TestAPIRepoByIDPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/repositories/1").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/api/v1/repositories/2").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var activities []api.Activity
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(orgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(publicOrgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
}
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
t.Helper()
for _, activity := range activities {
assert.False(t, activity.IsPrivate)
if activity.Repo != nil {
assert.False(t, activity.Repo.Private)
}
}
}
+5 -5
View File
@@ -29,10 +29,10 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
session := loginUser(t, user1.LowerName)
// public only token should be forbidden
// public-only token cannot see a private repo
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
@@ -46,7 +46,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.Equal(t, "master", branches[1].Name)
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
bs, err = io.ReadAll(resp.Body)
@@ -55,7 +55,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.NoError(t, json.Unmarshal(bs, &branch))
assert.Equal(t, "test_branch", branch.Name)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
req.Header.Add("Content-Type", "application/json")
@@ -81,7 +81,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
assert.NoError(t, err)
+41
View File
@@ -154,3 +154,44 @@ func TestMyOrgs(t *testing.T) {
},
}, orgs)
}
func TestMyOrgsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/orgs").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var orgs []*api.Organization
DecodeJSON(t, resp, &orgs)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
assert.Equal(t, []*api.Organization{
{
ID: 17,
Name: org17.Name,
UserName: org17.Name,
FullName: org17.FullName,
Email: org17.Email,
AvatarURL: org17.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{
ID: 3,
Name: org3.Name,
UserName: org3.Name,
FullName: org3.FullName,
Email: org3.Email,
AvatarURL: org3.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
}, orgs)
}
@@ -0,0 +1,177 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/require"
)
func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user31"})
require.True(t, privateUser.Visibility.IsPrivate())
privateSession := loginUser(t, privateUser.Name)
privateReadUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
)
privateWriteUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
)
t.Run("PrivateProfileForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
})
t.Run("PrivateSensitiveSelfRoutesForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hideEmail := true
settingsReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
HideEmail: &hideEmail,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, settingsReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/emails").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
emailReq := NewRequestWithJSON(t, "POST", "/api/v1/user/emails", &api.CreateEmailOption{
Emails: []string{"user31-public-only@example.com"},
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, emailReq, http.StatusForbidden)
keyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
Title: "public-only-private-key",
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, keyReq, http.StatusForbidden)
oauthReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
Name: "public-only-private-oauth-app",
RedirectURIs: []string{"https://example.com/callback"},
ConfidentialClient: true,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, oauthReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_keys").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgKeyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", &api.CreateGPGKeyOption{
ArmoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\ncomment\n-----END PGP PUBLIC KEY BLOCK-----",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgKeyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_key_token").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgVerifyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_key_verify", &api.VerifyGPGKeyOption{
KeyID: "deadbeef",
Signature: "invalid-signature",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgVerifyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/actions/variables").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/PRIVATE_SECRET").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
variableReq := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/variables/PRIVATE_VAR", api.CreateVariableOption{
Value: "private-value",
Description: "must stay private",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, variableReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/hooks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hookReq := NewRequestWithJSON(t, "POST", "/api/v1/user/hooks", api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
Name: "public-only-private-hook",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, hookReq, http.StatusForbidden)
avatarReq := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar", &api.UpdateUserAvatarOption{
Image: "aGVsbG8=",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, avatarReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/avatar").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/times").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/stopwatches").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/teams").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/blocks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/blocks/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
})
t.Run("PublicRepoRoutesFilterAndRejectMutations", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
publicSession := loginUser(t, "user2")
fullWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicOnlyReadRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
auth_model.AccessTokenScopeReadRepository,
)
publicOnlyWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicRepoName := "public-only-visible-self-repo"
privateRepoName := "public-only-hidden-self-repo"
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: publicRepoName,
Private: false,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
publicRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+publicRepoName, publicRepo.FullName)
resp = MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: privateRepoName,
Private: true,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
privateRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+privateRepoName, privateRepo.FullName)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/"+privateRepoName).AddTokenAuth(publicOnlyReadRepoToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/repos").AddTokenAuth(publicOnlyReadRepoToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
foundPublicRepo := false
for _, repo := range repos {
require.NotEqual(t, privateRepo.FullName, repo.FullName)
if repo.FullName == publicRepo.FullName {
foundPublicRepo = true
}
}
require.True(t, foundPublicRepo)
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: "public-only-rejected-self-repo",
Private: false,
}).AddTokenAuth(publicOnlyWriteRepoToken), http.StatusForbidden)
})
}
+22
View File
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIStar(t *testing.T) {
@@ -153,3 +154,24 @@ func TestAPIStarDisabled(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
}
func TestAPIStarPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/starred").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
req = NewRequest(t, "GET", "/api/v1/users/user2/starred").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
require.Len(t, repos, 1)
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
+25
View File
@@ -92,3 +92,28 @@ func TestAPIWatch(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
})
}
func TestAPIWatchPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
writeRepoToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadUser)
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo1/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo2/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /user/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user1/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /users/{username}/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
+107
View File
@@ -14,6 +14,7 @@ import (
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
@@ -26,6 +27,15 @@ import (
"github.com/stretchr/testify/require"
)
type attachmentScopeCase struct {
name string
url string
readIssueStatus int
readRepoStatus int
publicOnlyIssueStatus int
publicOnlyRepoStatus int
}
func testGeneratePngBytes() []byte {
myImage := image.NewRGBA(image.Rect(0, 0, 32, 32))
var buff bytes.Buffer
@@ -200,3 +210,100 @@ func testDeleteAttachmentPermissions(t *testing.T) {
// test deleting release attachment from another repo
testDeleteReleaseAttachment(t, ownerSession, "/user2/repo2", crossRepoUUID, http.StatusBadRequest)
}
func TestAttachmentTokenScopes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
for _, uuid := range []string{
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
} {
_, err := storage.Attachments.Save(repo_model.AttachmentRelativePath(uuid), strings.NewReader("hello world"), -1)
require.NoError(t, err)
}
readIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue)
readRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
publicOnlyIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
publicOnlyRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
cases := []attachmentScopeCase{
{
name: "GlobalPublicIssueAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusOK,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPublicIssueAttachment",
url: "/user2/repo1/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusOK,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "GlobalPrivateIssueAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPrivateIssueAttachment",
url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "GlobalPublicReleaseAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusOK,
},
{
name: "RepoPublicReleaseAttachment",
url: "/user2/repo1/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusOK,
},
{
name: "GlobalPrivateReleaseAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPrivateReleaseAttachment",
url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readIssueToken), tc.readIssueStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readRepoToken), tc.readRepoStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyIssueToken), tc.publicOnlyIssueStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyRepoToken), tc.publicOnlyRepoStatus)
})
}
}
+138
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
@@ -14,6 +15,14 @@ import (
"github.com/stretchr/testify/assert"
)
type downloadScopeCase struct {
name string
method string
url string
withScope int
publicOnlyOK bool
}
func TestDownloadRepoContent(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@@ -71,3 +80,132 @@ func TestDownloadRepoContent(t *testing.T) {
assert.Equal(t, "application/xml", resp.Header().Get("Content-Type"))
})
}
func TestDownloadRepoContentTokenScopes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ownerReadToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
publicOnlyToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
cases := []downloadScopeCase{
{
name: "PublicArchiveDownload",
method: http.MethodGet,
url: "/user2/repo1/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PrivateArchiveDownload",
method: http.MethodGet,
url: "/user2/repo2/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PublicArchiveInitiate",
method: http.MethodPost,
url: "/user2/repo1/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PrivateArchiveInitiate",
method: http.MethodPost,
url: "/user2/repo2/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PublicRawBlob",
method: http.MethodGet,
url: "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicRawBranch",
method: http.MethodGet,
url: "/user2/repo1/raw/branch/master/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicRawTag",
method: http.MethodGet,
url: "/user2/repo1/raw/tag/v1.1/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicRawCommit",
method: http.MethodGet,
url: "/user2/repo1/raw/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaBlob",
method: http.MethodGet,
url: "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaBranch",
method: http.MethodGet,
url: "/user2/repo1/media/branch/master/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaTag",
method: http.MethodGet,
url: "/user2/repo1/media/tag/v1.1/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaCommit",
method: http.MethodGet,
url: "/user2/repo1/media/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PrivateRawBranch",
method: http.MethodGet,
url: "/user2/repo2/raw/branch/master/test.xml",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PrivateRawBlob",
method: http.MethodGet,
url: "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PrivateMediaBranch",
method: http.MethodGet,
url: "/user2/repo2/media/branch/master/test.xml",
withScope: http.StatusOK,
publicOnlyOK: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(ownerReadToken), tc.withScope)
publicOnlyStatus := http.StatusForbidden
if tc.publicOnlyOK {
publicOnlyStatus = tc.withScope
}
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(publicOnlyToken), publicOnlyStatus)
})
}
}
+130 -1
View File
@@ -5,6 +5,7 @@ package integration
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
@@ -23,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/oauth2_provider"
@@ -35,6 +37,51 @@ import (
"github.com/stretchr/testify/require"
)
func createOAuthTestApplication(t *testing.T, userName, name string, redirectURIs []string) *api.OAuth2Application {
t.Helper()
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
Name: name,
RedirectURIs: redirectURIs,
ConfidentialClient: true,
}).AddBasicAuth(userName)
resp := MakeRequest(t, req, http.StatusCreated)
created := DecodeJSON(t, resp, &api.OAuth2Application{})
require.NotEmpty(t, created.ClientID)
require.NotEmpty(t, created.ClientSecret)
return created
}
func issueOAuthAuthorizationCode(t *testing.T, user *user_model.User, app *api.OAuth2Application, redirectURI, scope string) (string, string) {
t.Helper()
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: scope,
}
require.NoError(t, db.Insert(t.Context(), grant))
r1, err := util.CryptoRandomBytes(12)
require.NoError(t, err)
verifier := "phase3-verifier-" + base64.RawURLEncoding.EncodeToString(r1)
challengeBytes := sha256.Sum256([]byte(verifier))
r2, err := util.CryptoRandomBytes(10)
require.NoError(t, err)
code := "phase3-code-" + base64.RawURLEncoding.EncodeToString(r2)
require.NoError(t, db.Insert(t.Context(), &auth_model.OAuth2AuthorizationCode{
GrantID: grant.ID,
Code: code,
CodeChallenge: base64.RawURLEncoding.EncodeToString(challengeBytes[:]),
CodeChallengeMethod: "S256",
RedirectURI: redirectURI,
ValidUntil: timeutil.TimeStampNow() + 86400,
}))
return code, verifier
}
func TestOAuth2Provider(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@@ -44,6 +91,9 @@ func TestOAuth2Provider(t *testing.T) {
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("AccessTokenExchangeRedirectURIMismatch", testAccessTokenExchangeRedirectURIMismatch)
t.Run("RefreshTokenCrossClientUsage", testRefreshTokenCrossClientUsage)
t.Run("OAuth2WellKnown", testOAuth2WellKnown)
t.Run("OAuthSourceSpecialChars", testOAuthSourceSpecialChars)
// TODO: move more tests as sub-tests here, avoid unnecessary PrepareTestEnv
@@ -184,12 +234,43 @@ func TestAccessTokenExchange(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10)
}
func testAccessTokenExchangeRedirectURIMismatch(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
redirectURIs := []string{"https://phase3.example/callback", "https://phase3.example/callback-alt"}
app := createOAuthTestApplication(t, user.Name, "phase3-redirect-uri-guard", redirectURIs)
code, verifier := issueOAuthAuthorizationCode(t, user, app, redirectURIs[0], "openid profile")
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": redirectURIs[1],
"code": code,
"code_verifier": verifier,
})
resp := MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_grant", string(parsedError.ErrorCode))
assert.Equal(t, "redirect_uri differs from the original authorization request", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": redirectURIs[0],
"code": code,
"code_verifier": verifier,
})
MakeRequest(t, req, http.StatusOK)
}
func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
"redirect_uri": "http://127.0.0.1",
"redirect_uri": "http://127.0.0.1/",
"code": "authcodepublic",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -484,6 +565,54 @@ func TestRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
}
func testRefreshTokenCrossClientUsage(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
primaryApp := createOAuthTestApplication(t, user.Name, "phase3-refresh-token-primary", []string{"https://phase3.example/refresh-primary"})
secondaryApp := createOAuthTestApplication(t, user.Name, "refresh-token-client-guard", []string{"https://alt-client.example/oauth/callback"})
code, verifier := issueOAuthAuthorizationCode(t, user, primaryApp, primaryApp.RedirectURIs[0], "openid profile")
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": primaryApp.ClientID,
"client_secret": primaryApp.ClientSecret,
"redirect_uri": primaryApp.RedirectURIs[0],
"code": code,
"code_verifier": verifier,
})
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
assert.NotEmpty(t, parsed.RefreshToken)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": secondaryApp.ClientID,
"client_secret": secondaryApp.ClientSecret,
"redirect_uri": secondaryApp.RedirectURIs[0],
"refresh_token": parsed.RefreshToken,
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_grant", string(parsedError.ErrorCode))
assert.Equal(t, "refresh token belongs to a different client", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": primaryApp.ClientID,
"client_secret": primaryApp.ClientSecret,
"redirect_uri": primaryApp.RedirectURIs[0],
"refresh_token": parsed.RefreshToken,
})
MakeRequest(t, req, http.StatusOK)
}
func TestOAuthIntrospection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+64 -72
View File
@@ -13,52 +13,17 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func assertFileExist(t *testing.T, p string) {
exist, err := util.IsExist(p)
assert.NoError(t, err)
assert.True(t, exist)
}
func assertFileEqual(t *testing.T, p string, content []byte) {
bs, err := os.ReadFile(p)
assert.NoError(t, err)
assert.Equal(t, content, bs)
}
func TestRepoCloneWiki(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer tests.PrepareTestEnv(t)()
dstPath := t.TempDir()
r := u.String() + "user2/repo1.wiki.git"
u, _ = url.Parse(r)
u.User = url.UserPassword("user2", userPassword)
t.Run("Clone", func(t *testing.T) {
assert.NoError(t, git.Clone(t.Context(), u.String(), dstPath, git.CloneRepoOptions{}))
assertFileEqual(t, filepath.Join(dstPath, "Home.md"), []byte("# Home page\n\nThis is the home page!\n"))
assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md"))
assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md"))
assertFileExist(t, filepath.Join(dstPath, "images"))
assertFileExist(t, filepath.Join(dstPath, "files/Non-Renderable-File.zip"))
assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg"))
})
})
}
func Test_RepoWikiPages(t *testing.T) {
func TestRepoWikiPages(t *testing.T) {
defer tests.PrepareTestEnv(t)()
url := "/user2/repo1/wiki/?action=_pages"
req := NewRequest(t, "GET", url)
req := NewRequest(t, "GET", "/user2/repo1/wiki/?action=_pages")
resp := MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
@@ -74,45 +39,72 @@ func Test_RepoWikiPages(t *testing.T) {
})
}
func Test_WikiClone(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
username := "user2"
reponame := "repo1"
wikiPath := username + "/" + reponame + ".wiki.git"
keyname := "my-testing-key"
baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
func testRepoWikiCloneHTTP(t *testing.T, u *url.URL) {
// When proc-receive support is enabled globally, the HTTP receive-pack pre-check
// must still require write access for wiki repositories. Exercise this with a
// normal wiki push because the regression is about the pre-check, not agit refs.
require.True(t, git.DefaultFeatures().SupportProcReceive) // modern git should all support proc-receive
u.Path = wikiPath
wikiURL := *u
wikiURL.Path = "/user2/repo1.wiki.git"
t.Run("Clone HTTP", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
dstLocalPath := t.TempDir()
dstLocalPath := t.TempDir()
assert.NoError(t, git.Clone(t.Context(), u.String(), dstLocalPath, git.CloneRepoOptions{}))
content, err := os.ReadFile(filepath.Join(dstLocalPath, "Home.md"))
assert.NoError(t, err)
assert.Equal(t, "# Home page\n\nThis is the home page!\n", string(content))
})
// reader can clone
wikiURL.User = url.UserPassword("user20", userPassword)
require.NoError(t, git.Clone(t.Context(), wikiURL.String(), dstLocalPath, git.CloneRepoOptions{}))
_, _, runErr := gitcmd.NewCommand("fast-import").WithDir(dstLocalPath).WithStdinBytes([]byte(`commit refs/heads/master
committer unauthorized-user <user20@example.com> 1714310400 +0000
data <<EOM
dummy-message
EOM
from refs/heads/master^0
M 100644 inline Home.md
data <<EOF
changed-content
EOF
`)).RunStdString(t.Context())
require.NoError(t, runErr)
t.Run("Clone SSH", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
content, err := os.ReadFile(filepath.Join(dstLocalPath, "Home.md"))
assert.NoError(t, err)
assert.Equal(t, "# Home page\n\nThis is the home page!\n", string(content))
dstLocalPath := t.TempDir()
sshURL := createSSHUrl(wikiPath, u)
// reader can't push
_, _, runErr = gitcmd.NewCommand("push", "origin", "refs/heads/master").WithDir(dstLocalPath).RunStdString(t.Context())
assert.Contains(t, runErr.Error(), "remote: Repository not found\n")
req := NewRequest(t, "GET", "/user2/repo1/wiki/raw/Home.md")
resp := MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "This is the home page!")
withKeyFile(t, keyname, func(keyFile string) {
var keyID int64
t.Run("CreateUserKey", doAPICreateUserKey(baseAPITestContext, "test-key", keyFile, func(t *testing.T, key api.PublicKey) {
keyID = key.ID
}))
assert.NotZero(t, keyID)
// owner can push
wikiURL.User = url.UserPassword("user2", userPassword)
_, _, runErr = gitcmd.NewCommand("remote", "add", "origin-owner").AddDynamicArguments(wikiURL.String()).WithDir(dstLocalPath).RunStdString(t.Context())
require.NoError(t, runErr)
_, _, runErr = gitcmd.NewCommand("push", "origin-owner", "refs/heads/master").WithDir(dstLocalPath).RunStdString(t.Context())
assert.NoError(t, runErr)
req = NewRequest(t, "GET", "/user2/repo1/wiki/raw/Home.md")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "changed-content", strings.TrimSpace(resp.Body.String()))
}
// Setup clone folder
assert.NoError(t, git.Clone(t.Context(), sshURL.String(), dstLocalPath, git.CloneRepoOptions{}))
content, err := os.ReadFile(filepath.Join(dstLocalPath, "Home.md"))
assert.NoError(t, err)
assert.Equal(t, "# Home page\n\nThis is the home page!\n", string(content))
})
})
func testRepoWikiCloneSSH(t *testing.T, u *url.URL) {
dstLocalPath := t.TempDir()
baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
sshURL := createSSHUrl("/user2/repo1.wiki.git", u)
withKeyFile(t, "my-testing-key", func(keyFile string) {
t.Run("CreateUserKey", doAPICreateUserKey(baseAPITestContext, "test-key", keyFile))
assert.NoError(t, git.Clone(t.Context(), sshURL.String(), dstLocalPath, git.CloneRepoOptions{}))
content, err := os.ReadFile(filepath.Join(dstLocalPath, "Home.md"))
assert.NoError(t, err)
assert.Equal(t, "# Home page\n\nThis is the home page!\n", string(content))
})
}
func TestRepoWikiClonePush(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
t.Run("SSH", func(t *testing.T) { testRepoWikiCloneSSH(t, u) })
t.Run("HTTP", func(t *testing.T) { testRepoWikiCloneHTTP(t, u) })
})
}
+76
View File
@@ -0,0 +1,76 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 04.00.00
-->
<updates>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<element>mokogitea</element>
<type>application</type>
<version>04.01.00</version>
<client>server</client>
<tags><tag>stable</tag></tags>
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.04.00.00</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.00.00</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<element>mokogitea</element>
<type>application</type>
<version>04.01.00</version>
<client>server</client>
<tags><tag>dev</tag></tags>
<infourl title="MokoGitea Dev">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/dev</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:latest-dev</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea update</description>
<element>mokogitea</element>
<type>application</type>
<version>04.01.00</version>
<client>server</client>
<tags><tag>security</tag></tags>
<infourl title="MokoGitea Security">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/v1.26.1-moko.04.00.00</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.00.00</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea RC from PR #170</description>
<element>mokogitea</element>
<type>application</type>
<version>04.01.00-rc.170</version>
<client>server</client>
<tags><tag>rc</tag></tags>
<infourl title="MokoGitea RC">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/pulls/170</infourl>
<downloads>
<downloadurl type="full" format="docker">git.mokoconsulting.tech/mokoconsulting/mokogitea:v1.26.1-moko.04.01.00-rc.170</downloadurl>
</downloads>
<sha256></sha256>
<targetplatform name="mokogitea" version="((1\.25\.)|(1\.26\.))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
</updates>