diff --git a/make/common/templates/ui/env b/make/common/templates/ui/env index 1b3cea9e3..e4feedc2e 100644 --- a/make/common/templates/ui/env +++ b/make/common/templates/ui/env @@ -3,11 +3,11 @@ MYSQL_PORT=3306 MYSQL_USR=root MYSQL_PWD=$db_password REGISTRY_URL=http://registry:5000 +JOB_SERVICE_URL=http://jobservice UI_URL=http://ui CONFIG_PATH=/etc/ui/app.conf -HARBOR_REG_URL=$hostname +EXT_REG_URL=$hostname HARBOR_ADMIN_PASSWORD=$harbor_admin_password -HARBOR_URL=$ui_url AUTH_MODE=$auth_mode LDAP_URL=$ldap_url LDAP_SEARCH_DN=$ldap_searchdn @@ -23,6 +23,7 @@ USE_COMPRESSED_JS=$use_compressed_js LOG_LEVEL=debug GODEBUG=netdns=cgo EXT_ENDPOINT=$ui_url -TOKEN_URL=http://ui +TOKEN_ENDPOINT=http://ui VERIFY_REMOTE_CERT=$verify_remote_cert TOKEN_EXPIRATION=$token_expiration +PROJECT_CREATION_RESTRICTION=$project_creation_restriction diff --git a/make/harbor.cfg b/make/harbor.cfg index f2d00554e..697c55c87 100644 --- a/make/harbor.cfg +++ b/make/harbor.cfg @@ -83,6 +83,9 @@ crt_organizationalunit = organizational unit crt_commonname = example.com crt_email = example@example.com +#The flag to control what users have permission to create projects +#Be default everyone can create a project, set to "adminonly" such that only admin can create project. +project_creation_restriction = everyone #The path of cert and key files for nginx, they are applied only the protocol is set to https ssl_cert = /data/server.crt diff --git a/make/prepare b/make/prepare index 02f180ff1..b2ec7254f 100755 --- a/make/prepare +++ b/make/prepare @@ -32,6 +32,10 @@ def validate(conf): cert_key_path = rcp.get("configuration", "ssl_cert_key") if not os.path.isfile(cert_key_path): raise Exception("Error: The path for certificate key: %s is invalid" % cert_key_path) + project_creation = rcp.get("configuration", "project_creation_restriction") + + if project_creation != "everyone" and project_creation != "adminonly": + raise Exception("Error invalid value for project_creation_restriction: %s" % project_creation) def get_secret_key(path): key_file = os.path.join(path, "secretkey") @@ -115,6 +119,7 @@ crt_email = rcp.get("configuration", "crt_email") max_job_workers = rcp.get("configuration", "max_job_workers") token_expiration = rcp.get("configuration", "token_expiration") verify_remote_cert = rcp.get("configuration", "verify_remote_cert") +proj_cre_restriction = rcp.get("configuration", "project_creation_restriction") #secret_key = rcp.get("configuration", "secret_key") secret_key = get_secret_key(args.data_volume) ######## @@ -200,6 +205,7 @@ render(os.path.join(templates_dir, "ui", "env"), ui_secret=ui_secret, secret_key=secret_key, verify_remote_cert=verify_remote_cert, + project_creation_restriction=proj_cre_restriction, token_expiration=token_expiration) render(os.path.join(templates_dir, "ui", "app.conf"), diff --git a/src/common/api/base.go b/src/common/api/base.go index fd1802979..94375db43 100644 --- a/src/common/api/base.go +++ b/src/common/api/base.go @@ -19,14 +19,14 @@ import ( "encoding/json" "fmt" "net/http" - "os" "strconv" "github.com/astaxie/beego/validation" - "github.com/vmware/harbor/src/ui/auth" + "github.com/vmware/harbor/src/common/config" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/auth" "github.com/astaxie/beego" ) @@ -213,12 +213,5 @@ func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) { // GetIsInsecure ... func GetIsInsecure() bool { - insecure := false - - verifyRemoteCert := os.Getenv("VERIFY_REMOTE_CERT") - if verifyRemoteCert == "off" { - insecure = true - } - - return insecure + return config.VerifyRemoteCert() } diff --git a/src/common/config/config.go b/src/common/config/config.go new file mode 100644 index 000000000..e9c54c27c --- /dev/null +++ b/src/common/config/config.go @@ -0,0 +1,178 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package config provide methods to get the configurations reqruied by code in src/common +package config + +import ( + "fmt" + "os" + "strings" +) + +// ConfLoader is the interface to load configurations +type ConfLoader interface { + // Load will load configuration from different source into a string map, the values in the map will be parsed in to configurations. + Load() (map[string]string, error) +} + +// EnvConfigLoader loads the config from env vars. +type EnvConfigLoader struct { + Keys []string +} + +// Load ... +func (ec *EnvConfigLoader) Load() (map[string]string, error) { + m := make(map[string]string) + for _, k := range ec.Keys { + m[k] = os.Getenv(k) + } + return m, nil +} + +// ConfParser ... +type ConfParser interface { + + //Parse parse the input raw map into a config map + Parse(raw map[string]string, config map[string]interface{}) error +} + +// Config wraps a map for the processed configuration values, +// and loader parser to read configuration from external source and process the values. +type Config struct { + Config map[string]interface{} + Loader ConfLoader + Parser ConfParser +} + +// Load reload the configurations +func (conf *Config) Load() error { + rawMap, err := conf.Loader.Load() + if err != nil { + return err + } + err = conf.Parser.Parse(rawMap, conf.Config) + return err +} + +// MySQLSetting wraps the settings of a MySQL DB +type MySQLSetting struct { + Database string + User string + Password string + Host string + Port string +} + +// SQLiteSetting wraps the settings of a SQLite DB +type SQLiteSetting struct { + FilePath string +} + +type commonParser struct{} + +// Parse parses the db settings, veryfy_remote_cert, ext_endpoint, token_endpoint +func (cp *commonParser) Parse(raw map[string]string, config map[string]interface{}) error { + db := strings.ToLower(raw["DATABASE"]) + if db == "mysql" || db == "" { + db = "mysql" + mySQLDB := raw["MYSQL_DATABASE"] + if len(mySQLDB) == 0 { + mySQLDB = "registry" + } + setting := MySQLSetting{ + mySQLDB, + raw["MYSQL_USR"], + raw["MYSQL_PWD"], + raw["MYSQL_HOST"], + raw["MYSQL_PORT"], + } + config["mysql"] = setting + } else if db == "sqlite" { + f := raw["SQLITE_FILE"] + if len(f) == 0 { + f = "registry.db" + } + setting := SQLiteSetting{ + f, + } + config["sqlite"] = setting + } else { + return fmt.Errorf("Invalid DB: %s", db) + } + config["database"] = db + + //By default it's true + config["verify_remote_cert"] = raw["VERIFY_REMOTE_CERT"] != "off" + + config["ext_endpoint"] = raw["EXT_ENDPOINT"] + config["token_endpoint"] = raw["TOKEN_ENDPOINT"] + config["log_level"] = raw["LOG_LEVEL"] + return nil +} + +var commonConfig *Config + +func init() { + commonKeys := []string{"DATABASE", "MYSQL_DATABASE", "MYSQL_USR", "MYSQL_PWD", "MYSQL_HOST", "MYSQL_PORT", "SQLITE_FILE", "VERIFY_REMOTE_CERT", "EXT_ENDPOINT", "TOKEN_ENDPOINT", "LOG_LEVEL"} + commonConfig = &Config{ + Config: make(map[string]interface{}), + Loader: &EnvConfigLoader{Keys: commonKeys}, + Parser: &commonParser{}, + } + if err := commonConfig.Load(); err != nil { + panic(err) + } +} + +// Reload will reload the configuration. +func Reload() error { + return commonConfig.Load() +} + +// Database returns the DB type in configuration. +func Database() string { + return commonConfig.Config["database"].(string) +} + +// MySQL returns the mysql setting in configuration. +func MySQL() MySQLSetting { + return commonConfig.Config["mysql"].(MySQLSetting) +} + +// SQLite returns the SQLite setting +func SQLite() SQLiteSetting { + return commonConfig.Config["sqlite"].(SQLiteSetting) +} + +// VerifyRemoteCert returns bool value. +func VerifyRemoteCert() bool { + return commonConfig.Config["verify_remote_cert"].(bool) +} + +// ExtEndpoint ... +func ExtEndpoint() string { + return commonConfig.Config["ext_endpoint"].(string) +} + +// TokenEndpoint returns the endpoint string of token service, which can be accessed by internal service of Harbor. +func TokenEndpoint() string { + return commonConfig.Config["token_endpoint"].(string) +} + +// LogLevel returns the log level in string format. +func LogLevel() string { + return commonConfig.Config["log_level"].(string) +} diff --git a/src/common/config/config_test.go b/src/common/config/config_test.go new file mode 100644 index 000000000..95c954975 --- /dev/null +++ b/src/common/config/config_test.go @@ -0,0 +1,111 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package config + +import ( + "os" + "testing" +) + +func TestEnvConfLoader(t *testing.T) { + os.Unsetenv("KEY2") + os.Setenv("KEY1", "V1") + os.Setenv("KEY3", "V3") + keys := []string{"KEY1", "KEY2"} + ecl := EnvConfigLoader{ + keys, + } + m, err := ecl.Load() + if err != nil { + t.Errorf("Error loading the configuration via env: %v", err) + } + if m["KEY1"] != "V1" { + t.Errorf("The value for key KEY1 should be V1, but infact: %s", m["KEY1"]) + } + if len(m["KEY2"]) > 0 { + t.Errorf("The value for key KEY2 should be emptye, but infact: %s", m["KEY2"]) + } + if _, ok := m["KEY3"]; ok { + t.Errorf("The KEY3 should not be in result as it's not in the initial key list") + } + os.Unsetenv("KEY1") + os.Unsetenv("KEY3") +} + +func TestCommonConfig(t *testing.T) { + + mysql := MySQLSetting{"registry", "root", "password", "127.0.0.1", "3306"} + sqlite := SQLiteSetting{"file.db"} + verify := "off" + ext := "http://harbor" + token := "http://token" + loglevel := "info" + + os.Setenv("DATABASE", "") + os.Setenv("MYSQL_DATABASE", mysql.Database) + os.Setenv("MYSQL_USR", mysql.User) + os.Setenv("MYSQL_PWD", mysql.Password) + os.Setenv("MYSQL_HOST", mysql.Host) + os.Setenv("MYSQL_PORT", mysql.Port) + os.Setenv("SQLITE_FILE", sqlite.FilePath) + os.Setenv("VERIFY_REMOTE_CERT", verify) + os.Setenv("EXT_ENDPOINT", ext) + os.Setenv("TOKEN_ENDPOINT", token) + os.Setenv("LOG_LEVEL", loglevel) + + err := Reload() + if err != nil { + t.Errorf("Unexpected error when loading the configurations, error: %v", err) + } + if Database() != "mysql" { + t.Errorf("Expected Database value: mysql, fact: %s", mysql) + } + if MySQL() != mysql { + t.Errorf("Expected MySQL setting: %+v, fact: %+v", mysql, MySQL()) + } + if VerifyRemoteCert() { + t.Errorf("Expected VerifyRemoteCert: false, env var: %s, fact: %v", verify, VerifyRemoteCert()) + } + if ExtEndpoint() != ext { + t.Errorf("Expected ExtEndpoint: %s, fact: %s", ext, ExtEndpoint()) + } + if TokenEndpoint() != token { + t.Errorf("Expected TokenEndpoint: %s, fact: %s", token, TokenEndpoint()) + } + if LogLevel() != loglevel { + t.Errorf("Expected LogLevel: %s, fact: %s", loglevel, LogLevel()) + } + os.Setenv("DATABASE", "sqlite") + err = Reload() + if err != nil { + t.Errorf("Unexpected error when loading the configurations, error: %v", err) + } + if SQLite() != sqlite { + t.Errorf("Expected SQLite setting: %+v, fact %+v", sqlite, SQLite()) + } + + os.Unsetenv("DATABASE") + os.Unsetenv("MYSQL_DATABASE") + os.Unsetenv("MYSQL_USR") + os.Unsetenv("MYSQL_PWD") + os.Unsetenv("MYSQL_HOST") + os.Unsetenv("MYSQL_PORT") + os.Unsetenv("SQLITE_FILE") + os.Unsetenv("VERIFY_REMOTE_CERT") + os.Unsetenv("EXT_ENDPOINT") + os.Unsetenv("TOKEN_ENDPOINT") + os.Unsetenv("LOG_LEVEL") + +} diff --git a/src/common/dao/base.go b/src/common/dao/base.go index 55e452f7a..13dc1c3ae 100644 --- a/src/common/dao/base.go +++ b/src/common/dao/base.go @@ -17,11 +17,10 @@ package dao import ( "fmt" - "os" - "strings" "sync" "github.com/astaxie/beego/orm" + "github.com/vmware/harbor/src/common/config" "github.com/vmware/harbor/src/common/utils/log" ) @@ -52,42 +51,18 @@ func InitDatabase() { } func getDatabase() (db Database, err error) { - switch strings.ToLower(os.Getenv("DATABASE")) { + switch config.Database() { case "", "mysql": - host, port, usr, pwd, database := getMySQLConnInfo() - db = NewMySQL(host, port, usr, pwd, database) + db = NewMySQL(config.MySQL().Host, config.MySQL().Port, config.MySQL().User, + config.MySQL().Password, config.MySQL().Database) case "sqlite": - file := getSQLiteConnInfo() - db = NewSQLite(file) + db = NewSQLite(config.SQLite().FilePath) default: - err = fmt.Errorf("invalid database: %s", os.Getenv("DATABASE")) - } - - return -} - -// TODO read from config -func getMySQLConnInfo() (host, port, username, password, database string) { - host = os.Getenv("MYSQL_HOST") - port = os.Getenv("MYSQL_PORT") - username = os.Getenv("MYSQL_USR") - password = os.Getenv("MYSQL_PWD") - database = os.Getenv("MYSQL_DATABASE") - if len(database) == 0 { - database = "registry" + err = fmt.Errorf("invalid database: %s", config.Database()) } return } -// TODO read from config -func getSQLiteConnInfo() string { - file := os.Getenv("SQLITE_FILE") - if len(file) == 0 { - file = "registry.db" - } - return file -} - var globalOrm orm.Ormer var once sync.Once diff --git a/src/common/utils/log/logger.go b/src/common/utils/log/logger.go index c9a6c5072..dceb70a97 100644 --- a/src/common/utils/log/logger.go +++ b/src/common/utils/log/logger.go @@ -22,6 +22,8 @@ import ( "runtime" "sync" "time" + + "github.com/vmware/harbor/src/common/config" ) var logger = New(os.Stdout, NewTextFormatter(), WarningLevel) @@ -29,8 +31,7 @@ var logger = New(os.Stdout, NewTextFormatter(), WarningLevel) func init() { logger.callDepth = 4 - // TODO add item in configuaration file - lvl := os.Getenv("LOG_LEVEL") + lvl := config.LogLevel() if len(lvl) == 0 { logger.SetLevel(InfoLevel) return diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index c7147ea57..14b230e5a 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -21,15 +21,15 @@ import ( "io/ioutil" "net/http" "net/url" - "os" "strings" "sync" "time" - token_util "github.com/vmware/harbor/src/ui/service/token" + "github.com/vmware/harbor/src/common/config" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/registry" registry_error "github.com/vmware/harbor/src/common/utils/registry/error" + token_util "github.com/vmware/harbor/src/ui/service/token" ) const ( @@ -234,11 +234,11 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes [] // 2. the realm field returned by registry is an IP which can not reachable // inside Harbor func tokenURL(realm string) string { - extEndpoint := os.Getenv("EXT_ENDPOINT") - tokenURL := os.Getenv("TOKEN_URL") - if len(extEndpoint) != 0 && len(tokenURL) != 0 && + extEndpoint := config.ExtEndpoint() + tokenEndpoint := config.TokenEndpoint() + if len(extEndpoint) != 0 && len(tokenEndpoint) != 0 && strings.Contains(realm, extEndpoint) { - realm = strings.TrimRight(tokenURL, "/") + "/service/token" + realm = strings.TrimRight(tokenEndpoint, "/") + "/service/token" } return realm } diff --git a/src/ui/api/project.go b/src/ui/api/project.go index d4212f762..7ba50fa3b 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -24,6 +24,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/config" "strconv" "time" @@ -72,11 +73,19 @@ func (p *ProjectAPI) Prepare() { // Post ... func (p *ProjectAPI) Post() { p.userID = p.ValidateUser() - + isSysAdmin, err := dao.IsAdminRole(p.userID) + if err != nil { + log.Errorf("Failed to check admin role: %v", err) + } + if !isSysAdmin && config.OnlyAdminCreateProject() { + log.Errorf("Only sys admin can create project") + p.RenderError(http.StatusForbidden, "Only system admin can create project") + return + } var req projectReq p.DecodeJSONReq(&req) public := req.Public - err := validateProjectReq(req) + err = validateProjectReq(req) if err != nil { log.Errorf("Invalid project request, error: %v", err) p.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index 867988c3d..3a885de8b 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -19,23 +19,23 @@ import ( "fmt" "io/ioutil" "net/http" - "os" "sort" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" + "github.com/vmware/harbor/src/common/api" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/common/utils/registry" "github.com/vmware/harbor/src/ui/service/cache" svc_utils "github.com/vmware/harbor/src/ui/service/utils" - "github.com/vmware/harbor/src/common/utils/log" - "github.com/vmware/harbor/src/common/api" - "github.com/vmware/harbor/src/common/utils/registry" registry_error "github.com/vmware/harbor/src/common/utils/registry/error" "github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils/registry/auth" + "github.com/vmware/harbor/src/ui/config" ) // RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put @@ -361,7 +361,7 @@ func (ra *RepositoryAPI) GetManifests() { } func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) { - endpoint := os.Getenv("REGISTRY_URL") + endpoint := config.InternalRegistryURL() username, password, ok := ra.Ctx.Request.BasicAuth() if ok { diff --git a/src/ui/api/target.go b/src/ui/api/target.go index aa19d28f0..abe59a09c 100644 --- a/src/ui/api/target.go +++ b/src/ui/api/target.go @@ -20,17 +20,17 @@ import ( "net" "net/http" "net/url" - "os" "strconv" + "github.com/vmware/harbor/src/common/api" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils/log" - "github.com/vmware/harbor/src/common/api" "github.com/vmware/harbor/src/common/utils/registry" "github.com/vmware/harbor/src/common/utils/registry/auth" registry_error "github.com/vmware/harbor/src/common/utils/registry/error" + "github.com/vmware/harbor/src/ui/config" ) // TargetAPI handles request to /api/targets/ping /api/targets/{} @@ -41,8 +41,7 @@ type TargetAPI struct { // Prepare validates the user func (t *TargetAPI) Prepare() { - //TODO:move to config - t.secretKey = os.Getenv("SECRET_KEY") + t.secretKey = config.SecretKey() userID := t.ValidateUser() isSysAdmin, err := dao.IsAdminRole(userID) diff --git a/src/ui/api/user.go b/src/ui/api/user.go index 3a0387390..fbed94d3a 100644 --- a/src/ui/api/user.go +++ b/src/ui/api/user.go @@ -18,7 +18,6 @@ package api import ( "fmt" "net/http" - "os" "regexp" "strconv" "strings" @@ -27,6 +26,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/config" ) // UserAPI handles request to /api/users/{} @@ -47,16 +47,9 @@ type passwordReq struct { // Prepare validates the URL and parms func (ua *UserAPI) Prepare() { - authMode := strings.ToLower(os.Getenv("AUTH_MODE")) - if authMode == "" { - authMode = "db_auth" - } - ua.AuthMode = authMode + ua.AuthMode = config.AuthMode() - selfRegistration := strings.ToLower(os.Getenv("SELF_REGISTRATION")) - if selfRegistration == "on" { - ua.SelfRegistration = true - } + ua.SelfRegistration = config.SelfRegistration() if ua.Ctx.Input.IsPost() { sessionUserID := ua.GetSession("userId") @@ -241,9 +234,7 @@ func (ua *UserAPI) Delete() { return } - // TODO read from conifg - authMode := os.Getenv("AUTH_MODE") - if authMode == "ldap_auth" { + if config.AuthMode() == "ldap_auth" { ua.CustomAbort(http.StatusForbidden, "user can not be deleted in LDAP authentication mode") } diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go index 42acfe75b..185db1f18 100644 --- a/src/ui/api/utils.go +++ b/src/ui/api/utils.go @@ -22,18 +22,18 @@ import ( "io/ioutil" "net" "net/http" - "os" "sort" "strings" "time" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" - "github.com/vmware/harbor/src/ui/service/cache" "github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/registry" registry_error "github.com/vmware/harbor/src/common/utils/registry/error" + "github.com/vmware/harbor/src/ui/config" + "github.com/vmware/harbor/src/ui/service/cache" ) func checkProjectPermission(userID int, projectID int64) bool { @@ -233,9 +233,8 @@ func postReplicationAction(policyID int64, acton string) error { func addAuthentication(req *http.Request) { if req != nil { req.AddCookie(&http.Cookie{ - Name: models.UISecretCookie, - // TODO read secret from config - Value: os.Getenv("UI_SECRET"), + Name: models.UISecretCookie, + Value: config.UISecret(), }) } } @@ -351,8 +350,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string } // TODO remove the workaround when the bug of registry is fixed - // TODO read it from config - endpoint := os.Getenv("REGISTRY_URL") + endpoint := config.InternalRegistryURL() client, err := cache.NewRepositoryClient(endpoint, true, "admin", repoInR, "repository", repoInR) if err != nil { @@ -374,8 +372,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string j++ } else { // TODO remove the workaround when the bug of registry is fixed - // TODO read it from config - endpoint := os.Getenv("REGISTRY_URL") + endpoint := config.InternalRegistryURL() client, err := cache.NewRepositoryClient(endpoint, true, "admin", repoInR, "repository", repoInR) if err != nil { @@ -425,7 +422,7 @@ func projectExists(repository string) (bool, error) { } func initRegistryClient() (r *registry.Registry, err error) { - endpoint := os.Getenv("REGISTRY_URL") + endpoint := config.InternalRegistryURL() addr := endpoint if strings.Contains(endpoint, "/") { @@ -462,32 +459,20 @@ func initRegistryClient() (r *registry.Registry, err error) { } func buildReplicationURL() string { - url := getJobServiceURL() + url := config.InternalJobServiceURL() return fmt.Sprintf("%s/api/jobs/replication", url) } func buildJobLogURL(jobID string) string { - url := getJobServiceURL() + url := config.InternalJobServiceURL() return fmt.Sprintf("%s/api/jobs/replication/%s/log", url, jobID) } func buildReplicationActionURL() string { - url := getJobServiceURL() + url := config.InternalJobServiceURL() return fmt.Sprintf("%s/api/jobs/replication/actions", url) } -func getJobServiceURL() string { - url := os.Getenv("JOB_SERVICE_URL") - url = strings.TrimSpace(url) - url = strings.TrimRight(url, "/") - - if len(url) == 0 { - url = "http://jobservice" - } - - return url -} - func getReposByProject(name string, keyword ...string) ([]string, error) { repositories := []string{} diff --git a/src/ui/auth/authenticator.go b/src/ui/auth/authenticator.go index 4032e83c0..23abdc8f2 100644 --- a/src/ui/auth/authenticator.go +++ b/src/ui/auth/authenticator.go @@ -17,11 +17,11 @@ package auth import ( "fmt" - "github.com/vmware/harbor/src/common/utils/log" - "os" "time" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/config" ) // 1.5 seconds @@ -50,7 +50,7 @@ func Register(name string, authenticator Authenticator) { // Login authenticates user credentials based on setting. func Login(m models.AuthModel) (*models.User, error) { - var authMode = os.Getenv("AUTH_MODE") + var authMode = config.AuthMode() if authMode == "" || m.Principal == "admin" { authMode = "db_auth" } diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 6193485f1..46c563cbe 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -18,14 +18,14 @@ package ldap import ( "errors" "fmt" - "os" "strings" "github.com/vmware/harbor/src/common/utils/log" - "github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/ui/auth" + "github.com/vmware/harbor/src/ui/config" "github.com/mqu/openldap" ) @@ -46,7 +46,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { return nil, fmt.Errorf("the principal contains meta char: %q", c) } } - ldapURL := os.Getenv("LDAP_URL") + ldapURL := config.LDAP().URL if ldapURL == "" { return nil, errors.New("can not get any available LDAP_URL") } @@ -57,16 +57,16 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { } ldap.SetOption(openldap.LDAP_OPT_PROTOCOL_VERSION, openldap.LDAP_VERSION3) - ldapBaseDn := os.Getenv("LDAP_BASE_DN") + ldapBaseDn := config.LDAP().BaseDn if ldapBaseDn == "" { return nil, errors.New("can not get any available LDAP_BASE_DN") } log.Debug("baseDn:", ldapBaseDn) - ldapSearchDn := os.Getenv("LDAP_SEARCH_DN") + ldapSearchDn := config.LDAP().SearchDn if ldapSearchDn != "" { log.Debug("Search DN: ", ldapSearchDn) - ldapSearchPwd := os.Getenv("LDAP_SEARCH_PWD") + ldapSearchPwd := config.LDAP().SearchPwd err = ldap.Bind(ldapSearchDn, ldapSearchPwd) if err != nil { log.Debug("Bind search dn error", err) @@ -74,8 +74,8 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { } } - attrName := os.Getenv("LDAP_UID") - filter := os.Getenv("LDAP_FILTER") + attrName := config.LDAP().UID + filter := config.LDAP().Filter if filter != "" { filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))" } else { @@ -83,7 +83,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { } log.Debug("one or more filter", filter) - ldapScope := os.Getenv("LDAP_SCOPE") + ldapScope := config.LDAP().Scope var scope int if ldapScope == "1" { scope = openldap.LDAP_SCOPE_BASE diff --git a/src/ui/config/config.go b/src/ui/config/config.go new file mode 100644 index 000000000..783779c4d --- /dev/null +++ b/src/ui/config/config.go @@ -0,0 +1,155 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package config provides methods to get configurations required by code in src/ui +package config + +import ( + "strconv" + "strings" + + commonConfig "github.com/vmware/harbor/src/common/config" + "github.com/vmware/harbor/src/common/utils/log" +) + +// LDAPSetting wraps the setting of an LDAP server +type LDAPSetting struct { + URL string + BaseDn string + SearchDn string + SearchPwd string + UID string + Filter string + Scope string +} + +type uiParser struct{} + +// Parse parses the auth settings url settings and other configuration consumed by code under src/ui +func (up *uiParser) Parse(raw map[string]string, config map[string]interface{}) error { + mode := raw["AUTH_MODE"] + if mode == "ldap_auth" { + setting := LDAPSetting{ + URL: raw["LDAP_URL"], + BaseDn: raw["LDAP_BASE_DN"], + SearchDn: raw["LDAP_SEARCH_DN"], + SearchPwd: raw["LDAP_SEARCH_PWD"], + UID: raw["LDAP_UID"], + Filter: raw["LDAP_FILTER"], + Scope: raw["LDAP_SCOPE"], + } + config["ldap"] = setting + } + config["auth_mode"] = mode + var tokenExpiration = 30 //minutes + if len(raw["TOKEN_EXPIRATION"]) > 0 { + i, err := strconv.Atoi(raw["TOKEN_EXPIRATION"]) + if err != nil { + log.Warningf("failed to parse token expiration: %v, using default value %d", err, tokenExpiration) + } else if i <= 0 { + log.Warningf("invalid token expiration, using default value: %d minutes", tokenExpiration) + } else { + tokenExpiration = i + } + } + config["token_exp"] = tokenExpiration + config["admin_password"] = raw["HARBOR_ADMIN_PASSWORD"] + config["ext_reg_url"] = raw["EXT_REG_URL"] + config["ui_secret"] = raw["UI_SECRET"] + config["secret_key"] = raw["SECRET_KEY"] + config["self_registration"] = raw["SELF_REGISTRATION"] != "off" + config["admin_create_project"] = strings.ToLower(raw["PROJECT_CREATION_RESTRICTION"]) == "adminonly" + registryURL := raw["REGISTRY_URL"] + registryURL = strings.TrimRight(registryURL, "/") + config["internal_registry_url"] = registryURL + jobserviceURL := raw["JOB_SERVICE_URL"] + jobserviceURL = strings.TrimRight(jobserviceURL, "/") + config["internal_jobservice_url"] = jobserviceURL + return nil +} + +var uiConfig *commonConfig.Config + +func init() { + uiKeys := []string{"AUTH_MODE", "LDAP_URL", "LDAP_BASE_DN", "LDAP_SEARCH_DN", "LDAP_SEARCH_PWD", "LDAP_UID", "LDAP_FILTER", "LDAP_SCOPE", "TOKEN_EXPIRATION", "HARBOR_ADMIN_PASSWORD", "EXT_REG_URL", "UI_SECRET", "SECRET_KEY", "SELF_REGISTRATION", "PROJECT_CREATION_RESTRICTION", "REGISTRY_URL", "JOB_SERVICE_URL"} + uiConfig = &commonConfig.Config{ + Config: make(map[string]interface{}), + Loader: &commonConfig.EnvConfigLoader{Keys: uiKeys}, + Parser: &uiParser{}, + } + if err := uiConfig.Load(); err != nil { + panic(err) + } +} + +// Reload ... +func Reload() error { + return uiConfig.Load() +} + +// AuthMode ... +func AuthMode() string { + return uiConfig.Config["auth_mode"].(string) +} + +// LDAP returns the setting of ldap server +func LDAP() LDAPSetting { + return uiConfig.Config["ldap"].(LDAPSetting) +} + +// TokenExpiration returns the token expiration time (in minute) +func TokenExpiration() int { + return uiConfig.Config["token_exp"].(int) +} + +// ExtRegistryURL returns the registry URL to exposed to external client +func ExtRegistryURL() string { + return uiConfig.Config["ext_reg_url"].(string) +} + +// UISecret returns the value of UI secret cookie, used for communication between UI and JobService +func UISecret() string { + return uiConfig.Config["ui_secret"].(string) +} + +// SecretKey returns the secret key to encrypt the password of target +func SecretKey() string { + return uiConfig.Config["secret_key"].(string) +} + +// SelfRegistration returns the enablement of self registration +func SelfRegistration() bool { + return uiConfig.Config["self_registration"].(bool) +} + +// InternalRegistryURL returns registry URL for internal communication between Harbor containers +func InternalRegistryURL() string { + return uiConfig.Config["internal_registry_url"].(string) +} + +// InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers +func InternalJobServiceURL() string { + return uiConfig.Config["internal_jobservice_url"].(string) +} + +// InitialAdminPassword returns the initial password for administrator +func InitialAdminPassword() string { + return uiConfig.Config["admin_password"].(string) +} + +// OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project +func OnlyAdminCreateProject() bool { + return uiConfig.Config["admin_create_project"].(bool) +} diff --git a/src/ui/config/config_test.go b/src/ui/config/config_test.go new file mode 100644 index 000000000..38df6641b --- /dev/null +++ b/src/ui/config/config_test.go @@ -0,0 +1,144 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package config + +import ( + "os" + "testing" +) + +var ( + auth = "ldap_auth" + ldap = LDAPSetting{ + "ldap://test.ldap.com", + "ou=people", + "dc=whatever,dc=org", + "1234567", + "cn", + "uid", + "2", + } + tokenExp = "3" + tokenExpRes = 3 + adminPassword = "password" + externalRegURL = "127.0.0.1" + uiSecret = "ffadsdfsdf" + secretKey = "keykey" + selfRegistration = "off" + projectCreationRestriction = "adminonly" + internalRegistryURL = "http://registry:5000" + jobServiceURL = "http://jobservice" +) + +func TestMain(m *testing.M) { + + os.Setenv("AUTH_MODE", auth) + os.Setenv("LDAP_URL", ldap.URL) + os.Setenv("LDAP_BASE_DN", ldap.BaseDn) + os.Setenv("LDAP_SEARCH_DN", ldap.SearchDn) + os.Setenv("LDAP_SEARCH_PWD", ldap.SearchPwd) + os.Setenv("LDAP_UID", ldap.UID) + os.Setenv("LDAP_SCOPE", ldap.Scope) + os.Setenv("LDAP_FILTER", ldap.Filter) + os.Setenv("TOKEN_EXPIRATION", tokenExp) + os.Setenv("HARBOR_ADMIN_PASSWORD", adminPassword) + os.Setenv("EXT_REG_URL", externalRegURL) + os.Setenv("UI_SECRET", uiSecret) + os.Setenv("SECRET_KEY", secretKey) + os.Setenv("SELF_REGISTRATION", selfRegistration) + os.Setenv("PROJECT_CREATION_RESTRICTION", projectCreationRestriction) + os.Setenv("REGISTRY_URL", internalRegistryURL) + os.Setenv("JOB_SERVICE_URL", jobServiceURL) + + err := Reload() + if err != nil { + panic(err) + } + rc := m.Run() + + os.Unsetenv("AUTH_MODE") + os.Unsetenv("LDAP_URL") + os.Unsetenv("LDAP_BASE_DN") + os.Unsetenv("LDAP_SEARCH_DN") + os.Unsetenv("LDAP_SEARCH_PWD") + os.Unsetenv("LDAP_UID") + os.Unsetenv("LDAP_SCOPE") + os.Unsetenv("LDAP_FILTER") + os.Unsetenv("TOKEN_EXPIRATION") + os.Unsetenv("HARBOR_ADMIN_PASSWORD") + os.Unsetenv("EXT_REG_URL") + os.Unsetenv("UI_SECRET") + os.Unsetenv("SECRET_KEY") + os.Unsetenv("SELF_REGISTRATION") + os.Unsetenv("CREATE_PROJECT_RESTRICTION") + os.Unsetenv("REGISTRY_URL") + os.Unsetenv("JOB_SERVICE_URL") + + os.Exit(rc) +} + +func TestAuth(t *testing.T) { + if AuthMode() != auth { + t.Errorf("Expected auth mode:%s, in fact: %s", auth, AuthMode()) + } + if LDAP() != ldap { + t.Errorf("Expected ldap setting: %+v, in fact: %+v", ldap, LDAP()) + } +} + +func TestTokenExpiration(t *testing.T) { + if TokenExpiration() != tokenExpRes { + t.Errorf("Expected token expiration: %d, in fact: %d", tokenExpRes, TokenExpiration()) + } +} + +func TestURLs(t *testing.T) { + if InternalRegistryURL() != internalRegistryURL { + t.Errorf("Expected internal Registry URL: %s, in fact: %s", internalRegistryURL, InternalRegistryURL()) + } + if InternalJobServiceURL() != jobServiceURL { + t.Errorf("Expected internal jobservice URL: %s, in fact: %s", jobServiceURL, InternalJobServiceURL()) + } + if ExtRegistryURL() != externalRegURL { + t.Errorf("Expected External Registry URL: %s, in fact: %s", externalRegURL, ExtRegistryURL()) + } +} + +func TestSelfRegistration(t *testing.T) { + if SelfRegistration() { + t.Errorf("Expected Self Registration to be false") + } +} + +func TestSecrets(t *testing.T) { + if SecretKey() != secretKey { + t.Errorf("Expected Secrect Key :%s, in fact: %s", secretKey, SecretKey()) + } + if UISecret() != uiSecret { + t.Errorf("Expected UI Secret: %s, in fact: %s", uiSecret, UISecret()) + } +} + +func TestProjectCreationRestrict(t *testing.T) { + if !OnlyAdminCreateProject() { + t.Errorf("Expected OnlyAdminCreateProject to be true") + } +} + +func TestInitAdminPassword(t *testing.T) { + if InitialAdminPassword() != adminPassword { + t.Errorf("Expected adminPassword: %s, in fact: %s", adminPassword, InitialAdminPassword()) + } +} diff --git a/src/ui/controllers/base.go b/src/ui/controllers/base.go index fdfaa6b6b..beaa10cf8 100644 --- a/src/ui/controllers/base.go +++ b/src/ui/controllers/base.go @@ -12,6 +12,7 @@ import ( "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/auth" + "github.com/vmware/harbor/src/ui/config" ) // BaseController wraps common methods such as i18n support, forward, which can be leveraged by other UI render controllers. @@ -99,7 +100,7 @@ func (b *BaseController) Prepare() { b.Data["CurLang"] = curLang.Name b.Data["RestLangs"] = restLangs - authMode := strings.ToLower(os.Getenv("AUTH_MODE")) + authMode := config.AuthMode() if authMode == "" { authMode = "db_auth" } @@ -116,11 +117,9 @@ func (b *BaseController) Prepare() { b.UseCompressedJS = false } - selfRegistration := strings.ToLower(os.Getenv("SELF_REGISTRATION")) - if selfRegistration == "on" { - b.SelfRegistration = true - } - b.Data["SelfRegistration"] = b.SelfRegistration + b.SelfRegistration = config.SelfRegistration() + + b.Data["SelfRegistration"] = config.SelfRegistration() } // Forward to setup layout and template for content for a page. diff --git a/src/ui/controllers/password.go b/src/ui/controllers/password.go index 83d23a60d..c3dce0801 100644 --- a/src/ui/controllers/password.go +++ b/src/ui/controllers/password.go @@ -3,11 +3,11 @@ package controllers import ( "bytes" "net/http" - "os" "regexp" "text/template" "github.com/astaxie/beego" + "github.com/vmware/harbor/src/common/config" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils" @@ -49,7 +49,7 @@ func (cc *CommonController) SendEmail() { message := new(bytes.Buffer) - harborURL := os.Getenv("HARBOR_URL") + harborURL := config.ExtEndpoint() if harborURL == "" { harborURL = "localhost" } diff --git a/src/ui/controllers/repository.go b/src/ui/controllers/repository.go index 58ad4dc7e..b85ad8782 100644 --- a/src/ui/controllers/repository.go +++ b/src/ui/controllers/repository.go @@ -1,6 +1,8 @@ package controllers -import "os" +import ( + "github.com/vmware/harbor/src/ui/config" +) // RepositoryController handles request to /repository type RepositoryController struct { @@ -9,6 +11,6 @@ type RepositoryController struct { // Get renders repository page func (rc *RepositoryController) Get() { - rc.Data["HarborRegUrl"] = os.Getenv("HARBOR_REG_URL") + rc.Data["HarborRegUrl"] = config.ExtRegistryURL() rc.Forward("page_title_repository", "repository.htm") } diff --git a/src/ui/main.go b/src/ui/main.go index d355a11cc..6c4427279 100644 --- a/src/ui/main.go +++ b/src/ui/main.go @@ -25,11 +25,12 @@ import ( "github.com/astaxie/beego" _ "github.com/astaxie/beego/session/redis" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/ui/api" _ "github.com/vmware/harbor/src/ui/auth/db" _ "github.com/vmware/harbor/src/ui/auth/ldap" - "github.com/vmware/harbor/src/common/dao" - "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/ui/config" ) const ( @@ -76,7 +77,7 @@ func main() { dao.InitDatabase() - if err := updateInitPassword(adminUserID, os.Getenv("HARBOR_ADMIN_PASSWORD")); err != nil { + if err := updateInitPassword(adminUserID, config.InitialAdminPassword()); err != nil { log.Error(err) } initRouters() diff --git a/src/ui/service/token/authutils.go b/src/ui/service/token/authutils.go index 63e884f5b..1e687365b 100644 --- a/src/ui/service/token/authutils.go +++ b/src/ui/service/token/authutils.go @@ -21,13 +21,12 @@ import ( "encoding/base64" "encoding/json" "fmt" - "os" - "strconv" "strings" "time" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/config" "github.com/docker/distribution/registry/auth/token" "github.com/docker/libtrust" @@ -38,27 +37,10 @@ const ( privateKey = "/etc/ui/private_key.pem" ) -var ( - expiration = 30 //minutes -) +var expiration int //minutes func init() { - // TODO read it from config - expi := os.Getenv("TOKEN_EXPIRATION") - if len(expi) != 0 { - i, err := strconv.Atoi(expi) - if err != nil { - log.Errorf("failed to parse token expiration: %v, using default value: %d minutes", err, expiration) - return - } - - if i <= 0 { - log.Warningf("invalid token expiration, using default value: %d minutes", expiration) - return - } - - expiration = i - } + expiration = config.TokenExpiration() log.Infof("token expiration: %d minutes", expiration) } @@ -109,7 +91,7 @@ func FilterAccess(username string, a *token.ResourceActions) { repoLength := len(repoSplit) if repoLength > 1 { //Only check the permission when the requested image has a namespace, i.e. project var projectName string - registryURL := os.Getenv("HARBOR_REG_URL") + registryURL := config.ExtRegistryURL() if repoSplit[0] == registryURL { projectName = repoSplit[1] log.Infof("Detected Registry URL in Project Name. Assuming this is a notary request and setting Project Name as %s\n", projectName) diff --git a/src/ui/service/utils/utils.go b/src/ui/service/utils/utils.go index b64fb98cf..007b9b720 100644 --- a/src/ui/service/utils/utils.go +++ b/src/ui/service/utils/utils.go @@ -18,14 +18,14 @@ package utils import ( "net/http" - "os" "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/config" ) // VerifySecret verifies the UI_SECRET cookie in a http request. func VerifySecret(r *http.Request) bool { - secret := os.Getenv("UI_SECRET") + secret := config.UISecret() c, err := r.Cookie("uisecret") if err != nil { log.Warningf("Failed to get secret cookie, error: %v", err)