Added - Added optional sensitive-secret import for installer app.ini uploads.
- 1 - I added an explicit installer checkbox for importing sensitive secrets from `app.ini` in `templates/install.tmpl`. - 2 - I extended the installer form, submit pipeline, and final config writer so the optional import reuses `LFS_JWT_SECRET`, `INTERNAL_TOKEN`, and `oauth2.JWT_SECRET` from the uploaded `app.ini` instead of generating new values, including a submit-time fallback that re-reads the uploaded file if the checkbox was enabled after the first auto-import. - 3 - I finalized secret resolution for both direct values and `LFS_JWT_SECRET_URI` / `INTERNAL_TOKEN_URI` / `JWT_SECRET_URI` file-based references, and added regression coverage for direct imports, URI-based imports, the real `POST /import_app_ini` flow, and the persisted `app.ini` output.
This commit is contained in:
@@ -735,3 +735,8 @@ Project Change ID[date-time] - application-version - Type - Summary:
|
||||
|
||||
146 - [2026-05-12 01:30:10] - v1.27.0-dev-125-g1525c9c8ee - Type: Modified - Enabled shared branding assets by default in the installer.
|
||||
- 1 - I updated `routers/install/install.go` so `BrandingUseSharedAssets` starts as enabled on the install form, making `Use the same logo files for favicon assets` checked by default.
|
||||
|
||||
147 - [2026-05-12 01:43:10] - v1.27.0-dev-125-g1525c9c8ee - Type: Added - Added optional sensitive-secret import for installer `app.ini` uploads.
|
||||
- 1 - I added an explicit installer checkbox for importing sensitive secrets from `app.ini` in `templates/install.tmpl`.
|
||||
- 2 - I extended the installer form, submit pipeline, and final config writer so the optional import reuses `LFS_JWT_SECRET`, `INTERNAL_TOKEN`, and `oauth2.JWT_SECRET` from the uploaded `app.ini` instead of generating new values, including a submit-time fallback that re-reads the uploaded file if the checkbox was enabled after the first auto-import.
|
||||
- 3 - I finalized secret resolution for both direct values and `LFS_JWT_SECRET_URI` / `INTERNAL_TOKEN_URI` / `JWT_SECRET_URI` file-based references, and added regression coverage for direct imports, URI-based imports, the real `POST /import_app_ini` flow, and the persisted `app.ini` output.
|
||||
|
||||
@@ -113,7 +113,13 @@ prime/
|
||||
/.claude/
|
||||
/.cursorrules
|
||||
/.cursor/
|
||||
# /.configure.sh
|
||||
# /configure.sh
|
||||
/.frontend.hash
|
||||
# /.smart-build.sh
|
||||
# /smart-build.sh
|
||||
# /.update-gitea.sh
|
||||
# /update-gitea.sh
|
||||
/.goosehints
|
||||
/.windsurfrules
|
||||
/.github/copilot-instructions.md
|
||||
|
||||
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# This script safely updates the current local branch on top of the official
|
||||
# Gitea upstream without losing local work.
|
||||
# It:
|
||||
# 1. checks that no git operation is already in progress;
|
||||
# 2. ensures the upstream remote exists and points to the official repository;
|
||||
# 3. creates a local backup branch at the current HEAD;
|
||||
# 4. stashes tracked and untracked local changes;
|
||||
# 5. fetches the latest upstream changes and rebases the current branch on top
|
||||
# of upstream/main;
|
||||
# 6. reapplies the local stash only after a successful rebase.
|
||||
# If a conflict happens, the backup branch and the stash are both kept so the
|
||||
# local work can be recovered manually.
|
||||
|
||||
BRANCH="${BRANCH:-main}"
|
||||
REMOTE_NAME="${REMOTE_NAME:-upstream}"
|
||||
REMOTE_URL="${REMOTE_URL:-https://github.com/go-gitea/gitea.git}"
|
||||
|
||||
say() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
say "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ensure_git_repo() {
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1 || die "This script must be run inside a git repository."
|
||||
}
|
||||
|
||||
ensure_no_git_operation_in_progress() {
|
||||
local git_path
|
||||
for git_path in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD BISECT_LOG; do
|
||||
if [ -e "$(git rev-parse --git-path "$git_path")" ]; then
|
||||
die "A git operation is already in progress ($git_path). Finish it before running this script."
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git diff --name-only --diff-filter=U)" ]; then
|
||||
die "There are unmerged files in the working tree. Resolve them first."
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_current_branch() {
|
||||
CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD || true)"
|
||||
[ -n "$CURRENT_BRANCH" ] || die "Detached HEAD is not supported. Check out a branch first."
|
||||
}
|
||||
|
||||
ensure_upstream_remote() {
|
||||
local current_remote_url
|
||||
|
||||
if current_remote_url="$(git remote get-url "$REMOTE_NAME" 2>/dev/null)"; then
|
||||
if [ "$current_remote_url" != "$REMOTE_URL" ]; then
|
||||
die "Remote '$REMOTE_NAME' points to '$current_remote_url', expected '$REMOTE_URL'."
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
say "Adding remote '$REMOTE_NAME'..."
|
||||
git remote add "$REMOTE_NAME" "$REMOTE_URL"
|
||||
}
|
||||
|
||||
create_backup_branch() {
|
||||
local safe_branch_name timestamp
|
||||
safe_branch_name="${CURRENT_BRANCH//\//-}"
|
||||
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_BRANCH="backup/${safe_branch_name}-before-upstream-sync-${timestamp}"
|
||||
|
||||
git branch "$BACKUP_BRANCH" HEAD >/dev/null
|
||||
say "Backup branch created: $BACKUP_BRANCH"
|
||||
}
|
||||
|
||||
stash_local_changes() {
|
||||
local timestamp
|
||||
|
||||
STASH_REF=""
|
||||
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
|
||||
say "No local tracked/untracked changes to stash."
|
||||
return
|
||||
fi
|
||||
|
||||
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||
STASH_NAME="pre-upstream-sync-${CURRENT_BRANCH//\//-}-${timestamp}"
|
||||
|
||||
say "Stashing local tracked and untracked changes..."
|
||||
git stash push --include-untracked --message "$STASH_NAME" >/dev/null
|
||||
STASH_REF="$(git stash list -1 --format='%gd')"
|
||||
[ -n "$STASH_REF" ] || die "Failed to locate the created stash entry."
|
||||
say "Local changes saved in $STASH_REF"
|
||||
}
|
||||
|
||||
fetch_upstream() {
|
||||
say "Fetching latest changes from $REMOTE_NAME/$BRANCH..."
|
||||
git fetch --prune "$REMOTE_NAME"
|
||||
git show-ref --verify --quiet "refs/remotes/$REMOTE_NAME/$BRANCH" || die "Remote branch '$REMOTE_NAME/$BRANCH' was not found."
|
||||
}
|
||||
|
||||
rebase_onto_upstream() {
|
||||
say "Rebasing '$CURRENT_BRANCH' onto '$REMOTE_NAME/$BRANCH'..."
|
||||
if git rebase "$REMOTE_NAME/$BRANCH"; then
|
||||
say "Rebase completed successfully."
|
||||
return
|
||||
fi
|
||||
|
||||
say "Rebase stopped because of conflicts."
|
||||
say "Backup branch kept at: $BACKUP_BRANCH"
|
||||
if [ -n "$STASH_REF" ]; then
|
||||
say "Local stash kept at: $STASH_REF"
|
||||
fi
|
||||
say "Resolve conflicts and run 'git rebase --continue', or abort with 'git rebase --abort'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
restore_stash() {
|
||||
if [ -z "$STASH_REF" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
say "Restoring stashed local changes from $STASH_REF..."
|
||||
if git stash apply --index "$STASH_REF"; then
|
||||
git stash drop "$STASH_REF" >/dev/null
|
||||
say "Local changes restored successfully."
|
||||
return
|
||||
fi
|
||||
|
||||
say "Conflicts occurred while restoring local changes."
|
||||
say "Backup branch kept at: $BACKUP_BRANCH"
|
||||
say "Stash kept at: $STASH_REF"
|
||||
say "Resolve the conflicts manually. After that, drop the stash yourself if it is no longer needed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
ensure_git_repo
|
||||
ensure_no_git_operation_in_progress
|
||||
ensure_current_branch
|
||||
ensure_upstream_remote
|
||||
create_backup_branch
|
||||
stash_local_changes
|
||||
fetch_upstream
|
||||
rebase_onto_upstream
|
||||
restore_stash
|
||||
say "Project is now synchronized with $REMOTE_NAME/$BRANCH."
|
||||
say "Safety backup branch available at: $BACKUP_BRANCH"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -242,6 +242,8 @@
|
||||
"install.import_app_ini_desc": "Upload an existing app.ini file to prefill this installer form with the configuration values it already contains.",
|
||||
"install.import_app_ini_file": "app.ini File",
|
||||
"install.import_app_ini_helper": "Optional. INI file only, up to %d KB. The imported values are copied into the installer form so you can review and adjust them before saving.",
|
||||
"install.import_app_ini_sensitive_secrets": "Import sensitive secrets from app.ini",
|
||||
"install.import_app_ini_sensitive_secrets_helper": "Optional. Also import LFS_JWT_SECRET, INTERNAL_TOKEN, and JWT_SECRET from the uploaded configuration. Use this for restores or migrations where existing signed tokens must continue working.",
|
||||
"install.import_app_ini_button": "Load app.ini",
|
||||
"install.import_app_ini_missing": "Please choose an app.ini file to import.",
|
||||
"install.import_app_ini_read_failed": "Could not read the uploaded app.ini file: %v",
|
||||
|
||||
@@ -242,6 +242,8 @@
|
||||
"install.import_app_ini_desc": "Încarcă un fișier app.ini existent pentru a precompleta acest formular de instalare cu valorile de configurare deja definite în el.",
|
||||
"install.import_app_ini_file": "Fișier app.ini",
|
||||
"install.import_app_ini_helper": "Opțional. Doar fișier INI, până la %d KB. Valorile importate sunt copiate în formularul de instalare pentru a le putea verifica și ajusta înainte de salvare.",
|
||||
"install.import_app_ini_sensitive_secrets": "Importă secretele sensibile din app.ini",
|
||||
"install.import_app_ini_sensitive_secrets_helper": "Opțional. Importă și LFS_JWT_SECRET, INTERNAL_TOKEN și JWT_SECRET din configurația încărcată. Folosește asta la restaurări sau migrări unde tokenurile semnate existente trebuie să continue să funcționeze.",
|
||||
"install.import_app_ini_button": "Încarcă app.ini",
|
||||
"install.import_app_ini_missing": "Alege un fișier app.ini pentru import.",
|
||||
"install.import_app_ini_read_failed": "Fișierul app.ini încărcat nu a putut fi citit: %v",
|
||||
|
||||
+3
-3
@@ -157,8 +157,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"0-codex-gpt-login": "./.codex_gpt_login.sh",
|
||||
"1-configure-buid": "./configure.sh",
|
||||
"2-update-gitea": "./update-gitea.sh",
|
||||
"3-smart-build": "./smart-build.sh"
|
||||
"1-configure-buid": "./.configure.sh",
|
||||
"2-update-gitea": "./.update-gitea.sh",
|
||||
"3-smart-build": "./.smart-build.sh"
|
||||
}
|
||||
}
|
||||
|
||||
+115
-22
@@ -12,9 +12,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -466,11 +468,59 @@ func populateInstallFormFromConfig(form *forms.InstallForm, cfg setting.ConfigPr
|
||||
form.EnableUpdateChecker = setting.ConfigSectionKeyBool(securitySec, "ENABLE_UPDATE_CHECKER", form.EnableUpdateChecker)
|
||||
form.PasswordAlgorithm = setting.ConfigSectionKeyString(securitySec, "PASSWORD_HASH_ALGO", form.PasswordAlgorithm)
|
||||
form.AdminManagementPolicy = setting.ConfigSectionKeyString(adminSec, "ADMIN_MANAGEMENT_POLICY", form.AdminManagementPolicy)
|
||||
if form.ImportSensitiveSecrets {
|
||||
form.ImportedLFSJWTSecret = readImportedSecretValue(serverSec, "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
form.ImportedInternalToken = readImportedSecretValue(securitySec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
||||
form.ImportedOAuth2JWTSecret = readImportedSecretValue(cfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
|
||||
}
|
||||
normalizeInstallRegistrationOptions(form)
|
||||
|
||||
return form.DbType
|
||||
}
|
||||
|
||||
func readImportedSecretValue(sec setting.ConfigSection, uriKey, verbatimKey string) string {
|
||||
verbatim := setting.ConfigSectionKeyString(sec, verbatimKey)
|
||||
if verbatim != "" {
|
||||
return verbatim
|
||||
}
|
||||
|
||||
uriValue := setting.ConfigSectionKeyString(sec, uriKey)
|
||||
if uriValue == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(uriValue)
|
||||
if err != nil || parsed.Scheme != "file" {
|
||||
return ""
|
||||
}
|
||||
|
||||
path := parsed.Path
|
||||
if parsed.Opaque != "" {
|
||||
path = parsed.Opaque
|
||||
}
|
||||
if parsed.Host != "" {
|
||||
switch {
|
||||
case len(parsed.Host) == 2 && parsed.Host[1] == ':':
|
||||
path = parsed.Host + path
|
||||
default:
|
||||
path = "//" + parsed.Host + path
|
||||
}
|
||||
}
|
||||
path, err = url.PathUnescape(path)
|
||||
if err != nil || path == "" {
|
||||
return ""
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.HasPrefix(path, "/") && len(path) > 2 && path[2] == ':' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
func readImportedInstallConfig(ctx *context.Context) (setting.ConfigProvider, error) {
|
||||
file, header, err := ctx.Req.FormFile("app_ini_file")
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
@@ -500,6 +550,58 @@ func readImportedInstallConfig(ctx *context.Context) (setting.ConfigProvider, er
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func populateInstallSensitiveSecretsFromConfig(form *forms.InstallForm, cfg setting.ConfigProvider) {
|
||||
if !form.ImportSensitiveSecrets {
|
||||
return
|
||||
}
|
||||
if form.ImportedLFSJWTSecret == "" {
|
||||
form.ImportedLFSJWTSecret = readImportedSecretValue(cfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
}
|
||||
if form.ImportedInternalToken == "" {
|
||||
form.ImportedInternalToken = readImportedSecretValue(cfg.Section("security"), "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
||||
}
|
||||
if form.ImportedOAuth2JWTSecret == "" {
|
||||
form.ImportedOAuth2JWTSecret = readImportedSecretValue(cfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
|
||||
}
|
||||
}
|
||||
|
||||
func applyInstallSensitiveSecretsToConfig(cfg setting.ConfigProvider, form *forms.InstallForm) error {
|
||||
if form.LFSRootPath != "" {
|
||||
if form.ImportSensitiveSecrets && form.ImportedLFSJWTSecret != "" {
|
||||
cfg.Section("server").DeleteKey("LFS_JWT_SECRET_URI")
|
||||
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(form.ImportedLFSJWTSecret)
|
||||
} else if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") {
|
||||
_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
|
||||
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
|
||||
// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
|
||||
if form.ImportSensitiveSecrets && form.ImportedInternalToken != "" {
|
||||
cfg.Section("security").DeleteKey("INTERNAL_TOKEN_URI")
|
||||
cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(form.ImportedInternalToken)
|
||||
} else if setting.InternalToken == "" {
|
||||
internalToken, err := generate.NewInternalToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
|
||||
}
|
||||
|
||||
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
||||
// see the "loadOAuth2From" in "setting/oauth2.go"
|
||||
if form.ImportSensitiveSecrets && form.ImportedOAuth2JWTSecret != "" {
|
||||
cfg.Section("oauth2").DeleteKey("JWT_SECRET_URI")
|
||||
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(form.ImportedOAuth2JWTSecret)
|
||||
} else if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") {
|
||||
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
|
||||
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Install render installation page
|
||||
func Install(ctx *context.Context) {
|
||||
if setting.InstallLock {
|
||||
@@ -518,6 +620,7 @@ func ImportAppINI(ctx *context.Context) {
|
||||
}
|
||||
|
||||
form, curDBType := newInstallFormFromSettings()
|
||||
form.ImportSensitiveSecrets = ctx.FormBool("import_sensitive_secrets")
|
||||
cfg, err := readImportedInstallConfig(ctx)
|
||||
if err != nil {
|
||||
ctx.Data["CurDbType"] = curDBType
|
||||
@@ -700,6 +803,15 @@ func SubmitInstall(ctx *context.Context) {
|
||||
var err error
|
||||
|
||||
form := *web.GetForm(ctx).(*forms.InstallForm)
|
||||
form.ImportSensitiveSecrets = ctx.FormBool("import_sensitive_secrets")
|
||||
form.ImportedLFSJWTSecret = ctx.FormString("imported_lfs_jwt_secret")
|
||||
form.ImportedInternalToken = ctx.FormString("imported_internal_token")
|
||||
form.ImportedOAuth2JWTSecret = ctx.FormString("imported_o_auth2_jwt_secret")
|
||||
if form.ImportSensitiveSecrets && (form.ImportedLFSJWTSecret == "" || form.ImportedInternalToken == "" || form.ImportedOAuth2JWTSecret == "") {
|
||||
if importedCfg, err := readImportedInstallConfig(ctx); err == nil {
|
||||
populateInstallSensitiveSecretsFromConfig(&form, importedCfg)
|
||||
}
|
||||
}
|
||||
|
||||
// fix form values
|
||||
if form.AppURL != "" && form.AppURL[len(form.AppURL)-1] != '/' {
|
||||
@@ -882,11 +994,6 @@ func SubmitInstall(ctx *context.Context) {
|
||||
if form.LFSRootPath != "" {
|
||||
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
|
||||
cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
|
||||
|
||||
if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") {
|
||||
_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
|
||||
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
|
||||
}
|
||||
} else {
|
||||
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
|
||||
}
|
||||
@@ -941,23 +1048,9 @@ func SubmitInstall(ctx *context.Context) {
|
||||
cfg.Section("repository.signing").Key("DEFAULT_TRUST_MODEL").SetValue("committer")
|
||||
|
||||
cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
|
||||
|
||||
// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
|
||||
// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
|
||||
if setting.InternalToken == "" {
|
||||
var internalToken string
|
||||
if internalToken, err = generate.NewInternalToken(); err != nil {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
|
||||
return
|
||||
}
|
||||
cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
|
||||
}
|
||||
|
||||
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
||||
// see the "loadOAuth2From" in "setting/oauth2.go"
|
||||
if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") {
|
||||
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
|
||||
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err = applyInstallSensitiveSecretsToConfig(cfg, &form); err != nil {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
|
||||
return
|
||||
}
|
||||
|
||||
// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -166,6 +171,151 @@ LANGS = de-DE,en-US
|
||||
assert.Equal(t, "super_admin_only", form.AdminManagementPolicy)
|
||||
}
|
||||
|
||||
func TestPopulateInstallFormFromConfigWithSensitiveSecrets(t *testing.T) {
|
||||
cfg, err := setting.NewConfigProviderFromData(`
|
||||
[server]
|
||||
LFS_JWT_SECRET = lfs-secret
|
||||
|
||||
[security]
|
||||
INTERNAL_TOKEN = internal-secret
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = oauth-secret
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
form, curDBType := newInstallFormFromSettings()
|
||||
form.ImportSensitiveSecrets = true
|
||||
populateInstallFormFromConfig(&form, cfg, curDBType)
|
||||
|
||||
assert.Equal(t, "lfs-secret", form.ImportedLFSJWTSecret)
|
||||
assert.Equal(t, "internal-secret", form.ImportedInternalToken)
|
||||
assert.Equal(t, "oauth-secret", form.ImportedOAuth2JWTSecret)
|
||||
}
|
||||
|
||||
func TestPopulateInstallFormFromConfigWithSensitiveSecretURIs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
lfsSecretPath := filepath.Join(tmpDir, "lfs_secret")
|
||||
internalTokenPath := filepath.Join(tmpDir, "internal_token")
|
||||
oauthSecretPath := filepath.Join(tmpDir, "oauth_secret")
|
||||
|
||||
require.NoError(t, os.WriteFile(lfsSecretPath, []byte("lfs-secret-uri\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(internalTokenPath, []byte("internal-secret-uri\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(oauthSecretPath, []byte("oauth-secret-uri\n"), 0o644))
|
||||
|
||||
cfg, err := setting.NewConfigProviderFromData(`
|
||||
[server]
|
||||
LFS_JWT_SECRET_URI = file:` + filepath.ToSlash(lfsSecretPath) + `
|
||||
|
||||
[security]
|
||||
INTERNAL_TOKEN_URI = file:` + filepath.ToSlash(internalTokenPath) + `
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET_URI = file:` + filepath.ToSlash(oauthSecretPath) + `
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
form, curDBType := newInstallFormFromSettings()
|
||||
form.ImportSensitiveSecrets = true
|
||||
populateInstallFormFromConfig(&form, cfg, curDBType)
|
||||
|
||||
assert.Equal(t, "lfs-secret-uri", form.ImportedLFSJWTSecret)
|
||||
assert.Equal(t, "internal-secret-uri", form.ImportedInternalToken)
|
||||
assert.Equal(t, "oauth-secret-uri", form.ImportedOAuth2JWTSecret)
|
||||
}
|
||||
|
||||
func TestPopulateInstallSensitiveSecretsFromConfigFillsMissingValuesOnly(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
internalTokenPath := filepath.Join(tmpDir, "internal_token")
|
||||
require.NoError(t, os.WriteFile(internalTokenPath, []byte("internal-secret-uri\n"), 0o644))
|
||||
|
||||
cfg, err := setting.NewConfigProviderFromData(`
|
||||
[server]
|
||||
LFS_JWT_SECRET = lfs-secret
|
||||
|
||||
[security]
|
||||
INTERNAL_TOKEN_URI = file:` + filepath.ToSlash(internalTokenPath) + `
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = oauth-secret
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
form := forms.InstallForm{
|
||||
ImportSensitiveSecrets: true,
|
||||
ImportedLFSJWTSecret: "",
|
||||
ImportedInternalToken: "",
|
||||
ImportedOAuth2JWTSecret: "already-set",
|
||||
}
|
||||
populateInstallSensitiveSecretsFromConfig(&form, cfg)
|
||||
|
||||
assert.Equal(t, "lfs-secret", form.ImportedLFSJWTSecret)
|
||||
assert.Equal(t, "internal-secret-uri", form.ImportedInternalToken)
|
||||
assert.Equal(t, "already-set", form.ImportedOAuth2JWTSecret)
|
||||
}
|
||||
|
||||
func TestImportAppINIWithSensitiveSecrets(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.InstallLock, false)()
|
||||
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
require.NoError(t, writer.WriteField("import_sensitive_secrets", "on"))
|
||||
|
||||
fileWriter, err := writer.CreateFormFile("app_ini_file", "app.ini")
|
||||
require.NoError(t, err)
|
||||
_, err = fileWriter.Write([]byte(`
|
||||
[server]
|
||||
LFS_JWT_SECRET = lfs-secret
|
||||
|
||||
[security]
|
||||
INTERNAL_TOKEN = internal-secret
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = oauth-secret
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
r := Routes()
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/import_app_ini", &body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), `name="imported_lfs_jwt_secret" value="lfs-secret"`)
|
||||
assert.Contains(t, w.Body.String(), `name="imported_internal_token" value="internal-secret"`)
|
||||
assert.Contains(t, w.Body.String(), `name="imported_o_auth2_jwt_secret" value="oauth-secret"`)
|
||||
}
|
||||
|
||||
func TestApplyInstallSensitiveSecretsToConfigPersistsImportedValues(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "app.ini")
|
||||
|
||||
defer test.MockVariableValue(&setting.InternalToken, "")()
|
||||
|
||||
cfg, err := setting.NewConfigProviderFromData("")
|
||||
require.NoError(t, err)
|
||||
|
||||
form := forms.InstallForm{
|
||||
LFSRootPath: filepath.Join(tmpDir, "lfs"),
|
||||
ImportSensitiveSecrets: true,
|
||||
ImportedLFSJWTSecret: "lfs-secret",
|
||||
ImportedInternalToken: "internal-secret",
|
||||
ImportedOAuth2JWTSecret: "oauth-secret",
|
||||
}
|
||||
|
||||
require.NoError(t, applyInstallSensitiveSecretsToConfig(cfg, &form))
|
||||
require.NoError(t, cfg.SaveTo(configPath))
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
require.NoError(t, err)
|
||||
content := string(data)
|
||||
assert.Contains(t, content, "LFS_JWT_SECRET = lfs-secret")
|
||||
assert.Contains(t, content, "INTERNAL_TOKEN = internal-secret")
|
||||
assert.Contains(t, content, "JWT_SECRET = oauth-secret")
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ type InstallForm struct {
|
||||
SMTPFrom string
|
||||
SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"`
|
||||
SMTPPasswd string
|
||||
ImportSensitiveSecrets bool `form:"import_sensitive_secrets"`
|
||||
ImportedLFSJWTSecret string `form:"imported_lfs_jwt_secret"`
|
||||
ImportedInternalToken string `form:"imported_internal_token"`
|
||||
ImportedOAuth2JWTSecret string `form:"imported_o_auth2_jwt_secret"`
|
||||
RegisterConfirm bool
|
||||
RegisterManualConfirm bool
|
||||
MailNotify bool
|
||||
|
||||
+18
-1
@@ -18,6 +18,16 @@
|
||||
<input id="app_ini_file" name="app_ini_file" type="file" accept=".ini,text/plain" data-import-action="{{AppSubUrl}}/import_app_ini">
|
||||
<span class="help">{{ctx.Locale.Tr "install.import_app_ini_helper" .AppINIImportMaxSizeKB}}</span>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label for="import_sensitive_secrets">{{ctx.Locale.Tr "install.import_app_ini_sensitive_secrets"}}</label>
|
||||
<input id="import_sensitive_secrets" name="import_sensitive_secrets" type="checkbox" {{if .import_sensitive_secrets}}checked{{end}}>
|
||||
</div>
|
||||
<span class="help">{{ctx.Locale.Tr "install.import_app_ini_sensitive_secrets_helper"}}</span>
|
||||
</div>
|
||||
<input type="hidden" name="imported_lfs_jwt_secret" value="{{.imported_lfs_jwt_secret}}">
|
||||
<input type="hidden" name="imported_internal_token" value="{{.imported_internal_token}}">
|
||||
<input type="hidden" name="imported_o_auth2_jwt_secret" value="{{.imported_o_auth2_jwt_secret}}">
|
||||
|
||||
<!-- Database Settings -->
|
||||
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.db_title"}}</h4>
|
||||
@@ -530,7 +540,14 @@
|
||||
}
|
||||
if (importedInput.type === 'checkbox') {
|
||||
const currentCheckbox = form.querySelector(`input[type="checkbox"][name="${CSS.escape(importedInput.name)}"]`);
|
||||
if (currentCheckbox instanceof HTMLInputElement) currentCheckbox.checked = importedInput.checked;
|
||||
if (currentCheckbox instanceof HTMLInputElement) {
|
||||
currentCheckbox.checked = importedInput.checked;
|
||||
currentCheckbox.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
const checkboxContainer = currentCheckbox.closest('.ui.checkbox');
|
||||
if (checkboxContainer) {
|
||||
checkboxContainer.classList.toggle('checked', importedInput.checked);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configurare nume ramură (main sau master)
|
||||
BRANCH="main"
|
||||
REMOTE_NAME="upstream"
|
||||
REMOTE_URL="https://github.com/go-gitea/gitea.git"
|
||||
|
||||
echo "🔍 Verificare sursă upstream..."
|
||||
|
||||
# Verificăm dacă remote-ul 'upstream' există, dacă nu, îl adăugăm
|
||||
if ! git remote | grep -q "$REMOTE_NAME"; then
|
||||
echo "➕ Adăugare remote $REMOTE_NAME..."
|
||||
git remote add $REMOTE_NAME $REMOTE_URL
|
||||
fi
|
||||
|
||||
echo "📦 Salvare modificări locale (Stash)..."
|
||||
git stash
|
||||
|
||||
echo "🔄 Descărcare noutăți de la Gitea oficial..."
|
||||
git fetch $REMOTE_NAME
|
||||
|
||||
echo "🚀 Aplicare noutăți peste modificările tale (Rebase)..."
|
||||
if git rebase $REMOTE_NAME/$BRANCH; then
|
||||
echo "✅ Actualizare reușită!"
|
||||
else
|
||||
echo "⚠️ CONFLICTE DETECTATE!"
|
||||
echo "Te rog rezolvă conflictele manual în panoul Source Control din code-server,"
|
||||
echo "apoi rulează 'git rebase --continue'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📥 Recuperare modificări locale din stash..."
|
||||
git stash pop
|
||||
|
||||
echo "✨ Proiectul este acum la zi cu $REMOTE_NAME/$BRANCH"
|
||||
Reference in New Issue
Block a user