-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LDAP Public SSH Keys synchronization #1844
Changes from 7 commits
64312ab
d17981b
cc046c7
26a9205
d005086
e0da1d7
b6e2985
a20d4f1
1d4dbed
55424bf
922e76c
03690de
ec95606
71cd39d
eddb932
ab9f88b
056734f
83cd353
56453ac
027c832
9915566
23b343d
98a8083
934223d
615e9c5
45439fd
a199541
93f11ea
8b6f96c
0d8d954
cf6ba93
68a1dc7
68e206a
3f3ab5e
aff1346
b4876d3
dc85ddc
3ff65c7
3181cb9
f05fe63
ee1fcd9
3b75a05
9f10f71
237ae23
f3b4b31
32d8e0f
47cab1d
bc32826
cb13880
8a56bf3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright 2017 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package migrations | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/go-xorm/xorm" | ||
) | ||
|
||
func addLoginSourceLdapPublicSSHKeySyncEnabled(x *xorm.Engine) error { | ||
// LoginSource see models/login_source.go | ||
type LoginSource struct { | ||
IsLdapPublicSSHKeySyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see this field used any more, so this sync should be just replaced with added LoginSourceID column for PublicKey There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, right. I changed this behaviour, now the sync is enabled as soon as it's configured and there's no longer any option to enable/disable the sync from the app.ini file. I've just somehow forgotten that I did this, and to remove this part of the code as well. Grr :) Nice catch, thnx! |
||
} | ||
|
||
if err := x.Sync2(new(LoginSource)); err != nil { | ||
return fmt.Errorf("Sync2: %v", err) | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,13 +46,14 @@ const ( | |
|
||
// PublicKey represents a user or deploy SSH public key. | ||
type PublicKey struct { | ||
ID int64 `xorm:"pk autoincr"` | ||
OwnerID int64 `xorm:"INDEX NOT NULL"` | ||
Name string `xorm:"NOT NULL"` | ||
Fingerprint string `xorm:"NOT NULL"` | ||
Content string `xorm:"TEXT NOT NULL"` | ||
Mode AccessMode `xorm:"NOT NULL DEFAULT 2"` | ||
Type KeyType `xorm:"NOT NULL DEFAULT 1"` | ||
ID int64 `xorm:"pk autoincr"` | ||
OwnerID int64 `xorm:"INDEX NOT NULL"` | ||
Name string `xorm:"NOT NULL"` | ||
Fingerprint string `xorm:"NOT NULL"` | ||
Content string `xorm:"TEXT NOT NULL"` | ||
Mode AccessMode `xorm:"NOT NULL DEFAULT 2"` | ||
Type KeyType `xorm:"NOT NULL DEFAULT 1"` | ||
LoginSourceID int64 `xorm:"NULL DEFAULT NULL"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
Created time.Time `xorm:"-"` | ||
CreatedUnix int64 `xorm:"created"` | ||
|
@@ -393,7 +394,7 @@ func addKey(e Engine, key *PublicKey) (err error) { | |
} | ||
|
||
// AddPublicKey adds new public key to database and authorized_keys file. | ||
func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) { | ||
func AddPublicKey(ownerID int64, name, content string, LoginSourceID int64) (*PublicKey, error) { | ||
log.Trace(content) | ||
|
||
fingerprint, err := calcFingerprint(content) | ||
|
@@ -422,12 +423,13 @@ func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) { | |
} | ||
|
||
key := &PublicKey{ | ||
OwnerID: ownerID, | ||
Name: name, | ||
Fingerprint: fingerprint, | ||
Content: content, | ||
Mode: AccessModeWrite, | ||
Type: KeyTypeUser, | ||
OwnerID: ownerID, | ||
Name: name, | ||
Fingerprint: fingerprint, | ||
Content: content, | ||
Mode: AccessModeWrite, | ||
Type: KeyTypeUser, | ||
LoginSourceID: LoginSourceID, | ||
} | ||
if err = addKey(sess, key); err != nil { | ||
return nil, fmt.Errorf("addKey: %v", err) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ import ( | |
"image/png" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
"time" | ||
"unicode/utf8" | ||
|
@@ -1355,6 +1356,138 @@ func GetWatchedRepos(userID int64, private bool) ([]*Repository, error) { | |
return repos, nil | ||
} | ||
|
||
// existsInSlice returns true if string exists in slice | ||
func existsInSlice(target string, slice []string) bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please move |
||
i := sort.Search(len(slice), | ||
func(i int) bool { return slice[i] == target }) | ||
return i < len(slice) | ||
} | ||
|
||
// IsEqualSlice returns true if slices are equal | ||
func IsEqualSlice(target []string, source []string) bool { | ||
if len(target) != len(source) { | ||
return false | ||
} | ||
|
||
if (target == nil) != (source == nil) { | ||
return false | ||
} | ||
|
||
sort.Strings(target) | ||
sort.Strings(source) | ||
|
||
for i, v := range target { | ||
if v != source[i] { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
// deleteKeysMarkedForDeletion returns true if ssh keys needs update | ||
func deleteKeysMarkedForDeletion(keys []string) (sshKeysNeedUpdate bool, err error) { | ||
// Start session | ||
sess := x.NewSession() | ||
defer sess.Close() | ||
if err = sess.Begin(); err != nil { | ||
return false, err | ||
} | ||
|
||
// Delete keys marked for deletion | ||
for _, KeyToDelete := range keys { | ||
key, err := SearchPublicKeyByContent(KeyToDelete) | ||
err = deletePublicKeys(sess, key.ID) | ||
if err != nil { | ||
sshKeysNeedUpdate = false | ||
log.Trace("deleteKeysMarkedForDeletion: Error: %v", key) | ||
} | ||
sshKeysNeedUpdate = true | ||
} | ||
|
||
return sshKeysNeedUpdate, sess.Commit() | ||
} | ||
|
||
func addLdapSSHPublicKeys(s *LoginSource, usr *User, SSHPublicKeys []string) (sshKeysNeedUpdate bool, err error) { | ||
for _, LDAPPublicSSHKey := range SSHPublicKeys { | ||
LDAPPublicSSHKeyName := strings.Join([]string{s.Name, LDAPPublicSSHKey[0:40]}, "-") | ||
_, err := AddPublicKey(usr.ID, LDAPPublicSSHKeyName, LDAPPublicSSHKey, s.ID) | ||
if err != nil { | ||
log.Error(4, "addLdapSSHPublicKeys[%s]: Error adding LDAP Public SSH Key for user %s: %v", s.Name, usr.Name, err) | ||
} else { | ||
log.Trace("addLdapSSHPublicKeys[%s]: Added LDAP Public SSH Key for user %s: %v", s.Name, usr.Name, LDAPPublicSSHKey) | ||
sshKeysNeedUpdate = true | ||
} | ||
|
||
} | ||
return sshKeysNeedUpdate, err | ||
} | ||
|
||
func synchronizeLdapSSHPublicKeys(s *LoginSource, SSHPublicKeys []string, usr *User) (sshKeysNeedUpdate bool, err error) { | ||
log.Trace("synchronizeLdapSSHPublicKeys[%s]: Handling LDAP Public SSH Key synchronization for user %s", s.Name, usr.Name) | ||
|
||
// Get Public Keys from DB with LDAP source | ||
var giteaKeys []string | ||
keys, err := ListPublicKeys(usr.ID) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just only load all public keys where login_source_id > 0? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, that would be more efficient. Would you recommend modifying the current ListPublicKeys for this purpose (adding optional 'where login_source_id>x'), creating a new function or any other method to accomplish that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dnmgns Yes, I think you should do that for performance. |
||
if err != nil { | ||
log.Error(4, "synchronizeLdapSSHPublicKeys[%s]: Error listing LDAP Public SSH Keys for user %s: %v", s.Name, usr.Name, err) | ||
} | ||
|
||
// Get Public Keys from DB, which has been synchronized from LDAP | ||
for _, v := range keys { | ||
// If key was synced from LDAP, add it to list | ||
if v.LoginSourceID > 0 { | ||
giteaKeys = append(giteaKeys, v.OmitEmail()) | ||
} | ||
} | ||
sort.Strings(giteaKeys) | ||
|
||
// Get Public Keys from LDAP and skip duplicate keys | ||
var ldapKeys []string | ||
for _, v := range SSHPublicKeys { | ||
ldapKey := strings.Join(strings.Split(v, " ")[:2], " ") | ||
if !existsInSlice(ldapKey, ldapKeys) { | ||
ldapKeys = append(ldapKeys, ldapKey) | ||
} | ||
} | ||
sort.Strings(ldapKeys) | ||
|
||
// Check if Public Key sync is needed | ||
var giteaKeysToDelete []string | ||
if IsEqualSlice(giteaKeys, ldapKeys) { | ||
log.Trace("synchronizeLdapSSHPublicKeys[%s]: LDAP Public Keys are already in sync for %s (LDAP:%v/DB:%v)", s.Name, usr.Name, len(ldapKeys), len(giteaKeys)) | ||
} else { | ||
log.Trace("synchronizeLdapSSHPublicKeys[%s]: LDAP Public Key needs update for user %s (LDAP:%v/DB:%v)", s.Name, usr.Name, len(ldapKeys), len(giteaKeys)) | ||
// Delete giteaKeys that doesn't exist in ldapKeys and has been synced from LDAP earlier | ||
for _, giteaKey := range giteaKeys { | ||
if !existsInSlice(giteaKey, ldapKeys) { | ||
log.Trace("synchronizeLdapSSHPublicKeys[%s]: Marking LDAP Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey) | ||
giteaKeysToDelete = append(giteaKeysToDelete, giteaKey) | ||
} | ||
} | ||
} | ||
|
||
// Delete LDAP keys from DB that doesn't exist in LDAP | ||
sshKeysNeedUpdate, err = deleteKeysMarkedForDeletion(giteaKeysToDelete) | ||
if err != nil { | ||
log.Error(4, "synchronizeLdapSSHPublicKeys[%s]: Error deleting LDAP Public SSH Keys marked for deletion for user %s: %v", s.Name, usr.Name, err) | ||
} | ||
|
||
// Add LDAP Public SSH Keys that doesn't already exist in DB | ||
var newLdapSSHKeys []string | ||
for _, LDAPPublicSSHKey := range ldapKeys { | ||
if !existsInSlice(LDAPPublicSSHKey, giteaKeys) { | ||
newLdapSSHKeys = append(newLdapSSHKeys, LDAPPublicSSHKey) | ||
} | ||
} | ||
sshKeysNeedUpdate, err = addLdapSSHPublicKeys(s, usr, newLdapSSHKeys) | ||
if err != nil { | ||
log.Error(4, "synchronizeLdapSSHPublicKeys[%s]: Error updating LDAP Public SSH Keys for user %s: %v", s.Name, usr.Name, err) | ||
} | ||
|
||
return sshKeysNeedUpdate, err | ||
} | ||
|
||
// SyncExternalUsers is used to synchronize users with external authorization source | ||
func SyncExternalUsers() { | ||
if !taskStatusTable.StartIfNotRunning(syncExternalUsers) { | ||
|
@@ -1376,10 +1509,13 @@ func SyncExternalUsers() { | |
if !s.IsActived || !s.IsSyncEnabled { | ||
continue | ||
} | ||
|
||
if s.IsLDAP() { | ||
log.Trace("Doing: SyncExternalUsers[%s]", s.Name) | ||
|
||
var existingUsers []int64 | ||
var isAttributeSSHPublicKeySet = len(strings.TrimSpace(s.LDAP().AttributeSSHPublicKey)) > 0 | ||
var sshKeysNeedUpdate bool | ||
|
||
// Find all users with this login type | ||
var users []User | ||
|
@@ -1388,7 +1524,6 @@ func SyncExternalUsers() { | |
Find(&users) | ||
|
||
sr := s.LDAP().SearchEntries() | ||
|
||
for _, su := range sr { | ||
if len(su.Username) == 0 { | ||
continue | ||
|
@@ -1425,11 +1560,28 @@ func SyncExternalUsers() { | |
} | ||
|
||
err = CreateUser(usr) | ||
|
||
if err != nil { | ||
log.Error(4, "SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err) | ||
} else if isAttributeSSHPublicKeySet { | ||
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", s.Name, usr.Name) | ||
sshKeysNeedUpdate, err = addLdapSSHPublicKeys(s, usr, su.SSHPublicKey) | ||
if err != nil { | ||
log.Error(4, "SyncExternalUsers[%s]: Error adding LDAP Public SSH Keys for user %s: %v", s.Name, su.Username, err) | ||
} | ||
} | ||
} else if updateExisting { | ||
existingUsers = append(existingUsers, usr.ID) | ||
|
||
err = UpdateUserCols(usr, "full_name", "email", "is_admin", "is_active") | ||
if err != nil { | ||
log.Error(4, "SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err) | ||
} | ||
|
||
if isAttributeSSHPublicKeySet { | ||
synchronizeLdapSSHPublicKeys(s, su.SSHPublicKey, usr) | ||
} | ||
|
||
// Check if user data has changed | ||
if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) || | ||
strings.ToLower(usr.Email) != strings.ToLower(su.Mail) || | ||
|
@@ -1446,14 +1598,19 @@ func SyncExternalUsers() { | |
} | ||
usr.IsActive = true | ||
|
||
err = UpdateUserCols(usr, "full_name", "email", "is_admin", "is_active") | ||
err = UpdateUser(usr) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why update all the columns? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if err != nil { | ||
log.Error(4, "SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed | ||
if isAttributeSSHPublicKeySet && sshKeysNeedUpdate { | ||
RewriteAllPublicKeys() | ||
} | ||
|
||
// Deactivate users not present in LDAP | ||
if updateExisting { | ||
for _, usr := range users { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to add also x.Sync2 for
PublicKey
table