Skip to content

Allow LDAP Sources to provide Avatars #16851

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

Merged
merged 17 commits into from
Sep 27, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/admin_auth_ldap.go
Original file line number Diff line number Diff line change
@@ -93,6 +93,10 @@ var (
Name: "skip-local-2fa",
Usage: "Set to true to skip local 2fa for users authenticated by this source",
},
cli.StringFlag{
Name: "avatar-attribute",
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
},
}

ldapBindDnCLIFlags = append(commonLdapCLIFlags,
@@ -234,6 +238,9 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("public-ssh-key-attribute") {
config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
}
if c.IsSet("avatar-attribute") {
config.AttributeAvatar = c.String("avatar-attribute")
}
if c.IsSet("page-size") {
config.SearchPageSize = uint32(c.Uint("page-size"))
}
8 changes: 8 additions & 0 deletions cmd/admin_auth_ldap_test.go
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ func TestAddLdapBindDn(t *testing.T) {
"--surname-attribute", "sn-bind full",
"--email-attribute", "mail-bind full",
"--public-ssh-key-attribute", "publickey-bind full",
"--avatar-attribute", "avatar-bind full",
"--bind-dn", "cn=readonly,dc=full-domain-bind,dc=org",
"--bind-password", "secret-bind-full",
"--attributes-in-bind",
@@ -71,6 +72,7 @@ func TestAddLdapBindDn(t *testing.T) {
AttributeMail: "mail-bind full",
AttributesInBind: true,
AttributeSSHPublicKey: "publickey-bind full",
AttributeAvatar: "avatar-bind full",
SearchPageSize: 99,
Filter: "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
@@ -269,6 +271,7 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--surname-attribute", "sn-simple full",
"--email-attribute", "mail-simple full",
"--public-ssh-key-attribute", "publickey-simple full",
"--avatar-attribute", "avatar-simple full",
"--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
},
loginSource: &login.Source{
@@ -288,6 +291,7 @@ func TestAddLdapSimpleAuth(t *testing.T) {
AttributeSurname: "sn-simple full",
AttributeMail: "mail-simple full",
AttributeSSHPublicKey: "publickey-simple full",
AttributeAvatar: "avatar-simple full",
Filter: "(&(objectClass=posixAccount)(full-simple-cn=%s))",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
@@ -501,6 +505,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--surname-attribute", "sn-bind full",
"--email-attribute", "mail-bind full",
"--public-ssh-key-attribute", "publickey-bind full",
"--avatar-attribute", "avatar-bind full",
"--bind-dn", "cn=readonly,dc=full-domain-bind,dc=org",
"--bind-password", "secret-bind-full",
"--synchronize-users",
@@ -534,6 +539,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
AttributeMail: "mail-bind full",
AttributesInBind: false,
AttributeSSHPublicKey: "publickey-bind full",
AttributeAvatar: "avatar-bind full",
SearchPageSize: 99,
Filter: "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
@@ -932,6 +938,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
"--surname-attribute", "sn-simple full",
"--email-attribute", "mail-simple full",
"--public-ssh-key-attribute", "publickey-simple full",
"--avatar-attribute", "avatar-simple full",
"--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
},
id: 7,
@@ -952,6 +959,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
AttributeSurname: "sn-simple full",
AttributeMail: "mail-simple full",
AttributeSSHPublicKey: "publickey-simple full",
AttributeAvatar: "avatar-simple full",
Filter: "(&(objectClass=posixAccount)(full-simple-cn=%s))",
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
4 changes: 4 additions & 0 deletions docs/content/doc/usage/command-line.en-us.md
Original file line number Diff line number Diff line change
@@ -152,6 +152,7 @@ Admin operations:
- `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
- `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address. Required.
- `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
- `--avatar-attribute value`: The attribute of the user’s LDAP record containing the user’s avatar.
- `--bind-dn value`: The DN to bind to the LDAP server with when searching for the user.
- `--bind-password value`: The password for the Bind DN, if any.
- `--attributes-in-bind`: Fetch attributes in bind DN context.
@@ -177,6 +178,7 @@ Admin operations:
- `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
- `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address.
- `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
- `--avatar-attribute value`: The attribute of the user’s LDAP record containing the user’s avatar.
- `--bind-dn value`: The DN to bind to the LDAP server with when searching for the user.
- `--bind-password value`: The password for the Bind DN, if any.
- `--attributes-in-bind`: Fetch attributes in bind DN context.
@@ -202,6 +204,7 @@ Admin operations:
- `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
- `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address. Required.
- `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
- `--avatar-attribute value`: The attribute of the user’s LDAP record containing the user’s avatar.
- `--user-dn value`: The user’s DN. Required.
- Examples:
- `gitea admin auth add-ldap-simple --name ldap --security-protocol unencrypted --host mydomain.org --port 389 --user-dn "cn=%s,ou=Users,dc=mydomain,dc=org" --user-filter "(&(objectClass=posixAccount)(cn=%s))" --email-attribute mail`
@@ -223,6 +226,7 @@ Admin operations:
- `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
- `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address.
- `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
- `--avatar-attribute value`: The attribute of the user’s LDAP record containing the user’s avatar.
- `--user-dn value`: The user’s DN.
- Examples:
- `gitea admin auth update-ldap-simple --id 1 --name "my ldap auth source"`
9 changes: 9 additions & 0 deletions models/user_avatar.go
Original file line number Diff line number Diff line change
@@ -153,6 +153,15 @@ func (u *User) UploadAvatar(data []byte) error {
return sess.Commit()
}

// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data
func (u *User) IsUploadAvatarChanged(data []byte) bool {
if !u.UseCustomAvatar || len(u.Avatar) == 0 {
return true
}
avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data)))))
return u.Avatar != avatarID
}

