diff --git a/.codex-history.md b/.codex-history.md index e29f1436fe..b0c95c5626 100644 --- a/.codex-history.md +++ b/.codex-history.md @@ -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. diff --git a/configure.sh b/.configure.sh similarity index 100% rename from configure.sh rename to .configure.sh diff --git a/.gitignore b/.gitignore index f29b31da55..4f27fb9f01 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/smart-build.sh b/.smart-build.sh similarity index 100% rename from smart-build.sh rename to .smart-build.sh diff --git a/.update-gitea.sh b/.update-gitea.sh new file mode 100755 index 0000000000..68079981f9 --- /dev/null +++ b/.update-gitea.sh @@ -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 "$@" diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index de0b2dafae..ee257ab5ad 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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", diff --git a/options/locale/locale_ro-RO.json b/options/locale/locale_ro-RO.json index 8ad3f5edf0..4038fdb392 100644 --- a/options/locale/locale_ro-RO.json +++ b/options/locale/locale_ro-RO.json @@ -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", diff --git a/package.json b/package.json index ff9a08852b..5403edafa6 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/routers/install/install.go b/routers/install/install.go index 9c6b5d07f8..a188f463a7 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -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 diff --git a/routers/install/routes_test.go b/routers/install/routes_test.go index 949a5e6b66..d1728b0941 100644 --- a/routers/install/routes_test.go +++ b/routers/install/routes_test.go @@ -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) } diff --git a/services/forms/user_form.go b/services/forms/user_form.go index c9dd34a781..c488fc549f 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -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 diff --git a/templates/install.tmpl b/templates/install.tmpl index a6704aad76..3f0b9c683d 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -18,6 +18,16 @@ {{ctx.Locale.Tr "install.import_app_ini_helper" .AppINIImportMaxSizeKB}} +