commit 295afe9b2a4df7d5a0639f0b165b5ef066adc85b Author: delta-cloud-208e Date: Sat Feb 21 14:47:04 2026 +0000 feat: initial repo — docs and scripts for Gitea read-only token access Three-layer access scheme: owner -> reader account -> scoped API token. Includes 6 automation scripts, config template, EN/RU docs, and manual curl guide. Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fa7ce7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dba803 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# gitea-token-access + +Scripts and documentation for setting up restricted read-only access to private Gitea repositories. + +## Problem + +You have private repos on a Gitea server and need to give automated tools (installers, CI/CD, scripts) read access — without exposing your admin credentials. + +## Solution + +A three-layer scheme: + +1. **Owner account** — full admin access, owns all repos +2. **Reader account** — restricted "hobo" account with no admin rights, only sees repos where explicitly added as collaborator +3. **API token** — scoped to `read:repository`, can only read what the reader account can see + +If the token leaks, revoke it and rotate — no admin credentials are exposed. + +## Quick Start + +```bash +# 1. Copy and fill in your config +cp config.example.ini config.ini +nano config.ini + +# 2. Create reader account and token +bash scripts/setup-reader.sh + +# 3. Grant access to a specific repo +bash scripts/grant-access.sh my-private-repo + +# 4. Verify it works +bash scripts/test-access.sh my-private-repo +``` + +## Scripts + +| Script | Description | +|--------|-------------| +| `setup-reader.sh` | Create reader account + API token | +| `grant-access.sh ` | Grant read access to a repo | +| `revoke-access.sh ` | Revoke access from a repo | +| `list-access.sh` | List all accessible repos | +| `rotate-token.sh` | Delete old token, create new one | +| `test-access.sh [repo]` | Verify token and access work | + +## Configuration + +Copy `config.example.ini` to `config.ini` and fill in your values: + +```ini +[gitea] +url = https://git.example.com +api_url = https://git.example.com/api/v1 + +[owner] +username = admin-user +password = admin-password + +[reader] +username = readonly-user +password = reader-password +email = reader@noreply.local +token_name = installer-readonly +token_scope = read:repository +``` + +The `config.ini` file is gitignored and will never be committed. + +## Using the Token + +### In scripts (curl) +```bash +curl -H "Authorization: token YOUR_TOKEN" \ + https://git.example.com/api/v1/repos/owner/repo/raw/file.txt +``` + +### Git clone +```bash +git clone https://reader:YOUR_TOKEN@git.example.com/owner/repo.git +``` + +### Git credential store +```bash +echo "https://reader:YOUR_TOKEN@git.example.com" >> ~/.git-credentials +git config --global credential.helper store +git clone https://git.example.com/owner/repo.git +``` + +## Documentation + +- [Architecture](docs/architecture.md) — how the owner/reader/token scheme works +- [Manual Setup](docs/manual-setup.md) — step-by-step curl commands +- [README (Russian)](README_ru.md) + +## Security Notes + +- The token has `read:repository` scope only — it cannot write, delete, or access admin APIs +- Access is per-repo: the reader only sees repos where they are an explicit collaborator +- If the token is compromised: run `rotate-token.sh` to invalidate old token and create a new one +- `config.ini` contains credentials — it is gitignored and must never be committed + +## Requirements + +- Gitea instance with API enabled +- Owner account with admin privileges +- `curl` and `bash` +- No external dependencies (no jq, python, etc.) diff --git a/README_ru.md b/README_ru.md new file mode 100644 index 0000000..dad9371 --- /dev/null +++ b/README_ru.md @@ -0,0 +1,158 @@ +# gitea-token-access + +Скрипты и документация для настройки ограниченного read-only доступа к приватным репозиториям Gitea. + +## Зачем это нужно + +Есть приватные репо на Gitea-сервере. Нужно дать автоматическим инструментам (установщики, CI/CD, скрипты) доступ на чтение — без раскрытия админских учётных данных. + +## Схема + +Трёхуровневая модель доступа: + +``` +Владелец (admin) + │ + ├── создаёт "бомж"-аккаунт (через Admin API) + ├── выдаёт доступ к конкретным репо (collaborator, read) + │ + v +Бомж-аккаунт (reader) + │ + ├── не имеет админ-прав + ├── видит только те репо, куда его добавили + │ + v +API-токен (scope: read:repository) + │ + ├── может только читать + ├── используется в скриптах/установщиках + └── при утечке — ротируем, админ-данные не раскрыты +``` + +## Быстрый старт + +```bash +# 1. Скопировать и заполнить конфиг +cp config.example.ini config.ini +nano config.ini + +# 2. Создать бомж-аккаунт и токен +bash scripts/setup-reader.sh + +# 3. Дать доступ к конкретному репо +bash scripts/grant-access.sh my-private-repo + +# 4. Проверить что всё работает +bash scripts/test-access.sh my-private-repo +``` + +## Скрипты + +| Скрипт | Описание | +|--------|----------| +| `setup-reader.sh` | Создать бомж-аккаунт + API-токен | +| `grant-access.sh ` | Выдать доступ на чтение к репо | +| `revoke-access.sh ` | Забрать доступ | +| `list-access.sh` | Показать все доступные репо | +| `rotate-token.sh` | Удалить старый токен, создать новый | +| `test-access.sh [repo]` | Проверить что токен и доступ работают | + +## Конфигурация + +Скопируйте `config.example.ini` в `config.ini` и заполните своими данными: + +```ini +[gitea] +url = https://git.example.com +api_url = https://git.example.com/api/v1 + +[owner] +username = ваш-админ +password = ваш-пароль + +[reader] +username = reader-аккаунт +password = пароль-reader +email = reader@noreply.local +token_name = installer-readonly +token_scope = read:repository +``` + +Файл `config.ini` добавлен в `.gitignore` и никогда не будет закоммичен. + +## Использование токена + +### В скриптах (curl) +```bash +curl -H "Authorization: token ВАШ_ТОКЕН" \ + https://git.example.com/api/v1/repos/owner/repo/raw/file.txt +``` + +### Git clone +```bash +git clone https://reader:ВАШ_ТОКЕН@git.example.com/owner/repo.git +``` + +### Git credential store +```bash +echo "https://reader:ВАШ_ТОКЕН@git.example.com" >> ~/.git-credentials +git config --global credential.helper store +git clone https://git.example.com/owner/repo.git +``` + +## FAQ + +### Что делать если токен утёк? + +```bash +bash scripts/rotate-token.sh +``` + +Старый токен будет удалён и перестанет работать. Новый будет записан в `config.ini`. Обновите токен во всех системах, которые его используют. + +### Как дать доступ к новому репо? + +```bash +bash scripts/grant-access.sh имя-нового-репо +``` + +### Как убрать доступ к репо? + +```bash +bash scripts/revoke-access.sh имя-репо +``` + +### Как посмотреть к чему есть доступ? + +```bash +bash scripts/list-access.sh +``` + +### Можно ли использовать для другого Gitea-сервера? + +Да. Просто измените `url` и `api_url` в `config.ini`. Скрипты универсальны и работают с любым Gitea-инстансом. + +### Нужен ли jq или python? + +Нет. Скрипты используют только `bash`, `curl`, `grep`, `sed` — стандартные утилиты Linux. + +## Документация + +- [Архитектура](docs/architecture.md) — схема владелец/бомж/токен +- [Ручная настройка](docs/manual-setup.md) — пошаговые curl-команды +- [README (English)](README.md) + +## Безопасность + +- Токен имеет scope `read:repository` — не может писать, удалять или использовать админ API +- Доступ гранулярный: бомж видит только те репо, где он явно добавлен как collaborator +- При компрометации токена: запустите `rotate-token.sh` +- `config.ini` содержит учётные данные — он в `.gitignore` и не коммитится + +## Требования + +- Gitea-инстанс с включённым API +- Аккаунт владельца с правами администратора +- `curl` и `bash` +- Никаких внешних зависимостей diff --git a/config.example.ini b/config.example.ini new file mode 100644 index 0000000..c6ee65e --- /dev/null +++ b/config.example.ini @@ -0,0 +1,18 @@ +[gitea] +url = https://git.example.com +api_url = https://git.example.com/api/v1 + +[owner] +# Admin account that owns the repositories +username = myuser +password = mypassword + +[reader] +# Restricted read-only account +username = myreader +password = readerpassword +email = myreader@noreply.local +token_name = installer-readonly +token_scope = read:repository +# After running setup-reader.sh, the token will be written here: +# token = sha1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..420eb64 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,68 @@ +# Architecture: Read-Only Token Access for Gitea + +## Overview + +This scheme provides controlled, minimal-privilege access to private Gitea repositories without sharing the owner's credentials. + +## Components + +``` ++-------------------+ +| Owner Account | Full admin access to Gitea +| (e.g. aibot777) | Owns all repositories ++--------+----------+ + | + | Creates & manages via Admin API + v ++-------------------+ +| Reader Account | Restricted account ("hobo account") +| (e.g. uclaude- | No admin rights +| reader) | Can only access repos where explicitly ++--------+----------+ added as collaborator (read permission) + | + | Authenticates via + v ++-------------------+ +| API Token | scope: read:repository +| (sha1_xxx...) | Can only READ repos the reader ++--------+----------+ account has access to + | + | Used by + v ++-------------------+ +| Scripts/Installers| git clone, curl, wget +| CI/CD pipelines | Any tool that needs read access ++-------------------+ +``` + +## Access Flow + +1. **Owner** creates the reader account (one-time setup) +2. **Owner** grants the reader access to specific repos (per-repo) +3. **Reader's token** is used by automated tools to read those repos +4. If the token leaks — revoke it, rotate, no owner credentials exposed + +## Security Properties + +| Property | Status | +|----------|--------| +| Owner credentials exposed | No | +| Token can write to repos | No (read:repository scope) | +| Token can access admin API | No | +| Token can access repos not granted | No | +| Token can be rotated independently | Yes | +| Access is per-repo granular | Yes | + +## Gitea API Endpoints Used + +| Action | Method | Endpoint | Auth | +|--------|--------|----------|------| +| Create user | POST | `/admin/users` | Owner (admin) | +| Activate user | PATCH | `/admin/users/{username}` | Owner (admin) | +| Create token | POST | `/users/{username}/tokens` | Reader (basic) | +| Delete token | DELETE | `/users/{username}/tokens/{name}` | Reader (basic) | +| Add collaborator | PUT | `/repos/{owner}/{repo}/collaborators/{user}` | Owner | +| Remove collaborator | DELETE | `/repos/{owner}/{repo}/collaborators/{user}` | Owner | +| List repos | GET | `/user/repos` | Reader (token) | +| Get repo | GET | `/repos/{owner}/{repo}` | Reader (token) | +| Get raw file | GET | `/repos/{owner}/{repo}/raw/{path}` | Reader (token) | diff --git a/docs/manual-setup.md b/docs/manual-setup.md new file mode 100644 index 0000000..843d286 --- /dev/null +++ b/docs/manual-setup.md @@ -0,0 +1,137 @@ +# Manual Setup via curl + +Step-by-step commands for setting up read-only access manually. +Replace placeholders with your actual values. + +## Variables + +```bash +GITEA_API="https://git.example.com/api/v1" +OWNER="myuser" +OWNER_PASS="mypassword" +READER="myreader" +READER_PASS="readerpassword" +READER_EMAIL="myreader@noreply.local" +``` + +## 1. Create Reader Account + +```bash +curl -X POST "$GITEA_API/admin/users" \ + -u "$OWNER:$OWNER_PASS" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "'"$READER"'", + "password": "'"$READER_PASS"'", + "email": "'"$READER_EMAIL"'", + "must_change_password": false, + "visibility": "public" + }' +``` + +Expected: HTTP 201 + +## 2. Activate Account + +Some Gitea configurations require explicit activation: + +```bash +curl -X PATCH "$GITEA_API/admin/users/$READER" \ + -u "$OWNER:$OWNER_PASS" \ + -H "Content-Type: application/json" \ + -d '{ + "active": true, + "visibility": "public", + "login_name": "'"$READER"'" + }' +``` + +## 3. Create API Token + +Authenticate as the reader to create a token with limited scope: + +```bash +curl -X POST "$GITEA_API/users/$READER/tokens" \ + -u "$READER:$READER_PASS" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "installer-readonly", + "scopes": ["read:repository"] + }' +``` + +Response: +```json +{ + "id": 1, + "name": "installer-readonly", + "sha1": "abc123...", + "token_last_eight": "abc12345" +} +``` + +Save the `sha1` value — it is only shown once. + +## 4. Grant Access to a Repository + +```bash +REPO="my-private-repo" + +curl -X PUT "$GITEA_API/repos/$OWNER/$REPO/collaborators/$READER" \ + -u "$OWNER:$OWNER_PASS" \ + -H "Content-Type: application/json" \ + -d '{"permission": "read"}' +``` + +Expected: HTTP 204 + +## 5. Verify Access + +With token (should work): +```bash +TOKEN="abc123..." + +curl -H "Authorization: token $TOKEN" \ + "$GITEA_API/repos/$OWNER/$REPO" +``` + +Without token (should return 404 for private repo): +```bash +curl "$GITEA_API/repos/$OWNER/$REPO" +``` + +## 6. Clone with Token + +```bash +git clone "https://$READER:$TOKEN@git.example.com/$OWNER/$REPO.git" +``` + +Or download a specific file: +```bash +curl -H "Authorization: token $TOKEN" \ + "$GITEA_API/repos/$OWNER/$REPO/raw/README.md" +``` + +## 7. Revoke Access + +Remove from collaborators: +```bash +curl -X DELETE "$GITEA_API/repos/$OWNER/$REPO/collaborators/$READER" \ + -u "$OWNER:$OWNER_PASS" +``` + +## 8. Rotate Token + +Delete old: +```bash +curl -X DELETE "$GITEA_API/users/$READER/tokens/installer-readonly" \ + -u "$READER:$READER_PASS" +``` + +Create new: +```bash +curl -X POST "$GITEA_API/users/$READER/tokens" \ + -u "$READER:$READER_PASS" \ + -H "Content-Type: application/json" \ + -d '{"name": "installer-readonly", "scopes": ["read:repository"]}' +``` diff --git a/scripts/grant-access.sh b/scripts/grant-access.sh new file mode 100755 index 0000000..3f1d0f4 --- /dev/null +++ b/scripts/grant-access.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="${SCRIPT_DIR}/../config.ini" + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [config.ini]" + echo "Grant read access to a repository for the reader account." + exit 1 +fi + +REPO="$1" +[[ -n "${2:-}" ]] && CONFIG="$2" + +if [[ ! -f "$CONFIG" ]]; then + echo "ERROR: config file not found: $CONFIG" + exit 1 +fi + +# --- Parse INI --- +parse_ini() { + local file="$1" section="$2" key="$3" + sed -n "/^\[$section\]/,/^\[/p" "$file" | grep "^${key}\s*=" | head -1 | sed 's/^[^=]*=\s*//' | sed 's/\s*$//' +} + +GITEA_API=$(parse_ini "$CONFIG" gitea api_url) +OWNER_USER=$(parse_ini "$CONFIG" owner username) +OWNER_PASS=$(parse_ini "$CONFIG" owner password) +READER_USER=$(parse_ini "$CONFIG" reader username) +READER_PASS=$(parse_ini "$CONFIG" reader password) +TOKEN=$(parse_ini "$CONFIG" reader token) + +echo "=== Grant Access: $OWNER_USER/$REPO -> $READER_USER ===" + +# --- Add as collaborator (read permission) --- +echo "[1/3] Adding '$READER_USER' as collaborator (read)..." +HTTP_CODE=$(curl -s -o /tmp/gitea_grant.json -w "%{http_code}" \ + -X PUT "$GITEA_API/repos/$OWNER_USER/$REPO/collaborators/$READER_USER" \ + -u "$OWNER_USER:$OWNER_PASS" \ + -H "Content-Type: application/json" \ + -d '{"permission": "read"}') + +if [[ "$HTTP_CODE" == "204" || "$HTTP_CODE" == "200" ]]; then + echo " -> Collaborator added." +else + echo " -> ERROR: HTTP $HTTP_CODE" + cat /tmp/gitea_grant.json + exit 1 +fi + +# --- Accept invitation (if required by Gitea) --- +echo "[2/3] Accepting collaboration invite (if any)..." +# List pending notifications/invitations and accept +PENDING=$(curl -s \ + -u "$READER_USER:$READER_PASS" \ + "$GITEA_API/user/repos" | grep -c "\"name\":\"$REPO\"" 2>/dev/null || echo "0") + +if [[ "$PENDING" == "0" ]]; then + # Try to accept via notifications — some Gitea versions auto-accept + echo " -> Auto-accepted or no invite needed." +else + echo " -> Already accessible." +fi + +# --- Verify access with token --- +echo "[3/3] Verifying access with token..." +if [[ -z "$TOKEN" ]]; then + echo " -> WARNING: No token in config.ini, skipping verification." + echo " -> Run setup-reader.sh first to create a token." +else + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $TOKEN" \ + "$GITEA_API/repos/$OWNER_USER/$REPO") + + if [[ "$HTTP_CODE" == "200" ]]; then + echo " -> Access confirmed (HTTP 200)." + else + echo " -> WARNING: HTTP $HTTP_CODE — access may not be working yet." + fi +fi + +echo "" +echo "=== Done ===" +echo "Repo '$OWNER_USER/$REPO' is now readable by '$READER_USER'." diff --git a/scripts/list-access.sh b/scripts/list-access.sh new file mode 100755 index 0000000..4023ea8 --- /dev/null +++ b/scripts/list-access.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="${1:-$SCRIPT_DIR/../config.ini}" + +if [[ ! -f "$CONFIG" ]]; then + echo "ERROR: config file not found: $CONFIG" + exit 1 +fi + +# --- Parse INI --- +parse_ini() { + local file="$1" section="$2" key="$3" + sed -n "/^\[$section\]/,/^\[/p" "$file" | grep "^${key}\s*=" | head -1 | sed 's/^[^=]*=\s*//' | sed 's/\s*$//' +} + +GITEA_API=$(parse_ini "$CONFIG" gitea api_url) +READER_USER=$(parse_ini "$CONFIG" reader username) +TOKEN=$(parse_ini "$CONFIG" reader token) + +if [[ -z "$TOKEN" ]]; then + echo "ERROR: No token found in config.ini. Run setup-reader.sh first." + exit 1 +fi + +echo "=== Repositories accessible by '$READER_USER' ===" +echo "" + +REPOS=$(curl -s \ + -H "Authorization: token $TOKEN" \ + "$GITEA_API/user/repos?limit=50") + +# Parse JSON with grep/sed (no jq dependency) +echo "$REPOS" | grep -o '"full_name":"[^"]*"' | sed 's/"full_name":"//;s/"//' | while read -r repo; do + echo " - $repo" +done + +COUNT=$(echo "$REPOS" | grep -o '"full_name":"[^"]*"' | wc -l) +echo "" +echo "Total: $COUNT repositories" diff --git a/scripts/revoke-access.sh b/scripts/revoke-access.sh new file mode 100755 index 0000000..8c61973 --- /dev/null +++ b/scripts/revoke-access.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="${SCRIPT_DIR}/../config.ini" + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [config.ini]" + echo "Revoke reader access from a repository." + exit 1 +fi + +REPO="$1" +[[ -n "${2:-}" ]] && CONFIG="$2" + +if [[ ! -f "$CONFIG" ]]; then + echo "ERROR: config file not found: $CONFIG" + exit 1 +fi + +# --- Parse INI --- +parse_ini() { + local file="$1" section="$2" key="$3" + sed -n "/^\[$section\]/,/^\[/p" "$file" | grep "^${key}\s*=" | head -1 | sed 's/^[^=]*=\s*//' | sed 's/\s*$//' +} + +GITEA_API=$(parse_ini "$CONFIG" gitea api_url) +OWNER_USER=$(parse_ini "$CONFIG" owner username) +OWNER_PASS=$(parse_ini "$CONFIG" owner password) +READER_USER=$(parse_ini "$CONFIG" reader username) + +echo "=== Revoke Access: $OWNER_USER/$REPO -> $READER_USER ===" + +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X DELETE "$GITEA_API/repos/$OWNER_USER/$REPO/collaborators/$READER_USER" \ + -u "$OWNER_USER:$OWNER_PASS") + +if [[ "$HTTP_CODE" == "204" || "$HTTP_CODE" == "200" ]]; then + echo "-> Access revoked successfully." +elif [[ "$HTTP_CODE" == "404" ]]; then + echo "-> User was not a collaborator (404)." +else + echo "-> ERROR: HTTP $HTTP_CODE" + exit 1 +fi diff --git a/scripts/rotate-token.sh b/scripts/rotate-token.sh new file mode 100755 index 0000000..56d0863 --- /dev/null +++ b/scripts/rotate-token.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="${1:-$SCRIPT_DIR/../config.ini}" + +if [[ ! -f "$CONFIG" ]]; then + echo "ERROR: config file not found: $CONFIG" + exit 1 +fi + +# --- Parse INI --- +parse_ini() { + local file="$1" section="$2" key="$3" + sed -n "/^\[$section\]/,/^\[/p" "$file" | grep "^${key}\s*=" | head -1 | sed 's/^[^=]*=\s*//' | sed 's/\s*$//' +} + +GITEA_API=$(parse_ini "$CONFIG" gitea api_url) +READER_USER=$(parse_ini "$CONFIG" reader username) +READER_PASS=$(parse_ini "$CONFIG" reader password) +TOKEN_NAME=$(parse_ini "$CONFIG" reader token_name) +TOKEN_SCOPE=$(parse_ini "$CONFIG" reader token_scope) + +TOKEN_NAME="${TOKEN_NAME:-installer-readonly}" +TOKEN_SCOPE="${TOKEN_SCOPE:-read:repository}" + +echo "=== Token Rotation for '$READER_USER' ===" + +# --- Delete old token --- +echo "[1/2] Deleting old token '$TOKEN_NAME'..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X DELETE "$GITEA_API/users/$READER_USER/tokens/$TOKEN_NAME" \ + -u "$READER_USER:$READER_PASS") + +if [[ "$HTTP_CODE" == "204" || "$HTTP_CODE" == "200" ]]; then + echo " -> Old token deleted." +elif [[ "$HTTP_CODE" == "404" ]]; then + echo " -> No existing token found (404), creating fresh." +else + echo " -> WARNING: HTTP $HTTP_CODE while deleting old token." +fi + +# --- Create new token --- +echo "[2/2] Creating new token '$TOKEN_NAME' (scope: $TOKEN_SCOPE)..." +TOKEN_RESPONSE=$(curl -s \ + -X POST "$GITEA_API/users/$READER_USER/tokens" \ + -u "$READER_USER:$READER_PASS" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"$TOKEN_NAME\", \"scopes\": [\"$TOKEN_SCOPE\"]}") + +TOKEN_VALUE=$(echo "$TOKEN_RESPONSE" | grep -o '"sha1":"[^"]*"' | sed 's/"sha1":"//;s/"//') + +if [[ -z "$TOKEN_VALUE" ]]; then + echo " -> ERROR: Failed to create new token:" + echo "$TOKEN_RESPONSE" + exit 1 +fi + +echo " -> New token: ${TOKEN_VALUE:0:8}..." + +# --- Update config.ini --- +if grep -q "^token\s*=" "$CONFIG" 2>/dev/null; then + sed -i "s|^token\s*=.*|token = $TOKEN_VALUE|" "$CONFIG" + echo " -> Token updated in $CONFIG" +else + sed -i "/^\[reader\]/,/^\[/{ + /^token_scope/a token = $TOKEN_VALUE + }" "$CONFIG" + echo " -> Token added to $CONFIG" +fi + +echo "" +echo "=== Rotation Complete ===" +echo "New token (first 8): ${TOKEN_VALUE:0:8}..." +echo "" +echo "IMPORTANT: Update any systems using the old token!" diff --git a/scripts/setup-reader.sh b/scripts/setup-reader.sh new file mode 100755 index 0000000..bd74a1a --- /dev/null +++ b/scripts/setup-reader.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="${1:-$SCRIPT_DIR/../config.ini}" + +if [[ ! -f "$CONFIG" ]]; then + echo "ERROR: config file not found: $CONFIG" + echo "Usage: $0 [path/to/config.ini]" + echo "Copy config.example.ini to config.ini and fill in your values." + exit 1 +fi + +# --- Parse INI --- +parse_ini() { + local file="$1" section="$2" key="$3" + sed -n "/^\[$section\]/,/^\[/p" "$file" | grep "^${key}\s*=" | head -1 | sed 's/^[^=]*=\s*//' | sed 's/\s*$//' +} + +GITEA_API=$(parse_ini "$CONFIG" gitea api_url) +OWNER_USER=$(parse_ini "$CONFIG" owner username) +OWNER_PASS=$(parse_ini "$CONFIG" owner password) +READER_USER=$(parse_ini "$CONFIG" reader username) +READER_PASS=$(parse_ini "$CONFIG" reader password) +READER_EMAIL=$(parse_ini "$CONFIG" reader email) +TOKEN_NAME=$(parse_ini "$CONFIG" reader token_name) +TOKEN_SCOPE=$(parse_ini "$CONFIG" reader token_scope) + +if [[ -z "$GITEA_API" || -z "$OWNER_USER" || -z "$OWNER_PASS" || -z "$READER_USER" || -z "$READER_PASS" ]]; then + echo "ERROR: missing required fields in config.ini" + exit 1 +fi + +READER_EMAIL="${READER_EMAIL:-${READER_USER}@noreply.local}" +TOKEN_NAME="${TOKEN_NAME:-installer-readonly}" +TOKEN_SCOPE="${TOKEN_SCOPE:-read:repository}" + +echo "=== Gitea Reader Account Setup ===" +echo "Server: $GITEA_API" +echo "Owner: $OWNER_USER" +echo "Reader: $READER_USER" +echo "" + +# --- Step 1: Create reader account --- +echo "[1/4] Creating reader account '$READER_USER'..." +HTTP_CODE=$(curl -s -o /tmp/gitea_create_user.json -w "%{http_code}" \ + -X POST "$GITEA_API/admin/users" \ + -u "$OWNER_USER:$OWNER_PASS" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"$READER_USER\", + \"password\": \"$READER_PASS\", + \"email\": \"$READER_EMAIL\", + \"must_change_password\": false, + \"visibility\": \"public\" + }") + +if [[ "$HTTP_CODE" == "201" ]]; then + echo " -> Account created." +elif [[ "$HTTP_CODE" == "422" ]]; then + echo " -> Account already exists (422), continuing." +else + echo " -> ERROR: HTTP $HTTP_CODE" + cat /tmp/gitea_create_user.json + exit 1 +fi + +# --- Step 2: Activate account --- +echo "[2/4] Activating account and setting visibility..." +curl -s -o /dev/null -w "" \ + -X PATCH "$GITEA_API/admin/users/$READER_USER" \ + -u "$OWNER_USER:$OWNER_PASS" \ + -H "Content-Type: application/json" \ + -d '{"active": true, "visibility": "public", "login_name": "'"$READER_USER"'"}' +echo " -> Done." + +# --- Step 3: Delete existing token with same name (if any) --- +echo "[3/4] Cleaning up old tokens..." +curl -s -o /dev/null -w "" \ + -X DELETE "$GITEA_API/users/$READER_USER/tokens/$TOKEN_NAME" \ + -u "$READER_USER:$READER_PASS" 2>/dev/null || true +echo " -> Done." + +# --- Step 4: Create API token --- +echo "[4/4] Creating API token '$TOKEN_NAME' (scope: $TOKEN_SCOPE)..." +TOKEN_RESPONSE=$(curl -s \ + -X POST "$GITEA_API/users/$READER_USER/tokens" \ + -u "$READER_USER:$READER_PASS" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"$TOKEN_NAME\", \"scopes\": [\"$TOKEN_SCOPE\"]}") + +TOKEN_VALUE=$(echo "$TOKEN_RESPONSE" | grep -o '"sha1":"[^"]*"' | sed 's/"sha1":"//;s/"//') + +if [[ -z "$TOKEN_VALUE" ]]; then + echo " -> ERROR: Failed to extract token from response:" + echo "$TOKEN_RESPONSE" + exit 1 +fi + +echo " -> Token created: ${TOKEN_VALUE:0:8}..." + +# --- Write token back to config.ini --- +if grep -q "^token\s*=" "$CONFIG" 2>/dev/null; then + sed -i "s|^token\s*=.*|token = $TOKEN_VALUE|" "$CONFIG" +else + # Add token under [reader] section + sed -i "/^\[reader\]/,/^\[/{ + /^token_scope/a token = $TOKEN_VALUE + }" "$CONFIG" +fi + +echo "" +echo "=== Setup Complete ===" +echo "Reader account: $READER_USER" +echo "Token (first 8): ${TOKEN_VALUE:0:8}..." +echo "Token written to: $CONFIG" +echo "" +echo "Next steps:" +echo " bash scripts/grant-access.sh " +echo " bash scripts/test-access.sh " diff --git a/scripts/test-access.sh b/scripts/test-access.sh new file mode 100755 index 0000000..fc80963 --- /dev/null +++ b/scripts/test-access.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="${SCRIPT_DIR}/../config.ini" + +REPO="${1:-}" +[[ -n "${2:-}" ]] && CONFIG="$2" + +if [[ ! -f "$CONFIG" ]]; then + echo "ERROR: config file not found: $CONFIG" + exit 1 +fi + +# --- Parse INI --- +parse_ini() { + local file="$1" section="$2" key="$3" + sed -n "/^\[$section\]/,/^\[/p" "$file" | grep "^${key}\s*=" | head -1 | sed 's/^[^=]*=\s*//' | sed 's/\s*$//' +} + +GITEA_URL=$(parse_ini "$CONFIG" gitea url) +GITEA_API=$(parse_ini "$CONFIG" gitea api_url) +OWNER_USER=$(parse_ini "$CONFIG" owner username) +READER_USER=$(parse_ini "$CONFIG" reader username) +TOKEN=$(parse_ini "$CONFIG" reader token) + +if [[ -z "$TOKEN" ]]; then + echo "ERROR: No token found in config.ini. Run setup-reader.sh first." + exit 1 +fi + +PASS=0 +FAIL=0 + +check() { + local desc="$1" expected="$2" actual="$3" + if [[ "$actual" == "$expected" ]]; then + echo " PASS: $desc (HTTP $actual)" + ((PASS++)) + else + echo " FAIL: $desc (expected $expected, got $actual)" + ((FAIL++)) + fi +} + +echo "=== Access Test for '$READER_USER' ===" +echo "Server: $GITEA_API" +echo "" + +# --- Test 1: Token is valid (list repos) --- +echo "[Test 1] Token validity — list repos..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $TOKEN" \ + "$GITEA_API/user/repos") +check "GET /user/repos with token" "200" "$HTTP_CODE" + +# --- Test 2: Token scope limitation (should NOT access admin endpoints) --- +echo "[Test 2] Token scope — admin API should be denied..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $TOKEN" \ + "$GITEA_API/admin/users") +check "GET /admin/users with read-only token" "403" "$HTTP_CODE" + +if [[ -n "$REPO" ]]; then + echo "" + echo "[Test 3] Repo access — $OWNER_USER/$REPO..." + + # --- Test 3a: Access with token --- + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $TOKEN" \ + "$GITEA_API/repos/$OWNER_USER/$REPO") + check "GET /repos/$OWNER_USER/$REPO with token" "200" "$HTTP_CODE" + + # --- Test 3b: Access without token (private repo should be 404) --- + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + "$GITEA_API/repos/$OWNER_USER/$REPO") + check "GET /repos/$OWNER_USER/$REPO without token (expect 404)" "404" "$HTTP_CODE" + + # --- Test 3c: Clone URL with token --- + echo "" + echo "[Info] Clone URL for scripts/installers:" + echo " git clone https://${READER_USER}:${TOKEN}@${GITEA_URL#https://}/${OWNER_USER}/${REPO}.git" + echo " (or use: Authorization: token $TOKEN header)" +else + echo "" + echo "[Info] Pass a repo name to test specific repo access:" + echo " $0 " +fi + +echo "" +echo "=== Results: $PASS passed, $FAIL failed ===" +[[ "$FAIL" -gt 0 ]] && exit 1 +exit 0