From d62c1382cacff2432a546bcf1b5f6a3a7f55114e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jochum?= Date: Sun, 4 Oct 2020 23:52:24 +0200 Subject: [PATCH] Detect if a user is Admin and do not send AuthUser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is done by parsing users.mk with python, the users.mk will be watched by inotify for changes. Signed-off-by: René Jochum --- README.md | 2 +- cmd/localclient.go | 6 +- cmd/localserver.go | 4 +- debian/changelog | 6 ++ go.mod | 2 + go.sum | 4 ++ lql/interfaces.go | 1 + lql/multi_client.go | 17 ++++- lql/single_client.go | 4 ++ lql/users.go | 166 +++++++++++++++++++++++++++++++++++++++++++ lql/v1ping.go | 3 + lql/v1raw.go | 3 + lql/v1stats.go | 3 + lql/v1table.go | 6 ++ 14 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 lql/users.go diff --git a/README.md b/README.md index 449ae0d..bc185c7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ LQL API Client/Server for check_mk -See [the LQL Docs](https://checkmk.com/cms_livestatus.html) for what LQL can do for you. +Look at [the LQL Docs](https://checkmk.com/cms_livestatus.html) to see what LQL can do for you. ## Commands the client supports diff --git a/cmd/localclient.go b/cmd/localclient.go index 4a0934b..4ee0cf7 100644 --- a/cmd/localclient.go +++ b/cmd/localclient.go @@ -20,11 +20,12 @@ func init() { localClientCmdLimit := 0 localClientCmd.Flags().StringP("socket", "s", "/opt/omd/sites/{site}/tmp/run/live", "Socket on the Server") localClientCmd.Flags().StringP("liveproxydir", "p", "/opt/omd/sites/{site}/tmp/run/liveproxy", "Directory which contains liveproxy sockets") + localClientCmd.Flags().StringP("multsiteusers", "m", "/opt/omd/sites/{site}/etc/check_mk/multisite.d/wato/users.mk", "Your checkmks users.mk file") localClientCmd.Flags().BoolP("debug", "d", false, "Enable Debug on stderr") localClientCmd.Flags().StringP("format", "f", "jsonparsed", "Format one of: python, python3, json, csv, CSV, jsonparsed (default is jsonparsed, I parse json from the server)") localClientCmd.Flags().StringP("table", "t", "", "Produce a GET request for the given table (default: supply request by stdin)") localClientCmd.Flags().StringArrayP("columns", "c", []string{""}, "Columns to show from the given table, this is required if you give a table!") - localClientCmd.Flags().StringP("user", "u", "", "LQL user to limit this on") + localClientCmd.Flags().StringP("user", "u", "", "CheckMK user to limit this request on") localClientCmd.Flags().IntVarP(&localClientCmdLimit, "limit", "l", 0, "Limit request lines") rootCmd.AddCommand(localClientCmd) } @@ -52,6 +53,7 @@ Examples: sReplacer := strings.NewReplacer("{site}", args[0]) destSocket := sReplacer.Replace(cmd.Flag("socket").Value.String()) liveproxyDir := sReplacer.Replace(cmd.Flag("liveproxydir").Value.String()) + multisiteUsersFile := sReplacer.Replace(cmd.Flag("multsiteusers").Value.String()) var lqlClient lql.Client logger := log.New() @@ -107,7 +109,7 @@ Examples: os.Exit(1) }(sigc) - lqlClient, err := lql.NewMultiClient(1, 1, destSocket, liveproxyDir) + lqlClient, err := lql.NewMultiClient(1, 1, destSocket, liveproxyDir, multisiteUsersFile) if err != nil { logger.WithField("error", err).Error() return diff --git a/cmd/localserver.go b/cmd/localserver.go index 16e9ea7..69f1662 100644 --- a/cmd/localserver.go +++ b/cmd/localserver.go @@ -21,6 +21,7 @@ func init() { localServerCmd.Flags().StringP("socket", "s", "/opt/omd/sites/{site}/tmp/run/live", "Socket") localServerCmd.Flags().StringP("liveproxydir", "p", "/opt/omd/sites/{site}/tmp/run/liveproxy", "Directory which contains liveproxy sockets") + localServerCmd.Flags().StringP("multsiteusers", "u", "/opt/omd/sites/{site}/etc/check_mk/multisite.d/wato/users.mk", "Your checkmks users.mk file") localServerCmd.Flags().StringP("htpasswd", "t", "/opt/omd/sites/{site}/etc/htpasswd", "htpasswd file") localServerCmd.Flags().BoolP("debug", "d", false, "Enable Debug on stderr") localServerCmd.Flags().StringP("listen", "l", ":8080", "Address to listen on") @@ -38,6 +39,7 @@ Requires a local lql unix socket.`, Run: func(cmd *cobra.Command, args []string) { sReplacer := strings.NewReplacer("{site}", args[0]) liveproxyDir := sReplacer.Replace(cmd.Flag("liveproxydir").Value.String()) + multisiteUsersFile := sReplacer.Replace(cmd.Flag("multsiteusers").Value.String()) logfile, err := cmd.Flags().GetString("logfile") if err != nil { @@ -97,7 +99,7 @@ Requires a local lql unix socket.`, return } - lqlClient, err = lql.NewMultiClient(minConns, maxConns, localSocket, liveproxyDir) + lqlClient, err = lql.NewMultiClient(minConns, maxConns, localSocket, liveproxyDir, multisiteUsersFile) if err != nil { logger.WithField("error", err).Error() return diff --git a/debian/changelog b/debian/changelog index 8d96505..5db0c81 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +lql-api (0.0.11-alpha) UNRELEASED; urgency=medium + + * No changes yet + + -- René Jochum Sun, 04 Oct 2020 21:39:06 +0200 + lql-api (0.0.10-alpha) RELEASED; urgency=medium * Implement Multisite support (multiple sockets) diff --git a/go.mod b/go.mod index 0a7161f..d2d9df0 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/hashicorp/go-multierror v1.1.0 github.com/loopfz/gadgeto v0.10.1 + github.com/micro/go-micro/v2 v2.9.1 github.com/sirupsen/logrus v1.7.0 github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.4.0 @@ -16,4 +17,5 @@ require ( github.com/webmeisterei/go-http-auth v0.5.0 github.com/wolviecb/basic-auth v0.5.0 golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae + gopkg.in/fsnotify.v1 v1.4.7 ) diff --git a/go.sum b/go.sum index 6163f39..d9ccc9b 100644 --- a/go.sum +++ b/go.sum @@ -253,6 +253,7 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= @@ -416,6 +417,7 @@ github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4f github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/micro/cli/v2 v2.1.2 h1:43J1lChg/rZCC1rvdqZNFSQDrGT7qfMrtp6/ztpIkEM= github.com/micro/cli/v2 v2.1.2/go.mod h1:EguNh6DAoWKm9nmk+k/Rg0H3lQnDxqzu5x5srOtGtYg= +github.com/micro/go-micro v1.18.0 h1:gP70EZVHpJuUIT0YWth192JmlIci+qMOEByHm83XE9E= github.com/micro/go-micro/v2 v2.9.1 h1:+S9koIrNWARjpP6k2TZ7kt0uC9zUJtNXzIdZTZRms7Q= github.com/micro/go-micro/v2 v2.9.1/go.mod h1:x55ZM3Puy0FyvvkR3e0ha0xsE9DFwfPSUMWAIbFY0SY= github.com/micro/go-micro/v3 v3.0.0-beta.2.0.20200929133051-87e898f4fc62 h1:yFK8GK2Lt43YHgF7k7q8NNsgz+0vfTOdv0R/YPafFWQ= @@ -804,6 +806,7 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c h1:38q6VNPWR010vN82/SB121GujZNIfAUb4YttE2rhGuc= @@ -897,6 +900,7 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= diff --git a/lql/interfaces.go b/lql/interfaces.go index b84b676..add7dc8 100644 --- a/lql/interfaces.go +++ b/lql/interfaces.go @@ -9,6 +9,7 @@ import ( type Client interface { ClientCount() int + IsAdmin(username string) bool SetLogger(logger *log.Logger) Close() error Request(context context.Context, request, authUser string, limit int) ([]gin.H, error) diff --git a/lql/multi_client.go b/lql/multi_client.go index 3e2aa0e..0865b51 100644 --- a/lql/multi_client.go +++ b/lql/multi_client.go @@ -17,18 +17,25 @@ type MultiClient struct { localSocket string liveproxyDir string clients map[string]Client + usersWatcher *UsersWatcher } -func NewMultiClient(minConn, maxConn int, localSocket, liveproxyDir string) (Client, error) { +func NewMultiClient(minConn, maxConn int, localSocket, liveproxyDir string, multisiteUsersFile string) (Client, error) { + uw, err := NewUsersWatcher(multisiteUsersFile) + if err != nil { + return nil, err + } + mc := &MultiClient{ minConn: minConn, maxConn: maxConn, localSocket: localSocket, liveproxyDir: liveproxyDir, clients: make(map[string]Client), + usersWatcher: uw, } - err := mc.CreateClients() + err = mc.CreateClients() if err != nil { return nil, err } @@ -72,7 +79,13 @@ func (c *MultiClient) CreateClients() error { return result } +func (c *MultiClient) IsAdmin(username string) bool { + return c.usersWatcher.IsAdmin(username) +} + func (c *MultiClient) SetLogger(logger *log.Logger) { + c.usersWatcher.SetLogger(logger) + for _, client := range c.clients { client.SetLogger(logger) } diff --git a/lql/single_client.go b/lql/single_client.go index 930fd98..8eb3b91 100644 --- a/lql/single_client.go +++ b/lql/single_client.go @@ -53,6 +53,10 @@ func (c *SingleClient) SetLogger(logger *log.Logger) { c.logger = logger } +func (c *SingleClient) IsAdmin(username string) bool { + return false +} + func (c *SingleClient) Close() error { c.logger.WithFields(log.Fields{"address": c.address}).Debug("Closing pool") return c.pool.Close() diff --git a/lql/users.go b/lql/users.go new file mode 100644 index 0000000..c141b5d --- /dev/null +++ b/lql/users.go @@ -0,0 +1,166 @@ +package lql + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" + "sync" + + log "github.com/sirupsen/logrus" + "gopkg.in/fsnotify.v1" +) + +const usersExporterFile = `from __future__ import print_function + +import json + +class MultiSiteUsers(object): + def update(self, data): + print(json.dumps(data)); + +multisite_users = MultiSiteUsers() + +eval(open("%s").read()) +` + +type UserData struct { + ForceAuthUserWebservice bool `json:"force_authuser_webservice"` + Looked bool `json:"locked"` + Roles []string `json:"roles"` + ForceAuthUser bool `json:"force_authuser"` + Alias string `json:"alias"` + StartUrl string `json:"start_url"` +} + +type UsersWatcher struct { + usersfile string + users map[string]UserData + lock *sync.RWMutex + logger *log.Logger + isWatching bool + watcher *fsnotify.Watcher +} + +func NewUsersWatcher(usersfile string) (*UsersWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + uw := &UsersWatcher{ + usersfile: usersfile, + lock: &sync.RWMutex{}, + isWatching: false, + watcher: watcher, + } + + return uw, nil +} + +func (uw *UsersWatcher) Close() { + uw.watcher.Close() +} + +func (uw *UsersWatcher) SetLogger(logger *log.Logger) { + uw.logger = logger +} + +func (uw *UsersWatcher) StartWatching() { + go func() { + for { + select { + case event, ok := <-uw.watcher.Events: + if !ok { + return + } + + if event.Op&fsnotify.Write == fsnotify.Write { + uw.FetchUsers() + } + case err, ok := <-uw.watcher.Errors: + if !ok { + return + } + uw.logger.WithField("error", err).Error() + } + } + }() + + uw.lock.Lock() + uw.isWatching = true + uw.lock.Unlock() + + err := uw.watcher.Add(uw.usersfile) + if err != nil { + uw.logger.WithField("error", err).Error() + } +} + +func (uw *UsersWatcher) IsAdmin(userName string) bool { + uw.lock.RLock() + if uw.users == nil { + uw.lock.RUnlock() + + if !uw.isWatching { + uw.StartWatching() + } + + uw.FetchUsers() + uw.lock.RLock() + } + defer uw.lock.RUnlock() + + userData, ok := uw.users[userName] + if !ok { + uw.logger.WithField("user_name", userName).Debug("Failed to fetch user from db") + return false + } + + for _, role := range userData.Roles { + if role == "admin" { + uw.logger.WithField("user_name", userName).Trace("User is admin") + return true + } + } + + uw.logger.WithField("user_name", userName).Trace("User is not admin") + return false +} + +func (uw *UsersWatcher) FetchUsers() error { + uw.logger.WithField("usersfile", uw.usersfile).Debug("Reading users") + + dir, err := ioutil.TempDir("", "lql-api") + if err != nil { + uw.logger.WithField("error", err).Error() + return err + } + + tmpfn := filepath.Join(dir, "lql-api-user-reader.py") + if err := ioutil.WriteFile(tmpfn, []byte(fmt.Sprintf(usersExporterFile, uw.usersfile)), 0700); err != nil { + uw.logger.WithField("error", err).Error() + return err + } + + cmd := exec.Command("python", tmpfn) + uw.logger.WithField("args", cmd.Args).Debug("Executing") + out, err := cmd.CombinedOutput() + if err != nil { + uw.logger.WithField("error", err).Error() + return err + } + + result := make(map[string]UserData, 1) + if err = json.Unmarshal(out, &result); err != nil { + uw.logger.WithField("error", err).Error() + return err + } + + uw.lock.Lock() + uw.users = result + uw.lock.Unlock() + + return nil +} diff --git a/lql/v1ping.go b/lql/v1ping.go index 120afb7..7955924 100644 --- a/lql/v1ping.go +++ b/lql/v1ping.go @@ -8,6 +8,9 @@ func v1Ping(c *gin.Context) (gin.H, error) { return nil, err } user := c.GetString("user") + if client.IsAdmin(user) { + user = "" + } msg := `GET hosts Columns: name` diff --git a/lql/v1raw.go b/lql/v1raw.go index e575238..a5a2a64 100644 --- a/lql/v1raw.go +++ b/lql/v1raw.go @@ -31,6 +31,9 @@ func v1RawPost(c *gin.Context, params *V1RawRequestParams) ([]gin.H, error) { return nil, err } user := c.GetString("user") + if client.IsAdmin(user) { + user = "" + } // Param validation and request building request := []string{} diff --git a/lql/v1stats.go b/lql/v1stats.go index 84fa81e..13cb5fd 100644 --- a/lql/v1stats.go +++ b/lql/v1stats.go @@ -25,6 +25,9 @@ func v1StatsGetTacticalOverview(c *gin.Context) (*V1StatsTacticalOverview, error return nil, err } user := c.GetString("user") + if client.IsAdmin(user) { + user = "" + } msg := `GET hosts Stats: state >= 0 diff --git a/lql/v1table.go b/lql/v1table.go index 4042ebc..deae163 100644 --- a/lql/v1table.go +++ b/lql/v1table.go @@ -65,6 +65,9 @@ func v1TableGet(c *gin.Context, params *v1TableGetParams) ([]gin.H, error) { return nil, err } user := c.GetString("user") + if client.IsAdmin(user) { + user = "" + } columns := "" containsAll := false @@ -129,6 +132,9 @@ func v1TableGetColumns(c *gin.Context, params *v1TableGetColumnsParams) ([]strin return nil, err } user := c.GetString("user") + if client.IsAdmin(user) { + user = "" + } msg := fmt.Sprintf("GET columns\nColumns: name\nFilter: table = %s", params.Table) resp, err := client.Request(c, msg, user, 0)