summaryrefslogtreecommitdiffstats
path: root/services/asymkey
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /services/asymkey
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--services/asymkey/deploy_key.go31
-rw-r--r--services/asymkey/main_test.go17
-rw-r--r--services/asymkey/sign.go404
-rw-r--r--services/asymkey/ssh_key.go50
-rw-r--r--services/asymkey/ssh_key_test.go88
5 files changed, 590 insertions, 0 deletions
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
new file mode 100644
index 0000000..e127cbf
--- /dev/null
+++ b/services/asymkey/deploy_key.go
@@ -0,0 +1,31 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models"
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
+func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error {
+ dbCtx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := models.DeleteDeployKey(dbCtx, doer, id); err != nil {
+ return err
+ }
+ if err := committer.Commit(); err != nil {
+ return err
+ }
+
+ return asymkey_model.RewriteAllPublicKeys(ctx)
+}
diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go
new file mode 100644
index 0000000..3505b26
--- /dev/null
+++ b/services/asymkey/main_test.go
@@ -0,0 +1,17 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+
+ _ "code.gitea.io/gitea/models/actions"
+ _ "code.gitea.io/gitea/models/activities"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m)
+}
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
new file mode 100644
index 0000000..8fb5699
--- /dev/null
+++ b/services/asymkey/sign.go
@@ -0,0 +1,404 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+type signingMode string
+
+const (
+ never signingMode = "never"
+ always signingMode = "always"
+ pubkey signingMode = "pubkey"
+ twofa signingMode = "twofa"
+ parentSigned signingMode = "parentsigned"
+ baseSigned signingMode = "basesigned"
+ headSigned signingMode = "headsigned"
+ commitsSigned signingMode = "commitssigned"
+ approved signingMode = "approved"
+ noKey signingMode = "nokey"
+)
+
+func signingModeFromStrings(modeStrings []string) []signingMode {
+ returnable := make([]signingMode, 0, len(modeStrings))
+ for _, mode := range modeStrings {
+ signMode := signingMode(strings.ToLower(strings.TrimSpace(mode)))
+ switch signMode {
+ case never:
+ return []signingMode{never}
+ case always:
+ return []signingMode{always}
+ case pubkey:
+ fallthrough
+ case twofa:
+ fallthrough
+ case parentSigned:
+ fallthrough
+ case baseSigned:
+ fallthrough
+ case headSigned:
+ fallthrough
+ case approved:
+ fallthrough
+ case commitsSigned:
+ returnable = append(returnable, signMode)
+ }
+ }
+ if len(returnable) == 0 {
+ return []signingMode{never}
+ }
+ return returnable
+}
+
+// ErrWontSign explains the first reason why a commit would not be signed
+// There may be other reasons - this is just the first reason found
+type ErrWontSign struct {
+ Reason signingMode
+}
+
+func (e *ErrWontSign) Error() string {
+ return fmt.Sprintf("won't sign: %s", e.Reason)
+}
+
+// IsErrWontSign checks if an error is a ErrWontSign
+func IsErrWontSign(err error) bool {
+ _, ok := err.(*ErrWontSign)
+ return ok
+}
+
+// SigningKey returns the KeyID and git Signature for the repo
+func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
+ if setting.Repository.Signing.SigningKey == "none" {
+ return "", nil
+ }
+
+ if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
+ // Can ignore the error here as it means that commit.gpgsign is not set
+ value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath})
+ sign, valid := git.ParseBool(strings.TrimSpace(value))
+ if !sign || !valid {
+ return "", nil
+ }
+
+ signingKey, _, _ := git.NewCommand(ctx, "config", "--get", "user.signingkey").RunStdString(&git.RunOpts{Dir: repoPath})
+ signingName, _, _ := git.NewCommand(ctx, "config", "--get", "user.name").RunStdString(&git.RunOpts{Dir: repoPath})
+ signingEmail, _, _ := git.NewCommand(ctx, "config", "--get", "user.email").RunStdString(&git.RunOpts{Dir: repoPath})
+ return strings.TrimSpace(signingKey), &git.Signature{
+ Name: strings.TrimSpace(signingName),
+ Email: strings.TrimSpace(signingEmail),
+ }
+ }
+
+ return setting.Repository.Signing.SigningKey, &git.Signature{
+ Name: setting.Repository.Signing.SigningName,
+ Email: setting.Repository.Signing.SigningEmail,
+ }
+}
+
+// PublicSigningKey gets the public signing key within a provided repository directory
+func PublicSigningKey(ctx context.Context, repoPath string) (string, error) {
+ signingKey, _ := SigningKey(ctx, repoPath)
+ if signingKey == "" {
+ return "", nil
+ }
+
+ content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
+ "gpg --export -a", "gpg", "--export", "-a", signingKey)
+ if err != nil {
+ log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
+ return "", err
+ }
+ return content, nil
+}
+
+// SignInitialCommit determines if we should sign the initial commit to this repository
+func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, string, *git.Signature, error) {
+ rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
+ signingKey, sig := SigningKey(ctx, repoPath)
+ if signingKey == "" {
+ return false, "", nil, &ErrWontSign{noKey}
+ }
+
+Loop:
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, "", nil, &ErrWontSign{never}
+ case always:
+ break Loop
+ case pubkey:
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
+ if err != nil {
+ return false, "", nil, err
+ }
+ if len(keys) == 0 {
+ return false, "", nil, &ErrWontSign{pubkey}
+ }
+ case twofa:
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ return false, "", nil, err
+ }
+ if twofaModel == nil {
+ return false, "", nil, &ErrWontSign{twofa}
+ }
+ }
+ }
+ return true, signingKey, sig, nil
+}
+
+// SignWikiCommit determines if we should sign the commits to this repository wiki
+func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, string, *git.Signature, error) {
+ repoWikiPath := repo.WikiPath()
+ rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
+ signingKey, sig := SigningKey(ctx, repoWikiPath)
+ if signingKey == "" {
+ return false, "", nil, &ErrWontSign{noKey}
+ }
+
+Loop:
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, "", nil, &ErrWontSign{never}
+ case always:
+ break Loop
+ case pubkey:
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
+ if err != nil {
+ return false, "", nil, err
+ }
+ if len(keys) == 0 {
+ return false, "", nil, &ErrWontSign{pubkey}
+ }
+ case twofa:
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ return false, "", nil, err
+ }
+ if twofaModel == nil {
+ return false, "", nil, &ErrWontSign{twofa}
+ }
+ case parentSigned:
+ gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
+ if err != nil {
+ return false, "", nil, err
+ }
+ defer gitRepo.Close()
+ commit, err := gitRepo.GetCommit("HEAD")
+ if err != nil {
+ return false, "", nil, err
+ }
+ if commit.Signature == nil {
+ return false, "", nil, &ErrWontSign{parentSigned}
+ }
+ verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ if !verification.Verified {
+ return false, "", nil, &ErrWontSign{parentSigned}
+ }
+ }
+ }
+ return true, signingKey, sig, nil
+}
+
+// SignCRUDAction determines if we should sign a CRUD commit to this repository
+func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
+ rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
+ signingKey, sig := SigningKey(ctx, repoPath)
+ if signingKey == "" {
+ return false, "", nil, &ErrWontSign{noKey}
+ }
+
+Loop:
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, "", nil, &ErrWontSign{never}
+ case always:
+ break Loop
+ case pubkey:
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
+ if err != nil {
+ return false, "", nil, err
+ }
+ if len(keys) == 0 {
+ return false, "", nil, &ErrWontSign{pubkey}
+ }
+ case twofa:
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ return false, "", nil, err
+ }
+ if twofaModel == nil {
+ return false, "", nil, &ErrWontSign{twofa}
+ }
+ case parentSigned:
+ gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
+ if err != nil {
+ return false, "", nil, err
+ }
+ defer gitRepo.Close()
+ commit, err := gitRepo.GetCommit(parentCommit)
+ if err != nil {
+ return false, "", nil, err
+ }
+ if commit.Signature == nil {
+ return false, "", nil, &ErrWontSign{parentSigned}
+ }
+ verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ if !verification.Verified {
+ return false, "", nil, &ErrWontSign{parentSigned}
+ }
+ }
+ }
+ return true, signingKey, sig, nil
+}
+
+// SignMerge determines if we should sign a PR merge commit to the base repository
+func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ log.Error("Unable to get Base Repo for pull request")
+ return false, "", nil, err
+ }
+ repo := pr.BaseRepo
+
+ signingKey, signer := SigningKey(ctx, repo.RepoPath())
+ if signingKey == "" {
+ return false, "", nil, &ErrWontSign{noKey}
+ }
+ rules := signingModeFromStrings(setting.Repository.Signing.Merges)
+
+ var gitRepo *git.Repository
+ var err error
+
+Loop:
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, "", nil, &ErrWontSign{never}
+ case always:
+ break Loop
+ case pubkey:
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
+ if err != nil {
+ return false, "", nil, err
+ }
+ if len(keys) == 0 {
+ return false, "", nil, &ErrWontSign{pubkey}
+ }
+ case twofa:
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ return false, "", nil, err
+ }
+ if twofaModel == nil {
+ return false, "", nil, &ErrWontSign{twofa}
+ }
+ case approved:
+ protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
+ if err != nil {
+ return false, "", nil, err
+ }
+ if protectedBranch == nil {
+ return false, "", nil, &ErrWontSign{approved}
+ }
+ if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
+ return false, "", nil, &ErrWontSign{approved}
+ }
+ case baseSigned:
+ if gitRepo == nil {
+ gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
+ if err != nil {
+ return false, "", nil, err
+ }
+ defer gitRepo.Close()
+ }
+ commit, err := gitRepo.GetCommit(baseCommit)
+ if err != nil {
+ return false, "", nil, err
+ }
+ verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ if !verification.Verified {
+ return false, "", nil, &ErrWontSign{baseSigned}
+ }
+ case headSigned:
+ if gitRepo == nil {
+ gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
+ if err != nil {
+ return false, "", nil, err
+ }
+ defer gitRepo.Close()
+ }
+ commit, err := gitRepo.GetCommit(headCommit)
+ if err != nil {
+ return false, "", nil, err
+ }
+ verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ if !verification.Verified {
+ return false, "", nil, &ErrWontSign{headSigned}
+ }
+ case commitsSigned:
+ if gitRepo == nil {
+ gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
+ if err != nil {
+ return false, "", nil, err
+ }
+ defer gitRepo.Close()
+ }
+ commit, err := gitRepo.GetCommit(headCommit)
+ if err != nil {
+ return false, "", nil, err
+ }
+ verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ if !verification.Verified {
+ return false, "", nil, &ErrWontSign{commitsSigned}
+ }
+ // need to work out merge-base
+ mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
+ if err != nil {
+ return false, "", nil, err
+ }
+ commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
+ if err != nil {
+ return false, "", nil, err
+ }
+ for _, commit := range commitList {
+ verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ if !verification.Verified {
+ return false, "", nil, &ErrWontSign{commitsSigned}
+ }
+ }
+ }
+ }
+ return true, signingKey, signer, nil
+}
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
new file mode 100644
index 0000000..83d7eda
--- /dev/null
+++ b/services/asymkey/ssh_key.go
@@ -0,0 +1,50 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "context"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
+func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err error) {
+ key, err := asymkey_model.GetPublicKeyByID(ctx, id)
+ if err != nil {
+ return err
+ }
+
+ // Check if user has access to delete this key.
+ if !doer.IsAdmin && doer.ID != key.OwnerID {
+ return asymkey_model.ErrKeyAccessDenied{
+ UserID: doer.ID,
+ KeyID: key.ID,
+ Note: "public",
+ }
+ }
+
+ dbCtx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if _, err = db.DeleteByID[asymkey_model.PublicKey](dbCtx, id); err != nil {
+ return err
+ }
+
+ if err = committer.Commit(); err != nil {
+ return err
+ }
+ committer.Close()
+
+ if key.Type == asymkey_model.KeyTypePrincipal {
+ return asymkey_model.RewriteAllPrincipalKeys(ctx)
+ }
+
+ return asymkey_model.RewriteAllPublicKeys(ctx)
+}
diff --git a/services/asymkey/ssh_key_test.go b/services/asymkey/ssh_key_test.go
new file mode 100644
index 0000000..d667a02
--- /dev/null
+++ b/services/asymkey/ssh_key_test.go
@@ -0,0 +1,88 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "testing"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAddLdapSSHPublicKeys(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ s := &auth.Source{ID: 1}
+
+ testCases := []struct {
+ keyString string
+ number int
+ keyContents []string
+ }{
+ {
+ keyString: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+ number: 1,
+ keyContents: []string{
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+ },
+ },
+ {
+ keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
+ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
+ number: 2,
+ keyContents: []string{
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+ "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
+ },
+ },
+ {
+ keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
+# comment asmdna,ndp
+ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
+ number: 2,
+ keyContents: []string{
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+ "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
+ },
+ },
+ {
+ keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
+382488320jasdj1lasmva/vasodifipi4193-fksma.cm
+ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
+ number: 2,
+ keyContents: []string{
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+ "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
+ },
+ },
+ }
+
+ for i, kase := range testCases {
+ s.ID = int64(i) + 20
+ asymkey_model.AddPublicKeysBySource(db.DefaultContext, user, s, []string{kase.keyString})
+ keys, err := db.Find[asymkey_model.PublicKey](db.DefaultContext, asymkey_model.FindPublicKeyOptions{
+ OwnerID: user.ID,
+ LoginSourceID: s.ID,
+ })
+ require.NoError(t, err)
+ if err != nil {
+ continue
+ }
+ assert.Len(t, keys, kase.number)
+
+ for _, key := range keys {
+ assert.Contains(t, kase.keyContents, key.Content)
+ }
+ for _, key := range keys {
+ DeletePublicKey(db.DefaultContext, user, key.ID)
+ }
+ }
+}