metabase.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package metabase
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "context"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "os"
  11. "path"
  12. "runtime"
  13. "strings"
  14. "time"
  15. "github.com/docker/docker/client"
  16. log "github.com/sirupsen/logrus"
  17. "gopkg.in/yaml.v2"
  18. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  19. )
  20. type Metabase struct {
  21. Config *Config
  22. Client *MBClient
  23. Container *Container
  24. Database *Database
  25. InternalDBURL string
  26. }
  27. type Config struct {
  28. Database *csconfig.DatabaseCfg `yaml:"database"`
  29. ListenAddr string `yaml:"listen_addr"`
  30. ListenPort string `yaml:"listen_port"`
  31. ListenURL string `yaml:"listen_url"`
  32. Username string `yaml:"username"`
  33. Password string `yaml:"password"`
  34. DBPath string `yaml:"metabase_db_path"`
  35. DockerGroupID string `yaml:"-"`
  36. }
  37. var (
  38. metabaseDefaultUser = "crowdsec@crowdsec.net"
  39. metabaseDefaultPassword = "!!Cr0wdS3c_M3t4b4s3??"
  40. metabaseImage = "metabase/metabase:v0.41.5"
  41. containerSharedFolder = "/metabase-data"
  42. metabaseSQLiteDBURL = "https://crowdsec-statics-assets.s3-eu-west-1.amazonaws.com/metabase_sqlite.zip"
  43. )
  44. func TestAvailability() error {
  45. if runtime.GOARCH != "amd64" {
  46. return fmt.Errorf("cscli dashboard is only available on amd64, but you are running %s", runtime.GOARCH)
  47. }
  48. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  49. if err != nil {
  50. return fmt.Errorf("failed to create docker client : %s", err)
  51. }
  52. _, err = cli.Ping(context.TODO())
  53. return err
  54. }
  55. func (m *Metabase) Init(containerName string) error {
  56. var err error
  57. var DBConnectionURI string
  58. var remoteDBAddr string
  59. switch m.Config.Database.Type {
  60. case "mysql":
  61. return fmt.Errorf("'mysql' is not supported yet for cscli dashboard")
  62. //DBConnectionURI = fmt.Sprintf("MB_DB_CONNECTION_URI=mysql://%s:%d/%s?user=%s&password=%s&allowPublicKeyRetrieval=true", remoteDBAddr, m.Config.Database.Port, m.Config.Database.DbName, m.Config.Database.User, m.Config.Database.Password)
  63. case "sqlite":
  64. m.InternalDBURL = metabaseSQLiteDBURL
  65. case "postgresql", "postgres", "pgsql":
  66. return fmt.Errorf("'postgresql' is not supported yet by cscli dashboard")
  67. default:
  68. return fmt.Errorf("database '%s' not supported", m.Config.Database.Type)
  69. }
  70. m.Client, err = NewMBClient(m.Config.ListenURL)
  71. if err != nil {
  72. return err
  73. }
  74. m.Database, err = NewDatabase(m.Config.Database, m.Client, remoteDBAddr)
  75. if err != nil {
  76. return err
  77. }
  78. m.Container, err = NewContainer(m.Config.ListenAddr, m.Config.ListenPort, m.Config.DBPath, containerName, metabaseImage, DBConnectionURI, m.Config.DockerGroupID)
  79. if err != nil {
  80. return fmt.Errorf("container init: %w", err)
  81. }
  82. return nil
  83. }
  84. func NewMetabase(configPath string, containerName string) (*Metabase, error) {
  85. m := &Metabase{}
  86. if err := m.LoadConfig(configPath); err != nil {
  87. return m, err
  88. }
  89. if err := m.Init(containerName); err != nil {
  90. return m, err
  91. }
  92. return m, nil
  93. }
  94. func (m *Metabase) LoadConfig(configPath string) error {
  95. yamlFile, err := os.ReadFile(configPath)
  96. if err != nil {
  97. return err
  98. }
  99. config := &Config{}
  100. err = yaml.Unmarshal(yamlFile, config)
  101. if err != nil {
  102. return err
  103. }
  104. if config.Username == "" {
  105. return fmt.Errorf("'username' not found in configuration file '%s'", configPath)
  106. }
  107. if config.Password == "" {
  108. return fmt.Errorf("'password' not found in configuration file '%s'", configPath)
  109. }
  110. if config.ListenURL == "" {
  111. return fmt.Errorf("'listen_url' not found in configuration file '%s'", configPath)
  112. }
  113. m.Config = config
  114. return nil
  115. }
  116. func SetupMetabase(dbConfig *csconfig.DatabaseCfg, listenAddr string, listenPort string, username string, password string, mbDBPath string, dockerGroupID string, containerName string) (*Metabase, error) {
  117. metabase := &Metabase{
  118. Config: &Config{
  119. Database: dbConfig,
  120. ListenAddr: listenAddr,
  121. ListenPort: listenPort,
  122. Username: username,
  123. Password: password,
  124. ListenURL: fmt.Sprintf("http://%s:%s", listenAddr, listenPort),
  125. DBPath: mbDBPath,
  126. DockerGroupID: dockerGroupID,
  127. },
  128. }
  129. if err := metabase.Init(containerName); err != nil {
  130. return nil, fmt.Errorf("metabase setup init: %w", err)
  131. }
  132. if err := metabase.DownloadDatabase(false); err != nil {
  133. return nil, fmt.Errorf("metabase db download: %w", err)
  134. }
  135. if err := metabase.Container.Create(); err != nil {
  136. return nil, fmt.Errorf("container create: %w", err)
  137. }
  138. if err := metabase.Container.Start(); err != nil {
  139. return nil, fmt.Errorf("container start: %w", err)
  140. }
  141. log.Infof("waiting for metabase to be up (can take up to a minute)")
  142. if err := metabase.WaitAlive(); err != nil {
  143. return nil, fmt.Errorf("wait alive: %w", err)
  144. }
  145. if err := metabase.Database.Update(); err != nil {
  146. return nil, fmt.Errorf("update database: %w", err)
  147. }
  148. if err := metabase.Scan(); err != nil {
  149. return nil, fmt.Errorf("db scan: %w", err)
  150. }
  151. if err := metabase.ResetCredentials(); err != nil {
  152. return nil, fmt.Errorf("reset creds: %w", err)
  153. }
  154. return metabase, nil
  155. }
  156. func (m *Metabase) WaitAlive() error {
  157. var err error
  158. for {
  159. err = m.Login(metabaseDefaultUser, metabaseDefaultPassword)
  160. if err != nil {
  161. if strings.Contains(err.Error(), "password:did not match stored password") {
  162. log.Errorf("Password mismatch error, is your dashboard already setup ? Run 'cscli dashboard remove' to reset it.")
  163. return fmt.Errorf("password mismatch error: %w", err)
  164. }
  165. log.Debugf("%+v", err)
  166. } else {
  167. break
  168. }
  169. fmt.Printf(".")
  170. time.Sleep(2 * time.Second)
  171. }
  172. fmt.Printf("\n")
  173. return nil
  174. }
  175. func (m *Metabase) Login(username string, password string) error {
  176. body := map[string]string{"username": username, "password": password}
  177. successmsg, errormsg, err := m.Client.Do("POST", routes[sessionEndpoint], body)
  178. if err != nil {
  179. return err
  180. }
  181. if errormsg != nil {
  182. return fmt.Errorf("http login: %s", errormsg)
  183. }
  184. resp, ok := successmsg.(map[string]interface{})
  185. if !ok {
  186. return fmt.Errorf("login: bad response type: %+v", successmsg)
  187. }
  188. if _, ok = resp["id"]; !ok {
  189. return fmt.Errorf("login: can't update session id, no id in response: %v", successmsg)
  190. }
  191. id, ok := resp["id"].(string)
  192. if !ok {
  193. return fmt.Errorf("login: bad id type: %+v", resp["id"])
  194. }
  195. m.Client.Set("Cookie", fmt.Sprintf("metabase.SESSION=%s", id))
  196. return nil
  197. }
  198. func (m *Metabase) Scan() error {
  199. _, errormsg, err := m.Client.Do("POST", routes[scanEndpoint], nil)
  200. if err != nil {
  201. return err
  202. }
  203. if errormsg != nil {
  204. return fmt.Errorf("http scan: %s", errormsg)
  205. }
  206. return nil
  207. }
  208. func (m *Metabase) ResetPassword(current string, newPassword string) error {
  209. body := map[string]string{
  210. "id": "1",
  211. "password": newPassword,
  212. "old_password": current,
  213. }
  214. _, errormsg, err := m.Client.Do("PUT", routes[resetPasswordEndpoint], body)
  215. if err != nil {
  216. return fmt.Errorf("reset username: %w", err)
  217. }
  218. if errormsg != nil {
  219. return fmt.Errorf("http reset password: %s", errormsg)
  220. }
  221. return nil
  222. }
  223. func (m *Metabase) ResetUsername(username string) error {
  224. body := struct {
  225. FirstName string `json:"first_name"`
  226. LastName string `json:"last_name"`
  227. Email string `json:"email"`
  228. GroupIDs []int `json:"group_ids"`
  229. }{
  230. FirstName: "Crowdsec",
  231. LastName: "Crowdsec",
  232. Email: username,
  233. GroupIDs: []int{1, 2},
  234. }
  235. _, errormsg, err := m.Client.Do("PUT", routes[userEndpoint], body)
  236. if err != nil {
  237. return fmt.Errorf("reset username: %w", err)
  238. }
  239. if errormsg != nil {
  240. return fmt.Errorf("http reset username: %s", errormsg)
  241. }
  242. return nil
  243. }
  244. func (m *Metabase) ResetCredentials() error {
  245. if err := m.ResetPassword(metabaseDefaultPassword, m.Config.Password); err != nil {
  246. return err
  247. }
  248. /*if err := m.ResetUsername(m.Config.Username); err != nil {
  249. return err
  250. }*/
  251. return nil
  252. }
  253. func (m *Metabase) DumpConfig(path string) error {
  254. data, err := yaml.Marshal(m.Config)
  255. if err != nil {
  256. return err
  257. }
  258. return os.WriteFile(path, data, 0600)
  259. }
  260. func (m *Metabase) DownloadDatabase(force bool) error {
  261. metabaseDBSubpath := path.Join(m.Config.DBPath, "metabase.db")
  262. _, err := os.Stat(metabaseDBSubpath)
  263. if err == nil && !force {
  264. log.Printf("%s exists, skip.", metabaseDBSubpath)
  265. return nil
  266. }
  267. if err := os.MkdirAll(metabaseDBSubpath, 0755); err != nil {
  268. return fmt.Errorf("failed to create %s : %s", metabaseDBSubpath, err)
  269. }
  270. req, err := http.NewRequest(http.MethodGet, m.InternalDBURL, nil)
  271. if err != nil {
  272. return fmt.Errorf("failed to build request to fetch metabase db : %s", err)
  273. }
  274. //This needs to be removed once we move the zip out of github
  275. //req.Header.Add("Accept", `application/vnd.github.v3.raw`)
  276. resp, err := http.DefaultClient.Do(req)
  277. if err != nil {
  278. return fmt.Errorf("failed request to fetch metabase db : %s", err)
  279. }
  280. if resp.StatusCode != http.StatusOK {
  281. return fmt.Errorf("got http %d while requesting metabase db %s, stop", resp.StatusCode, m.InternalDBURL)
  282. }
  283. defer resp.Body.Close()
  284. body, err := io.ReadAll(resp.Body)
  285. if err != nil {
  286. return fmt.Errorf("failed request read while fetching metabase db : %s", err)
  287. }
  288. log.Debugf("Got %d bytes archive", len(body))
  289. if err := m.ExtractDatabase(bytes.NewReader(body)); err != nil {
  290. return fmt.Errorf("while extracting zip : %s", err)
  291. }
  292. return nil
  293. }
  294. func (m *Metabase) ExtractDatabase(buf *bytes.Reader) error {
  295. r, err := zip.NewReader(buf, int64(buf.Len()))
  296. if err != nil {
  297. return err
  298. }
  299. for _, f := range r.File {
  300. if strings.Contains(f.Name, "..") {
  301. return fmt.Errorf("invalid path '%s' in archive", f.Name)
  302. }
  303. tfname := fmt.Sprintf("%s/%s", m.Config.DBPath, f.Name)
  304. log.Tracef("%s -> %d", f.Name, f.UncompressedSize64)
  305. if f.UncompressedSize64 == 0 {
  306. continue
  307. }
  308. tfd, err := os.OpenFile(tfname, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
  309. if err != nil {
  310. return fmt.Errorf("failed opening target file '%s' : %s", tfname, err)
  311. }
  312. rc, err := f.Open()
  313. if err != nil {
  314. return fmt.Errorf("while opening zip content %s : %s", f.Name, err)
  315. }
  316. written, err := io.Copy(tfd, rc)
  317. if errors.Is(err, io.EOF) {
  318. log.Printf("files finished ok")
  319. } else if err != nil {
  320. return fmt.Errorf("while copying content to %s : %s", tfname, err)
  321. }
  322. log.Debugf("written %d bytes to %s", written, tfname)
  323. rc.Close()
  324. }
  325. return nil
  326. }
  327. func RemoveDatabase(dataDir string) error {
  328. return os.RemoveAll(path.Join(dataDir, "metabase.db"))
  329. }