grim/hgkeeper

8556579d5c27
Parents ff3e6be55b62
Children 337882c91d45
Add support to fetch username from LDAP

Reviewed at https://reviews.imfreedom.org/r/2450/
--- a/access/access.go Mon Jul 03 23:55:09 2023 -0500
+++ b/access/access.go Mon Jul 03 23:57:37 2023 -0500
@@ -26,11 +26,16 @@
repositories map[string]string
)
-func Setup(repositoriesPath, adminRepo string) error {
+func Setup(repositoriesPath, adminRepo, ldapConfig string) error {
reposPath = repositoriesPath
adminRepoName = adminRepo
adminRepoPath = filepath.Join(reposPath, adminRepo)
+ if err := refreshLdapConfig(ldapConfig); err != nil {
+ zap.S().Error("cannot load ldap config")
+ return err
+ }
+
configPath, err := os.CreateTemp("", "hgkeeper-hgweb-access-*.config")
if err != nil {
return err
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/access/ldap.go Mon Jul 03 23:57:37 2023 -0500
@@ -0,0 +1,96 @@
+package access
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/go-ldap/ldap/v3"
+ "go.uber.org/zap"
+)
+
+type LdapConfig struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ Address string `json:"address"`
+ BaseDN string `json:"baseDN"`
+ Filter string `json:"filter"`
+ Attribute string `json:"attribute"`
+}
+
+var (
+ ldapCfg *LdapConfig = nil
+)
+
+func refreshLdapConfig(path string) error {
+ if path == "" {
+ return nil
+ }
+
+ if _, err := os.Stat(path); err != nil {
+ return err
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ return json.Unmarshal(data, &ldapCfg)
+}
+
+func searchLdapPubkey(pubkey string) string {
+ if ldapCfg == nil {
+ return ""
+ }
+
+ connection, err := ldap.DialURL(ldapCfg.Address)
+ if err == nil {
+ defer connection.Close()
+ result, err := bindAndSearch(connection, pubkey)
+ if err == nil {
+ return result
+ }
+ }
+
+ zap.S().Warn(err)
+ return ""
+}
+
+func bindAndSearch(connection *ldap.Conn, pubkey string) (string, error) {
+ err := bind(connection)
+ if err != nil {
+ return "", err
+ }
+
+ request := ldap.NewSearchRequest(
+ ldapCfg.BaseDN,
+ ldap.ScopeWholeSubtree,
+ ldap.NeverDerefAliases,
+ 0,
+ 0,
+ false,
+ fmt.Sprintf(ldapCfg.Filter, pubkey),
+ []string{ldapCfg.Attribute},
+ nil,
+ )
+
+ result, err := connection.Search(request)
+ if err != nil {
+ return "", err
+ }
+
+ if len(result.Entries) != 1 {
+ return "", fmt.Errorf("cannot find unique user for: %s", pubkey)
+ }
+
+ return result.Entries[0].GetAttributeValue(ldapCfg.Attribute), nil
+}
+
+func bind(connection *ldap.Conn) error {
+ if ldapCfg.Password == "" {
+ return connection.UnauthenticatedBind(ldapCfg.Username)
+ }
+
+ return connection.Bind(ldapCfg.Username, ldapCfg.Password)
+}
--- a/access/users.go Mon Jul 03 23:55:09 2023 -0500
+++ b/access/users.go Mon Jul 03 23:57:37 2023 -0500
@@ -93,5 +93,11 @@
func UsernameFromPubkey(pubkey string) string {
keysLock.Lock()
defer keysLock.Unlock()
- return usernames[pubkey]
+ username := usernames[pubkey]
+
+ if username == "" {
+ username = searchLdapPubkey(pubkey)
+ }
+
+ return username
}
--- a/authorized_keys/command.go Mon Jul 03 23:55:09 2023 -0500
+++ b/authorized_keys/command.go Mon Jul 03 23:57:37 2023 -0500
@@ -14,7 +14,7 @@
}
func (c *Command) Run(g *globals.Globals) error {
- if err := access.Setup(g.ReposPath, g.AdminRepo); err != nil {
+ if err := access.Setup(g.ReposPath, g.AdminRepo, g.LdapConfig); err != nil {
return err
}
defer access.Teardown()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/docs/ldap.md Mon Jul 03 23:57:37 2023 -0500
@@ -0,0 +1,26 @@
+# LDAP
+
+In order to enable LDAP to fetch usernames for a SSH public key you need to
+pass a json configuration via ```--ldap-config="/path/to/ldap.json".
+Local public keys in ```./keys``` have precedence.
+
+ * username: the bind user name to LDAP (optional).
+ * password: the password for the bind user (optional, if empty it uses anonymous bind).
+ * address: the full address and port to LDAP server.
+ * baseDN: the base DN of your user scope.
+ * filter: the filter to fetch a username. %s will be replaced by the requested
+ authorization key.
+ * attribute: the attribute name of the username.
+
+## config
+
+```
+{
+ "username": "MyLdapBindUser",
+ "password": "",
+ "address": "ldap://server.local:389",
+ "baseDN": "OU=Developer,DC=Domain,DC=local",
+ "filter": "(sshPublicKeys=%s*)",
+ "attribute": "sAMAccountName"
+}
+```
--- a/docs/mkdocs.yml Mon Jul 03 23:55:09 2023 -0500
+++ b/docs/mkdocs.yml Mon Jul 03 23:57:37 2023 -0500
@@ -9,6 +9,7 @@
- SSH Access: sshaccess.md
- Access Control: accesscontrol.md
- Command Reference: commandreference.md
+ - LDAP: ldap.md
- Running:
- 'Run Modes': runmodes.md
- 'On Demand': ondemand.md
--- a/globals/globals.go Mon Jul 03 23:55:09 2023 -0500
+++ b/globals/globals.go Mon Jul 03 23:57:37 2023 -0500
@@ -4,4 +4,5 @@
HgExecutable string `kong:"flag,name='hg-exe',env='HGK_HG_EXE',default='hg',help='The hg executable to use. This can be a relative or absolute path.'"`
ReposPath string `kong:"flag,name='repos-path',env='HGK_REPOS_PATH',default='repos',help='the directory where the repositories are stored'"`
AdminRepo string `kong:"flag,name='admin-repo',env='HGK_ADMIN_REPO',default='hgkeeper',help='the name of the admin repo to create/use'"`
+ LdapConfig string `kong:"flag,name='ldap-config',env='HGK_LDAP_CONFIG',default='',help='the path to ldap config file'"`
}
--- a/go.mod Mon Jul 03 23:55:09 2023 -0500
+++ b/go.mod Mon Jul 03 23:57:37 2023 -0500
@@ -12,9 +12,12 @@
)
require (
+ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
+ github.com/go-ldap/ldap/v3 v3.4.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
--- a/go.sum Mon Jul 03 23:55:09 2023 -0500
+++ b/go.sum Mon Jul 03 23:57:37 2023 -0500
@@ -1,3 +1,5 @@
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
@@ -13,6 +15,10 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
+github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
+github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
+github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -26,6 +32,7 @@
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
@@ -39,6 +46,7 @@
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
--- a/once/command.go Mon Jul 03 23:55:09 2023 -0500
+++ b/once/command.go Mon Jul 03 23:57:37 2023 -0500
@@ -40,7 +40,7 @@
}
func (c *Command) Run(g *globals.Globals) error {
- if err := access.Setup(g.ReposPath, g.AdminRepo); err != nil {
+ if err := access.Setup(g.ReposPath, g.AdminRepo, g.LdapConfig); err != nil {
return err
}
defer access.Teardown()
--- a/serve/command.go Mon Jul 03 23:55:09 2023 -0500
+++ b/serve/command.go Mon Jul 03 23:57:37 2023 -0500
@@ -29,7 +29,7 @@
return fmt.Errorf("both HGWeb and SSH servers have been disabled")
}
- if err := access.Setup(g.ReposPath, g.AdminRepo); err != nil {
+ if err := access.Setup(g.ReposPath, g.AdminRepo, g.LdapConfig); err != nil {
return err
}
defer access.Teardown()