// DeleteAvatar deletes the user's custom avatar.
func (u *User) DeleteAvatar() error {
aPath := u.CustomAvatarRelativePath()
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
@@ -2421,6 +2421,7 @@ auths.attribute_name = First Name Attribute
auths.attribute_surname = Surname Attribute
auths.attribute_mail = Email Attribute
auths.attribute_ssh_public_key = Public SSH Key Attribute
auths.attribute_avatar = Avatar Attribute
auths.attributes_in_bind = Fetch Attributes in Bind DN Context
auths.allow_deactivate_all = Allow an empty search result to deactivate all users
auths.use_paged_search = Use Paged Search
1 change: 1 addition & 0 deletions routers/web/admin/auths.go
Original file line number Diff line number Diff line change
@@ -136,6 +136,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
AttributeMail: form.AttributeMail,
AttributesInBind: form.AttributesInBind,
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
AttributeAvatar: form.AttributeAvatar,
SearchPageSize: pageSize,
Filter: form.Filter,
GroupsEnabled: form.GroupsEnabled,
1 change: 1 addition & 0 deletions services/auth/source/ldap/source.go
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ type Source struct {
AttributeMail string // E-mail attribute
AttributesInBind bool // fetch attributes in bind context (not user)
AttributeSSHPublicKey string // LDAP SSH Public Key attribute
AttributeAvatar string
SearchPageSize uint32 // Search with paging page size
Filter string // Query filter to validate entry
AdminFilter string // Query filter to check if user is admin
4 changes: 4 additions & 0 deletions services/auth/source/ldap/source_authenticate.go
Original file line number Diff line number Diff line change
@@ -96,6 +96,10 @@ func (source *Source) Authenticate(user *models.User, userName, password string)
err = models.RewriteAllPublicKeys()
}

if err == nil && len(source.AttributeAvatar) > 0 {
_ = user.UploadAvatar(sr.Avatar)
}

return user, err
}

27 changes: 22 additions & 5 deletions services/auth/source/ldap/source_search.go
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ type SearchResult struct {
IsAdmin bool // if user is administrator
IsRestricted bool // if user is restricted
LowerName string // Lowername
Avatar []byte
}

