]> source.dussan.org Git - gitea.git/commitdiff
Disable SSH key deletion of externally managed Keys (#13985)
authorzeripath <art27@cantab.net>
Sat, 26 Dec 2020 04:24:47 +0000 (04:24 +0000)
committerGitHub <noreply@github.com>
Sat, 26 Dec 2020 04:24:47 +0000 (23:24 -0500)
* Disable SSH key addition and deletion when externally managed

When a user has a login source which has SSH key management
key addition and deletion using the UI should be disabled.

Fix #13983

Signed-off-by: Andrew Thornton <art27@cantab.net>
* Make only externally managed keys disabled

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
models/ssh_key.go
options/locale/locale_en-US.ini
routers/api/v1/user/key.go
routers/user/setting/keys.go
templates/user/settings/keys_ssh.tmpl

index b2e4326559a4c0182b2575cf0089b5b25e921d87..70512dccf57dcf85c8cc18451ff8dcdfba053cbd 100644 (file)
@@ -665,6 +665,82 @@ func deletePublicKeys(e Engine, keyIDs ...int64) error {
        return err
 }
 
+// PublicKeysAreExternallyManaged returns whether the provided KeyID represents an externally managed Key
+func PublicKeysAreExternallyManaged(keys []*PublicKey) ([]bool, error) {
+       sources := make([]*LoginSource, 0, 5)
+       externals := make([]bool, len(keys))
+keyloop:
+       for i, key := range keys {
+               if key.LoginSourceID == 0 {
+                       externals[i] = false
+                       continue keyloop
+               }
+
+               var source *LoginSource
+
+       sourceloop:
+               for _, s := range sources {
+                       if s.ID == key.LoginSourceID {
+                               source = s
+                               break sourceloop
+                       }
+               }
+
+               if source == nil {
+                       var err error
+                       source, err = GetLoginSourceByID(key.LoginSourceID)
+                       if err != nil {
+                               if IsErrLoginSourceNotExist(err) {
+                                       externals[i] = false
+                                       sources[i] = &LoginSource{
+                                               ID: key.LoginSourceID,
+                                       }
+                                       continue keyloop
+                               }
+                               return nil, err
+                       }
+               }
+
+               ldapSource := source.LDAP()
+               if ldapSource != nil &&
+                       source.IsSyncEnabled &&
+                       (source.Type == LoginLDAP || source.Type == LoginDLDAP) &&
+                       len(strings.TrimSpace(ldapSource.AttributeSSHPublicKey)) > 0 {
+                       // Disable setting SSH keys for this user
+                       externals[i] = true
+               }
+       }
+
+       return externals, nil
+}
+
+// PublicKeyIsExternallyManaged returns whether the provided KeyID represents an externally managed Key
+func PublicKeyIsExternallyManaged(id int64) (bool, error) {
+       key, err := GetPublicKeyByID(id)
+       if err != nil {
+               return false, err
+       }
+       if key.LoginSourceID == 0 {
+               return false, nil
+       }
+       source, err := GetLoginSourceByID(key.LoginSourceID)
+       if err != nil {
+               if IsErrLoginSourceNotExist(err) {
+                       return false, nil
+               }
+               return false, err
+       }
+       ldapSource := source.LDAP()
+       if ldapSource != nil &&
+               source.IsSyncEnabled &&
+               (source.Type == LoginLDAP || source.Type == LoginDLDAP) &&
+               len(strings.TrimSpace(ldapSource.AttributeSSHPublicKey)) > 0 {
+               // Disable setting SSH keys for this user
+               return true, nil
+       }
+       return false, nil
+}
+
 // DeletePublicKey deletes SSH key information both in database and authorized_keys file.
 func DeletePublicKey(doer *User, id int64) (err error) {
        key, err := GetPublicKeyByID(id)
index d7d6b751f6a4adde7500aba4e523446c992e07a3..6b772d2392544248c8ac86f3ea0c0b80988066ef 100644 (file)
@@ -556,6 +556,7 @@ principal_state_desc = This principal has been used in the last 7 days
 show_openid = Show on profile
 hide_openid = Hide from profile
 ssh_disabled = SSH Disabled
+ssh_externally_managed = This SSH key is externally managed for this user
 manage_social = Manage Associated Social Accounts
 social_desc = These social accounts are linked to your Gitea account. Make sure you recognize all of them as they can be used to sign in to your Gitea account.
 unbind = Unlink
index 033b29f420da2c598274bc6edf971c2a7d26e19a..8069660653652c98e3673201e8e870f98cbadad7 100644 (file)
@@ -267,7 +267,16 @@ func DeletePublicKey(ctx *context.APIContext) {
        //   "404":
        //     "$ref": "#/responses/notFound"
 
-       if err := models.DeletePublicKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
+       id := ctx.ParamsInt64(":id")
+       externallyManaged, err := models.PublicKeyIsExternallyManaged(id)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err)
+       }
+       if externallyManaged {
+               ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user")
+       }
+
+       if err := models.DeletePublicKey(ctx.User, id); err != nil {
                if models.IsErrKeyNotExist(err) {
                        ctx.NotFound()
                } else if models.IsErrKeyAccessDenied(err) {
index 6a39666e944bb4b27cfe00196eaa61c3d6c104e8..76c7ef9da447030f4b846c72dea19038f4d19519 100644 (file)
@@ -160,7 +160,18 @@ func DeleteKey(ctx *context.Context) {
                        ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
                }
        case "ssh":
-               if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+               keyID := ctx.QueryInt64("id")
+               external, err := models.PublicKeyIsExternallyManaged(keyID)
+               if err != nil {
+                       ctx.ServerError("sshKeysExternalManaged", err)
+                       return
+               }
+               if external {
+                       ctx.Flash.Error(ctx.Tr("setting.ssh_externally_managed"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+                       return
+               }
+               if err := models.DeletePublicKey(ctx.User, keyID); err != nil {
                        ctx.Flash.Error("DeletePublicKey: " + err.Error())
                } else {
                        ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
@@ -188,6 +199,13 @@ func loadKeysData(ctx *context.Context) {
        }
        ctx.Data["Keys"] = keys
 
+       externalKeys, err := models.PublicKeysAreExternallyManaged(keys)
+       if err != nil {
+               ctx.ServerError("ListPublicKeys", err)
+               return
+       }
+       ctx.Data["ExternalKeys"] = externalKeys
+
        gpgkeys, err := models.ListGPGKeys(ctx.User.ID, models.ListOptions{})
        if err != nil {
                ctx.ServerError("ListGPGKeys", err)
index 9a4db09c2d1586d47cdd089814ba7c14e64dcb46..95e95b0ddba524d7bfda63738fe742afa1a0e715 100644 (file)
@@ -1,7 +1,7 @@
 <h4 class="ui top attached header">
        {{.i18n.Tr "settings.manage_ssh_keys"}}
        <div class="ui right">
-       {{if not .DisableSSH}}
+       {{if not .DisableSSH }}
                <div class="ui blue tiny show-panel button" data-panel="#add-ssh-key-panel">{{.i18n.Tr "settings.add_key"}}</div>
        {{else}}
                <div class="ui blue tiny button disabled">{{.i18n.Tr "settings.ssh_disabled"}}</div>
                <div class="item">
                        {{.i18n.Tr "settings.ssh_desc"}}
                </div>
-               {{range .Keys}}
+               {{range $index, $key := .Keys}}
                        <div class="item">
-                <div class="right floated content">
-                    <button class="ui red tiny button delete-button" id="delete-ssh" data-url="{{$.Link}}/delete?type=ssh" data-id="{{.ID}}">
-                        {{$.i18n.Tr "settings.delete_key"}}
-                    </button>
-                </div>
-                <div class="left floated content">
-                       <span class="{{if .HasRecentActivity}}green{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted tiny"{{end}}>{{svg "octicon-key" 32}}</span>
-                </div>
-                <div class="content">
-                    <strong>{{.Name}}</strong>
-                    <div class="print meta">
-                        {{.Fingerprint}}
-                    </div>
-                    <div class="activity meta">
-                        <i>{{$.i18n.Tr "settings.add_on"}} <span>{{.CreatedUnix.FormatShort}}</span> —       {{svg "octicon-info"}} {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{.UpdatedUnix.FormatShort}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i>
-                    </div>
-                </div>
+                               <div class="right floated content">
+                                       <button class="ui red tiny button delete-button{{if index $.ExternalKeys $index}} disabled{{end}}" id="delete-ssh" data-url="{{$.Link}}/delete?type=ssh" data-id="{{.ID}}"{{if index $.ExternalKeys $index}} title="{{$.i18n.Tr "settings.ssh_externally_managed"}}"{{end}}>
+                                               {{$.i18n.Tr "settings.delete_key"}}
+                                       </button>
+                               </div>
+                               <div class="left floated content">
+                                       <span class="{{if .HasRecentActivity}}green{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted tiny"{{end}}>{{svg "octicon-key" 32}}</span>
+                               </div>
+                               <div class="content">
+                                               <strong>{{.Name}}</strong>
+                                               <div class="print meta">
+                                                               {{.Fingerprint}}
+                                               </div>
+                                               <div class="activity meta">
+                                                               <i>{{$.i18n.Tr "settings.add_on"}} <span>{{.CreatedUnix.FormatShort}}</span> —        {{svg "octicon-info"}} {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{.UpdatedUnix.FormatShort}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i>
+                                               </div>
+                               </div>
                        </div>
                {{end}}
        </div>