From 4722223603c6e5db92d802206fb9891bd8d5f272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Thu, 11 Feb 2021 20:44:07 +0100 Subject: [PATCH] Refactored into multiple files * The main program, command line argument parsing and logging initialization remains in main.go * Configuration structure and loader are in config.go * LDAP connection and querying is in ldap.go * Anything that has to do with Graylog, including the privilege mapping, is in graylog.go --- config.go | 100 ++++++++++ graylog.go | 254 ++++++++++++++++++++++++++ ldap.go | 197 ++++++++++++++++++++ main.go | 523 ----------------------------------------------------- 4 files changed, 551 insertions(+), 523 deletions(-) create mode 100644 config.go create mode 100644 graylog.go create mode 100644 ldap.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..6e24009 --- /dev/null +++ b/config.go @@ -0,0 +1,100 @@ +package main + +import ( + "io/ioutil" + + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +type ( + /* * + * CONFIGURATION DATA * + * */ + + // LDAP server configuration + LdapConfig struct { + Host string + Port uint16 + Tls string + TlsNoVerify bool `yaml:"tls_skip_verify"` + CaChain string `yaml:"cachain"` + BindUser string `yaml:"bind_user"` + BindPassword string `yaml:"bind_password"` + MemberFields []string `yaml:"member_fields"` + UsernameAttr string `yaml:"username_attribute"` + } + + // Graylog server configuration + GraylogConfig struct { + ApiBase string `yaml:"api_base"` + Username string + Password string + DeleteAccounts bool `yaml:"delete_accounts"` + } + + // A Graylog object on which privileges are defined + GraylogObject struct { + Type string + Id string + Level string + } + + // A mapping from a LDAP group to a set of privileges + GroupPrivileges struct { + Roles []string + Privileges []GraylogObject + } + + // All group mappings + GroupMapping map[string]GroupPrivileges + + // The whole configuration + Configuration struct { + Ldap LdapConfig + Graylog GraylogConfig + Mapping GroupMapping + } +) + +// Check group/privilege mapping configuration +func checkPrivMapping(cfg GroupMapping, log *logrus.Entry) { + for group, info := range cfg { + log := log.WithField("group", group) + for index, priv := range info.Privileges { + log := log.WithField("entry", index) + if !graylogItems[priv.Type] { + log.WithField("item", priv.Type). + Fatal("Invalid Graylog item") + } + if _, ok := privLevels[priv.Level]; !ok { + log.WithField("level", priv.Level). + Fatal("Invalid privilege level") + } + } + } +} + +// Load and check the configuration file +func loadConfiguration(flags cliFlags) (configuration Configuration) { + log := log.WithField("config", flags.cfgFile) + log.Trace("Loading configuration") + cfgData, err := ioutil.ReadFile(flags.cfgFile) + if err != nil { + log.WithField("error", err).Fatal("Could not load configuration") + } + + configuration = Configuration{ + Ldap: LdapConfig{ + Port: 389, + Tls: "no", + }, + } + err = yaml.Unmarshal(cfgData, &configuration) + if err != nil { + log.WithField("error", err).Fatal("Could not parse configuration") + } + + checkPrivMapping(configuration.Mapping, log) + return +} diff --git a/graylog.go b/graylog.go new file mode 100644 index 0000000..37e9024 --- /dev/null +++ b/graylog.go @@ -0,0 +1,254 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/sirupsen/logrus" +) + +type ( + // A Graylog user and associated roles + GraylogUser struct { + Username string + Roles []string + } + + // The response obtained when querying the Graylog server for a list of users. + GlUsers struct { + Users []struct { + GraylogUser + External bool + } + } +) + +var ( + // Privilege levels + privLevels = map[string]int{ + "read": 0, + "write": 1, + } + + // Privilege level string representation + privStr = []string{"read", "write"} + + // Graylog items on which privileges may be set + graylogItems = map[string]bool{ + "dashboard": true, + "stream": true, + } + + // Grayog privilege string templates + graylogPriv = map[string][]string{ + "dashboard:read": {"dashboards:read:%s", "view:read:%s"}, + "dashboard:write": {"dashboards:read:%s", "dashboards:edit:%s", "view:read:%s", "view:edit:%s"}, + "stream:read": {"streams:read:%s"}, + "stream:write": {"streams:read:%s", "streams:edit:%s", "streams:changestate:%s"}, + } +) + +// Execute a Graylog API request, returning the status code and the body +func executeApiCall(cfg GraylogConfig, method string, path string, data io.Reader) (status int, body []byte) { + log := log.WithFields(logrus.Fields{ + "base": cfg.ApiBase, + "username": cfg.Username, + "method": method, + "path": path, + }) + log.Trace("Executing Graylog API call") + client := &http.Client{} + request, err := http.NewRequest(method, fmt.Sprintf("%s/%s", cfg.ApiBase, path), data) + if err != nil { + log.WithField("error", err).Fatal("Could not create HTTP request") + } + request.SetBasicAuth(cfg.Username, cfg.Password) + if data != nil { + request.Header.Add("Content-Type", "application/json") + } + request.Header.Add("X-Requested-By", "graylog-groups") + response, err := client.Do(request) + if err != nil { + log.WithField("error", err).Fatal("Could not execute HTTP request") + } + defer response.Body.Close() + status = response.StatusCode + body, err = ioutil.ReadAll(response.Body) + if err != nil { + log.WithField("error", err).Fatal("Could not read Graylog response") + } + log.WithField("status", status).Trace("Executed Graylog API call") + return +} + +// Get the list of Graylog users that have been imported from LDAP +func getGraylogUsers(configuration GraylogConfig) (users []GraylogUser) { + log.Trace("Getting users from the Graylog API") + status, body := executeApiCall(configuration, "GET", "users", nil) + if status != 200 { + log.WithField("status", status).Fatal("Could not read users") + } + + data := GlUsers{} + if err := json.Unmarshal(body, &data); err != nil { + log.WithField("error", err).Fatal("Could not parse Graylog's user list") + } + + users = make([]GraylogUser, 0) + for _, item := range data.Users { + if item.External { + users = append(users, item.GraylogUser) + } + } + log.WithField("users", len(users)).Info("Obtained users from the Graylog API") + return +} + +// List groups an user is a member of. +func getUserGroups(user string, membership GroupMembers) (groups []string) { + groups = make([]string, 0) + for group, members := range membership { + for _, member := range members { + if member == user { + groups = append(groups, group) + break + } + } + } + return +} + +// Compute roles that should apply to an user +func computeRoles(mapping GroupMapping, membership []string) (roles []string) { + rset := make(map[string]bool) + for _, group := range membership { + for _, role := range mapping[group].Roles { + rset[role] = true + } + } + + roles = make([]string, len(rset)) + i := 0 + for group := range rset { + roles[i] = group + i++ + } + return +} + +// Compute privileges on Graylog objects that should be granted to an user +func computePrivileges(mapping GroupMapping, membership []string) (privileges []string) { + type privInfo struct { + otp, oid string + priv int + } + rset := make(map[string]privInfo) + for _, group := range membership { + for _, priv := range mapping[group].Privileges { + key := fmt.Sprintf("%s:%s", priv.Type, priv.Id) + record, ok := rset[key] + level := privLevels[priv.Level] + if ok && level <= record.priv { + continue + } + if !ok { + record.otp = priv.Type + record.oid = priv.Id + } + record.priv = level + rset[key] = record + } + } + + privileges = make([]string, 0) + for _, record := range rset { + key := fmt.Sprintf("%s:%s", record.otp, privStr[record.priv]) + for _, p := range graylogPriv[key] { + pval := fmt.Sprintf(p, record.oid) + privileges = append(privileges, pval) + } + } + return +} + +// Delete a Graylog user account +func deleteAccount(cfg GraylogConfig, user string) { + log := log.WithField("user", user) + log.Warning("Deleting Graylog account") + code, body := executeApiCall(cfg, "DELETE", fmt.Sprintf("/users/%s", user), nil) + if code != 204 { + log.WithFields(logrus.Fields{ + "status": code, + "body": string(body), + }).Fatal("Could not delete user") + } +} + +// Returns the strings that are in a but not in b. +func getDifference(a []string, b []string) (diff []string) { + diff = make([]string, 0) + for _, sa := range a { + found := false + for _, sb := range b { + if sa == sb { + found = true + break + } + } + if !found { + diff = append(diff, sa) + } + } + return +} + +// Set an account's roles and grant it access to Graylog objects +func setUserPrivileges(cfg GraylogConfig, user GraylogUser, roles []string, privileges []string) { + type perms struct { + Permissions []string `json:"permissions"` + } + p := perms{Permissions: privileges} + data, err := json.Marshal(p) + if err != nil { + log.Fatalf("unable to generate permissions JSON for %s: %v", user, err) + } + + code, body := executeApiCall(cfg, "PUT", fmt.Sprintf("users/%s/permissions", user.Username), bytes.NewBuffer(data)) + if code != 204 { + log.Fatalf("could not set permissions for %s: code %d, body '%s'", user.Username, code, string(body)) + } + + placeholder := bytes.NewBuffer([]byte("{}")) + for _, role := range getDifference(roles, user.Roles) { + ep := fmt.Sprintf("roles/%s/members/%s", role, user.Username) + code, body := executeApiCall(cfg, "PUT", ep, placeholder) + if code != 204 { + log.Fatalf("could not add role %s to %s: code %d, body '%s'", role, user.Username, code, string(body)) + } + } + for _, role := range getDifference(user.Roles, roles) { + ep := fmt.Sprintf("roles/%s/members/%s", role, user.Username) + code, body := executeApiCall(cfg, "DELETE", ep, nil) + if code != 204 { + log.Fatalf("could not remove role %s from %s: code %d, body '%s'", role, user.Username, code, string(body)) + } + } +} + +// Apply privilege mappings to the external Graylog users +func applyMapping(cfg Configuration, users []GraylogUser, groups GroupMembers) { + for _, user := range users { + membership := getUserGroups(user.Username, groups) + roles := computeRoles(cfg.Mapping, membership) + privileges := computePrivileges(cfg.Mapping, membership) + if cfg.Graylog.DeleteAccounts && len(roles) == 0 && len(privileges) == 0 { + deleteAccount(cfg.Graylog, user.Username) + } else { + setUserPrivileges(cfg.Graylog, user, roles, privileges) + } + } +} diff --git a/ldap.go b/ldap.go new file mode 100644 index 0000000..125e47e --- /dev/null +++ b/ldap.go @@ -0,0 +1,197 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "strings" + + "github.com/go-ldap/ldap" + "github.com/sirupsen/logrus" +) + +type ( + // LDAP connection encapsulation. This includes the connection itself, as well as a logger + // that includes fields related to the LDAP server and a copy of the initial configuration. + ldapConn struct { + conn *ldap.Conn + log *logrus.Entry + cfg LdapConfig + } + + // LDAP group members + GroupMembers map[string][]string +) + +// Establish a connection to the LDAP server +func getLdapConnection(cfg LdapConfig) ldapConn { + dest := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + log := log.WithFields(logrus.Fields{ + "ldap_server": dest, + "ldap_tls": cfg.Tls, + }) + log.Trace("Establishing LDAP connection") + + tlsConfig := &tls.Config{ + InsecureSkipVerify: cfg.TlsNoVerify, + } + if cfg.Tls != "no" && cfg.CaChain != "" { + log := log.WithField("cachain", cfg.CaChain) + data, err := ioutil.ReadFile(cfg.CaChain) + if err != nil { + log.WithField("error", err).Fatal("Failed to read CA certificate chain") + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(data) { + log.Fatal("Could not add CA certificates") + } + tlsConfig.RootCAs = pool + } + + var err error + var lc *ldap.Conn + if cfg.Tls == "yes" { + lc, err = ldap.DialTLS("tcp", dest, tlsConfig) + } else { + lc, err = ldap.Dial("tcp", dest) + } + if err != nil { + log.WithField("error", err).Fatal("Failed to connect to the LDAP server") + } + + if cfg.Tls == "starttls" { + err = lc.StartTLS(tlsConfig) + if err != nil { + lc.Close() + log.WithField("error", err).Fatal("StartTLS failed") + } + } + + if cfg.BindUser != "" { + log = log.WithField("ldap_user", cfg.BindUser) + err := lc.Bind(cfg.BindUser, cfg.BindPassword) + if err != nil { + lc.Close() + log.WithField("error", err).Fatal("Could not bind") + } + } + log.Debug("LDAP connection established") + return ldapConn{ + conn: lc, + log: log, + cfg: cfg, + } +} + +// Run a LDAP query to obtain a single object. +func (conn ldapConn) query(dn string, attrs []string) (bool, *ldap.Entry) { + log := conn.log.WithFields(logrus.Fields{ + "dn": dn, + "attributes": attrs, + }) + log.Trace("Accessing DN") + req := ldap.NewSearchRequest( + dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, + "(objectClass=*)", attrs, nil) + res, err := conn.conn.Search(req) + if err != nil { + ldapError, ok := err.(*ldap.Error) + if ok && ldapError.ResultCode == ldap.LDAPResultNoSuchObject { + log.Trace("DN not found") + return false, nil + } + log.WithField("error", err).Fatal("LDAP query failed") + } + if len(res.Entries) > 1 { + log.WithField("results", len(res.Entries)). + Warning("LDAP search returned more than 1 record") + return false, nil + } + log.Trace("Obtained LDAP object") + return true, res.Entries[0] +} + +// Close a LDAP connection +func (conn ldapConn) close() { + conn.log.Trace("Closing LDAP connection") + conn.conn.Close() +} + +// Read a username from a LDAP record based on a DN. +func (conn ldapConn) readUsername(dn string) (bool, string) { + log := conn.log.WithFields(logrus.Fields{ + "dn": dn, + "attribute": conn.cfg.UsernameAttr, + }) + log.Trace("Converting DN to username") + ok, res := conn.query(dn, []string{conn.cfg.UsernameAttr}) + if !ok { + return false, "" + } + values := res.GetAttributeValues(conn.cfg.UsernameAttr) + if len(values) != 1 { + log.WithField("count", len(values)). + Warning("Attribute does not have 1 value exactly.") + return false, "" + } + log.WithField("username", values[0]).Trace("Mapped DN to username") + return true, values[0] +} + +// Extract an username from something that may be an username or a DN. +func (conn ldapConn) usernameFromMember(member string) (bool, string) { + eqPos := strings.Index(member, "=") + if eqPos == -1 { + return true, member + } + if conn.cfg.UsernameAttr != "" { + return conn.readUsername(member) + } + commaPos := strings.Index(member, ",") + if commaPos == -1 { + return true, member[eqPos+1:] + } + if eqPos > commaPos { + log.WithField("member", member).Warning("Couldn't extract user name") + return false, "" + } + return true, member[eqPos+1 : commaPos] +} + +// Read the list of members from a LDAP group +func (conn ldapConn) getGroupMembers(group string) (members []string) { + log := conn.log.WithField("group", group) + log.Trace("Obtaining group members") + ok, entry := conn.query(group, conn.cfg.MemberFields) + if !ok { + return + } + for _, attr := range conn.cfg.MemberFields { + values := entry.GetAttributeValues(attr) + if len(values) == 0 { + continue + } + for _, value := range values { + ok, name := conn.usernameFromMember(value) + if ok { + members = append(members, name) + } + } + break + } + log.WithField("members", members).Info("Obtained group members") + return +} + +// Read the list of group members from the LDAP server for all groups in the mapping section. +func readLdapGroups(configuration Configuration) GroupMembers { + conn := getLdapConnection(configuration.Ldap) + defer conn.close() + groups := make(GroupMembers) + for group := range configuration.Mapping { + groups[group] = conn.getGroupMembers(group) + } + return groups +} diff --git a/main.go b/main.go index 190fc96..74a88c4 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,8 @@ package main import ( - "bytes" - "crypto/tls" - "crypto/x509" - "encoding/json" "flag" - "fmt" - "io" - "io/ioutil" - "net/http" - "strings" - - "github.com/go-ldap/ldap" "github.com/sirupsen/logrus" - "gopkg.in/yaml.v2" ) type ( @@ -27,523 +15,13 @@ type ( // The log level. logLevel string } - - // LDAP connection encapsulation, including a logger. - ldapConn struct { - conn *ldap.Conn - log *logrus.Entry - } - - /* * - * CONFIGURATION DATA * - * */ - - // LDAP server configuration - LdapConfig struct { - Host string - Port uint16 - Tls string - TlsNoVerify bool `yaml:"tls_skip_verify"` - CaChain string `yaml:"cachain"` - BindUser string `yaml:"bind_user"` - BindPassword string `yaml:"bind_password"` - MemberFields []string `yaml:"member_fields"` - UsernameAttr string `yaml:"username_attribute"` - } - - // Graylog server configuration - GraylogConfig struct { - ApiBase string `yaml:"api_base"` - Username string - Password string - DeleteAccounts bool `yaml:"delete_accounts"` - } - - // A Graylog object on which privileges are defined - GraylogObject struct { - Type string - Id string - Level string - } - - // A mapping from a LDAP group to a set of privileges - GroupPrivileges struct { - Roles []string - Privileges []GraylogObject - } - - // All group mappings - GroupMapping map[string]GroupPrivileges - - // The whole configuration - Configuration struct { - Ldap LdapConfig - Graylog GraylogConfig - Mapping GroupMapping - } - - /* * - * SERVER DATA * - * */ - - // A Graylog user - GraylogUser struct { - Username string - Roles []string - } - - // The response obtained when querying the Graylog server for a list of users. - GlUsers struct { - Users []struct { - GraylogUser - External bool - } - } - - // LDAP group members - GroupMembers map[string][]string ) var ( - // Privilege levels - privLevels = map[string]int{ - "read": 0, - "write": 1, - } - // Privilege level string representation - privStr = []string{"read", "write"} - // Graylog items on which privileges may be set - graylogItems = map[string]bool{ - "dashboard": true, - "stream": true, - } - // Grayog privilege string templates - graylogPriv = map[string][]string{ - "dashboard:read": {"dashboards:read:%s", "view:read:%s"}, - "dashboard:write": {"dashboards:read:%s", "dashboards:edit:%s", "view:read:%s", "view:edit:%s"}, - "stream:read": {"streams:read:%s"}, - "stream:write": {"streams:read:%s", "streams:edit:%s", "streams:changestate:%s"}, - } // The logging context. log *logrus.Entry ) -// Check group/privilege mapping configuration -func checkPrivMapping(cfg GroupMapping, log *logrus.Entry) { - for group, info := range cfg { - log := log.WithField("group", group) - for index, priv := range info.Privileges { - log := log.WithField("entry", index) - if !graylogItems[priv.Type] { - log.WithField("item", priv.Type). - Fatal("Invalid Graylog item") - } - if _, ok := privLevels[priv.Level]; !ok { - log.WithField("level", priv.Type). - Fatal("Invalid privilege level") - } - } - } -} - -// Load and check the configuration file -func loadConfiguration(flags cliFlags) (configuration Configuration) { - log := log.WithField("config", flags.cfgFile) - log.Trace("Loading configuration") - cfgData, err := ioutil.ReadFile(flags.cfgFile) - if err != nil { - log.WithField("error", err).Fatal("Could not load configuration") - } - - configuration = Configuration{ - Ldap: LdapConfig{ - Port: 389, - Tls: "no", - }, - } - err = yaml.Unmarshal(cfgData, &configuration) - if err != nil { - log.WithField("error", err).Fatal("Could not parse configuration") - } - - checkPrivMapping(configuration.Mapping, log) - return -} - -// Execute a Graylog API request, returning the status code and the body -func executeApiCall(cfg GraylogConfig, method string, path string, data io.Reader) (status int, body []byte) { - log := log.WithFields(logrus.Fields{ - "base": cfg.ApiBase, - "username": cfg.Username, - "method": method, - "path": path, - }) - log.Trace("Executing Graylog API call") - client := &http.Client{} - request, err := http.NewRequest(method, fmt.Sprintf("%s/%s", cfg.ApiBase, path), data) - if err != nil { - log.WithField("error", err).Fatal("Could not create HTTP request") - } - request.SetBasicAuth(cfg.Username, cfg.Password) - if data != nil { - request.Header.Add("Content-Type", "application/json") - } - request.Header.Add("X-Requested-By", "graylog-groups") - response, err := client.Do(request) - if err != nil { - log.WithField("error", err).Fatal("Could not execute HTTP request") - } - defer response.Body.Close() - status = response.StatusCode - body, err = ioutil.ReadAll(response.Body) - if err != nil { - log.WithField("error", err).Fatal("Could not read Graylog response") - } - log.WithField("status", status).Trace("Executed Graylog API call") - return -} - -// Get the list of Graylog users that have been imported from LDAP -func getGraylogUsers(configuration GraylogConfig) (users []GraylogUser) { - log.Trace("Getting users from the Graylog API") - status, body := executeApiCall(configuration, "GET", "users", nil) - if status != 200 { - log.WithField("status", status).Fatal("Could not read users") - } - - data := GlUsers{} - if err := json.Unmarshal(body, &data); err != nil { - log.WithField("error", err).Fatal("Could not parse Graylog's user list") - } - - users = make([]GraylogUser, 0) - for _, item := range data.Users { - if item.External { - users = append(users, item.GraylogUser) - } - } - log.WithField("users", len(users)).Info("Obtained users from the Graylog API") - return -} - -// Establish a connection to the LDAP server -func getLdapConnection(cfg LdapConfig) (conn ldapConn) { - dest := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - log := log.WithFields(logrus.Fields{ - "ldap_server": dest, - "ldap_tls": cfg.Tls, - }) - log.Trace("Establishing LDAP connection") - - tlsConfig := &tls.Config{ - InsecureSkipVerify: cfg.TlsNoVerify, - } - if cfg.Tls != "no" && cfg.CaChain != "" { - log := log.WithField("cachain", cfg.CaChain) - data, err := ioutil.ReadFile(cfg.CaChain) - if err != nil { - log.WithField("error", err).Fatal("Failed to read CA certificate chain") - } - pool := x509.NewCertPool() - if !pool.AppendCertsFromPEM(data) { - log.Fatal("Could not add CA certificates") - } - tlsConfig.RootCAs = pool - } - - var err error - var lc *ldap.Conn - if cfg.Tls == "yes" { - lc, err = ldap.DialTLS("tcp", dest, tlsConfig) - } else { - lc, err = ldap.Dial("tcp", dest) - } - conn = ldapConn{ - conn: lc, - log: log, - } - if err != nil { - conn.log.WithField("error", err).Fatal("Failed to connect to the LDAP server") - } - - if cfg.Tls == "starttls" { - err = lc.StartTLS(tlsConfig) - if err != nil { - lc.Close() - conn.log.WithField("error", err).Fatal("StartTLS failed") - } - } - - if cfg.BindUser != "" { - conn.log = conn.log.WithField("ldap_user", cfg.BindUser) - err := lc.Bind(cfg.BindUser, cfg.BindPassword) - if err != nil { - conn.close() - conn.log.WithField("error", err).Fatal("Could not bind") - } - } - log.Debug("LDAP connection established") - return -} - -// Run a LDAP query to obtain a single object. -func (conn ldapConn) query(dn string, attrs []string) (bool, *ldap.Entry) { - log := conn.log.WithFields(logrus.Fields{ - "dn": dn, - "attributes": attrs, - }) - log.Trace("Accessing DN") - req := ldap.NewSearchRequest( - dn, - ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, - "(objectClass=*)", attrs, nil) - res, err := conn.conn.Search(req) - if err != nil { - ldapError, ok := err.(*ldap.Error) - if ok && ldapError.ResultCode == ldap.LDAPResultNoSuchObject { - log.Trace("DN not found") - return false, nil - } - log.WithField("error", err).Fatal("LDAP query failed") - } - if len(res.Entries) > 1 { - log.WithField("results", len(res.Entries)). - Warning("LDAP search returned more than 1 record") - return false, nil - } - log.Trace("Obtained LDAP object") - return true, res.Entries[0] -} - -// Close a LDAP connection -func (conn ldapConn) close() { - conn.log.Trace("Closing LDAP connection") - conn.conn.Close() -} - -// Read a username from a LDAP record based on a DN. -func readUsernameFromLdap(dn string, conn ldapConn, attr string) (bool, string) { - log := conn.log.WithFields(logrus.Fields{ - "dn": dn, - "attribute": attr, - }) - log.Trace("Converting DN to username") - ok, res := conn.query(dn, []string{attr}) - if !ok { - return false, "" - } - values := res.GetAttributeValues(attr) - if len(values) != 1 { - log.WithField("count", len(values)). - Warning("Attribute does not have 1 value exactly.") - return false, "" - } - log.WithField("username", values[0]).Trace("Mapped DN to username") - return true, values[0] -} - -// Extract an username from something that may be an username or a DN. -func usernameFromMember(member string, conn ldapConn, config LdapConfig) (bool, string) { - eqPos := strings.Index(member, "=") - if eqPos == -1 { - return true, member - } - if config.UsernameAttr != "" { - return readUsernameFromLdap(member, conn, config.UsernameAttr) - } - commaPos := strings.Index(member, ",") - if commaPos == -1 { - return true, member[eqPos+1:] - } - if eqPos > commaPos { - log.Printf("couldn't extract user name from %s", member) - return false, "" - } - return true, member[eqPos+1 : commaPos] -} - -// Read the list of members from a LDAP group -func getGroupMembers(group string, conn ldapConn, config LdapConfig) (members []string) { - log := conn.log.WithField("group", group) - log.Trace("Obtaining group members") - ok, entry := conn.query(group, config.MemberFields) - if !ok { - return - } - for _, attr := range config.MemberFields { - values := entry.GetAttributeValues(attr) - if len(values) == 0 { - continue - } - for _, value := range values { - ok, name := usernameFromMember(value, conn, config) - if ok { - members = append(members, name) - } - } - break - } - log.WithField("members", members).Info("Obtained group members") - return -} - -// Read the list of group members from the LDAP server for all groups in the mapping section. -func readLdapGroups(configuration Configuration) (groups GroupMembers) { - conn := getLdapConnection(configuration.Ldap) - defer conn.close() - - groups = make(GroupMembers) - for group := range configuration.Mapping { - groups[group] = getGroupMembers(group, conn, configuration.Ldap) - } - return -} - -// List groups an user is a member of. -func getUserGroups(user string, membership GroupMembers) (groups []string) { - groups = make([]string, 0) - for group, members := range membership { - for _, member := range members { - if member == user { - groups = append(groups, group) - break - } - } - } - return -} - -// Compute roles that should apply to an user -func computeRoles(mapping GroupMapping, membership []string) (roles []string) { - rset := make(map[string]bool) - for _, group := range membership { - for _, role := range mapping[group].Roles { - rset[role] = true - } - } - - roles = make([]string, len(rset)) - i := 0 - for group := range rset { - roles[i] = group - i++ - } - return -} - -// Compute privileges on Graylog objects that should be granted to an user -func computePrivileges(mapping GroupMapping, membership []string) (privileges []string) { - type privInfo struct { - otp, oid string - priv int - } - rset := make(map[string]privInfo) - for _, group := range membership { - for _, priv := range mapping[group].Privileges { - key := fmt.Sprintf("%s:%s", priv.Type, priv.Id) - record, ok := rset[key] - level := privLevels[priv.Level] - if ok && level <= record.priv { - continue - } - if !ok { - record.otp = priv.Type - record.oid = priv.Id - } - record.priv = level - rset[key] = record - } - } - - privileges = make([]string, 0) - for _, record := range rset { - key := fmt.Sprintf("%s:%s", record.otp, privStr[record.priv]) - for _, p := range graylogPriv[key] { - pval := fmt.Sprintf(p, record.oid) - privileges = append(privileges, pval) - } - } - return -} - -// Delete a Graylog user account -func deleteAccount(cfg GraylogConfig, user string) { - log := log.WithField("user", user) - log.Warning("Deleting Graylog account") - code, body := executeApiCall(cfg, "DELETE", fmt.Sprintf("/users/%s", user), nil) - if code != 204 { - log.WithFields(logrus.Fields{ - "status": code, - "body": string(body), - }).Fatal("Could not delete user") - } -} - -// Returns the strings that are in a but not in b. -func getDifference(a []string, b []string) (diff []string) { - diff = make([]string, 0) - for _, sa := range a { - found := false - for _, sb := range b { - if sa == sb { - found = true - break - } - } - if !found { - diff = append(diff, sa) - } - } - return -} - -// Set an account's roles and grant it access to Graylog objects -func setUserPrivileges(cfg GraylogConfig, user GraylogUser, roles []string, privileges []string) { - type perms struct { - Permissions []string `json:"permissions"` - } - p := perms{Permissions: privileges} - data, err := json.Marshal(p) - if err != nil { - log.Fatalf("unable to generate permissions JSON for %s: %v", user, err) - } - - code, body := executeApiCall(cfg, "PUT", fmt.Sprintf("users/%s/permissions", user.Username), bytes.NewBuffer(data)) - if code != 204 { - log.Fatalf("could not set permissions for %s: code %d, body '%s'", user.Username, code, string(body)) - } - - placeholder := bytes.NewBuffer([]byte("{}")) - for _, role := range getDifference(roles, user.Roles) { - ep := fmt.Sprintf("roles/%s/members/%s", role, user.Username) - code, body := executeApiCall(cfg, "PUT", ep, placeholder) - if code != 204 { - log.Fatalf("could not add role %s to %s: code %d, body '%s'", role, user.Username, code, string(body)) - } - } - for _, role := range getDifference(user.Roles, roles) { - ep := fmt.Sprintf("roles/%s/members/%s", role, user.Username) - code, body := executeApiCall(cfg, "DELETE", ep, nil) - if code != 204 { - log.Fatalf("could not remove role %s from %s: code %d, body '%s'", role, user.Username, code, string(body)) - } - } -} - -// Apply privilege mappings to the external Graylog users -func applyMapping(cfg Configuration, users []GraylogUser, groups GroupMembers) { - for _, user := range users { - membership := getUserGroups(user.Username, groups) - roles := computeRoles(cfg.Mapping, membership) - privileges := computePrivileges(cfg.Mapping, membership) - if cfg.Graylog.DeleteAccounts && len(roles) == 0 && len(privileges) == 0 { - deleteAccount(cfg.Graylog, user.Username) - } else { - setUserPrivileges(cfg.Graylog, user, roles, privileges) - } - } -} - // Parse command line options. func parseCommandLine() cliFlags { flags := cliFlags{} @@ -593,7 +71,6 @@ func configureLogging(flags cliFlags) { func main() { flags := parseCommandLine() configureLogging(flags) - log.Debug("Starting synchronization") configuration := loadConfiguration(flags) glUsers := getGraylogUsers(configuration.Graylog) groups := readLdapGroups(configuration)