Skip to content
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

Allow LDAP Sources to provide Avatars #16851

Merged
merged 17 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from 13 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
Expand Up @@ -89,6 +89,10 @@ var (
Name: "public-ssh-key-attribute",
Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
},
cli.StringFlag{
Name: "avatar-attribute",
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
},
}

ldapBindDnCLIFlags = append(commonLdapCLIFlags,
Expand Down Expand Up @@ -230,6 +234,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"))
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/admin_auth_ldap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)",
Expand Down Expand Up @@ -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: &models.LoginSource{
Expand All @@ -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)",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)",
Expand Down Expand Up @@ -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,
Expand All @@ -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)",
Expand Down
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
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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`
Expand All @@ -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"`
Expand Down
6 changes: 6 additions & 0 deletions models/user_avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ 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 {
avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data)))))
return !u.UseCustomAvatar || u.Avatar != avatarID
}

// DeleteAvatar deletes the user's custom avatar.
func (u *User) DeleteAvatar() error {
aPath := u.CustomAvatarRelativePath()
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions routers/web/admin/auths.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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,
Expand Down
1 change: 1 addition & 0 deletions services/auth/source/ldap/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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
Expand Down
4 changes: 4 additions & 0 deletions services/auth/source/ldap/source_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,9 @@ func (source *Source) Authenticate(user *models.User, login, password string) (*
err = models.RewriteAllPublicKeys()
}

if err == nil && source.AttributeAvatar != "" {
_ = 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
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -372,6 +382,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
SSHPublicKey: sshPublicKey,
IsAdmin: isAdmin,
IsRestricted: isRestricted,
Avatar: Avatar,
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion services/auth/source/ldap/source_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 && source.AttributeAvatar != "" {
lafriks marked this conversation as resolved.
Show resolved Hide resolved
_ = 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) {
Expand Down Expand Up @@ -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 && source.AttributeAvatar != "" {
lafriks marked this conversation as resolved.
Show resolved Hide resolved
_ = usr.UploadAvatar(su.Avatar)
}

}
}
}

Expand Down
1 change: 1 addition & 0 deletions services/forms/auth_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type AuthenticationForm struct {
AttributeSurname string
AttributeMail string
AttributeSSHPublicKey string
AttributeAvatar string
AttributesInBind bool
UsePagedSearch bool
SearchPageSize int
Expand Down
4 changes: 4 additions & 0 deletions templates/admin/auth/edit.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
4 changes: 4 additions & 0 deletions templates/admin/auth/source/ldap.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down