func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -266,7 +267,8 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
return nil
}

var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0

attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
if len(strings.TrimSpace(ls.UserUID)) > 0 {
@@ -275,8 +277,11 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey)
}
if isAtributeAvatarSet {
attribs = append(attribs, ls.AttributeAvatar)
}

log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.UserUID, userFilter, userDN)
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.AttributeAvatar, ls.UserUID, userFilter, userDN)
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
@@ -296,6 +301,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
}

var sshPublicKey []string
var Avatar []byte

username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
@@ -363,6 +369,10 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
}
}

if isAtributeAvatarSet {
Avatar = sr.Entries[0].GetRawAttributeValue(ls.AttributeAvatar)
}

return &SearchResult{
LowerName: strings.ToLower(username),
Username: username,
@@ -372,6 +382,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
SSHPublicKey: sshPublicKey,
IsAdmin: isAdmin,
IsRestricted: isRestricted,
Avatar: Avatar,
}
}

@@ -403,14 +414,18 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {

userFilter := fmt.Sprintf(ls.Filter, "*")

var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0

attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey)
}
if isAtributeAvatarSet {
attribs = append(attribs, ls.AttributeAvatar)
}

log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, userFilter, ls.UserBase)
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.AttributeAvatar, userFilter, ls.UserBase)
search := ldap.NewSearchRequest(
ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
@@ -442,8 +457,10 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
if isAttributeSSHPublicKeySet {
result[i].SSHPublicKey = v.GetAttributeValues(ls.AttributeSSHPublicKey)
}
if isAtributeAvatarSet {
result[i].Avatar = v.GetRawAttributeValue(ls.AttributeAvatar)
}
result[i].LowerName = strings.ToLower(result[i].Username)

}

return result, nil
15 changes: 14 additions & 1 deletion services/auth/source/ldap/source_sync.go
Original file line number Diff line number Diff line change
@@ -112,12 +112,18 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {

if err != nil {
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.loginSource.Name, su.Username, err)
} else if isAttributeSSHPublicKeySet {
}

if err == nil && isAttributeSSHPublicKeySet {
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.loginSource.Name, usr.Name)
if models.AddPublicKeysBySource(usr, source.loginSource, su.SSHPublicKey) {
sshKeysNeedUpdate = true
}
}

if err == nil && len(source.AttributeAvatar) > 0 {
_ = usr.UploadAvatar(su.Avatar)
}
} else if updateExisting {
// Synchronize SSH Public Key if that attribute is set
if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
@@ -150,6 +156,13 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.loginSource.Name, usr.Name, err)
}
}

if usr.IsUploadAvatarChanged(su.Avatar) {
if err == nil && len(source.AttributeAvatar) > 0 {
_ = usr.UploadAvatar(su.Avatar)
}

}
}
}

1 change: 1 addition & 0 deletions services/forms/auth_form.go
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ type AuthenticationForm struct {
AttributeSurname string
AttributeMail string
AttributeSSHPublicKey string
AttributeAvatar string
AttributesInBind bool
UsePagedSearch bool
SearchPageSize int
4 changes: 4 additions & 0 deletions templates/admin/auth/edit.tmpl
Original file line number Diff line number Diff line change
@@ -104,6 +104,10 @@
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
<input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="e.g. SshPublicKey">
</div>
<div class="field">
<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label>
<input id="attribute_avatar" name="attribute_avatar" value="{{$cfg.AttributeAvatar}}" placeholder="e.g. jpegPhoto">
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
4 changes: 4 additions & 0 deletions templates/admin/auth/source/ldap.tmpl
Original file line number Diff line number Diff line change
@@ -76,6 +76,10 @@
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
<input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="e.g. SshPublicKey">
</div>
<div class="field">
<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label>
<input id="attribute_avatar" name="attribute_avatar" value="{{.attribute_avatar}}" placeholder="e.g. jpegPhoto">
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>