Merge latest updates.

This commit is contained in:
kunw 2017-03-21 11:51:12 +08:00
parent a80008d0f9
commit 5c3fd9fc43
271 changed files with 6146 additions and 1593 deletions

View File

@ -31,6 +31,7 @@ env:
before_install:
- sudo ./tests/hostcfg.sh
- sudo ./tests/generateCerts.sh
- sudo ./make/prepare
install:
@ -92,12 +93,12 @@ script:
- goveralls -coverprofile=profile.cov -service=travis-ci
- docker-compose -f make/docker-compose.test.yml down
- sudo make/prepare
- sudo rm -rf /data/config/*
- docker-compose -f make/dev/docker-compose.yml up -d
- sudo rm -rf /data/config/*
- ls /data/cert
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true
- docker ps
- go run tests/startuptest.go http://localhost/
- go run tests/startuptest.go https://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
# - sudo ./tests/testprepare.sh

View File

@ -79,6 +79,13 @@ REGISTRYSERVER=
REGISTRYPROJECTNAME=vmware
DEVFLAG=true
NOTARYFLAG=false
REGISTRYVERSION=2.6.0
NGINXVERSION=1.11.5
PHOTONVERSION=1.0
NOTARYVERSION=server-0.5.0-fix
NOTARYSIGNERVERSION=signer-0.5.0
MARIADBVERSION=10.1.10
HTTPPROXY=
#clarity parameters
CLARITYIMAGE=danieljt/harbor-clarity-base[:tag]
@ -206,7 +213,11 @@ compile_jobservice:
compile_clarity:
@echo "compiling binary for clarity ui..."
@$(DOCKERCMD) run --rm -v $(UIPATH)/static/new-ui:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src -v $(UINGPATH)/src/app:$(CLARITYSEEDPATH)/src/app $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT)
@if [ "$(HTTPPROXY)" != "" ] ; then \
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT) -p $(HTTPPROXY); \
else \
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT); \
fi
@echo "Done."
compile_normal: compile_clarity compile_adminserver compile_ui compile_jobservice
@ -290,13 +301,13 @@ package_offline: compile build modify_composefile
@cp NOTICE $(HARBORPKG)/NOTICE
@echo "pulling nginx and registry..."
@$(DOCKERPULL) registry:2.5.1
@$(DOCKERPULL) nginx:1.11.5
@$(DOCKERPULL) registry:$(REGISTRYVERSION)
@$(DOCKERPULL) nginx:$(NGINXVERSION)
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
echo "pulling notary and mariadb..."; \
$(DOCKERPULL) jiangd/notary:server-0.5.0-fix; \
$(DOCKERPULL) notary:signer-0.5.0; \
$(DOCKERPULL) mariadb:10.1.10; \
$(DOCKERPULL) jiangd/notary:$(NOTARYVERSION); \
$(DOCKERPULL) notary:$(NOTARYSIGNERVERSION); \
$(DOCKERPULL) mariadb:$(MARIADBVERSION); \
fi
@echo "saving harbor docker image"
@ -307,8 +318,8 @@ package_offline: compile build modify_composefile
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:1.11.5 registry:2.5.1 photon:1.0 \
jiangd/notary:server-0.5.0-fix notary:signer-0.5.0 mariadb:10.1.10; \
nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) \
jiangd/notary:$(NOTARYVERSION) notary:$(NOTARYSIGNERVERSION) mariadb:$(MARIADBVERSION); \
else \
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
@ -316,7 +327,7 @@ package_offline: compile build modify_composefile
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:1.11.5 registry:2.5.1 photon:1.0 ; \
nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) ; \
fi
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
@ -324,14 +335,14 @@ package_offline: compile build modify_composefile
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
else \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
fi
@rm -rf $(HARBORPKG)
@ -400,7 +411,7 @@ cleanimage:
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_DB):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
# - $(DOCKERRMIMAGE) -f registry:2.5.1
# - $(DOCKERRMIMAGE) -f registry:$(REGISTRYVERSION)
# - $(DOCKERRMIMAGE) -f nginx:1.11.5
cleandockercomposefile:

View File

@ -1529,6 +1529,22 @@ paths:
description: User does not have permission of admin role.
500:
description: Unexpected internal errors.
/configurations/reset:
post:
summary: Reset system configurations.
description: |
Reset system configurations from environment variables. Can only be accessed by admin user.
tags:
- Products
responses:
200:
description: Reset system configurations successfully.
401:
description: User need to log in first.
403:
description: User does not have permission of admin role.
500:
description: Unexpected internal errors.
/email/ping:
post:
summary: Test connection and authentication with email server.
@ -2009,6 +2025,9 @@ definitions:
self_registration:
type: boolean
description: Indicate whether the Harbor instance enable user to register himself.
has_ca_root:
type: boolean
description: Indicate whether there is a ca root cert file ready for download in the file system.
SystemInfo:
type: object
properties:
@ -2082,7 +2101,7 @@ definitions:
type: string
description: The host of email server.
email_port:
type: string
type: integer
description: The port of email server.
email_username:
type: string
@ -2091,7 +2110,7 @@ definitions:
type: string
description: The password of email server.
email_ssl:
type: string
type: boolean
description: Use ssl/tls or not.
email_identity:
type: string
@ -2146,4 +2165,4 @@ definitions:
description: The creation time of repository.
update_time:
type: string
description: The update time of repository.
description: The update time of repository.

View File

@ -191,14 +191,14 @@ Run the below commands on the host which Harbor is deployed on to preview what f
```sh
$ docker-compose stop
$ docker run -it --name gc --rm --volumes-from registry registry:2.5.1 garbage-collect --dry-run /etc/registry/config.yml
$ docker run -it --name gc --rm --volumes-from registry registry:2.6.0 garbage-collect --dry-run /etc/registry/config.yml
```
**NOTE:** The above option "--dry-run" will print the progress without removing any data.
Verify the result of the above test, then use the below commands to perform garbage collection and restart Harbor.
```sh
$ docker run -it --name gc --rm --volumes-from registry registry:2.5.1 garbage-collect /etc/registry/config.yml
$ docker run -it --name gc --rm --volumes-from registry registry:2.6.0 garbage-collect /etc/registry/config.yml
$ docker-compose start
```

View File

@ -37,3 +37,4 @@ USE_COMPRESSED_JS=$use_compressed_js
GODEBUG=netdns=cgo
ADMIRAL_URL=$admiral_url
WITH_NOTARY=$with_notary
RESET=false

View File

@ -8,7 +8,7 @@ events {
http {
tcp_nodelay on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.d/*.upstream.conf;
# this is necessary for us to be able to disable request buffering in all cases
proxy_http_version 1.1;
@ -42,6 +42,8 @@ http {
# required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
chunked_transfer_encoding on;
include /etc/nginx/conf.d/*.location.conf;
location / {
proxy_pass http://ui/;
proxy_set_header Host $$http_host;
@ -62,19 +64,6 @@ http {
return 404;
}
location /notary/v2/ {
proxy_pass http://notary-server/v2/;
proxy_set_header Host $$http_host;
proxy_set_header X-Real-IP $$remote_addr;
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
proxy_set_header X-Forwarded-Proto $$scheme;
proxy_buffering off;
proxy_request_buffering off;
}
location /v2/ {
proxy_pass http://registry/v2/;
proxy_set_header Host $$http_host;

View File

@ -0,0 +1,12 @@
location /notary/v2/ {
proxy_pass http://notary-server/v2/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
}

View File

@ -10,7 +10,7 @@ services:
ports:
- 1514:514
registry:
image: library/registry:2.5.1
image: library/registry:2.6.0
restart: always
volumes:
- /data/registry:/storage

View File

@ -1,12 +1,18 @@
FROM node:7.5.0
COPY angular-cli.json /
COPY index.html /
COPY entrypoint.sh /
RUN mkdir -p /clarity-seed
RUN npm install -g @angular/cli && \
chmod u+x entrypoint.sh
COPY src/ui_ng/package.json /clarity-seed
COPY src/ui_ng/tslint.json /clarity-seed
COPY src/ui_ng/typings.json /clarity-seed
COPY src/ui_ng/yarn.lock /clarity-seed
COPY make/dev/nodeclarity/angular-cli.json /clarity-seed
COPY make/dev/nodeclarity/entrypoint.sh /
VOLUME ["/clarity-seed", "/clarity-seed/dist"]
WORKDIR /clarity-seed
ENTRYPOINT ["/entrypoint.sh"]
RUN npm install -g @angular/cli && \
npm install && \
chmod u+x /entrypoint.sh
VOLUME ["/clarity-seed", "/clarity-seed/dist"]

View File

@ -1,11 +1,25 @@
#!/bin/bash
set -e
cd /clarity-seed
rm -rf dist/*
cp /angular-cli.json /clarity-seed
npm_proxy=
while getopts p: option
do
case "${option}"
in
p) npm_proxy=${OPTARG};;
esac
done
if [ ! -z "$npm_proxy" -a "$npm_proxy" != " " ]; then
npm config set proxy $npm_proxy
fi
npm install
ng build
cp /index.html dist/index.html
cp -r ./src/i18n/ dist/

View File

@ -11,7 +11,7 @@ services:
networks:
- harbor
registry:
image: registry:2.5.1
image: registry:2.6.0
container_name: registry
restart: always
volumes:

View File

@ -147,7 +147,10 @@ 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")
secretkey_path = rcp.get("configuration", "secretkey_path")
admiral_url = rcp.get("configuration", "admiral_url")
if rcp.has_option("configuration", "admiral_url"):
admiral_url = rcp.get("configuration", "admiral_url")
else:
admiral_url = ""
secret_key = get_secret_key(secretkey_path)
########
@ -313,20 +316,21 @@ if args.notary_mode:
shutil.rmtree(os.path.join(notary_config_dir, "mysql-initdb.d"))
shutil.copytree(os.path.join(notary_temp_dir, "mysql-initdb.d"), os.path.join(notary_config_dir, "mysql-initdb.d"))
#TODO:generate certs?
print ("Copying certs for notary signer")
print("Copying certs for notary signer")
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.crt"), notary_config_dir)
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.key"), notary_config_dir)
shutil.copy2(os.path.join(notary_temp_dir, "root-ca.crt"), notary_config_dir)
shutil.copy2(os.path.join(registry_config_dir, "root.crt"), notary_config_dir)
print ("Copying notary signer configuration file")
print("Copying notary signer configuration file")
shutil.copy2(os.path.join(notary_temp_dir, "signer-config.json"), notary_config_dir)
render(os.path.join(notary_temp_dir, "server-config.json"),
os.path.join(notary_config_dir, "server-config.json"),
token_endpoint=ui_url)
print ("Copying nginx configuration file for notary")
shutil.copy2(os.path.join(templates_dir, "nginx", "nginx.notary.conf"), nginx_conf_d)
print("Copying nginx configuration file for notary")
shutil.copy2(os.path.join(templates_dir, "nginx", "notary.upstream.conf"), nginx_conf_d)
shutil.copy2(os.path.join(templates_dir, "nginx", "notary.location.conf"), nginx_conf_d)
default_alias = ''.join(random.choice(string.ascii_letters) for i in range(8))
render(os.path.join(notary_temp_dir, "signer_env"), os.path.join(notary_config_dir, "signer_env"), alias = default_alias)

View File

@ -16,6 +16,7 @@
package api
import (
"encoding/json"
"net/http"
)
@ -32,3 +33,17 @@ func handleUnauthorized(w http.ResponseWriter) {
http.Error(w, http.StatusText(http.StatusUnauthorized),
http.StatusUnauthorized)
}
// response status code will be written automatically if there is an error
func writeJSON(w http.ResponseWriter, v interface{}) error {
b, err := json.Marshal(v)
if err != nil {
handleInternalServerError(w)
return err
}
if _, err = w.Write(b); err != nil {
return err
}
return nil
}

View File

@ -19,40 +19,13 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
cfg "github.com/vmware/harbor/src/adminserver/systemcfg"
"github.com/vmware/harbor/src/common/utils/log"
)
func isAuthenticated(r *http.Request) (bool, error) {
uiSecret := os.Getenv("UI_SECRET")
jobserviceSecret := os.Getenv("JOBSERVICE_SECRET")
c, err := r.Cookie("secret")
if err != nil {
if err == http.ErrNoCookie {
return false, nil
}
return false, err
}
return c != nil && (c.Value == uiSecret ||
c.Value == jobserviceSecret), nil
}
// ListCfgs lists configurations
func ListCfgs(w http.ResponseWriter, r *http.Request) {
authenticated, err := isAuthenticated(r)
if err != nil {
log.Errorf("failed to check whether the request is authenticated or not: %v", err)
handleInternalServerError(w)
return
}
if !authenticated {
handleUnauthorized(w)
return
}
cfg, err := cfg.GetSystemCfg()
if err != nil {
log.Errorf("failed to get system configurations: %v", err)
@ -60,31 +33,14 @@ func ListCfgs(w http.ResponseWriter, r *http.Request) {
return
}
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Errorf("failed to marshal configurations: %v", err)
handleInternalServerError(w)
return
}
if _, err = w.Write(b); err != nil {
if err = writeJSON(w, cfg); err != nil {
log.Errorf("failed to write response: %v", err)
return
}
}
// UpdateCfgs updates configurations
func UpdateCfgs(w http.ResponseWriter, r *http.Request) {
authenticated, err := isAuthenticated(r)
if err != nil {
log.Errorf("failed to check whether the request is authenticated or not: %v", err)
handleInternalServerError(w)
return
}
if !authenticated {
handleUnauthorized(w)
return
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Errorf("failed to read request body: %v", err)
@ -104,3 +60,12 @@ func UpdateCfgs(w http.ResponseWriter, r *http.Request) {
return
}
}
// ResetCfgs resets configurations from environment variables
func ResetCfgs(w http.ResponseWriter, r *http.Request) {
if err := cfg.Reset(); err != nil {
log.Errorf("failed to reset system configurations: %v", err)
handleInternalServerError(w)
return
}
}

View File

@ -43,7 +43,7 @@ func TestConfigAPI(t *testing.T) {
secret := "secret"
envs := map[string]string{
"AUTH_MODE": comcfg.DBAuth,
"JSON_CFG_STORE_PATH": configPath,
"KEY_PATH": secretKeyPath,
"UI_SECRET": secret,
@ -79,19 +79,12 @@ func TestConfigAPI(t *testing.T) {
return
}
w := httptest.NewRecorder()
ListCfgs(w, r)
if w.Code != http.StatusUnauthorized {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusUnauthorized)
return
}
r.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
w = httptest.NewRecorder()
w := httptest.NewRecorder()
ListCfgs(w, r)
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
@ -164,7 +157,55 @@ func TestConfigAPI(t *testing.T) {
mode := m[comcfg.AUTHMode].(string)
if mode != comcfg.LDAPAuth {
t.Errorf("unexpected ldap scope: %s != %s", mode, comcfg.LDAPAuth)
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth)
return
}
// reset configurations
w = httptest.NewRecorder()
r, err = http.NewRequest("POST", "", nil)
if err != nil {
t.Errorf("failed to create request: %v", err)
return
}
r.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
ResetCfgs(w, r)
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
return
}
// confirm the reset
r, err = http.NewRequest("GET", "", nil)
if err != nil {
t.Errorf("failed to create request: %v", err)
return
}
r.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
w = httptest.NewRecorder()
ListCfgs(w, r)
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
return
}
m, err = parse(w.Body)
if err != nil {
t.Errorf("failed to parse response body: %v", err)
return
}
mode = m[comcfg.AUTHMode].(string)
if mode != comcfg.DBAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth)
return
}
}

View File

@ -0,0 +1,38 @@
/*
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 api
import (
"net/http"
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
"github.com/vmware/harbor/src/common/utils/log"
)
// Capacity handles /api/systeminfo/capacity and returns system capacity
func Capacity(w http.ResponseWriter, r *http.Request) {
capacity, err := imagestorage.GlobalDriver.Cap()
if err != nil {
log.Errorf("failed to get capacity: %v", err)
handleInternalServerError(w)
return
}
if err = writeJSON(w, capacity); err != nil {
log.Errorf("failed to write response: %v", err)
return
}
}

View File

@ -0,0 +1,74 @@
/*
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 api
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
)
type fakeImageStorageDriver struct {
capacity *imagestorage.Capacity
err error
}
func (f *fakeImageStorageDriver) Name() string {
return "fake"
}
func (f *fakeImageStorageDriver) Cap() (*imagestorage.Capacity, error) {
return f.capacity, f.err
}
func TestCapacity(t *testing.T) {
cases := []struct {
driver imagestorage.Driver
responseCode int
capacity *imagestorage.Capacity
}{
{&fakeImageStorageDriver{nil, errors.New("error")}, http.StatusInternalServerError, nil},
{&fakeImageStorageDriver{&imagestorage.Capacity{100, 90}, nil}, http.StatusOK, &imagestorage.Capacity{100, 90}},
}
req, err := http.NewRequest("", "", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
for _, c := range cases {
imagestorage.GlobalDriver = c.driver
w := httptest.NewRecorder()
Capacity(w, req)
assert.Equal(t, c.responseCode, w.Code, "unexpected response code")
if c.responseCode == http.StatusOK {
b, err := ioutil.ReadAll(w.Body)
if err != nil {
t.Fatalf("failed to read from response body: %v", err)
}
capacity := &imagestorage.Capacity{}
if err = json.Unmarshal(b, capacity); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
assert.Equal(t, c.capacity, capacity)
}
}
}

View File

@ -0,0 +1,65 @@
/*
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 auth
import (
"net/http"
)
// Authenticator defines Authenticate function to authenticate requests
type Authenticator interface {
// Authenticate the request, if there is no error, the bool value
// determines whether the request is authenticated or not
Authenticate(req *http.Request) (bool, error)
}
type secretAuthenticator struct {
secrets map[string]string
}
// NewSecretAuthenticator returns an instance of secretAuthenticator
func NewSecretAuthenticator(secrets map[string]string) Authenticator {
return &secretAuthenticator{
secrets: secrets,
}
}
// Authenticate the request according the secret
func (s *secretAuthenticator) Authenticate(req *http.Request) (bool, error) {
if len(s.secrets) == 0 {
return true, nil
}
secret, err := req.Cookie("secret")
if err != nil {
if err == http.ErrNoCookie {
return false, nil
}
return false, err
}
if secret == nil {
return false, nil
}
for _, v := range s.secrets {
if secret.Value == v {
return true, nil
}
}
return false, nil
}

View File

@ -0,0 +1,57 @@
/*
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 auth
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuthenticate(t *testing.T) {
secret := "correct"
req1, err := http.NewRequest("", "", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req2, err := http.NewRequest("", "", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req2.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
cases := []struct {
secrets map[string]string
req *http.Request
result bool
}{
{nil, req1, true},
{map[string]string{"secret1": "incorrect"}, req2, false},
{map[string]string{"secret1": "incorrect", "secret2": secret}, req2, true},
}
for _, c := range cases {
authenticator := NewSecretAuthenticator(c.secrets)
authenticated, err := authenticator.Authenticate(c.req)
assert.Nil(t, err, "unexpected error")
assert.Equal(t, c.result, authenticated, "unexpected result")
}
}

View File

@ -0,0 +1,78 @@
/*
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 handlers
import (
"net/http"
"os"
gorilla_handlers "github.com/gorilla/handlers"
"github.com/vmware/harbor/src/adminserver/auth"
"github.com/vmware/harbor/src/common/utils/log"
)
// NewHandler returns a gorilla router which is wrapped by authenticate handler
// and logging handler
func NewHandler() http.Handler {
h := newRouter()
secrets := map[string]string{
"uiSecret": os.Getenv("UI_SECRET"),
"jobserviceSecret": os.Getenv("JOBSERVICE_SECRET"),
}
h = newAuthHandler(auth.NewSecretAuthenticator(secrets), h)
h = gorilla_handlers.LoggingHandler(os.Stdout, h)
return h
}
type authHandler struct {
authenticator auth.Authenticator
handler http.Handler
}
func newAuthHandler(authenticator auth.Authenticator, handler http.Handler) http.Handler {
return &authHandler{
authenticator: authenticator,
handler: handler,
}
}
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if a.authenticator == nil {
if a.handler != nil {
a.handler.ServeHTTP(w, r)
}
return
}
valid, err := a.authenticator.Authenticate(r)
if err != nil {
log.Errorf("failed to authenticate request: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
return
}
if !valid {
http.Error(w, http.StatusText(http.StatusUnauthorized),
http.StatusUnauthorized)
return
}
if a.handler != nil {
a.handler.ServeHTTP(w, r)
}
return
}

View File

@ -0,0 +1,73 @@
/*
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 handlers
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/adminserver/auth"
)
type fakeAuthenticator struct {
authenticated bool
err error
}
func (f *fakeAuthenticator) Authenticate(req *http.Request) (bool, error) {
return f.authenticated, f.err
}
type fakeHandler struct {
responseCode int
}
func (f *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(f.responseCode)
}
func TestNewAuthHandler(t *testing.T) {
cases := []struct {
authenticator auth.Authenticator
handler http.Handler
responseCode int
}{
{nil, nil, http.StatusOK},
{&fakeAuthenticator{
authenticated: false,
err: nil,
}, nil, http.StatusUnauthorized},
{&fakeAuthenticator{
authenticated: false,
err: errors.New("error"),
}, nil, http.StatusInternalServerError},
{&fakeAuthenticator{
authenticated: true,
err: nil,
}, &fakeHandler{http.StatusNotFound}, http.StatusNotFound},
}
for _, c := range cases {
handler := newAuthHandler(c.authenticator, c.handler)
w := httptest.NewRecorder()
handler.ServeHTTP(w, nil)
assert.Equal(t, c.responseCode, w.Code, "unexpected response code")
}
}

View File

@ -13,7 +13,7 @@
limitations under the License.
*/
package main
package handlers
import (
"net/http"
@ -22,9 +22,11 @@ import (
"github.com/vmware/harbor/src/adminserver/api"
)
func newHandler() http.Handler {
func newRouter() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/api/configurations", api.ListCfgs).Methods("GET")
r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT")
r.HandleFunc("/api/configurations/reset", api.ResetCfgs).Methods("POST")
r.HandleFunc("/api/systeminfo/capacity", api.Capacity).Methods("GET")
return r
}

View File

@ -19,7 +19,9 @@ import (
"net/http"
"os"
"github.com/vmware/harbor/src/adminserver/handlers"
syscfg "github.com/vmware/harbor/src/adminserver/systemcfg"
sysinfo "github.com/vmware/harbor/src/adminserver/systeminfo"
"github.com/vmware/harbor/src/common/utils/log"
)
@ -46,13 +48,15 @@ func main() {
}
log.Info("system initialization completed")
sysinfo.Init()
port := os.Getenv("PORT")
if len(port) == 0 {
port = "80"
}
server := &Server{
Port: port,
Handler: newHandler(),
Handler: handlers.NewHandler(),
}
if err := server.Serve(); err != nil {
log.Fatal(err)

View File

@ -175,6 +175,11 @@ func Init() (err error) {
//init key provider
initKeyProvider()
if os.Getenv("RESET") == "true" {
log.Info("RESET is set, resetting system configurations...")
return Reset()
}
cfg, err := GetSystemCfg()
if err != nil {
return err
@ -332,3 +337,16 @@ func decrypt(m map[string]interface{}, keys []string, secretKey string) error {
}
return nil
}
// Reset clears old system configurations and reloads them
// from environment variables
func Reset() error {
cfg := map[string]interface{}{}
if err := loadFromEnv(cfg, true); err != nil {
return err
}
//sync configurations into cfg store
log.Info("updating system configurations...")
return UpdateSystemCfg(cfg)
}

View File

@ -117,4 +117,21 @@ func TestSystemcfg(t *testing.T) {
cfg[comcfg.AUTHMode], comcfg.DBAuth)
return
}
if err = Reset(); err != nil {
t.Errorf("failed to reset system configurations: %v", err)
return
}
cfg, err = GetSystemCfg()
if err != nil {
t.Errorf("failed to get system configurations: %v", err)
return
}
if cfg[comcfg.AUTHMode] != comcfg.DBAuth {
t.Errorf("unexpected auth mode: %s != %s",
cfg[comcfg.AUTHMode], comcfg.DBAuth)
return
}
}

View File

@ -0,0 +1,35 @@
/*
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 imagestorage
// GlobalDriver is a global image storage driver
var GlobalDriver Driver
// Capacity holds information about capaticy of image storage
type Capacity struct {
// total size(byte)
Total uint64 `json:"total"`
// available size(byte)
Free uint64 `json:"free"`
}
// Driver defines methods that an image storage driver must implement
type Driver interface {
// Name returns a human-readable name of the driver
Name() string
// Cap returns the capacity of the image storage
Cap() (*Capacity, error)
}

View File

@ -0,0 +1,56 @@
/*
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 filesystem
import (
"syscall"
storage "github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
)
const (
driverName = "filesystem"
)
type driver struct {
path string
}
// NewDriver returns an instance of filesystem driver
func NewDriver(path string) storage.Driver {
return &driver{
path: path,
}
}
// Name returns a human-readable name of the fielsystem driver
func (d *driver) Name() string {
return driverName
}
// Cap returns the capacity of the filesystem storage
func (d *driver) Cap() (*storage.Capacity, error) {
var stat syscall.Statfs_t
err := syscall.Statfs(d.path, &stat)
if err != nil {
return nil, err
}
return &storage.Capacity{
Total: stat.Blocks * uint64(stat.Bsize),
Free: stat.Bavail * uint64(stat.Bsize),
}, nil
}

View File

@ -0,0 +1,35 @@
/*
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 filesystem
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestName(t *testing.T) {
path := "/tmp"
driver := NewDriver(path)
assert.Equal(t, driver.Name(), driverName, "unexpected driver name")
}
func TestCap(t *testing.T) {
path := "/tmp"
driver := NewDriver(path)
_, err := driver.Cap()
assert.Nil(t, err, "unexpected error")
}

View File

@ -0,0 +1,32 @@
/*
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 systeminfo
import (
"os"
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage/filesystem"
)
// Init image storage driver
func Init() {
path := os.Getenv("IMAGE_STORE_PATH")
if len(path) == 0 {
path = "/data"
}
imagestorage.GlobalDriver = filesystem.NewDriver(path)
}

View File

@ -136,6 +136,11 @@ func (m *Manager) Load() (map[string]interface{}, error) {
return c, nil
}
// Reset configurations
func (m *Manager) Reset() error {
return m.Loader.Reset()
}
func getCfgExpiration(m map[string]interface{}) (int, error) {
if m == nil {
return 0, fmt.Errorf("can not get cfg expiration as configurations are null")
@ -227,7 +232,7 @@ func (l *Loader) Load() ([]byte, error) {
return b, nil
}
// Upload configuratons to remote server
// Upload configurations to remote server
func (l *Loader) Upload(b []byte) error {
req, err := http.NewRequest("PUT", l.url+"/api/configurations", bytes.NewReader(b))
if err != nil {
@ -253,6 +258,33 @@ func (l *Loader) Upload(b []byte) error {
return nil
}
// Reset sends configurations resetting command to
// remote server
func (l *Loader) Reset() error {
req, err := http.NewRequest("POST", l.url+"/api/configurations/reset", nil)
if err != nil {
return err
}
req.AddCookie(&http.Cookie{
Name: "secret",
Value: l.secret,
})
resp, err := l.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
}
log.Debug("configurations resetted")
return nil
}
// Parser parses configurations
type Parser struct {
}

View File

@ -1 +1 @@
{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2027-02-26T20:58:40.741161013+08:00","keys":{"54c7a86e6f03a093c432c6f31d8cfcbc8637bb4ee9223de8a68971ddb9b53b35":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYgQ4QwWAHPDTlQvTSPyEDw0aAI9n9PY0hLtkgv2nbGo/mE5Da9gFX4o1wG8CNtzRWEf8RnHL1tpmmhQkRx5Byw=="}},"756dc9faa625646ff80e26a25e05e3df88254e9be68b92b68dbfea7b4697292e":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpVENDQVMrZ0F3SUJBZ0lRZFlWTnZQbGtqdHlCSjlaT3YxaDQ4VEFLQmdncWhrak9QUVFEQWpBck1Ta3cKSndZRFZRUURFeUF4TUM0eE1UY3VOQzR4TkRJdmJtOTBZWEo1TFdSbGJXOHZZblZ6ZVdKdmVEQWVGdzB4TnpBeQpNamd4TWpVNE16aGFGdzB5TnpBeU1qWXhNalU0TXpoYU1Dc3hLVEFuQmdOVkJBTVRJREV3TGpFeE55NDBMakUwCk1pOXViM1JoY25rdFpHVnRieTlpZFhONVltOTRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUKNktZRzdIeC90SHJPMUh6SkRuTSs0SmdyMFJBWWR3N0w5MVhMRTJHV0lCeUJjSTRXMktSQlMxUHY4RlQwd2V4Kwo1cHNvZGZtcTdObWFCYitUQU85ZWRhTTFNRE13RGdZRFZSMFBBUUgvQkFRREFnV2dNQk1HQTFVZEpRUU1NQW9HCkNDc0dBUVVGQndNRE1Bd0dBMVVkRXdFQi93UUNNQUF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnT25rNENWelgKU2dacFZjSy9wa01VTWFmOUpCeGRidHgvTkNxRWJpaHJUbEFDSVFEbytudkh6azF1SURLUlc5c01ZNG5zaUtxSAprcUR4UEhaRGlZVXE0UExoOHc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}},"8230fd1fbdf1d7de675cd93ec0a64685a63f0db50a65217555f321939efe59df":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDkmdEwFUhMC+NRy3TuchNprTD8HoRUE+X5RPxevxdl3qcWIFk+26GIYYMMTqFcsmDzaoGXqixdqcJA5WaTg79A=="}},"f45d9afcbb5afd810369f5c2ef84477b75502c22867e66e5f69465f18c6ae157":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESWYg9Ix/pC2lVu1WTBQ0obYVdT+P9Xh6qMkvD1YVv0t28vxoiKYobmVwAfOzdERfXTJM9jIbZwP94Q41HQB2Zg=="}}},"roles":{"root":{"keyids":["756dc9faa625646ff80e26a25e05e3df88254e9be68b92b68dbfea7b4697292e"],"threshold":1},"snapshot":{"keyids":["8230fd1fbdf1d7de675cd93ec0a64685a63f0db50a65217555f321939efe59df"],"threshold":1},"targets":{"keyids":["f45d9afcbb5afd810369f5c2ef84477b75502c22867e66e5f69465f18c6ae157"],"threshold":1},"timestamp":{"keyids":["54c7a86e6f03a093c432c6f31d8cfcbc8637bb4ee9223de8a68971ddb9b53b35"],"threshold":1}},"version":1},"signatures":[{"keyid":"756dc9faa625646ff80e26a25e05e3df88254e9be68b92b68dbfea7b4697292e","method":"ecdsa","sig":"b1V3xDWGp0YNFde9Hgx4yipiebzZedhBaVRJSfxKsjxRmFmvNty8hvTL1D7mURZkc7FJPcsN/o3xC9AlUz/isQ=="}]}
{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2027-03-13T19:40:40.104632513+08:00","keys":{"231efb776eb60d7a240a2b3deb3d458c32062cab05db69e62df838ac09214004":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpakNDQVRDZ0F3SUJBZ0lSQU5WNkpKblZLUHZtQ3NqVi9TcmFtVGt3Q2dZSUtvWkl6ajBFQXdJd0t6RXAKTUNjR0ExVUVBeE1nTVRBdU1URTNMalF1TVRReUwyNXZkR0Z5ZVMxa1pXMXZMMkoxYzNsaWIzZ3dIaGNOTVRjdwpNekUxTVRFME1ETTJXaGNOTWpjd016RXpNVEUwTURNMldqQXJNU2t3SndZRFZRUURFeUF4TUM0eE1UY3VOQzR4Ck5ESXZibTkwWVhKNUxXUmxiVzh2WW5WemVXSnZlREJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQkNIU0JDc0VXSjV2emVaY2NTYjJkM2FNZzBzY0JhclVGV3dxR3dTZTVEN1hQSktxTW9SYlRNeFRRbmZlc25DNAoyZkRGOGZhNHFWQVlQLytCbHRBbStseWpOVEF6TUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLCkJnZ3JCZ0VGQlFjREF6QU1CZ05WSFJNQkFmOEVBakFBTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUErWHZrblgKMEpxelQ0WDVqdEpFUG8xQjNpSEJRZUhMb043UnVXVE95SU85QWlFQXF6bWdmNDZKUFFkV05mOHZ1TEJlUmdJYQoxVHF6S25rV2dteVhFdmplS0ljPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}},"2e27b6898d94d3e134f1b223f8342a7f4ea18e95e51271b6d64eb7319a08b855":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf3dmn+ATN63G1xkiDPyshhISYmzsHrGXnWDDqkmqWVwLhqN8oV1IRSQLqMeHHxDB2MWSgPzo3zcRHzTJ699wTg=="}},"73c9c8cf155f7e4a4324ced1519461ceee185ce1405984f976ae3b8a4f332e43":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElK9qIW4lHiVDx3BO+UdwsIj2hOcf4kdaJcgp4+e0oHCJuJd06m/3rP55/1HpQWZ3hGbFL4jgJD/2nHKo0hFClg=="}},"bd2c62bff4fa172fb40605fb2d8658d2b9c09e655b8ebb38f30550f5bd717eea":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuAvewzw4oXj7aWBDFMyeFen0FxZO8+wxOkPElvY8l4ODIFSWHYToggerpLMKrPKN6SkNA+toDn7ZJd61nQHRDw=="}}},"roles":{"root":{"keyids":["231efb776eb60d7a240a2b3deb3d458c32062cab05db69e62df838ac09214004"],"threshold":1},"snapshot":{"keyids":["bd2c62bff4fa172fb40605fb2d8658d2b9c09e655b8ebb38f30550f5bd717eea"],"threshold":1},"targets":{"keyids":["2e27b6898d94d3e134f1b223f8342a7f4ea18e95e51271b6d64eb7319a08b855"],"threshold":1},"timestamp":{"keyids":["73c9c8cf155f7e4a4324ced1519461ceee185ce1405984f976ae3b8a4f332e43"],"threshold":1}},"version":1},"signatures":[{"keyid":"231efb776eb60d7a240a2b3deb3d458c32062cab05db69e62df838ac09214004","method":"ecdsa","sig":"hnIYijab4LoGbz1NqdyfGDkOOczhvdFtBcxIFGPovNuGDihiQuOUhFbKD2nVrxEA+t9ZZ+vKIm7LpqYE7ULXgA=="}]}

View File

@ -1 +0,0 @@
{"signed":{"_type":"Snapshot","expires":"2020-02-28T12:58:40.793595145Z","meta":{"root":{"hashes":{"sha256":"yy0hAbWa41HhHIVfmexYbqqzSc60ZB8Vz55BdPjEYxI=","sha512":"kT7I5pFqI35onn6tghIcwSR24jVgxQG/rF+Ct4eFJBpr7J8kMKL86vcD+pjbWkh/px6oPRm89+34i45woam89w=="},"length":2429},"targets":{"hashes":{"sha256":"iBYw3fp614v3WkMfhrf2gYLgsSAcS8bSuNHy3eQAKzA=","sha512":"ZC78eLxWK4o8PZoAkDOyxN8ggBDpLKXHKZUdu558B/lYVYFmXpdzKglw/87hE/jDXbfct1sh2EBmD68ESLjG1Q=="},"length":433}},"version":1},"signatures":[{"keyid":"8230fd1fbdf1d7de675cd93ec0a64685a63f0db50a65217555f321939efe59df","method":"ecdsa","sig":"/cx8YA5vwxRckZQjUQxQ+OghKEy1R2Ha8m1oHLtEfvqzKIKNyZvNo3I9AMKMDgukz85JDKRS7zFg88jkkghleA=="}]}

View File

@ -1 +0,0 @@
{"signed":{"_type":"Targets","delegations":{"keys":{},"roles":[]},"expires":"2020-02-28T20:58:40.770351325+08:00","targets":{"1.0":{"hashes":{"sha256":"E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="},"length":527}},"version":2},"signatures":[{"keyid":"f45d9afcbb5afd810369f5c2ef84477b75502c22867e66e5f69465f18c6ae157","method":"ecdsa","sig":"T070LEVEi5cdA1RRt0MOeYlxl+KEAyfa8uGkD0OJI/V9OQlh12aDDu7H6qhR5qk1LmQpwTHBOtdEnjZd2bkN6w=="}]}

View File

@ -1 +1 @@
{"signed":{"_type":"Timestamp","expires":"2017-03-14T12:58:40.833773016Z","meta":{"snapshot":{"hashes":{"sha256":"fPRwHL3qeGxQnGpBdKd7JcBoHMjSbQcUnpF0TTiq8io=","sha512":"vbJK5eX8iSIjWu2nZAHLzHLuETvz6kSzCOoYyn1C86BgieMHFpZmLgEj7AKuG9svBCYc17nii3B8ROfiLvNHaQ=="},"length":683}},"version":1},"signatures":[{"keyid":"54c7a86e6f03a093c432c6f31d8cfcbc8637bb4ee9223de8a68971ddb9b53b35","method":"ecdsa","sig":"fK6IF/jcigZ2mz5kqqb9Yma97zUOGB4OQqDfxQcAskW7DhpsKIWB1l+E7m0IPFBbIrL8q9l0GjlumCNVptmauw=="}]}
{"signed":{"_type":"Timestamp","expires":"2020-03-14T11:40:40.19936068Z","meta":{"snapshot":{"hashes":{"sha256":"YvH2+0LXsb5wx6D0thpEQnW724V8LQotNzpRGSryjUY=","sha512":"hTjtW7akumqZb0roV/n7k1LkaBnNPed5yTRlwBf9Y/QoSafgOxi2KDYYNLcuYOZwNT2raq9yilFZ80DNl0qjkw=="},"length":683}},"version":1},"signatures":[{"keyid":"73c9c8cf155f7e4a4324ced1519461ceee185ce1405984f976ae3b8a4f332e43","method":"ecdsa","sig":"nWv4Pyhjdmx0jEyIejjDqEAUQ5SlOGWUKs8xAnsEOO92DMZNa/GAYaRo9TT0kdI8rgrF+NHmCAGUXy/RRRXjdw=="}]}

View File

@ -22,7 +22,7 @@ import (
"strings"
"time"
au "github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/registry"
)
@ -40,7 +40,7 @@ type Authorizer interface {
type AuthorizerStore struct {
authorizers []Authorizer
ping *url.URL
challenges []au.Challenge
challenges []challenge.Challenge
}
// NewAuthorizerStore ...

View File

@ -21,7 +21,7 @@ import (
"strings"
"testing"
"github.com/docker/distribution/registry/client/auth"
ch "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/vmware/harbor/src/common/utils/test"
)
@ -61,7 +61,7 @@ func (s *simpleAuthorizer) Authorize(req *http.Request,
func TestModify(t *testing.T) {
authorizer := &simpleAuthorizer{}
challenge := auth.Challenge{
challenge := ch.Challenge{
Scheme: "bearer",
}
@ -72,7 +72,7 @@ func TestModify(t *testing.T) {
as := &AuthorizerStore{
authorizers: []Authorizer{authorizer},
ping: ping,
challenges: []auth.Challenge{challenge},
challenges: []ch.Challenge{challenge},
}
req, err := http.NewRequest("GET", "http://example.com/v2/ubuntu/manifests/14.04", nil)

View File

@ -18,12 +18,12 @@ package auth
import (
"net/http"
au "github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
)
// ParseChallengeFromResponse ...
func ParseChallengeFromResponse(resp *http.Response) []au.Challenge {
challenges := au.ResponseChallenges(resp)
func ParseChallengeFromResponse(resp *http.Response) []challenge.Challenge {
challenges := challenge.ResponseChallenges(resp)
return challenges
}

View File

@ -45,12 +45,17 @@ func TestUnMarshal(t *testing.T) {
}
refs := manifest.References()
if len(refs) != 1 {
t.Fatalf("unexpected length of reference: %d != %d", len(refs), 1)
if len(refs) != 2 {
t.Fatalf("unexpected length of reference: %d != %d", len(refs), 2)
}
digest := "sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c"
digest := "sha256:c54a2cc56cbb2f04003c1cd4507e118af7c0d340fe7e2720f70976c4b75237dc"
if refs[0].Digest.String() != digest {
t.Errorf("unexpected digest: %s != %s", refs[0].Digest.String(), digest)
}
digest = "sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c"
if refs[1].Digest.String() != digest {
t.Errorf("unexpected digest: %s != %s", refs[1].Digest.String(), digest)
}
}

View File

@ -93,5 +93,13 @@ func NewAdminserver(config map[string]interface{}) (*httptest.Server, error) {
}),
})
m = append(m, &RequestHandlerMapping{
Method: "POST",
Pattern: "/api/configurations/reset",
Handler: Handler(&Response{
StatusCode: http.StatusOK,
}),
})
return NewServer(m...), nil
}

View File

@ -323,12 +323,6 @@ func (m *ManifestPuller) enter() (string, error) {
blobs = append(blobs, discriptor.Digest.String())
}
// config is also need to be transferred if the schema of manifest is v2
manifest2, ok := manifest.(*schema2.DeserializedManifest)
if ok {
blobs = append(blobs, manifest2.Target().Digest.String())
}
m.logger.Infof("all blobs of %s:%s from %s: %v", name, tag, m.srcURL, blobs)
for _, blob := range blobs {

View File

@ -196,6 +196,14 @@ func (c *ConfigAPI) Put() {
}
}
// Reset system configurations
func (c *ConfigAPI) Reset() {
if err := config.Reset(); err != nil {
log.Errorf("failed to reset configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
func validateCfg(c map[string]string) (bool, error) {
isSysErr := false

View File

@ -68,3 +68,37 @@ func TestPutConfig(t *testing.T) {
return
}
}
func TestResetConfig(t *testing.T) {
fmt.Println("Testing resetting configurations")
assert := assert.New(t)
apiTest := newHarborAPI()
code, err := apiTest.ResetConfig(*admin)
if err != nil {
t.Errorf("failed to get configurations: %v", err)
return
}
if !assert.Equal(200, code, "unexpected response code") {
return
}
code, cfgs, err := apiTest.GetConfig(*admin)
if err != nil {
t.Errorf("failed to get configurations: %v", err)
return
}
if !assert.Equal(200, code, "unexpected response code") {
return
}
value, ok := cfgs[config.VerifyRemoteCert]
if !ok {
t.Errorf("%s not found", config.VerifyRemoteCert)
return
}
assert.Equal(value.Value.(bool), true, "unexpected value")
}

View File

@ -16,13 +16,11 @@
package api
import (
"fmt"
"net"
"net/http"
"strconv"
"github.com/vmware/harbor/src/common/api"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/email"
"github.com/vmware/harbor/src/common/utils/log"
@ -54,63 +52,60 @@ func (e *EmailAPI) Prepare() {
// Ping tests connection and authentication with email server
func (e *EmailAPI) Ping() {
m := map[string]string{}
e.DecodeJSONReq(&m)
settings, err := config.Email()
if err != nil {
e.CustomAbort(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
}
host, ok := m[comcfg.EmailHost]
if ok {
if len(host) == 0 {
e.CustomAbort(http.StatusBadRequest, "empty email server host")
var host, username, password, identity string
var port int
var ssl bool
body := e.Ctx.Input.CopyBody(1 << 32)
if body == nil || len(body) == 0 {
cfg, err := config.Email()
if err != nil {
log.Errorf("failed to get email configurations: %v", err)
e.CustomAbort(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
}
settings.Host = host
}
host = cfg.Host
port = cfg.Port
username = cfg.Username
password = cfg.Password
identity = cfg.Identity
ssl = cfg.SSL
} else {
settings := &struct {
Host string `json:"email_host"`
Port *int `json:"email_port"`
Username string `json:"email_username"`
Password *string `json:"email_password"`
SSL bool `json:"email_ssl"`
Identity string `json:"email_identity"`
}{}
e.DecodeJSONReq(&settings)
port, ok := m[comcfg.EmailPort]
if ok {
if len(port) == 0 {
e.CustomAbort(http.StatusBadRequest, "empty email server port")
if len(settings.Host) == 0 || settings.Port == nil {
e.CustomAbort(http.StatusBadRequest, "empty host or port")
}
p, err := strconv.Atoi(port)
if err != nil || p <= 0 {
e.CustomAbort(http.StatusBadRequest, "invalid email server port")
if settings.Password == nil {
cfg, err := config.Email()
if err != nil {
log.Errorf("failed to get email configurations: %v", err)
e.CustomAbort(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
}
settings.Password = &cfg.Password
}
settings.Port = p
host = settings.Host
port = *settings.Port
username = settings.Username
password = *settings.Password
identity = settings.Identity
ssl = settings.SSL
}
username, ok := m[comcfg.EmailUsername]
if ok {
settings.Username = username
}
password, ok := m[comcfg.EmailPassword]
if ok {
settings.Password = password
}
identity, ok := m[comcfg.EmailIdentity]
if ok {
settings.Identity = identity
}
ssl, ok := m[comcfg.EmailSSL]
if ok {
if ssl != "0" && ssl != "1" {
e.CustomAbort(http.StatusBadRequest,
fmt.Sprintf("%s should be 0 or 1", comcfg.EmailSSL))
}
settings.SSL = ssl == "1"
}
addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
if err := email.Ping(
addr, settings.Identity, settings.Username,
settings.Password, pingEmailTimeout, settings.SSL, false); err != nil {
addr := net.JoinHostPort(host, strconv.Itoa(port))
if err := email.Ping(addr, identity, username,
password, pingEmailTimeout, ssl, false); err != nil {
log.Debugf("ping %s failed: %v", addr, err)
e.CustomAbort(http.StatusBadRequest, err.Error())
}

View File

@ -21,7 +21,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
comcfg "github.com/vmware/harbor/src/common/config"
)
func TestPingEmail(t *testing.T) {
@ -38,17 +37,29 @@ func TestPingEmail(t *testing.T) {
assert.Equal(401, code, "the status code of ping email server with non-admin user should be 401")
settings := map[string]string{
comcfg.EmailHost: "smtp.gmail.com",
comcfg.EmailPort: "465",
comcfg.EmailIdentity: "",
comcfg.EmailUsername: "wrong_username",
comcfg.EmailPassword: "wrong_password",
comcfg.EmailSSL: "1",
//case 2: bad request
settings := `{
"email_host": ""
}`
code, _, err = apiTest.PingEmail(*admin, []byte(settings))
if err != nil {
t.Errorf("failed to test ping email server: %v", err)
return
}
//case 2: secure connection with admin role
code, body, err := apiTest.PingEmail(*admin, settings)
assert.Equal(400, code, "the status code of ping email server should be 400")
//case 3: secure connection with admin role
settings = `{
"email_host": "smtp.gmail.com",
"email_port": 465,
"email_identity": "",
"email_username": "wrong_username",
"email_ssl": true
}`
code, body, err := apiTest.PingEmail(*admin, []byte(settings))
if err != nil {
t.Errorf("failed to test ping email server: %v", err)
return
@ -60,4 +71,13 @@ func TestPingEmail(t *testing.T) {
t.Errorf("unexpected error: %s does not contains 535", body)
return
}
//case 4: ping email server whose settings are read from config
code, _, err = apiTest.PingEmail(*admin, nil)
if err != nil {
t.Errorf("failed to test ping email server: %v", err)
return
}
assert.Equal(400, code, "the status code of ping email server should be 400")
}

View File

@ -3,6 +3,7 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
@ -102,6 +103,7 @@ func init() {
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert")
beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping")
beego.Router("/api/configurations", &ConfigAPI{})
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
_ = updateInitPassword(1, "Harbor12345")
@ -1029,8 +1031,16 @@ func (a testapi) PutConfig(authInfo usrInfo, cfg map[string]string) (int, error)
return code, err
}
func (a testapi) PingEmail(authInfo usrInfo, settings map[string]string) (int, string, error) {
_sling := sling.New().Base(a.basePath).Post("/api/email/ping").BodyJSON(settings)
func (a testapi) ResetConfig(authInfo usrInfo) (int, error) {
_sling := sling.New().Base(a.basePath).Post("/api/configurations/reset")
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
return code, err
}
func (a testapi) PingEmail(authInfo usrInfo, settings []byte) (int, string, error) {
_sling := sling.New().Base(a.basePath).Post("/api/email/ping").Body(bytes.NewReader(settings))
code, body, err := request(_sling, jsonAcceptHeader, authInfo)

View File

@ -86,7 +86,9 @@ func (s *SearchAPI) Get() {
log.Errorf("failed to get user's project role: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
p.Role = roles[0].RoleID
if len(roles) != 0 {
p.Role = roles[0].RoleID
}
}
if p.Role == models.PROJECTADMIN {

View File

@ -44,6 +44,7 @@ type GeneralInfo struct {
RegistryURL string `json:"registry_url"`
ProjectCreationRestrict string `json:"project_creation_restriction"`
SelfRegistration bool `json:"self_registration"`
HasCARoot bool `json:"has_ca_root"`
}
// validate for validating user if an admin.
@ -88,13 +89,16 @@ func (sia *SystemInfoAPI) GetVolumeInfo() {
func (sia *SystemInfoAPI) GetCert() {
sia.validate()
if sia.isAdmin {
if _, err := os.Stat(defaultRootCert); !os.IsNotExist(err) {
if _, err := os.Stat(defaultRootCert); err == nil {
sia.Ctx.Output.Header("Content-Type", "application/octet-stream")
sia.Ctx.Output.Header("Content-Disposition", "attachment; filename=ca.crt")
http.ServeFile(sia.Ctx.ResponseWriter, sia.Ctx.Request, defaultRootCert)
} else {
} else if os.IsNotExist(err) {
log.Error("No certificate found.")
sia.CustomAbort(http.StatusNotFound, "No certificate found.")
} else {
log.Errorf("Unexpected error: %v", err)
sia.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
sia.CustomAbort(http.StatusForbidden, "")
@ -113,6 +117,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
} else {
registryURL = l[0]
}
_, caStatErr := os.Stat(defaultRootCert)
info := GeneralInfo{
AdmiralEndpoint: cfg[comcfg.AdmiralEndpoint].(string),
WithAdmiral: config.WithAdmiral(),
@ -121,6 +126,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
ProjectCreationRestrict: cfg[comcfg.ProjectCreationRestriction].(string),
SelfRegistration: cfg[comcfg.SelfRegistration].(bool),
RegistryURL: registryURL,
HasCARoot: caStatErr == nil,
}
sia.Data["json"] = info
sia.ServeJSON()

View File

@ -48,6 +48,7 @@ func TestGetGeneralInfo(t *testing.T) {
err = json.Unmarshal(body, g)
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(false, g.WithNotary, "with notary should be false")
assert.Equal(true, g.HasCARoot, "has ca root should be true")
}
func TestGetCert(t *testing.T) {

View File

@ -71,6 +71,11 @@ func Load() error {
return err
}
// Reset configurations
func Reset() error {
return mg.Reset()
}
// Upload uploads all system configutations to admin server
func Upload(cfg map[string]interface{}) error {
b, err := json.Marshal(cfg)

View File

@ -140,4 +140,17 @@ func TestConfig(t *testing.T) {
if extURL != "host01.com" {
t.Errorf(`extURL should be "host01.com".`)
}
// reset configurations
if err = Reset(); err != nil {
t.Errorf("failed to reset configurations: %v", err)
return
}
mode, err = AuthMode()
if err != nil {
t.Fatalf("failed to get auth mode: %v", err)
}
if mode != "db_auth" {
t.Errorf("unexpected mode: %s != %s", mode, "db_auth")
}
}

View File

@ -48,12 +48,6 @@ func init() {
beego.Router("/reset", &CommonController{}, "post:ResetPassword")
beego.Router("/userExists", &CommonController{}, "post:UserExists")
beego.Router("/sendEmail", &CommonController{}, "get:SendEmail")
beego.Router("/language", &CommonController{}, "get:SwitchLanguage")
beego.Router("/optional_menu", &OptionalMenuController{})
beego.Router("/navigation_header", &NavigationHeaderController{})
beego.Router("/navigation_detail", &NavigationDetailController{})
beego.Router("/sign_in", &SignInController{})
//Init user Info
//admin = &usrInfo{adminName, adminPwd}
@ -71,7 +65,11 @@ func TestMain(t *testing.T) {
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/' httpStatusCode should be 200")
<<<<<<< HEAD
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_index</title>"), "http respond should have '<title>page_title_index</title>'")
=======
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>Harbor</title>"), "http respond should have '<title>Harbor</title>'")
>>>>>>> upstream/dev
r, _ = http.NewRequest("POST", "/login", nil)
w = httptest.NewRecorder()

View File

@ -90,6 +90,7 @@ func initRouters() {
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/logs", &api.LogAPI{})
beego.Router("/api/configurations", &api.ConfigAPI{})
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")

View File

@ -1,67 +0,0 @@
{
"project": {
"version": "1.0.0-beta.20-4",
"name": "clarity-seed"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"images",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"test": "test.ts",
"tsconfig": "tsconfig.json",
"prefix": "app",
"mobile": false,
"styles": [
"../node_modules/clarity-icons/clarity-icons.min.css",
"../node_modules/clarity-ui/clarity-ui.min.css",
"styles.css"
],
"scripts": [
"../node_modules/core-js/client/shim.min.js",
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
"../node_modules/clarity-icons/clarity-icons.min.js",
"../node_modules/web-animations-js/web-animations.min.js"
],
"environments": {
"source": "environments/environment.ts",
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"addons": [],
"packages": [],
"e2e": {
"protractor": {
"config": "./protractor.config.js"
}
},
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"prefixInterfaces": false,
"inline": {
"style": false,
"template": false
},
"spec": {
"class": false,
"component": true,
"directive": true,
"module": false,
"pipe": true,
"service": true
}
}
}

BIN
src/ui_ng/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -4,7 +4,7 @@
"description": "Angular-CLI starter for a Clarity project",
"angular-cli": {},
"scripts": {
"start": "ng serve --host 0.0.0.0",
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.config.json",
"lint": "tslint \"src/**/*.ts\"",
"test": "ng test --single-run",
"pree2e": "webdriver-manager update",
@ -14,7 +14,7 @@
"dependencies": {
"@angular/common": "^2.4.1",
"@angular/compiler": "^2.4.1",
"@angular/core": "^2.4.1",
"@angular/core": "^2.4.9",
"@angular/forms": "^2.4.1",
"@angular/http": "^2.4.1",
"@angular/platform-browser": "^2.4.1",
@ -24,9 +24,9 @@
"@ngx-translate/http-loader": "0.0.3",
"@webcomponents/custom-elements": "1.0.0-alpha.3",
"angular2-cookie": "^1.2.6",
"clarity-angular": "^0.8.0",
"clarity-icons": "^0.8.0",
"clarity-ui": "^0.8.0",
"clarity-angular": "0.8.7",
"clarity-icons": "0.8.7",
"clarity-ui": "0.8.7",
"core-js": "^2.4.1",
"fs": "0.0.1-security",
"mutationobserver-shim": "^0.3.2",
@ -36,11 +36,12 @@
"zone.js": "^0.7.2"
},
"devDependencies": {
"@angular/cli": "^1.0.0-rc.1",
"@angular/compiler-cli": "^2.4.1",
"@types/core-js": "^0.9.34",
"@types/jasmine": "^2.2.30",
"@types/node": "^6.0.42",
"angular-cli": "^1.0.0-beta.24",
"@angular/cli": "^1.0.0-rc.2",
"bootstrap": "4.0.0-alpha.5",
"codelyzer": "~1.0.0-beta.3",
"enhanced-resolve": "^3.0.0",

View File

@ -1,36 +1,39 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop">
<h3 class="modal-title">{{'PROFILE.TITLE' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body" style="overflow-y: hidden;">
<form #accountSettingsFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="account_settings_username" class="col-md-4">{{'PROFILE.USER_NAME' | translate}}</label>
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="31">
<div class="form-group form-group-override">
<label for="account_settings_username" class="form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="33">
</div>
<div class="form-group">
<label for="account_settings_email" class="col-md-4 required">{{'PROFILE.EMAIL' | translate}}</label>
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
<div class="form-group form-group-override">
<label for="account_settings_email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("account_settings_email")'>
<input name="account_settings_email" type="text" #eamilInput="ngModel" [(ngModel)]="account.email"
required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="28">
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="30"
(input)='handleValidation("account_settings_email", false)'
(focusout)='handleValidation("account_settings_email", true)'>
<span class="tooltip-content">
{{'TOOLTIP.EMAIL' | translate}}
{{emailTooltip | translate}}
</span>
</label>
</label><span class="spinner spinner-inline" [hidden]="!checkProgress"></span>
</div>
<div class="form-group">
<label for="account_settings_full_name" class="col-md-4 required">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="account_settings_full_name" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="fullNameInput.invalid && (fullNameInput.dirty || fullNameInput.touched)">
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="28">
<div class="form-group form-group-override">
<label for="account_settings_full_name" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="account_settings_full_name" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("account_settings_full_name")'>
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="30" (input)='handleValidation("account_settings_full_name", false)' (focusout)='handleValidation("account_settings_full_name", true)'>
<span class="tooltip-content">
{{'TOOLTIP.FULL_NAME' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="account_settings_comments" class="col-md-4">{{'PROFILE.COMMENT' | translate}}</label>
<div class="form-group form-group-override">
<label for="account_settings_comments" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label>
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="28">
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="30">
<span class="tooltip-content">
{{'TOOLTIP.COMMENT' | translate}}
</span>
@ -38,7 +41,6 @@
</div>
</section>
</form>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>

View File

@ -10,7 +10,8 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
@Component({
selector: "account-settings-modal",
templateUrl: "account-settings-modal.component.html"
templateUrl: "account-settings-modal.component.html",
styleUrls: ['../../common.css']
})
export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
@ -19,9 +20,16 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
account: SessionUser;
error: any = null;
originalStaticData: SessionUser;
private emailTooltip: string = 'TOOLTIP.EMAIL';
private validationStateMap: any = {
"account_settings_email": true,
"account_settings_full_name": true
};
private mailAlreadyChecked = {};
private isOnCalling: boolean = false;
private formValueChanged: boolean = false;
private checkOnGoing: boolean = false;
accountFormRef: NgForm;
@ViewChild("accountSettingsFrom") accountForm: NgForm;
@ -37,6 +45,54 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.account = Object.assign({}, this.session.getCurrentUser());
}
private getValidationState(key: string): boolean {
return this.validationStateMap[key];
}
private handleValidation(key: string, flag: boolean): void {
if (flag) {
//Checking
let cont = this.accountForm.controls[key];
if (cont) {
this.validationStateMap[key] = cont.valid;
//Check email existing from backend
if (cont.valid && key === "account_settings_email") {
if (this.formValueChanged && this.account.email != this.originalStaticData.email) {
if (this.mailAlreadyChecked[this.account.email]) {
this.validationStateMap[key] = !this.mailAlreadyChecked[this.account.email].result;
if (!this.validationStateMap[key]) {
this.emailTooltip = "TOOLTIP.EMAIL_EXISTING";
}
return;
}
//Mail changed
this.checkOnGoing = true;
this.session.checkUserExisting("email", this.account.email)
.then((res: boolean) => {
this.checkOnGoing = false;
this.validationStateMap[key] = !res;
if (res) {
this.emailTooltip = "TOOLTIP.EMAIL_EXISTING";
}
this.mailAlreadyChecked[this.account.email] = {
result: res
}; //Tag it checked
})
.catch(error => {
this.checkOnGoing = false;
this.validationStateMap[key] = false;//Not valid @ backend
});
}
}
}
} else {
//Reset
this.validationStateMap[key] = true;
this.emailTooltip = "TOOLTIP.EMAIL";
}
}
private isUserDataChange(): boolean {
if (!this.originalStaticData || !this.account) {
return false;
@ -56,13 +112,20 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
public get isValid(): boolean {
return this.accountForm && this.accountForm.valid && this.error === null;
return this.accountForm &&
this.accountForm.valid &&
this.error === null &&
this.validationStateMap["account_settings_email"]; //backend check is valid as well
}
public get showProgress(): boolean {
return this.isOnCalling;
}
public get checkProgress(): boolean {
return this.checkOnGoing;
}
ngAfterViewChecked(): void {
if (this.accountFormRef != this.accountForm) {
this.accountFormRef = this.accountForm;
@ -118,15 +181,15 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.session.updateAccountSettings(this.account)
.then(() => {
this.isOnCalling = false;
this.close();
this.opened = false;
this.msgService.announceMessage(200, "PROFILE.SAVE_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.isOnCalling = false;
this.error = error;
if(accessErrorHandler(error, this.msgService)){
if (accessErrorHandler(error, this.msgService)) {
this.opened = false;
}else{
} else {
this.inlineAlert.showInlineError(error);
}
});

View File

@ -9,14 +9,17 @@ import { SharedModule } from '../shared/shared.module';
import { SignUpComponent } from './sign-up/sign-up.component';
import { ForgotPasswordComponent } from './password/forgot-password.component';
import { ResetPasswordComponent } from './password/reset-password.component';
import { SignUpPageComponent } from './sign-up/sign-up-page.component';
import { PasswordSettingService } from './password/password-setting.service';
import { RepositoryModule } from '../repository/repository.module';
@NgModule({
imports: [
CoreModule,
RouterModule,
SharedModule
SharedModule,
RepositoryModule
],
declarations: [
SignInComponent,
@ -24,12 +27,14 @@ import { PasswordSettingService } from './password/password-setting.service';
AccountSettingsModalComponent,
SignUpComponent,
ForgotPasswordComponent,
ResetPasswordComponent],
ResetPasswordComponent,
SignUpPageComponent],
exports: [
SignInComponent,
PasswordSettingComponent,
AccountSettingsModalComponent,
ResetPasswordComponent],
ResetPasswordComponent,
SignUpPageComponent],
providers: [PasswordSettingService]
})

View File

@ -1,17 +1,18 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'RESET_PWD.TITLE' | translate}}</h3>
<label class="modal-title reset-modal-title-override">{{'RESET_PWD.CAPTION' | translate}}</label>
<div class="modal-body" style="overflow-y: hidden;">
<inline-alert class="modal-title"></inline-alert>
<div class="modal-body" style="overflow-y: hidden; min-height: 130px;">
<form #forgotPasswordFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="reset_pwd_email" class="col-md-4 required">{{'RESET_PWD.EMAIL' | translate}}</label>
<div class="form-group form-group-override">
<label for="reset_pwd_email" class="required form-group-label-override">{{'RESET_PWD.EMAIL' | translate}}</label>
<label for="reset_pwd_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="validationState === false">
<input name="reset_pwd_email" type="text" #eamilInput="ngModel" [(ngModel)]="email"
required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'
id="reset_pwd_email"
size="36"
size="40"
(input)="handleValidation(true)"
(focusout)="handleValidation(false)">
<span class="tooltip-content">
@ -21,8 +22,6 @@
</div>
</section>
</form>
<inline-alert></inline-alert>
<div style="height: 30px;"></div>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>

View File

@ -8,7 +8,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
@Component({
selector: 'forgot-password',
templateUrl: "forgot-password.component.html",
styleUrls: ['password.component.css']
styleUrls: ['password.component.css', '../../common.css']
})
export class ForgotPasswordComponent {
opened: boolean = false;

View File

@ -1,51 +1,51 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'CHANGE_PWD.TITLE' | translate}}</h3>
<div class="modal-body" style="min-height: 250px; overflow-y: hidden;">
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body" style="overflow-y: hidden;">
<form #changepwdForm="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="oldPassword">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<div class="form-group form-group-override">
<label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)">
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}'
required
name="oldPassword"
[(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="25">
#oldPassInput="ngModel" size="30">
<span class="tooltip-content">
{{'TOOLTIP.CURRENT_PWD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="newPassword">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="newPassword"
[(ngModel)]="newPwd"
#newPassInput="ngModel" size="25">
#newPassInput="ngModel" size="30">
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<div class="form-group form-group-override">
<label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)">
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="reNewPassword"
[(ngModel)]="reNewPwd"
#reNewPassInput="ngModel" size="25">
#reNewPassInput="ngModel" size="30">
<span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}}
</span>
</label>
</div>
</section>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</form>
</div>
<div class="modal-footer">

View File

@ -11,7 +11,8 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
@Component({
selector: 'password-setting',
templateUrl: "password-setting.component.html"
templateUrl: "password-setting.component.html",
styleUrls: ['../../common.css']
})
export class PasswordSettingComponent implements AfterViewChecked {
opened: boolean = false;
@ -117,7 +118,7 @@ export class PasswordSettingComponent implements AfterViewChecked {
})
.then(() => {
this.onCalling = false;
this.close();
this.opened = false;
this.msgService.announceMessage(200, "CHANGE_PWD.SAVE_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import { Headers, Http, RequestOptions, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { PasswordSetting } from './password-setting';
@ -52,10 +52,18 @@ export class PasswordSettingService {
return Promise.reject("Invalid reset uuid or password");
}
return this.http.post(resetPasswordEndpoint, JSON.stringify({
"reset_uuid": uuid,
"password": newPassword
}), this.options)
let formHeaders = new Headers({
"Content-Type": 'application/x-www-form-urlencoded'
});
let formOptions: RequestOptions = new RequestOptions({
headers: formHeaders
});
let body: URLSearchParams = new URLSearchParams();
body.set("reset_uuid", uuid);
body.set("password", newPassword);
return this.http.post(resetPasswordEndpoint, body.toString(), formOptions)
.toPromise()
.then(response => response)
.catch(error => {

View File

@ -1,3 +1,7 @@
.reset-modal-title-override {
font-size: 14px !important;
}
.form-group-override {
padding-left: 130px !important;
}

View File

@ -5,7 +5,7 @@
<form #resetPwdForm="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="newPassword">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" class="form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword") === false'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
required
@ -22,7 +22,7 @@
</label>
</div>
<div class="form-group">
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" class="form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("reNewPassword") === false'>
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
required

View File

@ -11,7 +11,7 @@ import { MessageService } from '../../global-message/message.service';
@Component({
selector: 'reset-password',
templateUrl: "reset-password.component.html",
styleUrls: ['password.component.css']
styleUrls: ['password.component.css', '../../common.css']
})
export class ResetPasswordComponent implements OnInit{
opened: boolean = true;
@ -44,7 +44,9 @@ export class ResetPasswordComponent implements OnInit{
}
public getValidationState(key: string): boolean {
return this.validationState && this.validationState[key];
return this.validationState &&
this.validationState[key] &&
key === 'reNewPassword'?this.samePassword():true;
}
public open(): void {
@ -76,10 +78,12 @@ export class ResetPasswordComponent implements OnInit{
this.onGoing = true;
this.pwdService.resetPassword(this.resetUuid, this.password)
.then(() => {
this.onGoing = false;
this.resetOk = true;
this.inlineAlert.showInlineSuccess({message:'RESET_PWD.RESET_OK'});
})
.catch(error => {
this.onGoing = false;
if(accessErrorHandler(error, this.msgService)){
this.close();
}else{

View File

@ -12,4 +12,32 @@
font-size: 14px;
float: right;
top: -5px;
}
.popular-repo-wrapper {
background-color: white;
min-height: 100vh;
display: flex;
flex-grow: 1;
margin-top: 24px;
justify-content: center;
flex-direction: column;
height: auto;
position: relative;
margin-left: 1px;
}
.login-wrapper-override {
flex-wrap: wrap;
}
.repo-container {
width: 100%;
margin-top: -250px;
}
.more-info-link {
position: relative;
top: 100px;
left: 330px;
}

View File

@ -1,4 +1,4 @@
<div class="login-wrapper">
<div class="login-wrapper login-wrapper-override">
<form #signInForm="ngForm" class="login">
<label class="title">
VMware Harbor<span class="trademark">&#8482;</span>
@ -31,9 +31,15 @@
{{ 'SIGN_IN.INVALID_MSG' | translate }}
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
<a href="javascript:void(0)" class="signup" (click)="signUp()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
<div>
<a href="https://github.com/vmware/harbor" target="_blank" class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div>
</form>
<div id="pop_repo" class="popular-repo-wrapper">
<top-repo class="repo-container"></top-repo>
</div>
</div>
<sign-up #signupDialog></sign-up>>
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>
<forgot-password #forgotPwdDialog></forgot-password>

View File

@ -7,9 +7,13 @@ import { SessionService } from '../../shared/session.service';
import { SignInCredential } from '../../shared/sign-in-credential';
import { SignUpComponent } from '../sign-up/sign-up.component';
import { harborRootRoute } from '../../shared/shared.const';
import { CommonRoutes } from '../../shared/shared.const';
import { ForgotPasswordComponent } from '../password/forgot-password.component';
import { AppConfigService } from '../../app-config.service';
import { AppConfig } from '../../app-config';
import { User } from '../../user/user';
//Define status flags for signing in states
export const signInStatusNormal = 0;
export const signInStatusOnGoing = 1;
@ -23,6 +27,7 @@ export const signInStatusError = -1;
export class SignInComponent implements AfterViewChecked, OnInit {
private redirectUrl: string = "";
private appConfig: AppConfig = new AppConfig();
//Form reference
signInForm: NgForm;
@ViewChild('signInForm') currentForm: NgForm;
@ -41,13 +46,19 @@ export class SignInComponent implements AfterViewChecked, OnInit {
constructor(
private router: Router,
private session: SessionService,
private route: ActivatedRoute
private route: ActivatedRoute,
private appConfigService: AppConfigService
) { }
ngOnInit(): void {
this.appConfig = this.appConfigService.getConfig();
this.route.queryParams
.subscribe(params => {
this.redirectUrl = params["redirect_url"] || "";
let isSignUp = params["sign_up"] || "";
if (isSignUp != "") {
this.signUp();//Open sign up
}
});
}
@ -65,6 +76,12 @@ export class SignInComponent implements AfterViewChecked, OnInit {
return this.currentForm.form.valid;
}
//Whether show the 'sign up' link
public get selfSignUp(): boolean {
return this.appConfig.auth_mode === 'db_auth'
&& this.appConfig.self_registration;
}
//General error handler
private handleError(error) {
//Set error status
@ -89,6 +106,17 @@ export class SignInComponent implements AfterViewChecked, OnInit {
}
//Fill the new user info into the sign in form
private handleUserCreation(user: User): void {
if(user){
this.currentForm.setValue({
"login_username": user.username,
"login_password": user.password
});
}
}
//Implement interface
//Watch the view change only when view is in error state
ngAfterViewChecked() {
@ -123,8 +151,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
//Redirect to the right route
if (this.redirectUrl === "") {
//Routing to the default location
this.router.navigateByUrl(harborRootRoute);
}else{
this.router.navigateByUrl(CommonRoutes.HARBOR_DEFAULT);
} else {
this.router.navigateByUrl(this.redirectUrl);
}
})

View File

@ -0,0 +1,7 @@
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
<new-user-form isSelfRegistration="true" (valueChange)="formValueChange($event)"></new-user-form>
<div>
<span class="spinner spinner-inline" style="top:8px;" [hidden]="!inProgress"> </span>
<button type="button" class="btn btn-outline" [disabled]="!canBeCancelled" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">{{ 'BUTTON.SIGN_UP' | translate }}</button>
</div>

View File

@ -0,0 +1,97 @@
import { Component, Output, ViewChild, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
import { User } from '../../user/user';
import { UserService } from '../../user/user.service';
import { errorHandler } from '../../shared/shared.utils';
import { AlertType } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
@Component({
selector: 'sign-up-page',
templateUrl: "sign-up-page.component.html"
})
export class SignUpPageComponent implements OnInit {
private error: any;
private onGoing: boolean = false;
private formValueChanged: boolean = false;
constructor(
private userService: UserService,
private msgService: MessageService,
private router: Router) { }
@ViewChild(NewUserFormComponent)
private newUserForm: NewUserFormComponent;
private getNewUser(): User {
return this.newUserForm.getData();
}
public get inProgress(): boolean {
return this.onGoing;
}
public get isValid(): boolean {
return this.newUserForm.isValid && this.error == null;
}
public get canBeCancelled(): boolean {
return this.formValueChanged && this.newUserForm && !this.newUserForm.isEmpty();
}
ngOnInit(): void {
this.newUserForm.reset();//Reset form
this.formValueChanged = false;
}
formValueChange(flag: boolean): void {
if (flag) {
this.formValueChanged = true;
}
if (this.error != null) {
this.error = null;//clear error
}
}
cancel(): void {
if (this.newUserForm) {
this.newUserForm.reset();
}
}
//Create new user
create(): void {
//Double confirm everything is ok
//Form is valid
if (!this.isValid) {
return;
}
//We have new user data
let u = this.getNewUser();
if (!u) {
return;
}
//Start process
this.onGoing = true;
this.userService.addUser(u)
.then(() => {
this.onGoing = false;
this.msgService.announceMessage(200, "", AlertType.SUCCESS);
//Navigate to embeded sign-in
this.router.navigate(['harbor', 'sign-in']);
})
.catch(error => {
this.onGoing = false;
this.error = error
this.msgService.announceMessage(error.status | 500, "", AlertType.WARNING);
});
}
}

View File

@ -1,8 +1,8 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="true">
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body" style="overflow-y: hidden;">
<new-user-form isSelfRegistration="true" (valueChange)="formValueChange($event)"></new-user-form>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="inProgress === false"> </span>

View File

@ -1,4 +1,4 @@
import { Component, Output, ViewChild } from '@angular/core';
import { Component, Output, ViewChild, EventEmitter } from '@angular/core';
import { NgForm } from '@angular/forms';
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
@ -9,6 +9,8 @@ import { UserService } from '../../user/user.service';
import { errorHandler } from '../../shared/shared.utils';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
import { Modal } from 'clarity-angular';
@Component({
selector: 'sign-up',
templateUrl: "sign-up.component.html"
@ -20,6 +22,8 @@ export class SignUpComponent {
private onGoing: boolean = false;
private formValueChanged: boolean = false;
@Output() userCreation = new EventEmitter<User>();
constructor(
private session: SessionService,
private userService: UserService) { }
@ -30,6 +34,9 @@ export class SignUpComponent {
@ViewChild(InlineAlertComponent)
private inlienAlert: InlineAlertComponent;
@ViewChild(Modal)
private modal: Modal;
private getNewUser(): User {
return this.newUserForm.getData();
}
@ -55,7 +62,7 @@ export class SignUpComponent {
open(): void {
this.newUserForm.reset();//Reset form
this.formValueChanged = false;
this.opened = true;
this.modal.open();
}
close(): void {
@ -74,7 +81,7 @@ export class SignUpComponent {
}
confirmCancel(): void {
this.opened = false;
this.modal.close();
}
//Create new user
@ -97,7 +104,8 @@ export class SignUpComponent {
this.userService.addUser(u)
.then(() => {
this.onGoing = false;
this.close();
this.modal.close();
this.userCreation.emit(u);
})
.catch(error => {
this.onGoing = false;

View File

@ -0,0 +1,83 @@
import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { AppConfig } from './app-config';
import { CookieService } from 'angular2-cookie/core';
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils';
export const systemInfoEndpoint = "/api/systeminfo";
/**
* Declare service to handle the bootstrap options
*
*
* @export
* @class GlobalSearchService
*/
@Injectable()
export class AppConfigService {
private headers = new Headers({
"Content-Type": 'application/json'
});
private options = new RequestOptions({
headers: this.headers
});
//Store the application configuration
private configurations: AppConfig = new AppConfig();
constructor(
private http: Http,
private cookie: CookieService) { }
public load(): Promise<AppConfig> {
return this.http.get(systemInfoEndpoint, this.options).toPromise()
.then(response => {
this.configurations = response.json() as AppConfig;
//Read admiral endpoint from cookie if existing
let admiralUrlFromCookie: string = this.cookie.get(CookieKeyOfAdmiral);
if(admiralUrlFromCookie){
//Override the endpoint from configuration file
this.configurations.admiral_endpoint = decodeURIComponent(admiralUrlFromCookie);
}
return this.configurations;
})
.catch(error => {
//Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
}
public getConfig(): AppConfig {
return this.configurations;
}
public isIntegrationMode(): boolean {
return this.configurations &&
this.configurations.with_admiral &&
this.configurations.admiral_endpoint.trim() != "";
}
//Return the reconstructed admiral url
public getAdmiralEndpoint(currentHref: string): string {
let admiralUrl:string = this.configurations.admiral_endpoint;
if(admiralUrl.trim() === "" || currentHref.trim() === ""){
return "#";
}
return maintainUrlQueryParmas(admiralUrl, HarborQueryParamKey, encodeURIComponent(currentHref));
}
public saveAdmiralEndpoint(endpoint: string): void {
if(!(endpoint.trim())){
return;
}
//Save back to cookie
this.cookie.put(CookieKeyOfAdmiral, endpoint);
this.configurations.admiral_endpoint = endpoint;
}
}

View File

@ -0,0 +1,20 @@
export class AppConfig {
constructor(){
//Set default value
this.with_notary = false;
this.with_admiral = false;
this.admiral_endpoint = "";
this.auth_mode = "db_auth";
this.registry_url = "";
this.project_creation_restriction = "everyone";
this.self_registration = true;
}
with_notary: boolean;
with_admiral: boolean;
admiral_endpoint: string;
auth_mode: string;
registry_url: string;
project_creation_restriction: string;
self_registration: boolean;
}

View File

@ -16,17 +16,14 @@ import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http';
import { SessionService } from './shared/session.service';
import { AppConfigService } from './app-config.service';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
}
export function initConfig(session: SessionService) {
return () => {
console.info("app init here");
return Promise.resolve(true);
};
export function initConfig(configService: AppConfigService) {
return () => configService.load();
}
@NgModule({
@ -51,10 +48,12 @@ export function initConfig(session: SessionService) {
}
})
],
providers: [{
providers: [
AppConfigService,
{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [SessionService],
deps: [AppConfigService],
multi: true
}],
bootstrap: [AppComponent]

View File

@ -5,7 +5,6 @@ import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { SearchTriggerService } from './search-trigger.service';
import { harborRootRoute } from '../../shared/shared.const';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@ -35,7 +34,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.searchSub = this.searchTerms
.debounceTime(deBounceTime)
.distinctUntilChanged()
//.distinctUntilChanged()
.subscribe(term => {
this.searchTrigger.triggerSearch(term);
});

View File

@ -1,14 +1,16 @@
.search-overlay {
display: block;
position: absolute;
position: fixed;
height: 100%;
width: 98%;
width: 100%;
/*shoud be lesser than 1000 to aoivd override the popup menu*/
z-index: 999;
box-sizing: border-box;
background: #fafafa;
top: 0px;
padding-left: 24px;
top: 60px;
left: 0px;
padding-left: 36px;
padding-right: 24px;
}
.search-header {
@ -47,4 +49,8 @@
position: relative;
top: 8px;
margin: 0px auto 0px auto;
}
.search-header a:hover {
text-decoration: none;
}

View File

@ -1,20 +1,14 @@
<div class="search-overlay" *ngIf="state">
<div id="placeholder1" style="height: 24px;"></div>
<div class="search-header">
<span class="search-title">Search results for '{{currentTerm}}'</span>
<span class="search-close" (mouseover)="mouseAction(true)" (mouseout)="mouseAction(false)">
<clr-icon shape="close" [class.is-highlight]="hover" size="36" (click)="close()"></clr-icon>
</span>
<a href="javascript:void(0)" (click)="close()">&lt;&nbsp;{{'SEARCH.BACK' | translate}}</a>
</div>
<!-- spinner -->
<div class="spinner spinner-lg search-spinner" [hidden]="done">Search...</div>
<div class="spinner spinner-lg search-spinner" [hidden]="done">{{'SEARCH.IN_PROGRESS' | translate}}</div>
<div id="results">
<h2>Projects</h2>
<div class="grid-header-wrapper">
<grid-filter class="grid-filter" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilterProjects($event)"></grid-filter>
</div>
<h2>{{'PROJECT.PROJECTS' | translate}}</h2>
<list-project [projects]="searchResults.project" [mode]="listMode"></list-project>
<h2>Repositories</h2>
<h2>{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</h2>
<list-repository [repositories]="searchResults.repository" [mode]="listMode"></list-repository>
</div>
</div>

View File

@ -35,10 +35,6 @@ export class SearchResultComponent {
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
private doFilterProjects(event: string) {
this.searchResults.project = this.originalCopy.project.filter(pro => pro.name.indexOf(event) != -1);
}
private clone(src: SearchResults): SearchResults {
let res: SearchResults = new SearchResults();

View File

@ -7,7 +7,6 @@
}
.start-content-padding {
padding-top: 0px !important;
padding-bottom: 0px !important;
padding-left: 0px !important;
padding: 0px !important;
background-color: white;
}

View File

@ -2,7 +2,7 @@
<global-message [isAppLevel]="true"></global-message>
<navigator (showAccountSettingsModal)="openModal($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<div class="content-container">
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="isStartPage">
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="shouldOverrideContent">
<global-message [isAppLevel]="false"></global-message>
<!-- Only appear when searching -->
<search-result></search-result>
@ -10,10 +10,9 @@
</div>
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>
<section class="sidenav-content">
<a routerLink="/harbor/dashboard" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.DASHBOARD' | translate}}</a>
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.PROJECTS' | translate}}</a>
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.LOGS' | translate}}</a>
<section class="nav-group collapsible" *ngIf="isSystemAdmin">
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link" style="margin-top: 4px;">{{'SIDE_NAV.LOGS' | translate}}</a>
<section class="nav-group collapsible" *ngIf="isSystemAdmin" style="margin-top: 4px;">
<input id="tabsystem" type="checkbox">
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
<ul class="nav-list">
@ -28,5 +27,5 @@
</clr-main-container>
<account-settings-modal></account-settings-modal>
<password-setting></password-setting>
<deletion-dialog></deletion-dialog>
<confiramtion-dialog></confiramtion-dialog>
<about-dialog></about-dialog>

View File

@ -17,7 +17,7 @@ import { SearchTriggerService } from '../global-search/search-trigger.service';
import { Subscription } from 'rxjs/Subscription';
import { harborRootRoute } from '../../shared/shared.const';
import { CommonRoutes } from '../../shared/shared.const';
@Component({
selector: 'harbor-shell',
@ -82,8 +82,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
}
}
public get isStartPage(): boolean {
return this.router.routerState.snapshot.url.toString() === harborRootRoute;
public get shouldOverrideContent(): boolean {
return this.router.routerState.snapshot.url.toString().startsWith(CommonRoutes.EMBEDDED_SIGN_IN);
}
public get showSearch(): boolean {

View File

@ -17,4 +17,15 @@
.lang-selected {
font-weight: bold;
}
.nav-divider {
display: inline-block;
width: 1px;
height: 40px;
background-color: #fafafa;
position: relative;
top: 10px;
opacity: 0.15;
content: '';
}

View File

@ -5,12 +5,16 @@
<span class="title">Harbor</span>
</a>
</div>
<div class="header-nav">
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a>
<a href="javascript:void(0)" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
</div>
<global-search></global-search>
<div class="header-actions">
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
<button class="nav-icon" clrDropdownToggle style="width: 98px;">
<clr-icon shape="world" style="left:-8px;"></clr-icon>
<span>{{currentLang}}</span>
<span style="padding-right: 8px;">{{currentLang}}</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">

View File

@ -1,5 +1,5 @@
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Component, Output, EventEmitter, OnInit, Inject } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ModalEvent } from '../modal-event';
@ -9,7 +9,9 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames } from '../../shared/shared.const';
import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service';
@Component({
selector: 'navigator',
@ -22,17 +24,16 @@ export class NavigatorComponent implements OnInit {
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
private sessionUser: SessionUser = null;
private selectedLang: string = enLang;
constructor(
private session: SessionService,
private router: Router,
private translate: TranslateService,
private cookie: CookieService) { }
private cookie: CookieService,
private appConfigService: AppConfigService) { }
ngOnInit(): void {
this.sessionUser = this.session.getCurrentUser();
this.selectedLang = this.translate.currentLang;
this.translate.onLangChange.subscribe(langChange => {
this.selectedLang = langChange.lang;
@ -42,17 +43,25 @@ export class NavigatorComponent implements OnInit {
}
public get isSessionValid(): boolean {
return this.sessionUser != null;
return this.session.getCurrentUser() != null;
}
public get accountName(): string {
return this.sessionUser ? this.sessionUser.username : "";
return this.session.getCurrentUser() ? this.session.getCurrentUser().username : "N/A";
}
public get currentLang(): string {
return languageNames[this.selectedLang];
}
public get admiralLink(): string {
return this.appConfigService.getAdmiralEndpoint(window.location.href);
}
public get isIntegrationMode(): boolean {
return this.appConfigService.isIntegrationMode();
}
matchLang(lang: string): boolean {
return lang.trim() === this.selectedLang;
}
@ -85,21 +94,22 @@ export class NavigatorComponent implements OnInit {
logOut(): void {
this.session.signOff()
.then(() => {
this.sessionUser = null;
//Naviagte to the sign in route
this.router.navigate(["/sign-in"]);
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
})
.catch()//TODO:
.catch(error => {
console.error("Log out with error: ", error);
});
}
//Switch languages
switchLanguage(lang: string): void {
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())){
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())) {
this.translate.use(lang);
}else{
} else {
this.translate.use(enLang);//Use default
//TODO:
console.error('Language '+lang.trim()+' is not suppoted');
console.error('Language ' + lang.trim() + ' is not suppoted');
}
//Try to switch backend lang
//this.session.switchLanguage(lang).catch(error => console.error(error));
@ -107,12 +117,12 @@ export class NavigatorComponent implements OnInit {
//Handle the home action
homeAction(): void {
if(this.sessionUser != null){
if (this.session.getCurrentUser() != null) {
//Navigate to default page
this.router.navigate(['harbor']);
}else{
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
} else {
//Naviagte to signin page
this.router.navigate(['sign-in']);
this.router.navigate([CommonRoutes.HARBOR_ROOT]);
}
}
}

View File

@ -0,0 +1,8 @@
.form-group-override {
padding-left: 170px !important;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}

View File

@ -3,10 +3,9 @@
<div class="form-group">
<label for="authMode">{{'CONFIG.AUTH_MODE' | translate }}</label>
<div class="select">
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)
" [(ngModel)]="currentConfig.auth_mode.value">
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)" [(ngModel)]="currentConfig.auth_mode.value">
<option value="db_auth">{{'CONFIG.AUTH_MODE_DB' | translate }}</option>
<option value="ldap">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
<option value="ldap_auth">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">

View File

@ -20,7 +20,7 @@ export class ConfigurationAuthComponent {
public get showLdap(): boolean {
return this.currentConfig &&
this.currentConfig.auth_mode &&
this.currentConfig.auth_mode.value === 'ldap';
this.currentConfig.auth_mode.value === 'ldap_auth';
}
private disabled(prop: any): boolean {

View File

@ -0,0 +1,7 @@
.custom-h2 {
margin-top: 0px !important;
}
.config-container {
margin-left: 24px;
}

View File

@ -1,38 +1,39 @@
<h1 style="display: inline-block;">{{'CONFIG.TITLE' | translate }}</h1>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<div class="config-container">
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'">
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'">
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'">
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'">
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'">
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'">
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
required
pattern="^[1-9]{1}[\d]*$"
@ -42,17 +43,20 @@
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
</div>
</div>

View File

@ -5,16 +5,18 @@ import { NgForm } from '@angular/forms';
import { ConfigurationService } from './config.service';
import { Configuration } from './config';
import { MessageService } from '../global-message/message.service';
import { AlertType, DeletionTargets } from '../shared/shared.const';
import { AlertType, ConfirmationTargets, ConfirmationState } from '../shared/shared.const';
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
import { StringValueItem } from './config';
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { Subscription } from 'rxjs/Subscription';
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message'
import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
import { AppConfigService } from '../app-config.service';
const fakePass = "fakepassword";
@Component({
@ -28,6 +30,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
private currentTabId: string = "";
private originalCopy: Configuration;
private confirmSub: Subscription;
private testingOnGoing: boolean = false;
@ViewChild("repoConfigFrom") repoConfigForm: NgForm;
@ViewChild("systemConfigFrom") systemConfigForm: NgForm;
@ -37,14 +40,19 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
constructor(
private msgService: MessageService,
private configService: ConfigurationService,
private confirmService: DeletionDialogService) { }
private confirmService: ConfirmationDialogService,
private appConfigService: AppConfigService) { }
ngOnInit(): void {
//First load
this.retrieveConfig();
this.confirmSub = this.confirmService.deletionConfirm$.subscribe(confirmation => {
this.reset(confirmation.data);
this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
if (confirmation &&
confirmation.state === ConfirmationState.CONFIRMED &&
confirmation.source === ConfirmationTargets.CONFIG) {
this.reset(confirmation.data);
}
});
}
@ -58,6 +66,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.onGoing;
}
public get testingInProgress(): boolean {
return this.testingOnGoing;
}
public isValid(): boolean {
return this.repoConfigForm &&
this.repoConfigForm.valid &&
@ -82,6 +94,16 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.currentTabId === 'config-email';
}
public get showLdapServerBtn(): boolean {
return this.currentTabId === 'config-auth' &&
this.allConfig.auth_mode &&
this.allConfig.auth_mode.value === "ldap_auth";
}
public isLDAPConfigValid(): boolean {
return this.authConfig && this.authConfig.isValid();
}
public tabLinkChanged(tabLink: any) {
this.currentTabId = tabLink.id;
}
@ -105,6 +127,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
//or force refresh by calling service.
//HERE we choose force way
this.retrieveConfig();
//Reload bootstrap option
this.appConfigService.load().catch(error => console.error("Failed to reload bootstrap option with error: ", error));
this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
@ -128,12 +154,12 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
public cancel(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
let msg = new DeletionMessage(
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
DeletionTargets.EMPTY
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
} else {
@ -150,7 +176,46 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
* @memberOf ConfigurationComponent
*/
public testMailServer(): void {
let mailSettings = {};
let allChanges = this.getChanges();
for (let prop in allChanges) {
if (prop.startsWith("email_")) {
mailSettings[prop] = allChanges[prop];
}
}
this.testingOnGoing = true;
this.configService.testMailServer(mailSettings)
.then(response => {
this.testingOnGoing = false;
this.msgService.announceMessage(200, "CONFIG.TEST_MAIL_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.testingOnGoing = false;
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
});
}
public testLDAPServer(): void {
let ldapSettings = {};
let allChanges = this.getChanges();
for (let prop in allChanges) {
if (prop.startsWith("ldap_")) {
ldapSettings[prop] = allChanges[prop];
}
}
console.info(ldapSettings);
this.testingOnGoing = true;
this.configService.testLDAPServer(ldapSettings)
.then(respone => {
this.testingOnGoing = false;
this.msgService.announceMessage(200, "CONFIG.TEST_LDAP_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.testingOnGoing = false;
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
});
}
private retrieveConfig(): void {

View File

@ -5,6 +5,8 @@ import 'rxjs/add/operator/toPromise';
import { Configuration } from './config';
const configEndpoint = "/api/configurations";
const emailEndpoint = "/api/email/ping";
const ldapEndpoint = "/api/ldap/ping";
@Injectable()
export class ConfigurationService {
@ -30,4 +32,18 @@ export class ConfigurationService {
.then(response => response)
.catch(error => Promise.reject(error));
}
public testMailServer(mailSettings: any): Promise<any> {
return this.http.post(emailEndpoint, JSON.stringify(mailSettings), this.options)
.toPromise()
.then(response => response)
.catch(error => Promise.reject(error));
}
public testLDAPServer(ldapSettings: any): Promise<any> {
return this.http.post(ldapEndpoint, JSON.stringify(ldapSettings), this.options)
.toPromise()
.then(response => response)
.catch(error => Promise.reject(error));
}
}

View File

@ -29,7 +29,6 @@
<label for="emailUsername">{{'CONFIG.MAIL_USERNAME' | translate}}</label>
<label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value"
required
id="emailUsername"
size="40" [disabled]="disabled(currentConfig.email_username)">
<span class="tooltip-content">
@ -41,7 +40,6 @@
<label for="emailPassword">{{'CONFIG.MAIL_PASSWORD' | translate}}</label>
<label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value"
required
id="emailPassword"
size="40" [disabled]="disabled(currentConfig.email_password)">
<span class="tooltip-content">

View File

@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Message } from './message';
import { MessageService } from './message.service';
import { AlertType, dismissInterval, httpStatusCode } from '../shared/shared.const';
import { AlertType, dismissInterval, httpStatusCode, CommonRoutes } from '../shared/shared.const';
@Component({
selector: 'global-message',
@ -94,7 +94,7 @@ export class MessageComponent implements OnInit {
}
signIn(): void {
this.router.navigate(['sign-in']);
this.router.navigateByUrl(CommonRoutes.EMBEDDED_SIGN_IN);
}
onClose() {

View File

@ -19,7 +19,6 @@ import { ReplicationComponent } from './replication/replication.component';
import { MemberComponent } from './project/member/member.component';
import { AuditLogComponent } from './log/audit-log.component';
import { BaseRoutingResolver } from './shared/route/base-routing-resolver.service';
import { ProjectRoutingResolver } from './project/project-routing-resolver.service';
import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { SignUpComponent } from './account/sign-up/sign-up.component';
@ -28,25 +27,22 @@ import { RecentLogComponent } from './log/recent-log.component';
import { ConfigurationComponent } from './config/config.component';
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
import { StartPageComponent } from './base/start-page/start.component';
import { SignUpPageComponent } from './account/sign-up/sign-up-page.component';
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deactivate.service';
const harborRoutes: Routes = [
{ path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
{ path: 'sign-up', component: SignUpComponent},
{ path: 'reset_password', component: ResetPasswordComponent},
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'password-reset', component: ResetPasswordComponent },
{
path: 'harbor',
component: HarborShellComponent,
canActivateChild: [AuthCheckGuard],
children: [
{
path: 'dashboard',
component: StartPageComponent
},
{ path: '', redirectTo: 'sign-in', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
{
path: 'projects',
component: ProjectComponent
@ -109,10 +105,11 @@ const harborRoutes: Routes = [
path: 'configs',
component: ConfigurationComponent,
canActivate: [SystemAdminGuard],
canDeactivate: [LeavingConfigRouteDeactivate]
}
]
},
{ path: "**", component: PageNotFoundComponent}
{ path: "**", component: PageNotFoundComponent }
];
@NgModule({

View File

@ -0,0 +1,5 @@
.option-right {
padding-right: 16px;
margin-top: 22px;
margin-bottom: 2px;
}

View File

@ -1,18 +1,16 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right">
<div class="row flex-items-xs-right option-right">
<div class="flex-xs-middle">
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
</div>
<div class="flex-xs-middle">
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<div class="row flex-items-xs-right" [hidden]="currentOption === 0">
<div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
<button class="btn btn-link" clrDropdownToggle>
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
{{'AUDIT_LOG.OPERATIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
@ -24,6 +22,8 @@
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top ">
<clr-datagrid (clrDgRefresh)="retrieve($event)">
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>

View File

@ -30,9 +30,10 @@ class FilterOption {
}
@Component({
moduleId: module.id,
selector: 'audit-log',
templateUrl: './audit-log.component.html',
styleUrls: [ 'audit-log.css' ]
styleUrls: [ './audit-log.component.css' ]
})
export class AuditLogComponent implements OnInit {
@ -53,7 +54,7 @@ export class AuditLogComponent implements OnInit {
];
pageOffset: number = 1;
pageSize: number = 2;
pageSize: number = 15;
totalRecordCount: number;
totalPage: number;

Some files were not shown because too many files have changed in this diff Show More