mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
342 lines
12 KiB
Go
342 lines
12 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package setting
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
asymkey_model "github.com/hanzoai/git/models/asymkey"
|
|
"github.com/hanzoai/git/models/db"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/templates"
|
|
"github.com/hanzoai/git/modules/web"
|
|
asymkey_service "github.com/hanzoai/git/services/asymkey"
|
|
"github.com/hanzoai/git/services/context"
|
|
"github.com/hanzoai/git/services/forms"
|
|
)
|
|
|
|
const (
|
|
tplSettingsKeys templates.TplName = "user/settings/keys"
|
|
)
|
|
|
|
// Keys render user's SSH/GPG public keys page
|
|
func Keys(ctx *context.Context) {
|
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) {
|
|
ctx.NotFound(errors.New("keys setting is not allowed to be changed"))
|
|
return
|
|
}
|
|
|
|
ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys")
|
|
ctx.Data["PageIsSettingsKeys"] = true
|
|
ctx.Data["DisableSSH"] = setting.SSH.Disabled
|
|
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
|
|
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
|
|
|
|
loadKeysData(ctx)
|
|
|
|
ctx.HTML(http.StatusOK, tplSettingsKeys)
|
|
}
|
|
|
|
// KeysPost response for change user's SSH/GPG keys
|
|
func KeysPost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.AddKeyForm)
|
|
ctx.Data["Title"] = ctx.Tr("settings_title")
|
|
ctx.Data["PageIsSettingsKeys"] = true
|
|
ctx.Data["DisableSSH"] = setting.SSH.Disabled
|
|
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
|
|
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
|
|
|
|
if ctx.HasError() {
|
|
loadKeysData(ctx)
|
|
|
|
ctx.HTML(http.StatusOK, tplSettingsKeys)
|
|
return
|
|
}
|
|
switch form.Type {
|
|
case "principal":
|
|
content, err := asymkey_model.CheckPrincipalKeyString(ctx, ctx.Doer, form.Content)
|
|
if err != nil {
|
|
if db.IsErrSSHDisabled(err) {
|
|
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
|
|
} else {
|
|
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_principal", err.Error()))
|
|
}
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
return
|
|
}
|
|
if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
|
|
ctx.Data["HasPrincipalError"] = true
|
|
switch {
|
|
case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
|
|
loadKeysData(ctx)
|
|
|
|
ctx.Data["Err_Content"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form)
|
|
default:
|
|
ctx.ServerError("AddPrincipalKey", err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
case "gpg":
|
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
|
|
ctx.NotFound(errors.New("gpg keys setting is not allowed to be visited"))
|
|
return
|
|
}
|
|
|
|
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
|
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
|
|
|
|
keys, err := asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, token, form.Signature)
|
|
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
|
|
keys, err = asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, lastToken, form.Signature)
|
|
}
|
|
if err != nil {
|
|
ctx.Data["HasGPGError"] = true
|
|
switch {
|
|
case asymkey_model.IsErrGPGKeyParsing(err):
|
|
ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
|
|
loadKeysData(ctx)
|
|
|
|
ctx.Data["Err_Content"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
|
|
case asymkey_model.IsErrGPGInvalidTokenSignature(err):
|
|
loadKeysData(ctx)
|
|
ctx.Data["Err_Content"] = true
|
|
ctx.Data["Err_Signature"] = true
|
|
keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
|
|
ctx.Data["KeyID"] = keyID
|
|
ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
|
|
case asymkey_model.IsErrGPGNoEmailFound(err):
|
|
loadKeysData(ctx)
|
|
|
|
ctx.Data["Err_Content"] = true
|
|
ctx.Data["Err_Signature"] = true
|
|
keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID
|
|
ctx.Data["KeyID"] = keyID
|
|
ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
|
|
default:
|
|
ctx.ServerError("AddPublicKey", err)
|
|
}
|
|
return
|
|
}
|
|
keyIDs := ""
|
|
for _, key := range keys {
|
|
keyIDs += key.KeyID
|
|
keyIDs += ", "
|
|
}
|
|
if len(keyIDs) > 0 {
|
|
keyIDs = keyIDs[:len(keyIDs)-2]
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
case "verify_gpg":
|
|
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
|
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
|
|
|
|
keyID, err := asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, token, form.Signature)
|
|
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
|
|
keyID, err = asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, lastToken, form.Signature)
|
|
}
|
|
if err != nil {
|
|
ctx.Data["HasGPGVerifyError"] = true
|
|
switch {
|
|
case asymkey_model.IsErrGPGInvalidTokenSignature(err):
|
|
loadKeysData(ctx)
|
|
ctx.Data["VerifyingID"] = form.KeyID
|
|
ctx.Data["Err_Signature"] = true
|
|
keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
|
|
ctx.Data["KeyID"] = keyID
|
|
ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
|
|
default:
|
|
ctx.ServerError("VerifyGPG", err)
|
|
}
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
case "ssh":
|
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
|
|
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
|
|
return
|
|
}
|
|
|
|
content, err := asymkey_model.CheckPublicKeyString(form.Content)
|
|
if err != nil {
|
|
if db.IsErrSSHDisabled(err) {
|
|
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
|
|
} else if asymkey_model.IsErrKeyUnableVerify(err) {
|
|
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
|
|
} else if err == asymkey_model.ErrKeyIsPrivate {
|
|
ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
|
|
} else {
|
|
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
|
|
}
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
return
|
|
}
|
|
|
|
if _, err = asymkey_model.AddPublicKey(ctx, ctx.Doer.ID, form.Title, content, 0, false); err != nil {
|
|
ctx.Data["HasSSHError"] = true
|
|
switch {
|
|
case asymkey_model.IsErrKeyAlreadyExist(err):
|
|
loadKeysData(ctx)
|
|
|
|
ctx.Data["Err_Content"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
|
|
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
|
|
loadKeysData(ctx)
|
|
|
|
ctx.Data["Err_Title"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
|
|
case asymkey_model.IsErrKeyUnableVerify(err):
|
|
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
default:
|
|
ctx.ServerError("AddPublicKey", err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
case "verify_ssh":
|
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
|
|
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
|
|
return
|
|
}
|
|
|
|
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
|
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
|
|
|
|
fingerprint, err := asymkey_model.VerifySSHKey(ctx, ctx.Doer.ID, form.Fingerprint, token, form.Signature)
|
|
if err != nil && asymkey_model.IsErrSSHInvalidTokenSignature(err) {
|
|
fingerprint, err = asymkey_model.VerifySSHKey(ctx, ctx.Doer.ID, form.Fingerprint, lastToken, form.Signature)
|
|
}
|
|
if err != nil {
|
|
ctx.Data["HasSSHVerifyError"] = true
|
|
switch {
|
|
case asymkey_model.IsErrSSHInvalidTokenSignature(err):
|
|
loadKeysData(ctx)
|
|
ctx.Data["Err_Signature"] = true
|
|
ctx.Data["Fingerprint"] = err.(asymkey_model.ErrSSHInvalidTokenSignature).Fingerprint
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form)
|
|
default:
|
|
ctx.ServerError("VerifySSH", err)
|
|
}
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.verify_ssh_key_success", fingerprint))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
|
|
default:
|
|
ctx.Flash.Warning("Function not implemented")
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
|
}
|
|
}
|
|
|
|
// DeleteKey response for delete user's SSH/GPG key
|
|
func DeleteKey(ctx *context.Context) {
|
|
switch ctx.FormString("type") {
|
|
case "gpg":
|
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
|
|
ctx.JSONError("gpg keys setting is not allowed to be visited")
|
|
return
|
|
}
|
|
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
|
|
ctx.JSONError("Failed to delete PGP key")
|
|
return
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
|
|
case "ssh":
|
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
|
|
ctx.JSONError("ssh keys setting is not allowed to be visited")
|
|
return
|
|
}
|
|
|
|
keyID := ctx.FormInt64("id")
|
|
external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
|
|
if err != nil {
|
|
ctx.ServerError("sshKeysExternalManaged", err)
|
|
return
|
|
}
|
|
if external {
|
|
ctx.JSONError(ctx.Tr("settings.ssh_externally_managed"))
|
|
return
|
|
}
|
|
if err := asymkey_service.DeletePublicKey(ctx, ctx.Doer, keyID); err != nil {
|
|
ctx.JSONError("Failed to delete SSH key")
|
|
return
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
|
|
case "principal":
|
|
if err := asymkey_service.DeletePublicKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
|
|
ctx.JSONError("Failed to delete SSH principal key")
|
|
return
|
|
}
|
|
ctx.Flash.Success(ctx.Tr("settings.ssh_principal_deletion_success"))
|
|
default:
|
|
ctx.JSONError("unsupported key type")
|
|
return
|
|
}
|
|
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/keys")
|
|
}
|
|
|
|
func loadKeysData(ctx *context.Context) {
|
|
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
|
|
OwnerID: ctx.Doer.ID,
|
|
NotKeytype: asymkey_model.KeyTypePrincipal,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("ListPublicKeys", err)
|
|
return
|
|
}
|
|
ctx.Data["Keys"] = keys
|
|
|
|
externalKeys, err := asymkey_model.PublicKeysAreExternallyManaged(ctx, keys)
|
|
if err != nil {
|
|
ctx.ServerError("ListPublicKeys", err)
|
|
return
|
|
}
|
|
ctx.Data["ExternalKeys"] = externalKeys
|
|
|
|
gpgkeys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
|
|
ListOptions: db.ListOptionsAll,
|
|
OwnerID: ctx.Doer.ID,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("ListGPGKeys", err)
|
|
return
|
|
}
|
|
if err := asymkey_model.GPGKeyList(gpgkeys).LoadSubKeys(ctx); err != nil {
|
|
ctx.ServerError("LoadSubKeys", err)
|
|
return
|
|
}
|
|
ctx.Data["GPGKeys"] = gpgkeys
|
|
tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1)
|
|
|
|
// generate a new aes cipher using the token
|
|
ctx.Data["TokenToSign"] = tokenToSign
|
|
ctx.Data["SSHSignNamespace"] = asymkey_model.SSHSignatureNamespace
|
|
|
|
principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
|
|
ListOptions: db.ListOptionsAll,
|
|
OwnerID: ctx.Doer.ID,
|
|
KeyTypes: []asymkey_model.KeyType{asymkey_model.KeyTypePrincipal},
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("ListPrincipalKeys", err)
|
|
return
|
|
}
|
|
ctx.Data["Principals"] = principals
|
|
|
|
ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
|
|
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
|
|
}
|