Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c233878484 | |||
| 39150c6968 | |||
| 417bda1735 | |||
| f09aadf60c | |||
| fa54fe1ffc | |||
| 49fb7bb9a4 | |||
| 5dd98c04d8 | |||
| ea3957597c | |||
| 45e08616ac | |||
| 4dafcc5429 | |||
| 3159d53322 | |||
| fff64e6e7c | |||
| 1d4340a142 | |||
| 60670d066b | |||
| e8b2a485fc | |||
| cd496f159d | |||
| afbff02d81 | |||
| 8355b39ad4 | |||
| c2c9e053ff | |||
| 451b3022bd | |||
| 2153d7c916 | |||
| 63c801d595 | |||
| 0270be743f | |||
| dbf70a7def | |||
| 43b5a54ffa | |||
| b182855fc5 | |||
| ca6c8c958c | |||
| e9efbbc93b | |||
| 2482a3726e | |||
| 2f381dc16c | |||
| d2cdd9b1d6 | |||
| 6e0236d433 | |||
| ce83900967 | |||
| ee6405f4fd | |||
| 738117248d | |||
| d2d652e5b7 | |||
| 0166a6d02a | |||
| 0f543903fb | |||
| ec02fb9cf8 | |||
| f639940608 | |||
| 482ad13ff1 | |||
| 7a06e44e24 |
@@ -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:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
create-branch:
|
||||
name: Create feature branch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create branch and comment
|
||||
run: |
|
||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||
|
||||
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||
|
||||
# Check dev branch exists
|
||||
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/branches/dev" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "${DEV_EXISTS}" != "200" ]; then
|
||||
echo "No dev branch -- skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create branch from dev
|
||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/branches" \
|
||||
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "${HTTP}" = "201" ]; then
|
||||
echo "Created branch: ${BRANCH}"
|
||||
|
||||
# Comment on issue with branch link
|
||||
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE_NUM}/comments" \
|
||||
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
||||
|
||||
echo "Commented on issue #${ISSUE_NUM}"
|
||||
else
|
||||
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
||||
fi
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,7 +40,7 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content
|
||||
if !email.IsActivated {
|
||||
continue
|
||||
}
|
||||
if content == email.Email {
|
||||
if strings.EqualFold(content, email.LowerEmail) {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
|
||||
+66
-38
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
const IncomingEmailTokenPlaceholder = "%{token}"
|
||||
|
||||
var IncomingEmail = struct {
|
||||
Enabled bool
|
||||
ReplyToAddress string
|
||||
TokenPlaceholder string `ini:"-"`
|
||||
Host string
|
||||
Port int
|
||||
UseTLS bool `ini:"USE_TLS"`
|
||||
@@ -28,7 +29,6 @@ var IncomingEmail = struct {
|
||||
}{
|
||||
Mailbox: "INBOX",
|
||||
DeleteHandledMessage: true,
|
||||
TokenPlaceholder: "%{token}",
|
||||
MaximumMessageSize: 10485760,
|
||||
}
|
||||
|
||||
@@ -54,19 +54,10 @@ func checkReplyToAddress() error {
|
||||
return errors.New("name must not be set")
|
||||
}
|
||||
|
||||
c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
|
||||
switch c {
|
||||
case 0:
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
||||
case 1:
|
||||
default:
|
||||
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
|
||||
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
|
||||
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
|
||||
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
|
||||
}
|
||||
|
||||
parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
|
||||
if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+532
-116
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -132,14 +132,22 @@ func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleS
|
||||
}
|
||||
|
||||
func withScheduleInEventPayload(eventPayload, schedule string) string {
|
||||
if schedule == "" || eventPayload == "" {
|
||||
if schedule == "" {
|
||||
return eventPayload
|
||||
}
|
||||
|
||||
event := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
|
||||
log.Error("withScheduleInEventPayload: unmarshal: %v", err)
|
||||
return eventPayload
|
||||
// eventPayload originates from json.Marshal(input.Payload) in handleSchedules,
|
||||
// so a nil payload is stored as the literal "null" and pre-existing rows may be
|
||||
// empty. Both cases start from a fresh map so the schedule field can still be set.
|
||||
var event map[string]any
|
||||
if eventPayload != "" {
|
||||
if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
|
||||
log.Error("withScheduleInEventPayload: unmarshal: %v", err)
|
||||
return eventPayload
|
||||
}
|
||||
}
|
||||
if event == nil {
|
||||
event = map[string]any{}
|
||||
}
|
||||
|
||||
event["schedule"] = schedule
|
||||
|
||||
@@ -22,9 +22,20 @@ func TestWithScheduleInEventPayload(t *testing.T) {
|
||||
assert.Equal(t, "refs/heads/main", event["ref"])
|
||||
})
|
||||
|
||||
t.Run("keeps empty payload", func(t *testing.T) {
|
||||
t.Run("adds schedule to null payload", func(t *testing.T) {
|
||||
updated := withScheduleInEventPayload("null", "37 12 5 1 2")
|
||||
|
||||
event := map[string]any{}
|
||||
assert.NoError(t, json.Unmarshal([]byte(updated), &event))
|
||||
assert.Equal(t, "37 12 5 1 2", event["schedule"])
|
||||
})
|
||||
|
||||
t.Run("adds schedule to empty payload", func(t *testing.T) {
|
||||
updated := withScheduleInEventPayload("", "37 12 5 1 2")
|
||||
assert.Empty(t, updated)
|
||||
|
||||
event := map[string]any{}
|
||||
assert.NoError(t, json.Unmarshal([]byte(updated), &event))
|
||||
assert.Equal(t, "37 12 5 1 2", event["schedule"])
|
||||
})
|
||||
|
||||
t.Run("keeps payload when schedule empty", func(t *testing.T) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
net_mail "net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,31 +23,10 @@ import (
|
||||
"github.com/jhillyerd/enmime/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
addressTokenRegex *regexp.Regexp
|
||||
referenceTokenRegex *regexp.Regexp
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
if !setting.IncomingEmail.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
addressTokenRegex, err = regexp.Compile(
|
||||
fmt.Sprintf(
|
||||
`\A%s\z`,
|
||||
strings.Replace(regexp.QuoteMeta(setting.IncomingEmail.ReplyToAddress), regexp.QuoteMeta(setting.IncomingEmail.TokenPlaceholder), "(.+)", 1),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
referenceTokenRegex, err = regexp.Compile(fmt.Sprintf(`\Areply-(.+)@%s\z`, regexp.QuoteMeta(setting.Domain)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true)
|
||||
defer finished()
|
||||
@@ -241,7 +219,7 @@ loop:
|
||||
return nil
|
||||
}
|
||||
|
||||
handlerType, user, payload, err := token.ExtractToken(ctx, t)
|
||||
handlerType, user, payload, err := token.DecodeToken(ctx, t)
|
||||
if err != nil {
|
||||
if _, ok := err.(*token.ErrToken); ok {
|
||||
log.Info("Invalid incoming email token: %v", err)
|
||||
@@ -292,22 +270,31 @@ func isAutomaticReply(env *enmime.Envelope) bool {
|
||||
return autoRespond != ""
|
||||
}
|
||||
|
||||
func extractToken(s, tokenPrefix, tokenSuffix string) string {
|
||||
if len(s) <= len(tokenPrefix)+len(tokenSuffix) {
|
||||
return ""
|
||||
}
|
||||
prefix, suffix := s[0:len(tokenPrefix)], s[len(s)-len(tokenSuffix):]
|
||||
if strings.EqualFold(prefix, tokenPrefix) && strings.EqualFold(suffix, tokenSuffix) {
|
||||
return s[len(tokenPrefix) : len(s)-len(tokenSuffix)]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// searchTokenInHeaders looks for the token in To, Delivered-To and References
|
||||
func searchTokenInHeaders(env *enmime.Envelope) string {
|
||||
if addressTokenRegex != nil {
|
||||
to, _ := env.AddressList("To")
|
||||
to, _ := env.AddressList("To")
|
||||
|
||||
token := searchTokenInAddresses(to)
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
token := searchTokenInAddresses(to)
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
|
||||
deliveredTo, _ := env.AddressList("Delivered-To")
|
||||
deliveredTo, _ := env.AddressList("Delivered-To")
|
||||
|
||||
token = searchTokenInAddresses(deliveredTo)
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
token = searchTokenInAddresses(deliveredTo)
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
|
||||
references := env.GetHeader("References")
|
||||
@@ -322,10 +309,9 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
|
||||
if end == -1 || begin > end {
|
||||
break
|
||||
}
|
||||
|
||||
match := referenceTokenRegex.FindStringSubmatch(references[begin:end])
|
||||
if len(match) == 2 {
|
||||
return match[1]
|
||||
t := extractToken(references[begin:end], "reply-", "@"+setting.Domain)
|
||||
if t != "" {
|
||||
return t
|
||||
}
|
||||
|
||||
references = references[end+1:]
|
||||
@@ -336,15 +322,15 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
|
||||
|
||||
// searchTokenInAddresses looks for the token in an address
|
||||
func searchTokenInAddresses(addresses []*net_mail.Address) string {
|
||||
for _, address := range addresses {
|
||||
match := addressTokenRegex.FindStringSubmatch(address.Address)
|
||||
if len(match) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
return match[1]
|
||||
tokenPrefix, tokenSuffix, _ := strings.Cut(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder)
|
||||
if tokenSuffix == "" {
|
||||
return ""
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if t := extractToken(address.Address, tokenPrefix, tokenSuffix); t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/jhillyerd/enmime/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -68,6 +70,18 @@ func TestIsAutomaticReply(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchTokenInHeadersCaseInsensitive(t *testing.T) {
|
||||
setting.IncomingEmail.ReplyToAddress = "InComing+%{token}@ExAmPle.com"
|
||||
setting.Domain = "DoMain.com"
|
||||
mkEnv := func(s string) *enmime.Envelope {
|
||||
env, _ := enmime.ReadEnvelope(strings.NewReader(s + "\r\n\r\n"))
|
||||
return env
|
||||
}
|
||||
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("To: incoming+abc@EXAMPLE.COM")))
|
||||
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("Delivered-To: INCOMING+abc@example.com")))
|
||||
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("References: <ReplY-abc@DomaiN.COM>")))
|
||||
}
|
||||
|
||||
func TestGetContentFromMailReader(t *testing.T) {
|
||||
mailString := "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
|
||||
"\r\n" +
|
||||
|
||||
@@ -182,7 +182,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
||||
if err != nil {
|
||||
log.Error("CreateToken failed: %v", err)
|
||||
} else {
|
||||
replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
||||
replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1)
|
||||
msg.ReplyTo = replyAddress
|
||||
msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress))
|
||||
|
||||
@@ -194,7 +194,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
||||
if err != nil {
|
||||
log.Error("CreateToken failed: %v", err)
|
||||
} else {
|
||||
unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
||||
unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1)
|
||||
listUnsubscribe = append(listUnsubscribe, "<mailto:"+unsubAddress+">")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -73,9 +74,11 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
|
||||
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
|
||||
}
|
||||
|
||||
// ExtractToken extracts the action/user tuple from the token and verifies the content
|
||||
func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
||||
data, err := encodingWithoutPadding.DecodeString(token)
|
||||
// DecodeToken decodes the handler, user and payload from the token and verifies the content
|
||||
func DecodeToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
||||
// MTAs are permitted to alter the case of the local-part (RFC 5321 §2.4), so normalize
|
||||
// to the base32 alphabet before decoding to survive a lowercased reply-to address.
|
||||
data, err := encodingWithoutPadding.DecodeString(strings.ToUpper(token))
|
||||
if err != nil {
|
||||
return UnknownHandlerType, nil, nil, err
|
||||
}
|
||||
@@ -118,11 +121,11 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
|
||||
return handlerType, user, innerPayload, nil
|
||||
}
|
||||
|
||||
// generateHmac creates a trunkated HMAC for the given payload
|
||||
// generateHmac creates a truncated HMAC for the given payload
|
||||
func generateHmac(secret, payload []byte) []byte {
|
||||
mac := crypto_hmac.New(sha256.New, secret)
|
||||
mac.Write(payload)
|
||||
hmac := mac.Sum(nil)
|
||||
|
||||
return hmac[:10] // RFC2104 recommends not using less then 80 bits
|
||||
return hmac[:10] // RFC2104 recommends not using less than 80 bits
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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")}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,14 @@ func TestIncomingEmail(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
ht, u, p, err := token_service.ExtractToken(t.Context(), token)
|
||||
ht, u, p, err := token_service.DecodeToken(t.Context(), token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, token_service.ReplyHandlerType, ht)
|
||||
assert.Equal(t, user.ID, u.ID)
|
||||
assert.Equal(t, payload, p)
|
||||
|
||||
// MTAs may lowercase the local-part of the reply-to address (RFC 5321 §2.4).
|
||||
ht, u, p, err = token_service.DecodeToken(t.Context(), strings.ToLower(token))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, token_service.ReplyHandlerType, ht)
|
||||
assert.Equal(t, user.ID, u.ID)
|
||||
@@ -189,7 +196,7 @@ func TestIncomingEmail(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg := sender_service.NewMessageFrom(
|
||||
strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1),
|
||||
strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1),
|
||||
"",
|
||||
user.Email,
|
||||
"",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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>
|
||||
Reference in New Issue
Block a user