bitbucket.org/Aishee/synsec@v0.0.0-20210414005726-236fc01a153d/pkg/metabase/metabase.go (about)

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