From 38151c31cc80cc203946fe612e09828db1b3efb5 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 18:33:28 +0200 Subject: [PATCH 01/11] Updated LDAP --- go.mod | 5 + go.sum | 77 ++++++++++ pkg/handler/ldap/config.go | 52 +++++++ pkg/handler/ldap/ldap.go | 259 ++++++++++++++++++++++++++++++++++ pkg/handler/ldap/ldap_test.go | 78 ++++++++++ pkg/handler/ldap/object.go | 44 ++++++ pkg/handler/ldap/task.go | 164 +++++++++++++++++++++ 7 files changed, 679 insertions(+) create mode 100644 pkg/handler/ldap/config.go create mode 100644 pkg/handler/ldap/ldap.go create mode 100644 pkg/handler/ldap/ldap_test.go create mode 100644 pkg/handler/ldap/object.go create mode 100644 pkg/handler/ldap/task.go diff --git a/go.mod b/go.mod index 4a3db83..0448d2a 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,12 @@ require ( ) require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/go-ldap/ldap/v3 v3.4.8 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4a303b4..75a06e4 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,93 @@ +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/djthorpe/go-errors v1.0.3 h1:GZeMPkC1mx2vteXLI/gvxZS0Ee9zxzwD1mcYyKU5jD0= github.com/djthorpe/go-errors v1.0.3/go.mod h1:HtfrZnMd6HsX75Mtbv9Qcnn0BqOrrFArvCaj3RMnZhY= github.com/djthorpe/go-tablewriter v0.0.7 h1:jnNsJDjjLLCt0OAqB5DzGZN7V3beT1IpNMQ8GcOwZDU= github.com/djthorpe/go-tablewriter v0.0.7/go.mod h1:NVBvytpL+6fHfCKn0+3lSi15/G3A1HWf2cLNeHg6YBg= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= +github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/mutablelogic/go-client v1.0.8 h1:A3QtP0wdf+W3dE5k7dobwGYqqn4ZpIqRFu+h9vPoy7Y= github.com/mutablelogic/go-client v1.0.8/go.mod h1:aP9ecBd4R/acJEJSyp81U3mey9W3AHQV/G1XzfcrLx0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/handler/ldap/config.go b/pkg/handler/ldap/config.go new file mode 100644 index 0000000..a54ac27 --- /dev/null +++ b/pkg/handler/ldap/config.go @@ -0,0 +1,52 @@ +package ldap + +import ( + // Packages + "time" + + "github.com/mutablelogic/go-server" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// LDAP configuration +type Config struct { + URL string `hcl:"url,required" description:"LDAP server URL, user and optional password for bind"` + User string `hcl:"user,optional" description:"Bind user, if not specified in URL"` + Password string `hcl:"password,optional" description:"Bind password, if not specified in URL"` + DN string `hcl:"dn,required" description:"Distinguished name"` + TLS struct { + SkipVerify bool `hcl:"skip-verify" description:"Skip certificate verification"` + } `hcl:"tls,optional" description:"TLS configuration"` +} + +// Ensure that LDAP implements the Service interface +var _ server.Plugin = (*Config)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + defaultName = "ldap" + defaultMethodPlain = "ldap" + defaultPortPlain = 389 + defaultMethodSecure = "ldaps" + defaultPortSecure = 636 + deltaPingTime = time.Minute +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func (c Config) Name() string { + return defaultName +} + +func (c Config) Description() string { + return "Provides a client for LDAP communication" +} + +func (c Config) New() (server.Task, error) { + return New(c) +} diff --git a/pkg/handler/ldap/ldap.go b/pkg/handler/ldap/ldap.go new file mode 100644 index 0000000..dd380eb --- /dev/null +++ b/pkg/handler/ldap/ldap.go @@ -0,0 +1,259 @@ +package ldap + +import ( + "crypto/tls" + "errors" + "fmt" + "net/url" + "strconv" + "sync" + + // Packages + goldap "github.com/go-ldap/ldap/v3" + types "github.com/mutablelogic/go-server/pkg/types" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// ldap instance +type ldap struct { + sync.Mutex + + url *url.URL + tls *tls.Config + user, password string + dn string + conn *goldap.Conn +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func New(c Config) (*ldap, error) { + self := new(ldap) + + // Set the URL for the connection + if c.URL == "" { + return nil, ErrBadParameter.With("url") + } else if url, err := url.Parse(c.URL); err != nil { + return nil, err + } else { + self.url = url + } + + // Check the scheme + switch self.url.Scheme { + case defaultMethodPlain: + if self.url.Port() == "" { + self.url.Host = fmt.Sprintf("%s:%d", self.url.Hostname(), defaultPortPlain) + } + case defaultMethodSecure: + if self.url.Port() == "" { + self.url.Host = fmt.Sprintf("%s:%d", self.url.Hostname(), defaultPortSecure) + } + self.tls = &tls.Config{ + InsecureSkipVerify: c.TLS.SkipVerify, + } + default: + return nil, fmt.Errorf("scheme not supported: %q (expected: %q, %q)", self.url.Scheme, defaultMethodPlain, defaultMethodSecure) + } + + // Extract the user + if c.User != "" { + self.user = c.User + } else if self.url.User == nil { + return nil, ErrBadParameter.With("missing user parameter") + } else { + self.user = self.url.User.Username() + } + + // Extract the password + if c.Password != "" { + self.password = c.Password + } else if self.url.User != nil { + if password, ok := self.url.User.Password(); ok { + self.password = password + } + } + + // Blank out the user and password in the URL + self.url.User = nil + + // Set the Distinguished Name + if c.DN == "" { + return nil, ErrBadParameter.With("dn") + } else { + self.dn = c.DN + } + + // Return success + return self, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the port for the LDAP connection +func (ldap *ldap) Port() int { + port, err := strconv.ParseUint(ldap.url.Port(), 10, 32) + if err != nil { + return 0 + } else { + return int(port) + } +} + +// Return the host for the LDAP connection +func (ldap *ldap) Host() string { + return ldap.url.Hostname() +} + +// Return the user for the LDAP connection +func (ldap *ldap) User() string { + if types.IsIdentifier(ldap.user) { + return fmt.Sprint("cn=", ldap.user, ",", ldap.dn) + } else { + return ldap.user + } +} + +// Connect to the LDAP server, or ping the server if already connected +func (ldap *ldap) Connect() error { + ldap.Lock() + defer ldap.Unlock() + + if ldap.conn == nil { + if conn, err := ldapConnect(ldap.Host(), ldap.Port(), ldap.tls); err != nil { + return err + } else if err := ldapBind(conn, ldap.User(), ldap.password); err != nil { + if ldapErrorCode(err) == goldap.LDAPResultInvalidCredentials { + return ErrNotAuthorized.With("Invalid credentials") + } else { + return err + } + } else { + ldap.conn = conn + } + } else { + // TODO: Ping the connection + if whoami, err := ldap.conn.WhoAmI([]goldap.Control{}); err != nil { + return err + } else { + fmt.Println(whoami.AuthzID) + } + } + return nil +} + +// Disconnect from the LDAP server +func (ldap *ldap) Disconnect() error { + ldap.Lock() + defer ldap.Unlock() + + // Disconnect from LDAP connection + var result error + if ldap.conn != nil { + if err := ldapDisconnect(ldap.conn); err != nil { + result = errors.Join(result, err) + } + ldap.conn = nil + } + + // Return any errors + return result +} + +// Return the user who is currently authenticated +func (ldap *ldap) WhoAmI() (string, error) { + ldap.Lock() + defer ldap.Unlock() + + // Check connection + if ldap.conn == nil { + return "", ErrOutOfOrder.With("Not connected") + } + + // Ping + if whoami, err := ldap.conn.WhoAmI([]goldap.Control{}); err != nil { + return "", err + } else { + return whoami.AuthzID, nil + } +} + +// Return the objects of a particular class, or use "*" to return all objects +func (ldap *ldap) Get(objectClass string) ([]*object, error) { + ldap.Lock() + defer ldap.Unlock() + + // Check parameters + if !types.IsIdentifier(objectClass) && objectClass != "*" { + return nil, ErrBadParameter.With("objectClass") + } + + // Check connection + if ldap.conn == nil { + return nil, ErrOutOfOrder.With("Not connected") + } + + // Define the search request + searchRequest := goldap.NewSearchRequest( + ldap.dn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false, + fmt.Sprint("(&(objectClass=", objectClass, "))"), // The filter to apply + nil, // The attributes to retrieve + nil, + ) + + // Perform the search + sr, err := ldap.conn.Search(searchRequest) + if err != nil { + return nil, err + } + + // Print the results + result := make([]*object, 0, len(sr.Entries)) + for _, entry := range sr.Entries { + result = append(result, newObject(entry)) + } + + // Return success + return result, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func ldapConnect(host string, port int, tls *tls.Config) (*goldap.Conn, error) { + var url string + if tls != nil { + url = fmt.Sprintf("%s://%s:%d", defaultMethodSecure, host, port) + } else { + url = fmt.Sprintf("%s://%s:%d", defaultMethodPlain, host, port) + } + return goldap.DialURL(url, goldap.DialWithTLSConfig(tls)) +} + +func ldapDisconnect(conn *goldap.Conn) error { + return conn.Close() +} + +func ldapBind(conn *goldap.Conn, user, password string) error { + if user == "" || password == "" { + return conn.UnauthenticatedBind(user) + } else { + return conn.Bind(user, password) + } +} + +// Return the LDAP error code +func ldapErrorCode(err error) uint16 { + if err, ok := err.(*goldap.Error); ok { + return uint16(err.ResultCode) + } else { + return 0 + } +} diff --git a/pkg/handler/ldap/ldap_test.go b/pkg/handler/ldap/ldap_test.go new file mode 100644 index 0000000..9566a53 --- /dev/null +++ b/pkg/handler/ldap/ldap_test.go @@ -0,0 +1,78 @@ +package ldap_test + +import ( + "context" + "os" + "sync" + "testing" + "time" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/ldap" + provider "github.com/mutablelogic/go-server/pkg/provider" + "github.com/stretchr/testify/assert" +) + +func Test_ldap_001(t *testing.T) { + assert := assert.New(t) + ldap, err := ldap.New(ldap.Config{ + URL: Get(t, "LDAP_URL", true), + DN: Get(t, "LDAP_DN", true), + User: Get(t, "LDAP_USER", false), + }) + assert.NoError(err) + assert.NotNil(ldap) +} + +func Test_ldap_002(t *testing.T) { + assert := assert.New(t) + ldap, err := ldap.New(ldap.Config{ + URL: Get(t, "LDAP_URL", true), + DN: Get(t, "LDAP_DN", true), + User: Get(t, "LDAP_USER", false), + Password: Get(t, "LDAP_PASSWORD", false), + }) + if !assert.NoError(err) { + t.SkipNow() + } + + provider := provider.NewProvider(ldap) + + // Run the task in the background + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + if err := provider.Run(ctx); err != nil { + t.Error(err) + } + }() + + // Wait for connection + time.Sleep(1 * time.Second) + + users, err := ldap.Get("posixAccount") + assert.NoError(err) + t.Log(users) + + // End + cancel() + + // Wait for the task to complete + wg.Wait() +} + +/////////////////////////////////////////////////////////////////////////////// +// ENVIRONMENT + +func Get(t *testing.T, name string, required bool) string { + key := os.Getenv(name) + if key == "" && required { + t.Skip("not set:", name) + t.SkipNow() + } + return key +} diff --git a/pkg/handler/ldap/object.go b/pkg/handler/ldap/object.go new file mode 100644 index 0000000..23b38ff --- /dev/null +++ b/pkg/handler/ldap/object.go @@ -0,0 +1,44 @@ +package ldap + +import ( + // Packages + "encoding/json" + + goldap "github.com/go-ldap/ldap/v3" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type object struct { + *goldap.Entry +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func newObject(entry *goldap.Entry) *object { + return &object{Entry: entry} +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (o *object) MarshalJSON() ([]byte, error) { + j := struct { + DN string `json:"dn"` + Attrs map[string][]string `json:"attrs"` + }{ + DN: o.Entry.DN, + Attrs: make(map[string][]string), + } + for _, attr := range o.Entry.Attributes { + j.Attrs[attr.Name] = attr.Values + } + return json.Marshal(j) +} + +func (o *object) String() string { + data, _ := json.MarshalIndent(o, "", " ") + return string(data) +} diff --git a/pkg/handler/ldap/task.go b/pkg/handler/ldap/task.go new file mode 100644 index 0000000..c6f10e6 --- /dev/null +++ b/pkg/handler/ldap/task.go @@ -0,0 +1,164 @@ +package ldap + +import ( + "context" + "errors" + "time" + + // Packages + server "github.com/mutablelogic/go-server" + provider "github.com/mutablelogic/go-server/pkg/provider" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Check interfaces are satisfied +var _ server.Task = (*ldap)(nil) + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the label +func (task *ldap) Label() string { + // TODO + return defaultName +} + +// Run the task until the context is cancelled +func (task *ldap) Run(ctx context.Context) error { + var result error + var first bool + + // Getting logging object + log := provider.Logger(ctx) + + // Connect + timer := time.NewTimer(100 * time.Millisecond) + defer timer.Stop() + +FOR_LOOP: + for { + select { + case <-ctx.Done(): + break FOR_LOOP + case <-timer.C: + // Connect or ping connection + if err := task.Connect(); err != nil { + log.Print(ctx, err) + + // If this is the first time and we have bad credentials, then return the error + if errors.Is(err, ErrNotAuthorized) && !first { + result = err + break FOR_LOOP + } + } else { + // Indicate that we are connected. If subsequent connections fail, then + // we will log the error but continue to try to connect + log.Print(ctx, "Connected") + first = true + } + + // We attempt to ping the connection every minute + timer.Reset(deltaPingTime) + } + } + + // Return any errors + return result +} + +/* + +func (self *ldap) Run(ctx context.Context) error { + var result error + + // Bind to the connection, and re-bind occasionally if it fails + state := stNone + delta := durationMinBackoff + timer := time.NewTimer(delta) + +FOR_LOOP: + for { + select { + case <-ctx.Done(): + timer.Stop() + break FOR_LOOP + case <-timer.C: + var err error + state, delta, err = self.changeState(state, delta) + if err != nil { + // TODO: Emit error + fmt.Fprintln(os.Stderr, err) + } + timer.Reset(delta) + } + } + + // Disconnect from LDAP connection + if self.conn != nil { + if err := ldapDisconnect(self.conn); err != nil { + result = errors.Join(result, err) + } + self.conn = nil + } + + // Return any errors + return result +} + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (self *ldap) changeState(state connState, delta time.Duration) (connState, time.Duration, error) { + var result error + + // Lock operation + self.Mutex.Lock() + defer self.Mutex.Unlock() + + // Connect, disconnect, bind and ping + switch state { + case stNone: + // Disconnect + if self.conn != nil { + if err := ldapDisconnect(self.conn); err != nil { + result = errors.Join(result, err) + } + } + // Connect + if conn, err := ldapConnect(self.Host(), self.Port(), self.tls); err != nil { + delta = min(delta*2, durationMaxBackoff) + result = errors.Join(result, err) + } else { + self.conn = conn + state = stConnected + delta = durationMinBackoff + } + case stConnected: + if self.conn == nil { + result = errors.Join(result, ErrInternalAppError.With("state is connected but connection is nil")) + state = stNone + delta = durationMinBackoff + } else if err := ldapBind(self.conn, self.dn, self.password); err != nil { + result = errors.Join(result, err) + delta = min(delta*2, durationMaxBackoff) + } else { + state = stAuthenticated + delta = durationMinBackoff + } + case stAuthenticated: + // TODO: Check connection is still valid + delta = min(delta*2, durationMaxBackoff) + default: + result = errors.Join(result, ErrInternalAppError.With("changeState")) + } + + // Return any errors + return state, delta, result +} + +*/ From ed4521fbf5a253cf19bd7d667a89c772631c49de Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 18:52:10 +0200 Subject: [PATCH 02/11] Updated connection/disconnection code --- pkg/handler/ldap/ldap.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/handler/ldap/ldap.go b/pkg/handler/ldap/ldap.go index dd380eb..e71ad52 100644 --- a/pkg/handler/ldap/ldap.go +++ b/pkg/handler/ldap/ldap.go @@ -138,14 +138,11 @@ func (ldap *ldap) Connect() error { } else { ldap.conn = conn } - } else { - // TODO: Ping the connection - if whoami, err := ldap.conn.WhoAmI([]goldap.Control{}); err != nil { - return err - } else { - fmt.Println(whoami.AuthzID) - } + } else if _, err := ldap.conn.WhoAmI([]goldap.Control{}); err != nil { + return errors.Join(err, ldap.Disconnect()) } + + // Return success return nil } From b218d3bb6288c8e1bb255f44f12da06b8c3272b5 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 7 Jun 2024 20:09:25 +0200 Subject: [PATCH 03/11] Some ldap experiments --- pkg/handler/ldap/config.go | 29 ++++++++++ pkg/handler/ldap/ldap.go | 70 +++++++++++++++++++++++-- pkg/handler/ldap/ldap_test.go | 4 +- pkg/handler/ldap/{ => schema}/object.go | 26 ++++----- pkg/handler/ldap/schema/schema.go | 55 +++++++++++++++++++ 5 files changed, 167 insertions(+), 17 deletions(-) rename pkg/handler/ldap/{ => schema}/object.go (70%) create mode 100644 pkg/handler/ldap/schema/schema.go diff --git a/pkg/handler/ldap/config.go b/pkg/handler/ldap/config.go index a54ac27..f8b9da5 100644 --- a/pkg/handler/ldap/config.go +++ b/pkg/handler/ldap/config.go @@ -5,6 +5,7 @@ import ( "time" "github.com/mutablelogic/go-server" + "github.com/mutablelogic/go-server/pkg/handler/ldap/schema" ) /////////////////////////////////////////////////////////////////////////////// @@ -19,6 +20,10 @@ type Config struct { TLS struct { SkipVerify bool `hcl:"skip-verify" description:"Skip certificate verification"` } `hcl:"tls,optional" description:"TLS configuration"` + Schema struct { + User []string `hcl:"user,optional" description:"User objectClass, defaults to posixAccount, person, inetOrgPerson"` + Group []string `hcl:"group,optional" description:"Group objectClass, defaults to posixGroup"` + } `hcl:"schema,optional" description:"LDAP Schema"` } // Ensure that LDAP implements the Service interface @@ -34,6 +39,13 @@ const ( defaultMethodSecure = "ldaps" defaultPortSecure = 636 deltaPingTime = time.Minute + defaultUserOU = "users" + defaultGroupOU = "groups" +) + +var ( + defaultUserObjectClass = []string{"posixAccount", "person", "inetOrgPerson"} + defaultGroupObjectClass = []string{"posixGroup"} ) /////////////////////////////////////////////////////////////////////////////// @@ -50,3 +62,20 @@ func (c Config) Description() string { func (c Config) New() (server.Task, error) { return New(c) } + +func (c Config) ObjectSchema() (*schema.Schema, error) { + schema := &schema.Schema{ + DN: c.DN, + UserObjectClass: defaultUserObjectClass, + GroupObjectClass: defaultGroupObjectClass, + UserOU: defaultUserOU, + GroupOU: defaultGroupOU, + } + if len(c.Schema.User) > 0 { + schema.UserObjectClass = c.Schema.User + } + if len(c.Schema.Group) > 0 { + schema.GroupObjectClass = c.Schema.Group + } + return schema, nil +} diff --git a/pkg/handler/ldap/ldap.go b/pkg/handler/ldap/ldap.go index e71ad52..9b617e5 100644 --- a/pkg/handler/ldap/ldap.go +++ b/pkg/handler/ldap/ldap.go @@ -10,6 +10,7 @@ import ( // Packages goldap "github.com/go-ldap/ldap/v3" + schema "github.com/mutablelogic/go-server/pkg/handler/ldap/schema" types "github.com/mutablelogic/go-server/pkg/types" // Namespace imports @@ -28,6 +29,7 @@ type ldap struct { user, password string dn string conn *goldap.Conn + schema *schema.Schema } /////////////////////////////////////////////////////////////////////////////// @@ -90,6 +92,13 @@ func New(c Config) (*ldap, error) { self.dn = c.DN } + // Set the schema + if schema, err := c.ObjectSchema(); err != nil { + return nil, err + } else { + self.schema = schema + } + // Return success return self, nil } @@ -139,6 +148,8 @@ func (ldap *ldap) Connect() error { ldap.conn = conn } } else if _, err := ldap.conn.WhoAmI([]goldap.Control{}); err != nil { + // TODO: ldap.ErrorNetwork, ldap.LDAPResultBusy, ldap.LDAPResultUnavailable: + // would indicate that the connection is no longer valid return errors.Join(err, ldap.Disconnect()) } @@ -183,7 +194,7 @@ func (ldap *ldap) WhoAmI() (string, error) { } // Return the objects of a particular class, or use "*" to return all objects -func (ldap *ldap) Get(objectClass string) ([]*object, error) { +func (ldap *ldap) Get(objectClass string) ([]*schema.Object, error) { ldap.Lock() defer ldap.Unlock() @@ -212,15 +223,68 @@ func (ldap *ldap) Get(objectClass string) ([]*object, error) { } // Print the results - result := make([]*object, 0, len(sr.Entries)) + result := make([]*schema.Object, 0, len(sr.Entries)) for _, entry := range sr.Entries { - result = append(result, newObject(entry)) + result = append(result, ldap.schema.NewObject(entry)) } // Return success return result, nil } +// Return all users +func (ldap *ldap) GetUsers() ([]*schema.Object, error) { + return ldap.Get(ldap.schema.UserObjectClass[0]) +} + +// Return all groups +func (ldap *ldap) GetGroups() ([]*schema.Object, error) { + return ldap.Get(ldap.schema.GroupObjectClass[0]) +} + +// Create a user +func (ldap *ldap) CreateGroup(group string) error { + object := ldap.schema.NewGroup(group) + fmt.Println(object) + /* + addReq := ldp.NewAddRequest(group.DN, []ldp.Control{}) + + addReq.Attribute("objectClass", []string{"top", "group"}) + addReq.Attribute("name", []string{"testgroup"}) + addReq.Attribute("sAMAccountName", []string{"testgroup"}) + addReq.Attribute("instanceType", []string{fmt.Sprintf("%d", 0x00000004}) + addReq.Attribute("groupType", []string{fmt.Sprintf("%d", 0x00000004 | 0x80000000)}) + + if err := l.AddRequest(addReq); err != nil { + log.Fatal("error adding group:", addReq, err) + } + */ + return ErrNotImplemented +} + +// Bind a user with password to check if they are authenticated +func (ldap *ldap) Bind(user *schema.Object, password string) error { + ldap.Lock() + defer ldap.Unlock() + + // Check connection + if ldap.conn == nil { + return ErrOutOfOrder.With("Not connected") + } + + // Bind + if err := ldap.conn.Bind(user.DN, password); err != nil { + if ldapErrorCode(err) == goldap.LDAPResultInvalidCredentials { + return ErrNotAuthorized.With("Invalid credentials") + } else { + return err + } + } + + // Return success + return nil +} + /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS diff --git a/pkg/handler/ldap/ldap_test.go b/pkg/handler/ldap/ldap_test.go index 9566a53..f6eec0c 100644 --- a/pkg/handler/ldap/ldap_test.go +++ b/pkg/handler/ldap/ldap_test.go @@ -54,9 +54,9 @@ func Test_ldap_002(t *testing.T) { // Wait for connection time.Sleep(1 * time.Second) - users, err := ldap.Get("posixAccount") + groups, err := ldap.GetGroups() assert.NoError(err) - t.Log(users) + t.Log(groups) // End cancel() diff --git a/pkg/handler/ldap/object.go b/pkg/handler/ldap/schema/object.go similarity index 70% rename from pkg/handler/ldap/object.go rename to pkg/handler/ldap/schema/object.go index 23b38ff..1488439 100644 --- a/pkg/handler/ldap/object.go +++ b/pkg/handler/ldap/schema/object.go @@ -1,30 +1,23 @@ -package ldap +package schema import ( - // Packages "encoding/json" + // Packages goldap "github.com/go-ldap/ldap/v3" ) /////////////////////////////////////////////////////////////////////////////// // TYPES -type object struct { +type Object struct { *goldap.Entry } -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func newObject(entry *goldap.Entry) *object { - return &object{Entry: entry} -} - /////////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (o *object) MarshalJSON() ([]byte, error) { +func (o *Object) MarshalJSON() ([]byte, error) { j := struct { DN string `json:"dn"` Attrs map[string][]string `json:"attrs"` @@ -38,7 +31,16 @@ func (o *object) MarshalJSON() ([]byte, error) { return json.Marshal(j) } -func (o *object) String() string { +func (o *Object) String() string { data, _ := json.MarshalIndent(o, "", " ") return string(data) } + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (o *Object) Set(name string, values ...string) { + o.Entry.Attributes = []*goldap.EntryAttribute{ + &goldap.EntryAttribute{Name: name, Values: values}, + } +} diff --git a/pkg/handler/ldap/schema/schema.go b/pkg/handler/ldap/schema/schema.go new file mode 100644 index 0000000..c5d0719 --- /dev/null +++ b/pkg/handler/ldap/schema/schema.go @@ -0,0 +1,55 @@ +package schema + +import ( + "fmt" + "slices" + + goldap "github.com/go-ldap/ldap/v3" + "github.com/mutablelogic/go-server/pkg/types" +) + +///////////////////////////////////////////////////////////////////// +// TYPES + +// Schema is the schema for the LDAP server +type Schema struct { + DN string + UserOU string + GroupOU string + UserObjectClass []string + GroupObjectClass []string +} + +///////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + defaultPosixUserObjectClass = "posixAccount" +) + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (s Schema) IsPosix() bool { + return slices.Contains(s.UserObjectClass, defaultPosixUserObjectClass) +} + +func (s Schema) GroupDN(name string) string { + return fmt.Sprintf("cn=%s,ou=%s,%s", name, s.GroupOU, s.DN) +} + +func (s Schema) NewObject(entry *goldap.Entry) *Object { + return &Object{Entry: entry} +} + +func (s Schema) NewGroup(name string) *Object { + // Check parameters + if !types.IsIdentifier(name) || s.GroupOU == "" { + return nil + } + group := &Object{Entry: &goldap.Entry{ + DN: s.GroupDN(name), + }} + group.Set("objectClass", s.GroupObjectClass...) + return group +} From 069d9af0cde84128e15d22993cd0a878ad142f06 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 10:59:01 +0200 Subject: [PATCH 04/11] Added ldap changes --- cmd/nginx-server/main.go | 41 +++-- pkg/handler/ldap/config.go | 25 +-- pkg/handler/ldap/endpoints.go | 92 ++++++++++ pkg/handler/ldap/ldap.go | 225 +++++++++++++++++++++++-- pkg/handler/ldap/ldap_test.go | 77 ++++++++- pkg/handler/ldap/schema/attrs.go | 78 +++++++++ pkg/handler/ldap/schema/object.go | 67 ++++++-- pkg/handler/ldap/schema/object_test.go | 19 +++ pkg/handler/ldap/schema/schema.go | 155 ++++++++++++++--- pkg/handler/ldap/scope.go | 33 ++++ pkg/handler/ldap/task.go | 6 +- pkg/provider/provider.go | 14 +- 12 files changed, 747 insertions(+), 85 deletions(-) create mode 100644 pkg/handler/ldap/endpoints.go create mode 100644 pkg/handler/ldap/schema/attrs.go create mode 100644 pkg/handler/ldap/schema/object_test.go create mode 100644 pkg/handler/ldap/scope.go diff --git a/cmd/nginx-server/main.go b/cmd/nginx-server/main.go index 493b1f5..1cdf10d 100644 --- a/cmd/nginx-server/main.go +++ b/cmd/nginx-server/main.go @@ -15,6 +15,7 @@ import ( auth "github.com/mutablelogic/go-server/pkg/handler/auth" certmanager "github.com/mutablelogic/go-server/pkg/handler/certmanager" certstore "github.com/mutablelogic/go-server/pkg/handler/certmanager/certstore" + ldap "github.com/mutablelogic/go-server/pkg/handler/ldap" logger "github.com/mutablelogic/go-server/pkg/handler/logger" nginx "github.com/mutablelogic/go-server/pkg/handler/nginx" router "github.com/mutablelogic/go-server/pkg/handler/router" @@ -24,8 +25,9 @@ import ( ) var ( - binary = flag.String("path", "nginx", "Path to nginx binary") - group = flag.String("group", "", "Group to run unix socket as") + binary = flag.String("path", "nginx", "Path to nginx binary") + group = flag.String("group", "", "Group to run unix socket as") + ldap_password = flag.String("ldap-password", "", "LDAP admin password") ) /* command to test the nginx package */ @@ -42,13 +44,13 @@ func main() { // Logger logger, err := logger.Config{Flags: []string{"default", "prefix"}}.New() if err != nil { - log.Fatal(err) + log.Fatal("logger: ", err) } // Nginx handler n, err := nginx.Config{BinaryPath: *binary}.New() if err != nil { - log.Fatal(err) + log.Fatal("nginx: ", err) } // Token Jar @@ -57,7 +59,7 @@ func main() { WriteInterval: 30 * time.Second, }.New() if err != nil { - log.Fatal(err) + log.Fatal("tokenkar: ", err) } // Auth handler @@ -67,7 +69,7 @@ func main() { Bearer: true, // Use bearer token in requests for authorization }.New() if err != nil { - log.Fatal(err) + log.Fatal("auth: ", err) } // Cert Storage @@ -76,13 +78,23 @@ func main() { Group: *group, }.New() if err != nil { - log.Fatal(err) + log.Fatal("certstore: ", err) } certmanager, err := certmanager.Config{ CertStorage: certstore.(certmanager.CertStorage), }.New() if err != nil { - log.Fatal(err) + log.Fatal("certmanager: ", err) + } + + // LDAP + ldap, err := ldap.Config{ + URL: "ldap://admin@cm1.local/", + DN: "dc=mutablelogic,dc=com", + Password: *ldap_password, + }.New() + if err != nil { + log.Fatal("ldap: ", err) } // Location of the FCGI unix socket @@ -112,10 +124,17 @@ func main() { auth.(server.Middleware), }, }, + "ldap": { // /api/ldap/... + Service: ldap.(server.ServiceEndpoints), + Middleware: []server.Middleware{ + logger.(server.Middleware), + auth.(server.Middleware), + }, + }, }, }.New() if err != nil { - log.Fatal(err) + log.Fatal("router: ", err) } // HTTP Server @@ -125,11 +144,11 @@ func main() { Router: router.(http.Handler), }.New() if err != nil { - log.Fatal(err) + log.Fatal("httpserver: ", err) } // Run until we receive an interrupt - provider := provider.NewProvider(logger, n, jar, auth, certstore, certmanager, router, httpserver) + provider := provider.NewProvider(logger, n, jar, auth, certstore, certmanager, ldap, router, httpserver) provider.Print(ctx, "Press CTRL+C to exit") if err := provider.Run(ctx); err != nil { log.Fatal(err) diff --git a/pkg/handler/ldap/config.go b/pkg/handler/ldap/config.go index f8b9da5..6779fd2 100644 --- a/pkg/handler/ldap/config.go +++ b/pkg/handler/ldap/config.go @@ -20,10 +20,7 @@ type Config struct { TLS struct { SkipVerify bool `hcl:"skip-verify" description:"Skip certificate verification"` } `hcl:"tls,optional" description:"TLS configuration"` - Schema struct { - User []string `hcl:"user,optional" description:"User objectClass, defaults to posixAccount, person, inetOrgPerson"` - Group []string `hcl:"group,optional" description:"Group objectClass, defaults to posixGroup"` - } `hcl:"schema,optional" description:"LDAP Schema"` + Schema schema.Schema `hcl:"schema,optional" description:"LDAP Schema"` } // Ensure that LDAP implements the Service interface @@ -44,8 +41,8 @@ const ( ) var ( - defaultUserObjectClass = []string{"posixAccount", "person", "inetOrgPerson"} - defaultGroupObjectClass = []string{"posixGroup"} + defaultUserObjectClass = []string{"posixAccount", "top", "person", "inetOrgPerson"} + defaultGroupObjectClass = []string{"posixGroup", "top", "groupOfUniqueNames"} ) /////////////////////////////////////////////////////////////////////////////// @@ -65,17 +62,23 @@ func (c Config) New() (server.Task, error) { func (c Config) ObjectSchema() (*schema.Schema, error) { schema := &schema.Schema{ - DN: c.DN, UserObjectClass: defaultUserObjectClass, GroupObjectClass: defaultGroupObjectClass, UserOU: defaultUserOU, GroupOU: defaultGroupOU, } - if len(c.Schema.User) > 0 { - schema.UserObjectClass = c.Schema.User + + if len(c.Schema.UserObjectClass) > 0 { + schema.UserObjectClass = c.Schema.UserObjectClass + } + if len(c.Schema.GroupObjectClass) > 0 { + schema.GroupObjectClass = c.Schema.GroupObjectClass + } + if c.Schema.UserOU != "" { + schema.UserOU = c.Schema.UserOU } - if len(c.Schema.Group) > 0 { - schema.GroupObjectClass = c.Schema.Group + if c.Schema.GroupOU != "" { + schema.GroupOU = c.Schema.GroupOU } return schema, nil } diff --git a/pkg/handler/ldap/endpoints.go b/pkg/handler/ldap/endpoints.go new file mode 100644 index 0000000..deb2352 --- /dev/null +++ b/pkg/handler/ldap/endpoints.go @@ -0,0 +1,92 @@ +package ldap + +import ( + "context" + "net/http" + "regexp" + + // Packages + server "github.com/mutablelogic/go-server" + router "github.com/mutablelogic/go-server/pkg/handler/router" + httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse" + "github.com/mutablelogic/go-server/pkg/types" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Check interfaces are satisfied +var _ server.ServiceEndpoints = (*ldap)(nil) + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + jsonIndent = 2 +) + +var ( + reUsers = regexp.MustCompile(`^/u/?$`) + reUser = regexp.MustCompile(`^/u/(` + types.ReIdentifier + `)/?$`) + reGroups = regexp.MustCompile(`^/g/?$`) + reGroup = regexp.MustCompile(`^/g/(` + types.ReIdentifier + `)/?$`) +) + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - ENDPOINTS + +// Add endpoints to the router +func (service *ldap) AddEndpoints(ctx context.Context, r server.Router) { + // Path: /u + // Methods: GET + // Scopes: read + // Description: Return all users + r.AddHandlerFuncRe(ctx, reUsers, service.reqListUsers, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) + + // Path: /g + // Methods: GET + // Scopes: read + // Description: Return all groups + r.AddHandlerFuncRe(ctx, reGroups, service.reqListGroups, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) + + /* + // Path: /u/ + // Methods: GET + // Scopes: read + // Description: Return a user + r.AddHandlerFuncRe(ctx, reUser, service.reqGetUser, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) + + // Path: /g/ + // Methods: GET + // Scopes: read + // Description: Return a group + r.AddHandlerFuncRe(ctx, reGroup, service.reqGetGroup, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) + */ +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Get all users +func (service *ldap) reqListUsers(w http.ResponseWriter, r *http.Request) { + list, err := service.GetUsers() + if err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } + httpresponse.JSON(w, list, http.StatusOK, jsonIndent) +} + +// Get all groups +func (service *ldap) reqListGroups(w http.ResponseWriter, r *http.Request) { + list, err := service.GetGroups() + if err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } + httpresponse.JSON(w, list, http.StatusOK, jsonIndent) +} diff --git a/pkg/handler/ldap/ldap.go b/pkg/handler/ldap/ldap.go index 9b617e5..057558b 100644 --- a/pkg/handler/ldap/ldap.go +++ b/pkg/handler/ldap/ldap.go @@ -139,7 +139,7 @@ func (ldap *ldap) Connect() error { if conn, err := ldapConnect(ldap.Host(), ldap.Port(), ldap.tls); err != nil { return err } else if err := ldapBind(conn, ldap.User(), ldap.password); err != nil { - if ldapErrorCode(err) == goldap.LDAPResultInvalidCredentials { + if code := ldapErrorCode(err); code == goldap.LDAPResultInvalidCredentials || code == goldap.LDAPResultUnwillingToPerform { return ErrNotAuthorized.With("Invalid credentials") } else { return err @@ -225,7 +225,7 @@ func (ldap *ldap) Get(objectClass string) ([]*schema.Object, error) { // Print the results result := make([]*schema.Object, 0, len(sr.Entries)) for _, entry := range sr.Entries { - result = append(result, ldap.schema.NewObject(entry)) + result = append(result, schema.NewObjectFromEntry(entry)) } // Return success @@ -242,26 +242,184 @@ func (ldap *ldap) GetGroups() ([]*schema.Object, error) { return ldap.Get(ldap.schema.GroupObjectClass[0]) } -// Create a user -func (ldap *ldap) CreateGroup(group string) error { - object := ldap.schema.NewGroup(group) - fmt.Println(object) - /* - addReq := ldp.NewAddRequest(group.DN, []ldp.Control{}) - - addReq.Attribute("objectClass", []string{"top", "group"}) - addReq.Attribute("name", []string{"testgroup"}) - addReq.Attribute("sAMAccountName", []string{"testgroup"}) - addReq.Attribute("instanceType", []string{fmt.Sprintf("%d", 0x00000004}) - addReq.Attribute("groupType", []string{fmt.Sprintf("%d", 0x00000004 | 0x80000000)}) - - if err := l.AddRequest(addReq); err != nil { - log.Fatal("error adding group:", addReq, err) +// Create a group with the given attributes +func (ldap *ldap) CreateGroup(group string, attrs ...schema.Attr) (*schema.Object, error) { + ldap.Lock() + defer ldap.Unlock() + + // Check connection + if ldap.conn == nil { + return nil, ErrOutOfOrder.With("Not connected") + } + + // Create group + o, err := ldap.schema.NewGroup(ldap.dn, group, attrs...) + if err != nil { + return nil, err + } + + // If the gid is not set, then set it to the next available gid + var nextGid int + gid, err := ldap.SearchOne("(&(objectclass=device)(cn=lastgid))") + if err != nil { + return nil, err + } else if gid == nil { + return nil, ErrNotImplemented.With("lastgid not found") + } else if gid_, err := strconv.ParseInt(gid.Get("serialNumber"), 10, 32); err != nil { + return nil, ErrNotImplemented.With("lastgid not found") + } else { + nextGid = int(gid_) + 1 + if err := schema.OptGroupId(int(gid_))(o); err != nil { + return nil, err } - */ + } + + // Create the request + addReq := goldap.NewAddRequest(o.DN, []goldap.Control{}) + for name, values := range o.Values { + addReq.Attribute(name, values) + } + + // Request -> Response + if err := ldap.conn.Add(addReq); err != nil { + return nil, err + } + + // Increment the gid + if gid != nil && nextGid > 0 { + modify := goldap.NewModifyRequest(gid.DN, []goldap.Control{}) + modify.Replace("serialNumber", []string{fmt.Sprint(nextGid)}) + if err := ldap.conn.Modify(modify); err != nil { + return nil, err + } + } + + // TODO: Retrieve the group + + // Return success + return o, nil +} + +// Add a user to a group +func (ldap *ldap) AddGroupUser(user, group *schema.Object) error { + // Use uniqueMember for groupOfUniqueNames, + // use memberUid for posixGroup + // use member for groupOfNames or if not posix return ErrNotImplemented } +// Remove a user from a group +func (ldap *ldap) RemoveGroupUser(user, group *schema.Object) error { + // Use uniqueMember for groupOfUniqueNames, + // use memberUid for posixGroup + // use member for groupOfNames or if not posix + return ErrNotImplemented +} + +// Change a passsord for a user. If the new password is empty, then the password is reset +// to a new random password. The old password is required for the change +// if the ldap connection is not bound to the admin user. The new password +// is returned if the change is successful +func (ldap *ldap) ChangePassword(o *schema.Object, old, new string) (string, error) { + ldap.Lock() + defer ldap.Unlock() + + // Check object + if o == nil { + return "", ErrBadParameter + } + + // Check connection + if ldap.conn == nil { + return "", ErrOutOfOrder.With("Not connected") + } + + // Modify the password + modify := goldap.NewPasswordModifyRequest(o.DN, old, new) + if result, err := ldap.conn.PasswordModify(modify); err != nil { + return "", err + } else { + return result.GeneratedPassword, nil + } +} + +// Create a user in a specific group with the given attributes +func (ldap *ldap) CreateUser(name string, attrs ...schema.Attr) (*schema.Object, error) { + ldap.Lock() + defer ldap.Unlock() + + // Check connection + if ldap.conn == nil { + return nil, ErrOutOfOrder.With("Not connected") + } + + // Create user object + o, err := ldap.schema.NewUser(ldap.dn, name, attrs...) + if err != nil { + return nil, err + } + + // If the uid is not set, then set it to the next available uid + var nextId int + uid, err := ldap.SearchOne("(&(objectclass=device)(cn=lastuid))") + if err != nil { + return nil, err + } else if uid == nil { + return nil, ErrNotImplemented.With("lastuid not found") + } else if uid_, err := strconv.ParseInt(uid.Get("serialNumber"), 10, 32); err != nil { + return nil, ErrNotImplemented.With("lastuid not found") + } else { + nextId = int(uid_) + 1 + if err := schema.OptUserId(int(uid_))(o); err != nil { + return nil, err + } + } + + // Create the request + addReq := goldap.NewAddRequest(o.DN, []goldap.Control{}) + for name, values := range o.Values { + addReq.Attribute(name, values) + } + + // Request -> Response + if err := ldap.conn.Add(addReq); err != nil { + return nil, err + } + + // Increment the uid + if uid != nil && nextId > 0 { + modify := goldap.NewModifyRequest(uid.DN, []goldap.Control{}) + modify.Replace("serialNumber", []string{fmt.Sprint(nextId)}) + if err := ldap.conn.Modify(modify); err != nil { + return nil, err + } + } + + // TODO: Add the user to a group + + // Return success + return o, nil +} + +// Delete an object +func (ldap *ldap) Delete(o *schema.Object) error { + ldap.Lock() + defer ldap.Unlock() + + // Check object + if o == nil { + return ErrBadParameter + } + + // Check connection + if ldap.conn == nil { + return ErrOutOfOrder.With("Not connected") + } + + // Delete the object + return ldap.conn.Del(goldap.NewDelRequest(o.DN, []goldap.Control{})) +} + // Bind a user with password to check if they are authenticated func (ldap *ldap) Bind(user *schema.Object, password string) error { ldap.Lock() @@ -281,13 +439,42 @@ func (ldap *ldap) Bind(user *schema.Object, password string) error { } } + // TODO: Rebind with the original user + // Return success return nil } +// Return one record +func (ldap *ldap) SearchOne(filter string) (*schema.Object, error) { + // Check connection + if ldap.conn == nil { + return nil, ErrOutOfOrder.With("Not connected") + } + + // Define the search request + searchRequest := goldap.NewSearchRequest( + ldap.dn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 1, 0, false, + filter, // The filter to apply + nil, // The attributes to retrieve + nil, + ) + + // Perform the search, return first result + sr, err := ldap.conn.Search(searchRequest) + if err != nil { + return nil, err + } else if len(sr.Entries) == 0 { + return nil, nil + } else { + return schema.NewObjectFromEntry(sr.Entries[0]), nil + } +} + /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS +// Connect to the LDAP server func ldapConnect(host string, port int, tls *tls.Config) (*goldap.Conn, error) { var url string if tls != nil { @@ -298,10 +485,12 @@ func ldapConnect(host string, port int, tls *tls.Config) (*goldap.Conn, error) { return goldap.DialURL(url, goldap.DialWithTLSConfig(tls)) } +// Disconnect from the LDAP server func ldapDisconnect(conn *goldap.Conn) error { return conn.Close() } +// Bind to the LDAP server with a user and password func ldapBind(conn *goldap.Conn, user, password string) error { if user == "" || password == "" { return conn.UnauthenticatedBind(user) diff --git a/pkg/handler/ldap/ldap_test.go b/pkg/handler/ldap/ldap_test.go index f6eec0c..e406cdf 100644 --- a/pkg/handler/ldap/ldap_test.go +++ b/pkg/handler/ldap/ldap_test.go @@ -9,6 +9,7 @@ import ( // Packages "github.com/mutablelogic/go-server/pkg/handler/ldap" + "github.com/mutablelogic/go-server/pkg/handler/ldap/schema" provider "github.com/mutablelogic/go-server/pkg/provider" "github.com/stretchr/testify/assert" ) @@ -20,7 +21,9 @@ func Test_ldap_001(t *testing.T) { DN: Get(t, "LDAP_DN", true), User: Get(t, "LDAP_USER", false), }) - assert.NoError(err) + if !assert.NoError(err) { + t.SkipNow() + } assert.NotNil(ldap) } @@ -65,6 +68,78 @@ func Test_ldap_002(t *testing.T) { wg.Wait() } +func Test_ldap_003(t *testing.T) { + assert := assert.New(t) + ldap, err := ldap.New(ldap.Config{ + URL: Get(t, "LDAP_URL", true), + DN: Get(t, "LDAP_DN", true), + User: Get(t, "LDAP_USER", false), + Password: Get(t, "LDAP_PASSWORD", false), + }) + if !assert.NoError(err) { + t.SkipNow() + } + + provider := provider.NewProvider(ldap) + + // Run the task in the background + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + if err := provider.Run(ctx); err != nil { + t.Error(err) + } + }() + + // Wait for connection + time.Sleep(500 * time.Millisecond) + + // Create a new group + group, err := ldap.CreateGroup("testgroup3", schema.OptDescription("This is a test group")) + if !assert.NoError(err) { + t.SkipNow() + } + + // Create user + user, err := ldap.CreateUser("testuser3", + schema.OptGroupId(group.GroupId()), + schema.OptName("Test", "User"), + schema.OptDescription("This is a test user"), + schema.OptHomeDirectory("/home/testuser3"), + schema.OptMail("djt@test.com"), + ) + if !assert.NoError(err) { + ldap.Delete(group) + t.SkipNow() + } + + t.Log(user, group) + + if passwd, err := ldap.ChangePassword(user, "", ""); !assert.NoError(err) { + t.SkipNow() + } else { + t.Log("password=", passwd) + } + + if err := ldap.Delete(group); !assert.NoError(err) { + t.SkipNow() + } + + if err := ldap.Delete(user); !assert.NoError(err) { + t.SkipNow() + } + + // End + cancel() + + // Wait for the task to complete + wg.Wait() +} + /////////////////////////////////////////////////////////////////////////////// // ENVIRONMENT diff --git a/pkg/handler/ldap/schema/attrs.go b/pkg/handler/ldap/schema/attrs.go new file mode 100644 index 0000000..7007e65 --- /dev/null +++ b/pkg/handler/ldap/schema/attrs.go @@ -0,0 +1,78 @@ +package schema + +import "fmt" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Attr func(*Object) error + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Set description +func OptDescription(v string) Attr { + return func(o *Object) error { + o.Set("description", v) + return nil + } +} + +// Set Group ID +func OptGroupId(v int) Attr { + return func(o *Object) error { + if v < 0 { + return fmt.Errorf("OptGroupId: invalid value") + } + o.Set("gidNumber", fmt.Sprint(v)) + return nil + } +} + +// Set User ID +func OptUserId(v int) Attr { + return func(o *Object) error { + if v < 0 { + return fmt.Errorf("OptGroupId: invalid value") + } + o.Set("uidNumber", fmt.Sprint(v)) + return nil + } +} + +// Set Name +func OptName(givenName, surname string) Attr { + return func(o *Object) error { + if givenName != "" { + o.Set("givenName", givenName) + } + if surname != "" { + o.Set("sn", surname) + } + return nil + } +} + +// Set Mail +func OptMail(v string) Attr { + return func(o *Object) error { + o.Set("mail", v) + return nil + } +} + +// Set Home Directory +func OptHomeDirectory(v string) Attr { + return func(o *Object) error { + o.Set("homeDirectory", v) + return nil + } +} + +// Set Login Shell +func OptLoginShell(v string) Attr { + return func(o *Object) error { + o.Set("loginShell", v) + return nil + } +} diff --git a/pkg/handler/ldap/schema/object.go b/pkg/handler/ldap/schema/object.go index 1488439..d4c9e1e 100644 --- a/pkg/handler/ldap/schema/object.go +++ b/pkg/handler/ldap/schema/object.go @@ -2,35 +2,50 @@ package schema import ( "encoding/json" + "net/url" + "strconv" + "strings" - // Packages goldap "github.com/go-ldap/ldap/v3" + // Packages ) /////////////////////////////////////////////////////////////////////////////// // TYPES type Object struct { - *goldap.Entry + DN string `json:"dn"` + url.Values `json:"attrs,omitempty"` } /////////////////////////////////////////////////////////////////////////////// -// STRINGIFY +// GLOBALS -func (o *Object) MarshalJSON() ([]byte, error) { - j := struct { - DN string `json:"dn"` - Attrs map[string][]string `json:"attrs"` - }{ - DN: o.Entry.DN, - Attrs: make(map[string][]string), - } - for _, attr := range o.Entry.Attributes { - j.Attrs[attr.Name] = attr.Values +const ( + defaultCapacity = 10 +) + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewObject(v ...string) *Object { + o := new(Object) + o.DN = strings.Join(v, ",") + o.Values = url.Values{} + return o +} + +func NewObjectFromEntry(entry *goldap.Entry) *Object { + o := NewObject(entry.DN) + for _, attr := range entry.Attributes { + o.Values[attr.Name] = attr.Values } - return json.Marshal(j) + return o } +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + func (o *Object) String() string { data, _ := json.MarshalIndent(o, "", " ") return string(data) @@ -39,8 +54,26 @@ func (o *Object) String() string { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -func (o *Object) Set(name string, values ...string) { - o.Entry.Attributes = []*goldap.EntryAttribute{ - &goldap.EntryAttribute{Name: name, Values: values}, +func (o *Object) Set(attr string, values ...string) { + o.Values[attr] = values +} + +// Return gidNumber as an integer, returning -1 if not set +func (o *Object) GroupId() int { + if v := o.Get("gidNumber"); v != "" { + if v, err := strconv.ParseInt(v, 10, 32); err == nil && v >= 0 { + return int(v) + } + } + return -1 +} + +// Return uidNumber as an integer, returning -1 if not set +func (o *Object) UserId() int { + if v := o.Get("uidNumber"); v != "" { + if v, err := strconv.ParseInt(v, 10, 32); err == nil && v >= 0 { + return int(v) + } } + return -1 } diff --git a/pkg/handler/ldap/schema/object_test.go b/pkg/handler/ldap/schema/object_test.go new file mode 100644 index 0000000..173f17e --- /dev/null +++ b/pkg/handler/ldap/schema/object_test.go @@ -0,0 +1,19 @@ +package schema_test + +import ( + "testing" + + // Packages + "github.com/mutablelogic/go-server/pkg/handler/ldap/schema" + "github.com/stretchr/testify/assert" +) + +func Test_object_001(t *testing.T) { + assert := assert.New(t) + o := schema.NewObject("dn") + assert.NotNil(o) + + o.Set("objectClass", "class1", "class2") + assert.Equal("dn", o.DN) + assert.Equal("class1", o.Get("objectClass")) +} diff --git a/pkg/handler/ldap/schema/schema.go b/pkg/handler/ldap/schema/schema.go index c5d0719..fa5d043 100644 --- a/pkg/handler/ldap/schema/schema.go +++ b/pkg/handler/ldap/schema/schema.go @@ -4,8 +4,11 @@ import ( "fmt" "slices" - goldap "github.com/go-ldap/ldap/v3" - "github.com/mutablelogic/go-server/pkg/types" + // Packages + types "github.com/mutablelogic/go-server/pkg/types" + + // Namespace imports + . "github.com/djthorpe/go-errors" ) ///////////////////////////////////////////////////////////////////// @@ -13,11 +16,10 @@ import ( // Schema is the schema for the LDAP server type Schema struct { - DN string - UserOU string - GroupOU string - UserObjectClass []string - GroupObjectClass []string + UserOU string `hcl:"user-ou,optional" description:"User Organisational Unit"` + GroupOU string `hcl:"group-ou,optional" description:"Group Organisational Unit"` + UserObjectClass []string `hcl:"user-object-class,optional" description:"User object class"` + GroupObjectClass []string `hcl:"group-object-class,optional" description:"Group object class"` } ///////////////////////////////////////////////////////////////////// @@ -27,29 +29,138 @@ const ( defaultPosixUserObjectClass = "posixAccount" ) +const ( + // https://learn.microsoft.com/en-us/windows/win32/adschema/a-instancetype + INSTANCE_TYPE_WRITABLE = 0x00000004 +) + +const ( + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/11972272-09ec-4a42-bf5e-3e99b321cf55 + GROUP_TYPE_BUILTIN_LOCAL_GROUP = 0x00000001 + GROUP_TYPE_ACCOUNT_GROUP = 0x00000002 + GROUP_TYPE_RESOURCE_GROUP = 0x00000004 + GROUP_TYPE_UNIVERSAL_GROUP = 0x00000008 + GROUP_TYPE_APP_BASIC_GROUP = 0x00000010 + GROUP_TYPE_APP_QUERY_GROUP = 0x00000020 + GROUP_TYPE_SECURITY_ENABLED = 0x80000000 +) + +const ( + // https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/useraccountcontrol-manipulate-account-properties + USER_SCRIPT = 0x00000001 + USER_ACCOUNTDISABLE = 0x00000002 + USER_HOMEDIR_REQUIRED = 0x00000008 + USER_LOCKOUT = 0x00000010 + USER_PASSWD_NOTREQD = 0x00000020 + USER_PASSWD_CANT_CHANGE = 0x00000040 + USER_ENCRYPTED_TEXT_PWD_ALLOWED = 0x00000080 + USER_TEMP_DUPLICATE_ACCOUNT = 0x00000100 + USER_NORMAL_ACCOUNT = 0x00000200 + USER_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800 + USER_WORKSTATION_TRUST_ACCOUNT = 0x00001000 + USER_SERVER_TRUST_ACCOUNT = 0x00002000 + USER_DONT_EXPIRE_PASSWORD = 0x00010000 + USER_MNS_LOGON_ACCOUNT = 0x00020000 + USER_SMARTCARD_REQUIRED = 0x00040000 + USER_TRUSTED_FOR_DELEGATION = 0x00080000 + USER_NOT_DELEGATED = 0x00100000 + USER_USE_DES_KEY_ONLY = 0x00200000 + USER_DONT_REQ_PREAUTH = 0x00400000 + USER_PASSWORD_EXPIRED = 0x00800000 + USER_TRUSTED_TO_AUTH_FOR_DELEGATION = 0x01000000 + USER_PARTIAL_SECRETS_ACCOUNT = 0x04000000 +) + ///////////////////////////////////////////////////////////////////// // PUBLIC METHODS -func (s Schema) IsPosix() bool { - return slices.Contains(s.UserObjectClass, defaultPosixUserObjectClass) +// Returns a new group object +func (s Schema) NewGroup(dn, name string, attrs ...Attr) (*Object, error) { + // Check parameters + if !types.IsIdentifier(name) || s.GroupOU == "" { + return nil, ErrBadParameter.With("name") + } + + // Set attributes + group := NewObject(s.groupDN(dn, name)) + group.Set("objectClass", s.GroupObjectClass...) + group.Set("cn", name) + if !s.isPosix() { + // Active Directory Supported + group.Set("sAMAccountName", name) + group.Set("instanceType", fmt.Sprintf("0x%08X", INSTANCE_TYPE_WRITABLE)) + group.Set("groupType", fmt.Sprintf("0x%08X", GROUP_TYPE_RESOURCE_GROUP|GROUP_TYPE_SECURITY_ENABLED)) + } + + // Add additional attributes + for _, attr := range attrs { + if err := attr(group); err != nil { + return nil, err + } + } + + // groupOfUniqueNames support + if slices.Contains(s.GroupObjectClass, "groupOfUniqueNames") && !group.Has("uniqueMember") { + group.Set("uniqueMember", group.DN) + } + + // Return success + return group, nil } -func (s Schema) GroupDN(name string) string { - return fmt.Sprintf("cn=%s,ou=%s,%s", name, s.GroupOU, s.DN) +// Returns a new user object +func (s Schema) NewUser(dn, name string, attrs ...Attr) (*Object, error) { + // Check parameters + if !types.IsIdentifier(name) || s.UserOU == "" { + return nil, ErrBadParameter.With("name") + } + + // Set attributes + user := NewObject(s.userDN(dn, name)) + user.Set("objectClass", s.UserObjectClass...) + user.Set("cn", name) + if !s.isPosix() { + // Active Directory Supported + user.Set("name", name) + user.Set("sAMAccountName", name) + user.Set("userAccountControl", fmt.Sprintf("0x%08X", 0)) + user.Set("instanceType", fmt.Sprintf("0x%08X", INSTANCE_TYPE_WRITABLE)) + } + + // Add additional attributes + for _, attr := range attrs { + if err := attr(user); err != nil { + return nil, err + } + } + + // set userPrincipalName + if !s.isPosix() && user.Has("mail") { + user.Set("userPrincipalName", user.Get("mail")) + } + + // Return success + return user, nil } -func (s Schema) NewObject(entry *goldap.Entry) *Object { - return &Object{Entry: entry} +///////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +// Returns true if the schema is posix (objectClass includes posixAccount) +func (s Schema) isPosix() bool { + return slices.Contains(s.UserObjectClass, defaultPosixUserObjectClass) } -func (s Schema) NewGroup(name string) *Object { - // Check parameters - if !types.IsIdentifier(name) || s.GroupOU == "" { - return nil +// Returns the group DN for a given name +func (s Schema) groupDN(dn, name string) string { + return fmt.Sprintf("cn=%s,ou=%s,%s", name, s.GroupOU, dn) +} + +// Returns the user DN for a given name +func (s Schema) userDN(dn, name string) string { + if s.isPosix() { + return fmt.Sprintf("uid=%s,ou=%s,%s", name, s.UserOU, dn) + } else { + return fmt.Sprintf("cn=%s,ou=%s,%s", name, s.UserOU, dn) } - group := &Object{Entry: &goldap.Entry{ - DN: s.GroupDN(name), - }} - group.Set("objectClass", s.GroupObjectClass...) - return group } diff --git a/pkg/handler/ldap/scope.go b/pkg/handler/ldap/scope.go new file mode 100644 index 0000000..a333919 --- /dev/null +++ b/pkg/handler/ldap/scope.go @@ -0,0 +1,33 @@ +package ldap + +import ( + // Packages + "github.com/mutablelogic/go-server/pkg/version" +) + +//////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +var ( + // Prefix + scopePrefix = version.GitSource + "/scope/" +) + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (service *ldap) ScopeRead() []string { + // Return read (list, get) scopes + return []string{ + scopePrefix + service.Label() + "/read", + scopePrefix + defaultName + "/read", + } +} + +func (service *ldap) ScopeWrite() []string { + // Return write (create, delete) scopes + return []string{ + scopePrefix + service.Label() + "/write", + scopePrefix + defaultName + "/write", + } +} diff --git a/pkg/handler/ldap/task.go b/pkg/handler/ldap/task.go index c6f10e6..17cc6a7 100644 --- a/pkg/handler/ldap/task.go +++ b/pkg/handler/ldap/task.go @@ -58,8 +58,10 @@ FOR_LOOP: } else { // Indicate that we are connected. If subsequent connections fail, then // we will log the error but continue to try to connect - log.Print(ctx, "Connected") - first = true + if !first { + log.Printf(ctx, "Connected to %s:%d", task.Host(), task.Port()) + first = true + } } // We attempt to ping the connection every minute diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 0beb11d..7f2195a 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -3,6 +3,7 @@ package provider import ( "context" "errors" + "fmt" "log" "sync" @@ -72,6 +73,11 @@ func (p *provider) Run(ctx context.Context) error { var result error var wg sync.WaitGroup + // Create a child context which will allow us to cancel all the tasks + // prematurely if any of them fail + child, prematureCancel := context.WithCancel(ctx) + defer prematureCancel() + // Run all the tasks in parallel for i := range p.tasks { // Create a context for each task @@ -92,14 +98,16 @@ func (p *provider) Run(ctx context.Context) error { p.Print(ctx, "Running") if err := p.tasks[i].Run(ctx); err != nil { - p.Print(ctx, err) - result = errors.Join(result, err) + result = errors.Join(result, fmt.Errorf("[%s] %w", Label(ctx), err)) + + // We indicate we should cancel + prematureCancel() } }(i) } // Wait for the cancel - <-ctx.Done() + <-child.Done() // Cancel all the tasks in reverse order, waiting for each to complete // before cancelling the next From e47a324c6c725e390275c44589da3fba27b76d11 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 11:14:57 +0200 Subject: [PATCH 05/11] Chnages --- pkg/handler/ldap/config.go | 10 ++++------ pkg/handler/ldap/endpoints.go | 23 ++++++++++++++++++++++- pkg/handler/ldap/ldap.go | 6 +----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/pkg/handler/ldap/config.go b/pkg/handler/ldap/config.go index 6779fd2..19725c1 100644 --- a/pkg/handler/ldap/config.go +++ b/pkg/handler/ldap/config.go @@ -32,8 +32,8 @@ var _ server.Plugin = (*Config)(nil) const ( defaultName = "ldap" defaultMethodPlain = "ldap" - defaultPortPlain = 389 defaultMethodSecure = "ldaps" + defaultPortPlain = 389 defaultPortSecure = 636 deltaPingTime = time.Minute defaultUserOU = "users" @@ -60,7 +60,7 @@ func (c Config) New() (server.Task, error) { return New(c) } -func (c Config) ObjectSchema() (*schema.Schema, error) { +func (c Config) ObjectSchema() *schema.Schema { schema := &schema.Schema{ UserObjectClass: defaultUserObjectClass, GroupObjectClass: defaultGroupObjectClass, @@ -74,11 +74,9 @@ func (c Config) ObjectSchema() (*schema.Schema, error) { if len(c.Schema.GroupObjectClass) > 0 { schema.GroupObjectClass = c.Schema.GroupObjectClass } - if c.Schema.UserOU != "" { + if c.Schema.UserOU != "" && c.Schema.GroupOU != "" { schema.UserOU = c.Schema.UserOU - } - if c.Schema.GroupOU != "" { schema.GroupOU = c.Schema.GroupOU } - return schema, nil + return schema } diff --git a/pkg/handler/ldap/endpoints.go b/pkg/handler/ldap/endpoints.go index deb2352..567b4d5 100644 --- a/pkg/handler/ldap/endpoints.go +++ b/pkg/handler/ldap/endpoints.go @@ -9,7 +9,7 @@ import ( server "github.com/mutablelogic/go-server" router "github.com/mutablelogic/go-server/pkg/handler/router" httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse" - "github.com/mutablelogic/go-server/pkg/types" + types "github.com/mutablelogic/go-server/pkg/types" ) /////////////////////////////////////////////////////////////////////////////// @@ -18,6 +18,27 @@ import ( // Check interfaces are satisfied var _ server.ServiceEndpoints = (*ldap)(nil) +// Request to create a user +type reqCreateUser struct { + Name string `json:"name"` + UserId int `json:"uid,omitempty"` + GroupId int `json:"gid,omitempty"` + Description string `json:"description,omitempty"` + + // Other attributes - homeDirectory, loginShell, givenName, surname, mail, etc + Attrs map[string][]string `json:"attrs,omitempty"` +} + +// Request to create a group +type reqCreateGroup struct { + Name string `json:"name"` + GroupId int `json:"gid,omitempty"` + Description string `json:"description,omitempty"` + + // Other attributes + Attrs map[string][]string `json:"attrs,omitempty"` +} + /////////////////////////////////////////////////////////////////////////////// // GLOBALS diff --git a/pkg/handler/ldap/ldap.go b/pkg/handler/ldap/ldap.go index 057558b..3d88f0e 100644 --- a/pkg/handler/ldap/ldap.go +++ b/pkg/handler/ldap/ldap.go @@ -93,11 +93,7 @@ func New(c Config) (*ldap, error) { } // Set the schema - if schema, err := c.ObjectSchema(); err != nil { - return nil, err - } else { - self.schema = schema - } + self.schema = c.ObjectSchema() // Return success return self, nil From 083c2936998f3cedd388432bd871165fab5d0165 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 13:46:17 +0200 Subject: [PATCH 06/11] Updated nginx --- cmd/nginx-server/main.go | 87 ++++--- etc/docker/Dockerfile | 4 +- etc/docker/entrypoint.sh | 3 + pkg/handler/auth/token.go | 20 +- pkg/handler/nginx/conf/enabled/default.conf | 5 +- pkg/handler/nginx/conf/nginx.conf | 4 +- pkg/handler/nginx/config.go | 89 +++++++- pkg/handler/nginx/endpoints.go | 6 + pkg/handler/nginx/interface.go | 10 +- pkg/handler/nginx/nginx.go | 215 ++++++++++++++++++ .../nginx/{task_test.go => nginx_test.go} | 4 +- pkg/handler/nginx/task.go | 206 +++-------------- pkg/handler/router/interface.go | 3 + pkg/handler/router/router.go | 28 ++- pkg/handler/tokenjar/task.go | 3 +- pkg/handler/tokenjar/tokenjar.go | 14 +- 16 files changed, 458 insertions(+), 243 deletions(-) create mode 100644 pkg/handler/nginx/nginx.go rename pkg/handler/nginx/{task_test.go => nginx_test.go} (96%) diff --git a/cmd/nginx-server/main.go b/cmd/nginx-server/main.go index 1cdf10d..48e8fec 100644 --- a/cmd/nginx-server/main.go +++ b/cmd/nginx-server/main.go @@ -25,8 +25,10 @@ import ( ) var ( - binary = flag.String("path", "nginx", "Path to nginx binary") + binary = flag.String("nginx", "nginx", "Path to nginx binary") group = flag.String("group", "", "Group to run unix socket as") + data = flag.String("data", "", "Path to data (emphermeral) directory") + conf = flag.String("conf", "", "Path to conf (persistent) directory") ldap_password = flag.String("ldap-password", "", "LDAP admin password") ) @@ -41,25 +43,38 @@ func main() { // Create context which cancels on interrupt ctx := ctx.ContextForSignal(os.Interrupt, syscall.SIGQUIT) + // Set of tasks + var tasks []server.Task + // Logger logger, err := logger.Config{Flags: []string{"default", "prefix"}}.New() if err != nil { log.Fatal("logger: ", err) + } else { + tasks = append(tasks, logger) } // Nginx handler - n, err := nginx.Config{BinaryPath: *binary}.New() + n, err := nginx.Config{ + BinaryPath: *binary, + DataPath: *data, + ConfigPath: *conf, + }.New() if err != nil { log.Fatal("nginx: ", err) + } else { + tasks = append(tasks, n) } // Token Jar jar, err := tokenjar.Config{ - DataPath: n.(nginx.Nginx).Config(), + DataPath: n.(nginx.Nginx).ConfigPath(), WriteInterval: 30 * time.Second, }.New() if err != nil { - log.Fatal("tokenkar: ", err) + log.Fatal("tokenjar: ", err) + } else { + tasks = append(tasks, jar) } // Auth handler @@ -70,38 +85,39 @@ func main() { }.New() if err != nil { log.Fatal("auth: ", err) + } else { + tasks = append(tasks, auth) } - // Cert Storage + // Cert storage certstore, err := certstore.Config{ - DataPath: filepath.Join(n.(nginx.Nginx).Config(), "cert"), + DataPath: filepath.Join(n.(nginx.Nginx).ConfigPath(), "cert"), Group: *group, }.New() if err != nil { log.Fatal("certstore: ", err) + } else { + tasks = append(tasks, certstore) } + + // Cert manager certmanager, err := certmanager.Config{ CertStorage: certstore.(certmanager.CertStorage), }.New() if err != nil { log.Fatal("certmanager: ", err) + } else { + tasks = append(tasks, certmanager) } - // LDAP - ldap, err := ldap.Config{ - URL: "ldap://admin@cm1.local/", - DN: "dc=mutablelogic,dc=com", - Password: *ldap_password, - }.New() - if err != nil { - log.Fatal("ldap: ", err) - } - - // Location of the FCGI unix socket - socket := filepath.Join(n.(nginx.Nginx).Config(), "run/go-server.sock") + // Location of the FCGI unix socket - this should be the same + // as that listed in the nginx configuration + socket := filepath.Join(n.(nginx.Nginx).DataPath(), "nginx/go-server.sock") // Router - router, err := router.Config{ + // TODO: Promote middleware to the root of the configuration to reduce + // duplication + r, err := router.Config{ Services: router.ServiceConfig{ "nginx": { // /api/nginx/... Service: n.(server.ServiceEndpoints), @@ -124,31 +140,46 @@ func main() { auth.(server.Middleware), }, }, - "ldap": { // /api/ldap/... - Service: ldap.(server.ServiceEndpoints), - Middleware: []server.Middleware{ - logger.(server.Middleware), - auth.(server.Middleware), - }, - }, }, }.New() if err != nil { log.Fatal("router: ", err) + } else { + tasks = append(tasks, r) + } + + // Add router + r.(router.Router).AddServiceEndpoints("router", r.(server.ServiceEndpoints), logger.(server.Middleware), auth.(server.Middleware)) + + // LDAP + if *ldap_password != "" { + ldap, err := ldap.Config{ + URL: "ldap://admin@cm1.local/", + DN: "dc=mutablelogic,dc=com", + Password: *ldap_password, + }.New() + if err != nil { + log.Fatal("ldap: ", err) + } else { + r.(router.Router).AddServiceEndpoints("ldap", ldap.(server.ServiceEndpoints), logger.(server.Middleware), auth.(server.Middleware)) + tasks = append(tasks, ldap) + } } // HTTP Server httpserver, err := httpserver.Config{ Listen: socket, Group: *group, - Router: router.(http.Handler), + Router: r.(http.Handler), }.New() if err != nil { log.Fatal("httpserver: ", err) + } else { + tasks = append(tasks, httpserver) } // Run until we receive an interrupt - provider := provider.NewProvider(logger, n, jar, auth, certstore, certmanager, ldap, router, httpserver) + provider := provider.NewProvider(tasks...) provider.Print(ctx, "Press CTRL+C to exit") if err := provider.Run(ctx); err != nil { log.Fatal(err) diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index 268d34f..226e827 100755 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -39,4 +39,6 @@ EXPOSE 80 443 STOPSIGNAL SIGQUIT # Set group to nginx to set group permissions on the FCGI socket -CMD [ "/usr/local/bin/nginx-server", "-group", "nginx" ] +# Set data (ephermeral) directory to /var/run +# Set configuration (persistent) directory to /data +CMD [ "/usr/local/bin/nginx-server", "-group", "nginx", "-data", "/var/run", "-conf", "/data" ] diff --git a/etc/docker/entrypoint.sh b/etc/docker/entrypoint.sh index ee6ac69..4033ec2 100755 --- a/etc/docker/entrypoint.sh +++ b/etc/docker/entrypoint.sh @@ -8,6 +8,9 @@ fi # Create the /alloc/logs folder if it doesn't exist install -d -m 0755 /alloc/logs || exit 1 +# Create the persistent data folder if it doesn't exist +install -d -m 0755 /data || exit 1 + # Run the command set -e umask 022 diff --git a/pkg/handler/auth/token.go b/pkg/handler/auth/token.go index a1dbb92..f8461ba 100644 --- a/pkg/handler/auth/token.go +++ b/pkg/handler/auth/token.go @@ -20,6 +20,9 @@ type Token struct { Expire time.Time `json:"expire_time,omitempty" writer:",width:29"` // Time of expiration for the token Time time.Time `json:"access_time,omitempty" writer:",width:29"` // Time of last access Scope []string `json:"scopes,omitempty" writer:",wrap"` // Authentication scopes + + // Private field which when set true does not write the 'valid' field + write bool `json:"-"` } type TokenCreate struct { @@ -108,6 +111,12 @@ func (t Token) IsValid() bool { return false } +// Flag that the token is to be written to persistent storage +// and should not include the 'valid' field +func (t *Token) SetWrite() { + t.write = true +} + // Return true if the token is a zero token func (t Token) IsZero() bool { if t.Name == "" && t.Value == "" && t.Expire.IsZero() && t.Time.IsZero() && len(t.Scope) == 0 { @@ -165,12 +174,15 @@ func (t Token) MarshalJSON() ([]byte, error) { } else { buf.Write(data) } - buf.WriteRune(',') } - // Include the valid flag - buf.WriteString(`"valid":`) - buf.WriteString(strconv.FormatBool(t.IsValid())) + // Include the valid flag when write is false (means that + // the JSON is not for persistent storage) + if !t.write { + buf.WriteRune(',') + buf.WriteString(`"valid":`) + buf.WriteString(strconv.FormatBool(t.IsValid())) + } // Return success buf.WriteRune('}') diff --git a/pkg/handler/nginx/conf/enabled/default.conf b/pkg/handler/nginx/conf/enabled/default.conf index 79d0bd5..1d8ad1b 100755 --- a/pkg/handler/nginx/conf/enabled/default.conf +++ b/pkg/handler/nginx/conf/enabled/default.conf @@ -1,6 +1,7 @@ server { listen 80 default_server; listen [::]:80 default_server; + location / { root html; index index.html; @@ -16,8 +17,8 @@ server { fastcgi_param REQUEST_PREFIX /api; # where socket path is relative, the absolute path - # is based on the configuration folder - fastcgi_pass unix:run/go-server.sock; + # is based on the ephermeral data folder + fastcgi_pass unix:nginx/go-server.sock; } error_page 404 /404.html; error_page 500 502 503 504 /50x.html; diff --git a/pkg/handler/nginx/conf/nginx.conf b/pkg/handler/nginx/conf/nginx.conf index 0c6ae39..f6adcad 100755 --- a/pkg/handler/nginx/conf/nginx.conf +++ b/pkg/handler/nginx/conf/nginx.conf @@ -1,7 +1,8 @@ -user nginx; +user nobody; worker_processes auto; error_log stderr notice; pid run/nginx.pid; +# TODO: pid {{ .DataPath }}/nginx/nginx.pid; events { worker_connections 256; @@ -11,6 +12,7 @@ http { include mime.types; default_type application/octet-stream; access_log logs/nginx-access.log combined; +# TODO: access_log {{ .LogPath }}/nginx-access.log combined; sendfile on; keepalive_timeout 65; gzip on; diff --git a/pkg/handler/nginx/config.go b/pkg/handler/nginx/config.go index 2ea78dc..ff8a2fa 100644 --- a/pkg/handler/nginx/config.go +++ b/pkg/handler/nginx/config.go @@ -1,10 +1,16 @@ package nginx import ( - // Packages "fmt" + "os" + "path/filepath" + "time" + // Packages server "github.com/mutablelogic/go-server" + + // Namespace imports + . "github.com/djthorpe/go-errors" ) //////////////////////////////////////////////////////////////////////////// @@ -12,10 +18,16 @@ import ( type Config struct { BinaryPath string `hcl:"binary_path" description:"Path to nginx binary"` + ConfigPath string `hcl:"config" description:"Path to persistent configuration"` + DataPath string `hcl:"data" description:"Path to ephermeral data directory"` + LogPath string `hcl:"log" description:"Path to log directory"` + LogRotate time.Duration `hcl:"log_rotate_period" description:"TODO: Period for log rotations (1d)"` + LogKeep time.Duration `hcl:"log_keep_period" description:"TODO: Period for log deletions (28d)"` Env map[string]string `hcl:"env" description:"Environment variables to set"` Directives map[string]string `hcl:"directives" description:"Directives to set in nginx configuration"` - Available string `hcl:"available" description:"Path to available configuration files"` - Enabled string `hcl:"enabled" description:"Path to enabled configuration files"` + + // Private fields + deletePaths []string } // Check interfaces are satisfied @@ -31,6 +43,7 @@ const ( defaultConfExt = ".conf" defaultConfDirMode = 0755 defaultConfRecursive = true + defaultLogPath = "logs" // Relative to the ConfigPath ) /////////////////////////////////////////////////////////////////////////////// @@ -48,20 +61,80 @@ func (Config) Description() string { // Create a new task from the configuration func (c Config) New() (server.Task, error) { - if c.BinaryPath == "" { - c.BinaryPath = defaultExec - } - return New(c) + return New(&c) } +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + // Return the path to the nginx binary -func (c Config) Path() string { +func (c Config) ExecFile() string { if c.BinaryPath == "" { c.BinaryPath = defaultExec } return c.BinaryPath } +// Return the path to the persistent data (configurations) +func (c *Config) ConfigDir() (string, error) { + if c.ConfigPath == "" { + if path, err := os.MkdirTemp("", "nginx-conf-"); err != nil { + return "", err + } else { + c.ConfigPath = path + c.deletePaths = append(c.deletePaths, path) + } + } + if stat, err := os.Stat(c.ConfigPath); err != nil { + return "", err + } else if !stat.IsDir() { + return "", ErrBadParameter.With("not a directory: ", c.ConfigPath) + } else { + return c.ConfigPath, nil + } +} + +// Return the path to temporary data (pid, sockets) +func (c *Config) DataDir() (string, error) { + if c.DataPath == "" { + if path, err := os.MkdirTemp("", "nginx-data-"); err != nil { + return "", err + } else { + c.DataPath = path + c.deletePaths = append(c.deletePaths, path) + } + } + if stat, err := os.Stat(c.DataPath); err != nil { + return "", err + } else if !stat.IsDir() { + return "", ErrBadParameter.With("not a directory: ", c.DataPath) + } else { + return c.DataPath, nil + } +} + +// Return the path to logging data, relative to the configuration directory +func (c Config) LogDir(configDir string) (string, error) { + if c.LogPath == "" { + c.LogPath = defaultLogPath + } + if !filepath.IsAbs(c.LogPath) { + c.LogPath = filepath.Clean(filepath.Join(configDir, c.LogPath)) + if _, err := os.Stat(c.LogPath); os.IsNotExist(err) { + if err := os.MkdirAll(c.LogPath, defaultConfDirMode); err != nil { + return "", err + } + } + } + if stat, err := os.Stat(c.LogPath); err != nil { + return "", err + } else if !stat.IsDir() { + return "", ErrBadParameter.With("not a directory: ", c.LogPath) + } else { + return c.LogPath, nil + } +} + // Return the flags for the nginx binary func (c Config) Flags(configDir, prefix string) []string { result := make([]string, 0, 5) diff --git a/pkg/handler/nginx/endpoints.go b/pkg/handler/nginx/endpoints.go index 15fc731..61bb143 100644 --- a/pkg/handler/nginx/endpoints.go +++ b/pkg/handler/nginx/endpoints.go @@ -32,6 +32,12 @@ type responseTemplate struct { Body string `json:"body,omitempty"` // Can be used for PATCH } +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Check interfaces are satisfied +var _ server.ServiceEndpoints = (*nginx)(nil) + /////////////////////////////////////////////////////////////////////////////// // GLOBALS diff --git a/pkg/handler/nginx/interface.go b/pkg/handler/nginx/interface.go index ca2b041..7c62cdf 100644 --- a/pkg/handler/nginx/interface.go +++ b/pkg/handler/nginx/interface.go @@ -16,6 +16,12 @@ type Nginx interface { // return the nginx version string Version() string - // return the configuration path - Config() string + // return the persistent config path + ConfigPath() string + + // return the ephermeral data path + DataPath() string + + // return logfile path + LogPath() string } diff --git a/pkg/handler/nginx/nginx.go b/pkg/handler/nginx/nginx.go new file mode 100644 index 0000000..bb1ef55 --- /dev/null +++ b/pkg/handler/nginx/nginx.go @@ -0,0 +1,215 @@ +package nginx + +import ( + "bytes" + "context" + "log" + "os" + "path/filepath" + "strings" + "syscall" + + // Packages + + cmd "github.com/mutablelogic/go-server/pkg/handler/nginx/cmd" + folders "github.com/mutablelogic/go-server/pkg/handler/nginx/folders" + provider "github.com/mutablelogic/go-server/pkg/provider" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type nginx struct { + // The command to run nginx + run *cmd.Cmd + + // The command to test nginx configuration + test *cmd.Cmd + + // The persistent configuration path + configPath string + + // The ephemeral data path + dataPath string + + // The log path + logPath string + + // The version string from nginx + version []byte + + // The available and enabled configuration folders + folders *folders.Config + + // The temporarily created paths which should be removed on exit + deletePaths []string +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new http server from the configuration +func New(c *Config) (*nginx, error) { + task := new(nginx) + + // Set permissions on config and data dirs + configDir, err := c.ConfigDir() + if err != nil { + return nil, err + } else if err := os.Chmod(configDir, defaultConfDirMode); err != nil { + return nil, err + } else { + task.configPath = configDir + } + dataDir, err := c.DataDir() + if err != nil { + return nil, err + } else if err := os.Chmod(dataDir, defaultConfDirMode); err != nil { + return nil, err + } else { + task.dataPath = dataDir + } + logDir, err := c.LogDir(configDir) + if err != nil { + return nil, err + } else if err := os.Chmod(logDir, defaultConfDirMode); err != nil { + return nil, err + } else { + task.logPath = logDir + } + + // Create an available folder in the persistent data directory + availablePath := filepath.Join(configDir, "available") + if err := os.MkdirAll(availablePath, defaultConfDirMode); err != nil { + return nil, err + } + + // Create an enabled folder in the persistent data directory + enabledPath := filepath.Join(configDir, "enabled") + if err := os.MkdirAll(enabledPath, defaultConfDirMode); err != nil { + return nil, err + } + + // Read the configuration folders + if folders, err := folders.New(availablePath, enabledPath, defaultConfExt, defaultConfRecursive); err != nil { + return nil, err + } else { + folders.DirMode = defaultConfDirMode + task.folders = folders + } + + // Create a new command to run the server. Use prefix to ensure that + // the document root is contained within the temporary directory + if run, err := cmd.New(c.ExecFile(), c.Flags(task.configPath, task.configPath)...); err != nil { + return nil, err + } else if test, err := cmd.New(c.ExecFile(), c.Flags(task.configPath, task.configPath)...); err != nil { + return nil, err + } else { + task.run = run + task.test = test + task.test.SetArgs("-t", "-q") + } + + // Add the environment variables + if err := task.run.SetEnv(c.Env); err != nil { + return nil, err + } else if err := task.test.SetEnv(c.Env); err != nil { + return nil, err + } + + // Set the working directory to the ephemeral data directory + task.run.SetDir(task.dataPath) + task.test.SetDir(task.dataPath) + + // Set the paths which should be deleted on exit + task.deletePaths = c.deletePaths + + // Return success + return task, nil +} + +///////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// return the persistent config path +func (nginx *nginx) ConfigPath() string { + return nginx.configPath +} + +// return the ephermeral data path +func (nginx *nginx) DataPath() string { + return nginx.dataPath +} + +// return the log path +func (nginx *nginx) LogPath() string { + return nginx.logPath +} + +// Test configuration +func (nginx *nginx) Test() error { + return nginx.test.Run() +} + +// Test the configuration and then reload it (the SIGHUP signal) +func (nginx *nginx) Reload() error { + // Reload the folders + if err := nginx.folders.Reload(); err != nil { + return err + } + + // Test the configuration + if err := nginx.test.Run(); err != nil { + return err + } + + // Signal the server to reload + return nginx.run.Signal(syscall.SIGHUP) +} + +// Reopen log files (the SIGUSR1 signal) +func (nginx *nginx) Reopen() error { + return nginx.run.Signal(syscall.SIGUSR1) +} + +// Version returns the nginx version string +func (nginx *nginx) Version() string { + if nginx.version == nil { + if version, err := nginx.getVersion(); err == nil { + nginx.version = version + return string(bytes.TrimSpace(version)) + } + } + return string(bytes.TrimSpace(nginx.version)) +} + +///////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (nginx *nginx) log(ctx context.Context, line string) { + line = strings.TrimSpace(line) + if logger := provider.Logger(ctx); logger != nil { + logger.Print(ctx, line) + } else { + log.Println(line) + } +} + +func (nginx *nginx) getVersion() ([]byte, error) { + var result []byte + + // Run the version command to get the nginx version string + version, err := cmd.New(nginx.run.Path(), "-v") + if err != nil { + return nil, err + } + version.Err = func(data []byte) { + result = append(result, data...) + } + if err := version.Run(); err != nil { + return nil, err + } + + // Return success + return result, nil +} diff --git a/pkg/handler/nginx/task_test.go b/pkg/handler/nginx/nginx_test.go similarity index 96% rename from pkg/handler/nginx/task_test.go rename to pkg/handler/nginx/nginx_test.go index 7f4ffb2..c6fd3d1 100644 --- a/pkg/handler/nginx/task_test.go +++ b/pkg/handler/nginx/nginx_test.go @@ -23,7 +23,7 @@ func Test_nginx_001(t *testing.T) { func Test_nginx_002(t *testing.T) { assert := assert.New(t) - task, err := nginx.New(nginx.Config{ + task, err := nginx.New(&nginx.Config{ BinaryPath: BinaryExec(t), }) assert.NoError(err) @@ -35,7 +35,7 @@ func Test_nginx_003(t *testing.T) { // Create a new task assert := assert.New(t) - task, err := nginx.New(nginx.Config{ + task, err := nginx.New(&nginx.Config{ BinaryPath: BinaryExec(t), }) assert.NoError(err) diff --git a/pkg/handler/nginx/task.go b/pkg/handler/nginx/task.go index 90536db..177223b 100644 --- a/pkg/handler/nginx/task.go +++ b/pkg/handler/nginx/task.go @@ -1,122 +1,22 @@ package nginx import ( - "bytes" "context" "errors" - "log" "os" - "path/filepath" - "strings" "sync" "syscall" "time" // Packages server "github.com/mutablelogic/go-server" - cmd "github.com/mutablelogic/go-server/pkg/handler/nginx/cmd" - folders "github.com/mutablelogic/go-server/pkg/handler/nginx/folders" - provider "github.com/mutablelogic/go-server/pkg/provider" ) /////////////////////////////////////////////////////////////////////////////// // TYPES -type nginx struct { - // The command to run nginx - run *cmd.Cmd - - // The command to test nginx configuration - test *cmd.Cmd - - // The configuration directory - config string - - // The version string from nginx - version []byte - - // The available and enabled configuration folders - folders *folders.Config -} - // Check interfaces are satisfied var _ server.Task = (*nginx)(nil) -var _ server.ServiceEndpoints = (*nginx)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Create a new http server from the configuration -func New(c Config) (*nginx, error) { - task := new(nginx) - - // Set configuration to a temporary directory, and set - // appropriate permissions - if config, err := os.MkdirTemp("", "nginx-"); err != nil { - return nil, err - } else if err := os.Chmod(config, defaultConfDirMode); err != nil { - return nil, errors.Join(err, os.RemoveAll(config)) - } else { - task.config = config - } - - // Create an available folder if it's not set - if c.Available == "" { - c.Available = filepath.Join(task.config, "available") - if err := os.MkdirAll(c.Available, defaultConfDirMode); err != nil { - return nil, err - } - } - - // Create an enabled folder if it's not set - if c.Enabled == "" { - c.Enabled = filepath.Join(task.config, "enabled") - if err := os.MkdirAll(c.Enabled, defaultConfDirMode); err != nil { - return nil, err - } - } - - // Read the configuration folders - if folders, err := folders.New(c.Available, c.Enabled, defaultConfExt, defaultConfRecursive); err != nil { - return nil, err - } else { - folders.DirMode = defaultConfDirMode - task.folders = folders - } - - // We need to set up some folders: - // run - for the nginx.pid file, socket files, etc - // The run directory needs to be writableTODO: Make the run directory writable by the group - // Set group permission - // if err := os.Chmod(task.config, 0770); err != nil { - // .... - - // Create a new command to run the server. Use prefix to ensure that - // the document root is contained within the temporary directory - if run, err := cmd.New(c.Path(), c.Flags(task.config, task.config)...); err != nil { - return nil, err - } else if test, err := cmd.New(c.Path(), c.Flags(task.config, task.config)...); err != nil { - return nil, err - } else { - task.run = run - task.test = test - task.test.SetArgs("-t", "-q") - } - - // Add the environment variables - if err := task.run.SetEnv(c.Env); err != nil { - return nil, err - } else if err := task.test.SetEnv(c.Env); err != nil { - return nil, err - } - - // Set the working directory - task.run.SetDir(task.config) - task.test.SetDir(task.config) - - // Return success - return task, nil -} ///////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -127,28 +27,30 @@ func (task *nginx) Label() string { return defaultName } -// Return the path to the configuration -func (task *nginx) Config() string { - return task.config -} - // Run the http server until the context is cancelled func (task *nginx) Run(ctx context.Context) error { var wg sync.WaitGroup var result error - // Remove the temporary directory + // TODO Remove the temporary directies (Config,Data) if they were created defer func() { - if _, err := os.Stat(task.config); err == nil { - if err := os.RemoveAll(task.config); err != nil { - result = errors.Join(result, err) + var result error + for _, path := range task.deletePaths { + if _, err := os.Stat(path); err == nil { + task.log(ctx, "Removing temporary path: "+path) + if err := os.RemoveAll(path); err != nil { + result = errors.Join(result, err) + } } } + if result != nil { + task.log(ctx, result.Error()) + } }() // We need to copy the configuration files to the temporary directory // then reload the folder configuration - if err := fsCopyTo(task.config); err != nil { + if err := fsCopyTo(task.configPath); err != nil { return err } else if err := task.folders.Reload(); err != nil { return err @@ -177,8 +79,20 @@ func (task *nginx) Run(ctx context.Context) error { } }() - // Wait for the context to be cancelled - <-ctx.Done() + // Runloop for nginx + timer := time.NewTicker(time.Second) + defer timer.Stop() +RUN_LOOP: + for { + select { + case <-ctx.Done(): + break RUN_LOOP + case <-timer.C: + if task.run.Exited() { + break RUN_LOOP + } + } + } // Perform shutdown, escalating signals checkTicker := time.NewTicker(500 * time.Millisecond) @@ -222,71 +136,3 @@ FOR_LOOP: // Return any errors return result } - -// Test configuration -func (task *nginx) Test() error { - return task.test.Run() -} - -// Test the configuration and then reload it (the SIGHUP signal) -func (task *nginx) Reload() error { - // Reload the folders - if err := task.folders.Reload(); err != nil { - return err - } - - // Test the configuration - if err := task.test.Run(); err != nil { - return err - } - - // Signal the server to reload - return task.run.Signal(syscall.SIGHUP) -} - -// Reopen log files (the SIGUSR1 signal) -func (task *nginx) Reopen() error { - return task.run.Signal(syscall.SIGUSR1) -} - -// Version returns the nginx version string -func (task *nginx) Version() string { - if task.version == nil { - if version, err := task.getVersion(); err == nil { - task.version = version - return string(bytes.TrimSpace(version)) - } - } - return string(bytes.TrimSpace(task.version)) -} - -///////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func (task *nginx) log(ctx context.Context, line string) { - line = strings.TrimSpace(line) - if logger := provider.Logger(ctx); logger != nil { - logger.Print(ctx, line) - } else { - log.Println(line) - } -} - -func (task *nginx) getVersion() ([]byte, error) { - var result []byte - - // Run the version command to get the nginx version string - version, err := cmd.New(task.run.Path(), "-v") - if err != nil { - return nil, err - } - version.Err = func(data []byte) { - result = append(result, data...) - } - if err := version.Run(); err != nil { - return nil, err - } - - // Return success - return result, nil -} diff --git a/pkg/handler/router/interface.go b/pkg/handler/router/interface.go index c31033f..a3baf86 100644 --- a/pkg/handler/router/interface.go +++ b/pkg/handler/router/interface.go @@ -16,6 +16,9 @@ type Router interface { // path parameters extracted from the path. Match(host, method, path string) (*matchedRoute, int) + // Add a service endpoint to the router, with specified host/prefix combinarion. + AddServiceEndpoints(string, server.ServiceEndpoints, ...server.Middleware) + // Return all known scopes Scopes() []string } diff --git a/pkg/handler/router/router.go b/pkg/handler/router/router.go index 591a1d6..b4b0a8c 100644 --- a/pkg/handler/router/router.go +++ b/pkg/handler/router/router.go @@ -43,17 +43,7 @@ func New(c Config) (server.Task, error) { // Add services for key, service := range c.Services { - parts := strings.SplitN(key, pathSep, 2) - if len(parts) == 1 { - // Could be interpreted as a host if there is a dot in it, or else - // we assume it's a path - if strings.Contains(parts[0], hostSep) { - parts = append(parts, pathSep) - } else { - parts, parts[0] = append(parts, parts[0]), "" - } - } - r.addServiceEndpoints(parts[0], parts[1], service.Service, service.Middleware...) + r.AddServiceEndpoints(key, service.Service, service.Middleware...) } // Return success @@ -209,6 +199,22 @@ func (router *router) Scopes() []string { return result } +// Add a service endpoint to the router. Returns an error if the prefix is +// already in use. +func (router *router) AddServiceEndpoints(hostprefix string, service server.ServiceEndpoints, middleware ...server.Middleware) { + parts := strings.SplitN(hostprefix, pathSep, 2) + if len(parts) == 1 { + // Could be interpreted as a host if there is a dot in it, or else + // we assume it's a path + if strings.Contains(parts[0], hostSep) { + parts = append(parts, pathSep) + } else { + parts, parts[0] = append(parts, parts[0]), "" + } + } + router.addServiceEndpoints(parts[0], parts[1], service, middleware...) +} + /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS diff --git a/pkg/handler/tokenjar/task.go b/pkg/handler/tokenjar/task.go index ba8e361..5a05256 100644 --- a/pkg/handler/tokenjar/task.go +++ b/pkg/handler/tokenjar/task.go @@ -22,7 +22,7 @@ func (jar *tokenjar) Run(ctx context.Context) error { logger := provider.Logger(ctx) // Ticker for writing to disk - ticker := time.NewTicker(jar.writeInterval) + ticker := time.NewTimer(time.Second) defer ticker.Stop() // Loop until cancelled @@ -36,6 +36,7 @@ func (jar *tokenjar) Run(ctx context.Context) error { logger.Printf(ctx, "Sync %q", filepath.Base(jar.filename)) } } + ticker.Reset(jar.writeInterval) case <-ctx.Done(): return jar.Write() } diff --git a/pkg/handler/tokenjar/tokenjar.go b/pkg/handler/tokenjar/tokenjar.go index 50dedb4..2972423 100644 --- a/pkg/handler/tokenjar/tokenjar.go +++ b/pkg/handler/tokenjar/tokenjar.go @@ -6,6 +6,7 @@ package tokenjar import ( "encoding/json" + "errors" "os" "path/filepath" "sort" @@ -270,9 +271,16 @@ func (jar *tokenjar) Write() error { defer w.Close() // Write the tokens, unset modified flag - jar.modified = false - if err := json.NewEncoder(w).Encode(jar.jar); err != nil { - return err + tokens := make([]*auth.Token, 0, len(jar.jar)) + for _, token := range jar.jar { + writable := *token + writable.SetWrite() + tokens = append(tokens, &writable) + } + if err := json.NewEncoder(w).Encode(tokens); err != nil { + return errors.Join(err, os.Remove(jar.filename)) + } else { + jar.modified = false } // Return success From b7d228decc08f3e6ba4b3428fc397231d02900e8 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 13:50:54 +0200 Subject: [PATCH 07/11] Added ldap plugin and changed the user on the nginx conf --- go.mod | 10 +++++++--- go.sum | 22 ++++++++++++++++++++++ pkg/handler/nginx/conf/nginx.conf | 2 +- plugin/ldap/main.go | 11 +++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 plugin/ldap/main.go diff --git a/go.mod b/go.mod index 0448d2a..9b0fd0a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.22.3 require ( github.com/djthorpe/go-errors v1.0.3 github.com/djthorpe/go-tablewriter v0.0.7 + github.com/go-ldap/ldap/v3 v3.4.8 github.com/mutablelogic/go-client v1.0.8 github.com/stretchr/testify v1.9.0 ) @@ -14,10 +15,13 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect - github.com/go-ldap/ldap/v3 v3.4.8 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.23.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 75a06e4..5d5815a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,6 +11,8 @@ github.com/djthorpe/go-tablewriter v0.0.7 h1:jnNsJDjjLLCt0OAqB5DzGZN7V3beT1IpNMQ github.com/djthorpe/go-tablewriter v0.0.7/go.mod h1:NVBvytpL+6fHfCKn0+3lSi15/G3A1HWf2cLNeHg6YBg= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -17,17 +20,29 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mutablelogic/go-client v1.0.8 h1:A3QtP0wdf+W3dE5k7dobwGYqqn4ZpIqRFu+h9vPoy7Y= github.com/mutablelogic/go-client v1.0.8/go.mod h1:aP9ecBd4R/acJEJSyp81U3mey9W3AHQV/G1XzfcrLx0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -45,6 +60,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -55,6 +72,7 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -68,12 +86,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/pkg/handler/nginx/conf/nginx.conf b/pkg/handler/nginx/conf/nginx.conf index f6adcad..8734fa1 100755 --- a/pkg/handler/nginx/conf/nginx.conf +++ b/pkg/handler/nginx/conf/nginx.conf @@ -1,4 +1,4 @@ -user nobody; +user nginx; worker_processes auto; error_log stderr notice; pid run/nginx.pid; diff --git a/plugin/ldap/main.go b/plugin/ldap/main.go new file mode 100644 index 0000000..974f6c8 --- /dev/null +++ b/plugin/ldap/main.go @@ -0,0 +1,11 @@ +package main + +import ( + // Packages + server "github.com/mutablelogic/go-server" + ldap "github.com/mutablelogic/go-server/pkg/handler/ldap" +) + +func Plugin() server.Plugin { + return ldap.Config{} +} From c5155cb3afb1ce82a6da2c4c76cca29f99787a37 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 14:35:02 +0200 Subject: [PATCH 08/11] Updated certmanager --- pkg/handler/certmanager/endpoints.go | 45 +++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index f53a861..2d831e0 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -45,13 +45,15 @@ var _ server.ServiceEndpoints = (*certmanager)(nil) // GLOBALS const ( - jsonIndent = 2 + jsonIndent = 2 + mimetypePem = "application/x-pem-file" ) var ( reRoot = regexp.MustCompile(`^/?$`) reCA = regexp.MustCompile(`^/ca/?$`) reSerial = regexp.MustCompile(`^/([0-9]+)/?$`) + rePem = regexp.MustCompile(`^/([0-9]+)/(cert\.pem|key\.pem)?$`) ) /////////////////////////////////////////////////////////////////////////////// @@ -86,6 +88,13 @@ func (service *certmanager) AddEndpoints(ctx context.Context, r server.Router) { // Description: Read a certificate by serial number r.AddHandlerFuncRe(ctx, reSerial, service.reqGetCert, http.MethodGet).(router.Route). SetScope(service.ScopeRead()...) + + // Path: //key or //cert + // Methods: GET + // Scopes: read + // Description: Read a PEM file for a certificate or key by serial number + r.AddHandlerFuncRe(ctx, rePem, service.reqGetCertPEM, http.MethodGet).(router.Route). + SetScope(service.ScopeRead()...) } /////////////////////////////////////////////////////////////////////////////// @@ -145,6 +154,40 @@ func (service *certmanager) reqGetCert(w http.ResponseWriter, r *http.Request) { httpresponse.JSON(w, respCert, http.StatusOK, jsonIndent) } +// Get a certificate or CA +func (service *certmanager) reqGetCertPEM(w http.ResponseWriter, r *http.Request) { + urlParameters := router.Params(r.Context()) + + // Get the certificate + cert, err := service.Read(urlParameters[0]) + if errors.Is(err, ErrNotFound) { + httpresponse.Error(w, http.StatusNotFound, err.Error()) + return + } else if err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + // Key or Cert + w.Header().Set("Content-Type", mimetypePem) + switch urlParameters[1] { + case "cert": + if err := cert.WriteCertificate(w); err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } + case "key": + if cert.IsCA() { + httpresponse.Error(w, http.StatusForbidden, "Cannot return private key for CA") + return + } + if err := cert.WritePrivateKey(w); err != nil { + httpresponse.Error(w, http.StatusInternalServerError, err.Error()) + return + } + } +} + // Create a new certificate authority func (service *certmanager) reqCreateCA(w http.ResponseWriter, r *http.Request) { var req reqCreateCA From 0ebf760302c26e26093948e341f5c6f36240c703 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 14:35:16 +0200 Subject: [PATCH 09/11] Updated cert manager --- cmd/nginx-server/main.go | 9 +++++++++ pkg/handler/certmanager/endpoints.go | 8 ++++---- pkg/httpresponse/httpresponse.go | 6 +++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cmd/nginx-server/main.go b/cmd/nginx-server/main.go index 48e8fec..16c373d 100644 --- a/cmd/nginx-server/main.go +++ b/cmd/nginx-server/main.go @@ -103,6 +103,15 @@ func main() { // Cert manager certmanager, err := certmanager.Config{ CertStorage: certstore.(certmanager.CertStorage), + X509Name: certmanager.X509Name{ + OrganizationalUnit: "mutablelogic.com", + Organization: "mutablelogic", + StreetAddress: "N/A", + Locality: "Berlin", + Province: "Berlin", + PostalCode: "10967", + Country: "DE", + }, }.New() if err != nil { log.Fatal("certmanager: ", err) diff --git a/pkg/handler/certmanager/endpoints.go b/pkg/handler/certmanager/endpoints.go index 2d831e0..65dd59f 100644 --- a/pkg/handler/certmanager/endpoints.go +++ b/pkg/handler/certmanager/endpoints.go @@ -53,7 +53,7 @@ var ( reRoot = regexp.MustCompile(`^/?$`) reCA = regexp.MustCompile(`^/ca/?$`) reSerial = regexp.MustCompile(`^/([0-9]+)/?$`) - rePem = regexp.MustCompile(`^/([0-9]+)/(cert\.pem|key\.pem)?$`) + rePem = regexp.MustCompile(`^/([0-9]+)/(cert|key)\.pem$`) ) /////////////////////////////////////////////////////////////////////////////// @@ -154,7 +154,7 @@ func (service *certmanager) reqGetCert(w http.ResponseWriter, r *http.Request) { httpresponse.JSON(w, respCert, http.StatusOK, jsonIndent) } -// Get a certificate or CA +// Get a certificate or CA as a PEM file func (service *certmanager) reqGetCertPEM(w http.ResponseWriter, r *http.Request) { urlParameters := router.Params(r.Context()) @@ -168,8 +168,8 @@ func (service *certmanager) reqGetCertPEM(w http.ResponseWriter, r *http.Request return } - // Key or Cert - w.Header().Set("Content-Type", mimetypePem) + // Write the certificate or key + w.Header().Set(httpresponse.ContentTypeKey, mimetypePem) switch urlParameters[1] { case "cert": if err := cert.WriteCertificate(w); err != nil { diff --git a/pkg/httpresponse/httpresponse.go b/pkg/httpresponse/httpresponse.go index 5a02324..63aa003 100644 --- a/pkg/httpresponse/httpresponse.go +++ b/pkg/httpresponse/httpresponse.go @@ -34,7 +34,7 @@ func JSON(w http.ResponseWriter, v interface{}, code int, indent uint) error { if w == nil { return nil } - w.Header().Add(ContentTypeKey, ContentTypeJSON) + w.Header().Set(ContentTypeKey, ContentTypeJSON) w.WriteHeader(code) enc := json.NewEncoder(w) if indent > 0 { @@ -48,7 +48,7 @@ func Text(w http.ResponseWriter, v string, code int) { if w == nil { return } - w.Header().Add(ContentTypeKey, ContentTypeText) + w.Header().Set(ContentTypeKey, ContentTypeText) w.WriteHeader(code) w.Write([]byte(v + "\n")) } @@ -58,7 +58,7 @@ func Empty(w http.ResponseWriter, code int) { if w == nil { return } - w.Header().Add(ContentLengthKey, "0") + w.Header().Set(ContentLengthKey, "0") w.WriteHeader(code) } From 20242bb4195a9d0c5703ef818ef4b78a2da451f6 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 14:48:35 +0200 Subject: [PATCH 10/11] Updated documentation --- README.md | 19 ++++++++++++++----- pkg/handler/tokenjar/config.go | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8c1ad72..5294ccd 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,25 @@ is a large "monolith" server which can be composed of many smaller ## Running the server -The easiest way to run an nginx reverse proxy server, with an API to +The easiest way to run an nginx reverse proxy server, with an API to manage nginx configuration, is through docker: ```bash -docker run -p 8080:80 ghcr.io/mutablelogic/go-server +docker run -p 8080:80 -v /var/lib/go-server:/data ghcr.io/mutablelogic/go-server ``` -This will start a server on port 8080. Use API commands to manage the -nginx configuration. Ultimately you'll want to develop your own plugins -and can use this image as the base image for your own server. +This will start a server on port 8080 and use `/var/lib/go-server` for persistent +data. Use API commands to manage the nginx configuration. Ultimately you'll +want to develop your own plugins and can use this image as the base image for your +own server. + +When you first run the server, a "root" API token is created which is used to +authenticate API requests. You can find this token in the log output or by running +the following command: + +```bash +docker exec cat /data/tokenauth.json +``` ## Requirements and Building diff --git a/pkg/handler/tokenjar/config.go b/pkg/handler/tokenjar/config.go index d2216e6..ba117d8 100644 --- a/pkg/handler/tokenjar/config.go +++ b/pkg/handler/tokenjar/config.go @@ -13,6 +13,7 @@ import ( type Config struct { DataPath string `hcl:"datapath" description:"Path to persistent data"` WriteInterval time.Duration `hcl:"write-interval" description:"Interval to write data to disk"` + // TODO Group so that we can set permissions to either 640 or 600 } // Check interfaces are satisfied From 67516f04772dd55ef22e523d945a538bfea1b58d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 8 Jun 2024 14:49:55 +0200 Subject: [PATCH 11/11] Removed some documentation --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 5294ccd..d4fe8e8 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,6 @@ other make targets: - `DOCKER_REPOSITORY=docker.io/user make docker` to build a docker image. - `DOCKER_REPOSITORY=docker.io/user make docker-push` to push a docker image. -## Running the Server - -You can run the server: - - 1. With a HTTP server over network: You can specify TLS key and certificate - to serve requests over a secure connection; - 2. With a HTTP server with FastCGI over a unix socket: You would want to do - this if the server is behind a reverse proxy such as nginx. - 3. In a docker container, and expose the port outside the container. The docker - container targets `amd64` and `arm64` architectures on Linux. - ## Project Status This module is currently __in development__ and is not yet ready for any production