diff --git a/auth/token_issuer.go b/auth/token_issuer.go index 57d6369..2529297 100644 --- a/auth/token_issuer.go +++ b/auth/token_issuer.go @@ -15,6 +15,7 @@ type LDAPTokenIssuer struct { LDAPServer string LDAPAuthenticator ldap.Authenticator TokenSigner token.Signer + LDAPOU string } func (lti *LDAPTokenIssuer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { @@ -26,7 +27,7 @@ func (lti *LDAPTokenIssuer) ServeHTTP(resp http.ResponseWriter, req *http.Reques } // Authenticate the user via LDAP - ldapEntry, err := lti.LDAPAuthenticator.Authenticate(user, password) + ldapEntry, err := lti.LDAPAuthenticator.Authenticate(user, password, lti.LDAPOU) if err != nil { glog.Errorf("Error authenticating user: %v", err) resp.WriteHeader(http.StatusUnauthorized) diff --git a/auth/token_issuer_test.go b/auth/token_issuer_test.go index c1a10ee..9224560 100644 --- a/auth/token_issuer_test.go +++ b/auth/token_issuer_test.go @@ -16,7 +16,7 @@ type dummyLDAP struct { err error } -func (d dummyLDAP) Authenticate(username, password string) (*ldap.Entry, error) { +func (d dummyLDAP) Authenticate(username, password, ldapOU string) (*ldap.Entry, error) { return d.entry, d.err } diff --git a/auth/webhook.go b/auth/webhook.go index f04e3c6..189a335 100644 --- a/auth/webhook.go +++ b/auth/webhook.go @@ -11,12 +11,14 @@ import ( // TokenWebhook responds to requests from the K8s authentication webhook type TokenWebhook struct { tokenVerifier token.Verifier + ldapOU string } // NewTokenWebhook returns a TokenWebhook with the given verifier -func NewTokenWebhook(verifier token.Verifier) *TokenWebhook { +func NewTokenWebhook(verifier token.Verifier, ldapOU string) *TokenWebhook { return &TokenWebhook{ tokenVerifier: verifier, + ldapOU: ldapOU, } } @@ -46,11 +48,15 @@ func (tw *TokenWebhook) ServeHTTP(resp http.ResponseWriter, req *http.Request) { } // Token is valid. + userInfo := UserInfo{ + Username: token.Username, + } + if tw.ldapOU != "" { + userInfo.Groups = []string{tw.ldapOU} + } trr.Status = TokenReviewStatus{ Authenticated: true, - User: UserInfo{ - Username: token.Username, - }, + User: userInfo, } respJSON, err := json.Marshal(trr) diff --git a/auth/webhook_test.go b/auth/webhook_test.go index 552ba0c..9a0abe7 100644 --- a/auth/webhook_test.go +++ b/auth/webhook_test.go @@ -54,7 +54,7 @@ func TestWebhook(t *testing.T) { for i, c := range cases { v := &dummyVerifier{token: c.verifiedToken, err: c.verifyErr} - tw := NewTokenWebhook(v) + tw := NewTokenWebhook(v, "") trr := &TokenReviewRequest{ Spec: TokenReviewSpec{ diff --git a/cmd/kubernetes-ldap.go b/cmd/kubernetes-ldap.go index 3a7980c..a2fa8a1 100755 --- a/cmd/kubernetes-ldap.go +++ b/cmd/kubernetes-ldap.go @@ -24,6 +24,7 @@ var flLdapAllowInsecure = flag.Bool("ldap-insecure", false, "Disable LDAP TLS") var flLdapHost = flag.String("ldap-host", "", "Host or IP of the LDAP server") var flLdapPort = flag.Uint("ldap-port", 389, "LDAP server port") var flBaseDN = flag.String("ldap-base-dn", "", "LDAP user base DN in the form 'dc=example,dc=com'") +var flLdapOU = flag.String("ldap-ou", "", "LDAP group/organizational unit (ou) a user must be member of") var flUserLoginAttribute = flag.String("ldap-user-attribute", "uid", "LDAP Username attribute for login") var flSearchUserDN = flag.String("ldap-search-user-dn", "", "Search user DN for this app to find users (e.g.: cn=admin,dc=example,dc=com).") var flSearchUserPassword = flag.String("ldap-search-user-password", "", "Search user password") @@ -87,11 +88,12 @@ func main() { server := &http.Server{Addr: fmt.Sprintf(":%d", *flServerPort)} - webhook := auth.NewTokenWebhook(tokenVerifier) + webhook := auth.NewTokenWebhook(tokenVerifier, *flLdapOU) ldapTokenIssuer := &auth.LDAPTokenIssuer{ LDAPAuthenticator: ldapClient, TokenSigner: tokenSigner, + LDAPOU: *flLdapOU, } // Endpoint for authenticating with token diff --git a/ldap/client.go b/ldap/client.go index af147e1..0b25ba3 100644 --- a/ldap/client.go +++ b/ldap/client.go @@ -4,13 +4,14 @@ import ( "crypto/tls" "errors" "fmt" + "strings" "github.com/go-ldap/ldap" ) // Authenticator authenticates a user against an LDAP directory type Authenticator interface { - Authenticate(username, password string) (*ldap.Entry, error) + Authenticate(username, password, ldapOU string) (*ldap.Entry, error) } // Client represents a connection, and associated lookup strategy, @@ -28,7 +29,7 @@ type Client struct { // Authenticate a user against the LDAP directory. Returns an LDAP entry if password // is valid, otherwise returns an error. -func (c *Client) Authenticate(username, password string) (*ldap.Entry, error) { +func (c *Client) Authenticate(username, password, ldapOU string) (*ldap.Entry, error) { conn, err := c.dial() if err != nil { return nil, fmt.Errorf("Error opening LDAP connection: %v", err) @@ -60,6 +61,13 @@ func (c *Client) Authenticate(username, password string) (*ldap.Entry, error) { return nil, fmt.Errorf("Multiple entries found for the search filter '%s': %+v", req.Filter, res.Entries) } + if ldapOU != "" { + err = c.isInLdapOU(conn, username, ldapOU) + if err != nil { + return nil, fmt.Errorf("Organizational unit check failed: %s", err) + } + } + // Now that we know the user exists within the BaseDN scope // let's do user bind to check credentials using the full DN instead of // the attribute used for search @@ -105,3 +113,26 @@ func (c *Client) newUserSearchRequest(username string) *ldap.SearchRequest { Filter: userFilter, } } + +func (c *Client) isInLdapOU(conn *ldap.Conn, username, ldapOU string) error { + req := &ldap.SearchRequest{ + BaseDN: c.BaseDN, + Scope: ldap.ScopeWholeSubtree, + Filter: fmt.Sprintf("(memberUid=%s)", username), + } + + res, err := conn.Search(req) + if err != nil { + return fmt.Errorf("Error searching for user %s: %v", username, err) + } + if len(res.Entries) == 0 { + return fmt.Errorf("Received an empty response") + } + for _, entry := range res.Entries { + if strings.Contains(entry.DN, "cn="+ldapOU) { + return nil + } + } + + return fmt.Errorf("%q is not a member of %q", username, ldapOU) +}