2021-02-11 20:44:07 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
// A Graylog user and associated roles
|
2021-02-13 18:26:37 +01:00
|
|
|
graylogUser struct {
|
2021-02-11 20:44:07 +01:00
|
|
|
Username string
|
|
|
|
Roles []string
|
|
|
|
}
|
|
|
|
|
|
|
|
// The response obtained when querying the Graylog server for a list of users.
|
2021-02-13 18:26:37 +01:00
|
|
|
graylogUsers struct {
|
2021-02-11 20:44:07 +01:00
|
|
|
Users []struct {
|
2021-02-13 18:26:37 +01:00
|
|
|
graylogUser
|
2021-02-11 20:44:07 +01:00
|
|
|
External bool
|
|
|
|
}
|
|
|
|
}
|
2021-02-13 23:18:05 +01:00
|
|
|
|
|
|
|
// Privilege information
|
|
|
|
privInfo struct {
|
|
|
|
otp, oid string // Type and identifier of object
|
|
|
|
priv int // Privilege level
|
|
|
|
}
|
2021-02-11 20:44:07 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
2021-05-11 13:44:45 +02:00
|
|
|
"search": true,
|
2021-02-11 20:44:07 +01:00
|
|
|
"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"},
|
2021-05-11 13:44:45 +02:00
|
|
|
"search:read": {"view:read:%s"},
|
|
|
|
"search:write": {"view:read:%s", "view:edit:%s"},
|
2021-02-11 20:44:07 +01:00
|
|
|
"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
|
2021-02-13 18:26:37 +01:00
|
|
|
func executeAPICall(cfg graylogConfig, method string, path string, data io.Reader) (status int, body []byte) {
|
2021-02-11 20:44:07 +01:00
|
|
|
log := log.WithFields(logrus.Fields{
|
2021-02-13 18:26:37 +01:00
|
|
|
"base": cfg.APIBase,
|
2021-02-11 20:44:07 +01:00
|
|
|
"username": cfg.Username,
|
|
|
|
"method": method,
|
|
|
|
"path": path,
|
|
|
|
})
|
|
|
|
log.Trace("Executing Graylog API call")
|
|
|
|
client := &http.Client{}
|
2021-02-13 18:26:37 +01:00
|
|
|
request, err := http.NewRequest(method, fmt.Sprintf("%s/%s", cfg.APIBase, path), data)
|
2021-02-11 20:44:07 +01:00
|
|
|
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
|
2021-02-13 18:26:37 +01:00
|
|
|
func getGraylogUsers(configuration graylogConfig) (users []graylogUser) {
|
2021-02-11 20:44:07 +01:00
|
|
|
log.Trace("Getting users from the Graylog API")
|
2021-02-13 18:26:37 +01:00
|
|
|
status, body := executeAPICall(configuration, "GET", "users", nil)
|
2021-02-11 20:44:07 +01:00
|
|
|
if status != 200 {
|
|
|
|
log.WithField("status", status).Fatal("Could not read users")
|
|
|
|
}
|
|
|
|
|
2021-02-13 18:26:37 +01:00
|
|
|
data := graylogUsers{}
|
2021-02-11 20:44:07 +01:00
|
|
|
if err := json.Unmarshal(body, &data); err != nil {
|
|
|
|
log.WithField("error", err).Fatal("Could not parse Graylog's user list")
|
|
|
|
}
|
|
|
|
|
2021-02-13 18:26:37 +01:00
|
|
|
users = make([]graylogUser, 0)
|
2021-02-11 20:44:07 +01:00
|
|
|
for _, item := range data.Users {
|
|
|
|
if item.External {
|
2021-02-13 18:26:37 +01:00
|
|
|
users = append(users, item.graylogUser)
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
log.WithField("users", len(users)).Info("Obtained users from the Graylog API")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// List groups an user is a member of.
|
2021-02-13 18:26:37 +01:00
|
|
|
func getUserGroups(user string, membership ldapGroupMembers) (groups []string) {
|
2021-02-11 20:44:07 +01:00
|
|
|
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
|
2021-02-13 18:26:37 +01:00
|
|
|
func computeRoles(mapping groupMapping, membership []string) (roles []string) {
|
2021-02-11 20:44:07 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-02-13 23:18:05 +01:00
|
|
|
// Compute privilege levels for each Graylog object based on the user's group membership
|
|
|
|
func getObjectPrivileges(mapping groupMapping, membership []string) map[string]privInfo {
|
2021-02-11 20:44:07 +01:00
|
|
|
rset := make(map[string]privInfo)
|
|
|
|
for _, group := range membership {
|
|
|
|
for _, priv := range mapping[group].Privileges {
|
2021-02-13 18:26:37 +01:00
|
|
|
key := fmt.Sprintf("%s:%s", priv.Type, priv.ID)
|
2021-02-11 20:44:07 +01:00
|
|
|
record, ok := rset[key]
|
|
|
|
level := privLevels[priv.Level]
|
|
|
|
if ok && level <= record.priv {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
record.otp = priv.Type
|
2021-02-13 18:26:37 +01:00
|
|
|
record.oid = priv.ID
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
record.priv = level
|
|
|
|
rset[key] = record
|
|
|
|
}
|
|
|
|
}
|
2021-02-13 23:18:05 +01:00
|
|
|
return rset
|
|
|
|
}
|
2021-02-11 20:44:07 +01:00
|
|
|
|
2021-02-13 23:18:05 +01:00
|
|
|
// Compute privileges on Graylog objects that should be granted to an user
|
|
|
|
func computePrivileges(mapping groupMapping, membership []string) []string {
|
|
|
|
privileges := make([]string, 0)
|
|
|
|
for _, record := range getObjectPrivileges(mapping, membership) {
|
2021-02-11 20:44:07 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2021-02-13 23:18:05 +01:00
|
|
|
return privileges
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete a Graylog user account
|
2021-02-13 18:26:37 +01:00
|
|
|
func deleteAccount(cfg graylogConfig, user string) {
|
2021-02-11 20:44:07 +01:00
|
|
|
log := log.WithField("user", user)
|
|
|
|
log.Warning("Deleting Graylog account")
|
2021-02-13 18:26:37 +01:00
|
|
|
code, body := executeAPICall(cfg, "DELETE", fmt.Sprintf("/users/%s", user), nil)
|
2021-02-11 20:44:07 +01:00
|
|
|
if code != 204 {
|
|
|
|
log.WithFields(logrus.Fields{
|
|
|
|
"status": code,
|
|
|
|
"body": string(body),
|
2021-02-11 23:05:48 +01:00
|
|
|
}).Error("Could not delete user")
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2021-02-13 18:26:37 +01:00
|
|
|
func setUserPrivileges(cfg graylogConfig, user graylogUser, roles []string, privileges []string) {
|
2021-02-11 23:05:48 +01:00
|
|
|
log := log.WithField("user", user.Username)
|
|
|
|
|
2021-02-11 20:44:07 +01:00
|
|
|
type perms struct {
|
|
|
|
Permissions []string `json:"permissions"`
|
|
|
|
}
|
|
|
|
p := perms{Permissions: privileges}
|
|
|
|
data, err := json.Marshal(p)
|
|
|
|
if err != nil {
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("error", err).Fatal("Unable to generate permissions JSON")
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("privileges", privileges).Info("Setting permissions")
|
2021-02-13 18:26:37 +01:00
|
|
|
code, body := executeAPICall(cfg, "PUT",
|
2021-02-11 23:05:48 +01:00
|
|
|
fmt.Sprintf("users/%s/permissions", user.Username),
|
|
|
|
bytes.NewBuffer(data))
|
2021-02-11 20:44:07 +01:00
|
|
|
if code != 204 {
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithFields(logrus.Fields{
|
|
|
|
"status": code,
|
|
|
|
"body": string(body),
|
|
|
|
}).Error("Could not set permissions")
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
placeholder := bytes.NewBuffer([]byte("{}"))
|
|
|
|
for _, role := range getDifference(roles, user.Roles) {
|
|
|
|
ep := fmt.Sprintf("roles/%s/members/%s", role, user.Username)
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("role", role).Info("Adding role")
|
2021-02-13 18:26:37 +01:00
|
|
|
code, body := executeAPICall(cfg, "PUT", ep, placeholder)
|
2021-02-11 20:44:07 +01:00
|
|
|
if code != 204 {
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithFields(logrus.Fields{
|
|
|
|
"status": code,
|
|
|
|
"body": string(body),
|
|
|
|
"role": role,
|
|
|
|
}).Error("Could not add role")
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, role := range getDifference(user.Roles, roles) {
|
|
|
|
ep := fmt.Sprintf("roles/%s/members/%s", role, user.Username)
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("role", role).Info("Removing role")
|
2021-02-13 18:26:37 +01:00
|
|
|
code, body := executeAPICall(cfg, "DELETE", ep, nil)
|
2021-02-11 20:44:07 +01:00
|
|
|
if code != 204 {
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithFields(logrus.Fields{
|
|
|
|
"status": code,
|
|
|
|
"body": string(body),
|
|
|
|
"role": role,
|
|
|
|
}).Error("Could not remove role")
|
2021-02-11 20:44:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply privilege mappings to the external Graylog users
|
2021-02-13 18:26:37 +01:00
|
|
|
func applyMapping(cfg configuration, users []graylogUser, groups ldapGroupMembers) {
|
2021-02-11 20:44:07 +01:00
|
|
|
for _, user := range users {
|
2021-02-11 23:05:48 +01:00
|
|
|
log := log.WithField("user", user.Username)
|
2021-02-11 20:44:07 +01:00
|
|
|
membership := getUserGroups(user.Username, groups)
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("groups", membership).Trace("Computed group membership")
|
2021-02-11 20:44:07 +01:00
|
|
|
roles := computeRoles(cfg.Mapping, membership)
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("roles", roles).Trace("Computed roles")
|
2021-02-11 20:44:07 +01:00
|
|
|
privileges := computePrivileges(cfg.Mapping, membership)
|
2021-02-11 23:05:48 +01:00
|
|
|
log.WithField("privileges", privileges).Trace("Computed privileges")
|
2021-02-11 20:44:07 +01:00
|
|
|
if cfg.Graylog.DeleteAccounts && len(roles) == 0 && len(privileges) == 0 {
|
|
|
|
deleteAccount(cfg.Graylog, user.Username)
|
|
|
|
} else {
|
|
|
|
setUserPrivileges(cfg.Graylog, user, roles, privileges)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|