code.gitea.io/gitea@v1.22.3/models/asymkey/ssh_key.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package asymkey 6 7 import ( 8 "context" 9 "fmt" 10 "strings" 11 "time" 12 13 "code.gitea.io/gitea/models/auth" 14 "code.gitea.io/gitea/models/db" 15 "code.gitea.io/gitea/models/perm" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/log" 18 "code.gitea.io/gitea/modules/timeutil" 19 "code.gitea.io/gitea/modules/util" 20 21 "golang.org/x/crypto/ssh" 22 "xorm.io/builder" 23 ) 24 25 // KeyType specifies the key type 26 type KeyType int 27 28 const ( 29 // KeyTypeUser specifies the user key 30 KeyTypeUser = iota + 1 31 // KeyTypeDeploy specifies the deploy key 32 KeyTypeDeploy 33 // KeyTypePrincipal specifies the authorized principal key 34 KeyTypePrincipal 35 ) 36 37 // PublicKey represents a user or deploy SSH public key. 38 type PublicKey struct { 39 ID int64 `xorm:"pk autoincr"` 40 OwnerID int64 `xorm:"INDEX NOT NULL"` 41 Name string `xorm:"NOT NULL"` 42 Fingerprint string `xorm:"INDEX NOT NULL"` 43 Content string `xorm:"MEDIUMTEXT NOT NULL"` 44 Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 2"` 45 Type KeyType `xorm:"NOT NULL DEFAULT 1"` 46 LoginSourceID int64 `xorm:"NOT NULL DEFAULT 0"` 47 48 CreatedUnix timeutil.TimeStamp `xorm:"created"` 49 UpdatedUnix timeutil.TimeStamp `xorm:"updated"` 50 HasRecentActivity bool `xorm:"-"` 51 HasUsed bool `xorm:"-"` 52 Verified bool `xorm:"NOT NULL DEFAULT false"` 53 } 54 55 func init() { 56 db.RegisterModel(new(PublicKey)) 57 } 58 59 // AfterLoad is invoked from XORM after setting the values of all fields of this object. 60 func (key *PublicKey) AfterLoad() { 61 key.HasUsed = key.UpdatedUnix > key.CreatedUnix 62 key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow() 63 } 64 65 // OmitEmail returns content of public key without email address. 66 func (key *PublicKey) OmitEmail() string { 67 return strings.Join(strings.Split(key.Content, " ")[:2], " ") 68 } 69 70 // AuthorizedString returns formatted public key string for authorized_keys file. 71 // 72 // TODO: Consider dropping this function 73 func (key *PublicKey) AuthorizedString() string { 74 return AuthorizedStringForKey(key) 75 } 76 77 func addKey(ctx context.Context, key *PublicKey) (err error) { 78 if len(key.Fingerprint) == 0 { 79 key.Fingerprint, err = CalcFingerprint(key.Content) 80 if err != nil { 81 return err 82 } 83 } 84 85 // Save SSH key. 86 if err = db.Insert(ctx, key); err != nil { 87 return err 88 } 89 90 return appendAuthorizedKeysToFile(key) 91 } 92 93 // AddPublicKey adds new public key to database and authorized_keys file. 94 func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64) (*PublicKey, error) { 95 log.Trace(content) 96 97 fingerprint, err := CalcFingerprint(content) 98 if err != nil { 99 return nil, err 100 } 101 102 ctx, committer, err := db.TxContext(ctx) 103 if err != nil { 104 return nil, err 105 } 106 defer committer.Close() 107 108 if err := checkKeyFingerprint(ctx, fingerprint); err != nil { 109 return nil, err 110 } 111 112 // Key name of same user cannot be duplicated. 113 has, err := db.GetEngine(ctx). 114 Where("owner_id = ? AND name = ?", ownerID, name). 115 Get(new(PublicKey)) 116 if err != nil { 117 return nil, err 118 } else if has { 119 return nil, ErrKeyNameAlreadyUsed{ownerID, name} 120 } 121 122 key := &PublicKey{ 123 OwnerID: ownerID, 124 Name: name, 125 Fingerprint: fingerprint, 126 Content: content, 127 Mode: perm.AccessModeWrite, 128 Type: KeyTypeUser, 129 LoginSourceID: authSourceID, 130 } 131 if err = addKey(ctx, key); err != nil { 132 return nil, fmt.Errorf("addKey: %w", err) 133 } 134 135 return key, committer.Commit() 136 } 137 138 // GetPublicKeyByID returns public key by given ID. 139 func GetPublicKeyByID(ctx context.Context, keyID int64) (*PublicKey, error) { 140 key := new(PublicKey) 141 has, err := db.GetEngine(ctx). 142 ID(keyID). 143 Get(key) 144 if err != nil { 145 return nil, err 146 } else if !has { 147 return nil, ErrKeyNotExist{keyID} 148 } 149 return key, nil 150 } 151 152 // SearchPublicKeyByContent searches content as prefix (leak e-mail part) 153 // and returns public key found. 154 func SearchPublicKeyByContent(ctx context.Context, content string) (*PublicKey, error) { 155 key := new(PublicKey) 156 has, err := db.GetEngine(ctx). 157 Where("content like ?", content+"%"). 158 Get(key) 159 if err != nil { 160 return nil, err 161 } else if !has { 162 return nil, ErrKeyNotExist{} 163 } 164 return key, nil 165 } 166 167 // SearchPublicKeyByContentExact searches content 168 // and returns public key found. 169 func SearchPublicKeyByContentExact(ctx context.Context, content string) (*PublicKey, error) { 170 key := new(PublicKey) 171 has, err := db.GetEngine(ctx). 172 Where("content = ?", content). 173 Get(key) 174 if err != nil { 175 return nil, err 176 } else if !has { 177 return nil, ErrKeyNotExist{} 178 } 179 return key, nil 180 } 181 182 type FindPublicKeyOptions struct { 183 db.ListOptions 184 OwnerID int64 185 Fingerprint string 186 KeyTypes []KeyType 187 NotKeytype KeyType 188 LoginSourceID int64 189 } 190 191 func (opts FindPublicKeyOptions) ToConds() builder.Cond { 192 cond := builder.NewCond() 193 if opts.OwnerID > 0 { 194 cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) 195 } 196 if opts.Fingerprint != "" { 197 cond = cond.And(builder.Eq{"fingerprint": opts.Fingerprint}) 198 } 199 if len(opts.KeyTypes) > 0 { 200 cond = cond.And(builder.In("`type`", opts.KeyTypes)) 201 } 202 if opts.NotKeytype > 0 { 203 cond = cond.And(builder.Neq{"`type`": opts.NotKeytype}) 204 } 205 if opts.LoginSourceID > 0 { 206 cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID}) 207 } 208 return cond 209 } 210 211 // UpdatePublicKeyUpdated updates public key use time. 212 func UpdatePublicKeyUpdated(ctx context.Context, id int64) error { 213 // Check if key exists before update as affected rows count is unreliable 214 // and will return 0 affected rows if two updates are made at the same time 215 if cnt, err := db.GetEngine(ctx).ID(id).Count(&PublicKey{}); err != nil { 216 return err 217 } else if cnt != 1 { 218 return ErrKeyNotExist{id} 219 } 220 221 _, err := db.GetEngine(ctx).ID(id).Cols("updated_unix").Update(&PublicKey{ 222 UpdatedUnix: timeutil.TimeStampNow(), 223 }) 224 if err != nil { 225 return err 226 } 227 return nil 228 } 229 230 // PublicKeysAreExternallyManaged returns whether the provided KeyID represents an externally managed Key 231 func PublicKeysAreExternallyManaged(ctx context.Context, keys []*PublicKey) ([]bool, error) { 232 sourceCache := make(map[int64]*auth.Source, len(keys)) 233 externals := make([]bool, len(keys)) 234 235 for i, key := range keys { 236 if key.LoginSourceID == 0 { 237 externals[i] = false 238 continue 239 } 240 241 source, ok := sourceCache[key.LoginSourceID] 242 if !ok { 243 var err error 244 source, err = auth.GetSourceByID(ctx, key.LoginSourceID) 245 if err != nil { 246 if auth.IsErrSourceNotExist(err) { 247 externals[i] = false 248 sourceCache[key.LoginSourceID] = &auth.Source{ 249 ID: key.LoginSourceID, 250 } 251 continue 252 } 253 return nil, err 254 } 255 } 256 257 if sshKeyProvider, ok := source.Cfg.(auth.SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() { 258 // Disable setting SSH keys for this user 259 externals[i] = true 260 } 261 } 262 263 return externals, nil 264 } 265 266 // PublicKeyIsExternallyManaged returns whether the provided KeyID represents an externally managed Key 267 func PublicKeyIsExternallyManaged(ctx context.Context, id int64) (bool, error) { 268 key, err := GetPublicKeyByID(ctx, id) 269 if err != nil { 270 return false, err 271 } 272 if key.LoginSourceID == 0 { 273 return false, nil 274 } 275 source, err := auth.GetSourceByID(ctx, key.LoginSourceID) 276 if err != nil { 277 if auth.IsErrSourceNotExist(err) { 278 return false, nil 279 } 280 return false, err 281 } 282 if sshKeyProvider, ok := source.Cfg.(auth.SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() { 283 // Disable setting SSH keys for this user 284 return true, nil 285 } 286 return false, nil 287 } 288 289 // deleteKeysMarkedForDeletion returns true if ssh keys needs update 290 func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, error) { 291 // Start session 292 ctx, committer, err := db.TxContext(ctx) 293 if err != nil { 294 return false, err 295 } 296 defer committer.Close() 297 298 // Delete keys marked for deletion 299 var sshKeysNeedUpdate bool 300 for _, KeyToDelete := range keys { 301 key, err := SearchPublicKeyByContent(ctx, KeyToDelete) 302 if err != nil { 303 log.Error("SearchPublicKeyByContent: %v", err) 304 continue 305 } 306 if _, err = db.DeleteByID[PublicKey](ctx, key.ID); err != nil { 307 log.Error("DeleteByID[PublicKey]: %v", err) 308 continue 309 } 310 sshKeysNeedUpdate = true 311 } 312 313 if err := committer.Commit(); err != nil { 314 return false, err 315 } 316 317 return sshKeysNeedUpdate, nil 318 } 319 320 // AddPublicKeysBySource add a users public keys. Returns true if there are changes. 321 func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool { 322 var sshKeysNeedUpdate bool 323 for _, sshKey := range sshPublicKeys { 324 var err error 325 found := false 326 keys := []byte(sshKey) 327 loop: 328 for len(keys) > 0 && err == nil { 329 var out ssh.PublicKey 330 // We ignore options as they are not relevant to Gitea 331 out, _, _, keys, err = ssh.ParseAuthorizedKey(keys) 332 if err != nil { 333 break loop 334 } 335 found = true 336 marshalled := string(ssh.MarshalAuthorizedKey(out)) 337 marshalled = marshalled[:len(marshalled)-1] 338 sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out)) 339 340 if _, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID); err != nil { 341 if IsErrKeyAlreadyExist(err) { 342 log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name) 343 } else { 344 log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err) 345 } 346 } else { 347 log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name) 348 sshKeysNeedUpdate = true 349 } 350 } 351 if !found && err != nil { 352 log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey) 353 } 354 } 355 return sshKeysNeedUpdate 356 } 357 358 // SynchronizePublicKeys updates a users public keys. Returns true if there are changes. 359 func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool { 360 var sshKeysNeedUpdate bool 361 362 log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name) 363 364 // Get Public Keys from DB with current LDAP source 365 var giteaKeys []string 366 keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{ 367 OwnerID: usr.ID, 368 LoginSourceID: s.ID, 369 }) 370 if err != nil { 371 log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, err) 372 } 373 374 for _, v := range keys { 375 giteaKeys = append(giteaKeys, v.OmitEmail()) 376 } 377 378 // Process the provided keys to remove duplicates and name part 379 var providedKeys []string 380 for _, v := range sshPublicKeys { 381 sshKeySplit := strings.Split(v, " ") 382 if len(sshKeySplit) > 1 { 383 key := strings.Join(sshKeySplit[:2], " ") 384 if !util.SliceContainsString(providedKeys, key) { 385 providedKeys = append(providedKeys, key) 386 } 387 } 388 } 389 390 // Check if Public Key sync is needed 391 if util.SliceSortedEqual(giteaKeys, providedKeys) { 392 log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys)) 393 return false 394 } 395 log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys)) 396 397 // Add new Public SSH Keys that doesn't already exist in DB 398 var newKeys []string 399 for _, key := range providedKeys { 400 if !util.SliceContainsString(giteaKeys, key) { 401 newKeys = append(newKeys, key) 402 } 403 } 404 if AddPublicKeysBySource(ctx, usr, s, newKeys) { 405 sshKeysNeedUpdate = true 406 } 407 408 // Mark keys from DB that no longer exist in the source for deletion 409 var giteaKeysToDelete []string 410 for _, giteaKey := range giteaKeys { 411 if !util.SliceContainsString(providedKeys, giteaKey) { 412 log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey) 413 giteaKeysToDelete = append(giteaKeysToDelete, giteaKey) 414 } 415 } 416 417 // Delete keys from DB that no longer exist in the source 418 needUpd, err := deleteKeysMarkedForDeletion(ctx, giteaKeysToDelete) 419 if err != nil { 420 log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err) 421 } 422 if needUpd { 423 sshKeysNeedUpdate = true 424 } 425 426 return sshKeysNeedUpdate 427 }