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 }