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
This commit is contained in:
parent
dcd3f920c9
commit
4722223603
4 changed files with 551 additions and 523 deletions
100
config.go
Normal file
100
config.go
Normal file
|
@ -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
|
||||||
|
}
|
254
graylog.go
Normal file
254
graylog.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
197
ldap.go
Normal file
197
ldap.go
Normal file
|
@ -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
|
||||||
|
}
|
523
main.go
523
main.go
|
@ -1,20 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-ldap/ldap"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -27,523 +15,13 @@ type (
|
||||||
// The log level.
|
// The log level.
|
||||||
logLevel string
|
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 (
|
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.
|
// The logging context.
|
||||||
log *logrus.Entry
|
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.
|
// Parse command line options.
|
||||||
func parseCommandLine() cliFlags {
|
func parseCommandLine() cliFlags {
|
||||||
flags := cliFlags{}
|
flags := cliFlags{}
|
||||||
|
@ -593,7 +71,6 @@ func configureLogging(flags cliFlags) {
|
||||||
func main() {
|
func main() {
|
||||||
flags := parseCommandLine()
|
flags := parseCommandLine()
|
||||||
configureLogging(flags)
|
configureLogging(flags)
|
||||||
log.Debug("Starting synchronization")
|
|
||||||
configuration := loadConfiguration(flags)
|
configuration := loadConfiguration(flags)
|
||||||
glUsers := getGraylogUsers(configuration.Graylog)
|
glUsers := getGraylogUsers(configuration.Graylog)
|
||||||
groups := readLdapGroups(configuration)
|
groups := readLdapGroups(configuration)
|
||||||
|
|
Loading…
Reference in a new issue