merge with master latest

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
wang yan 2019-04-17 17:52:36 +08:00
commit e017294f71
101 changed files with 1872 additions and 940 deletions

View File

@ -98,7 +98,7 @@ VERSIONFILENAME=UIVERSION
PREPARE_VERSION_NAME=versions PREPARE_VERSION_NAME=versions
#versions #versions
REGISTRYVERSION=v2.7.1 REGISTRYVERSION=v2.7.1-patch-2819
NGINXVERSION=$(VERSIONTAG) NGINXVERSION=$(VERSIONTAG)
NOTARYVERSION=v0.6.1 NOTARYVERSION=v0.6.1
CLAIRVERSION=v2.0.7 CLAIRVERSION=v2.0.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -4899,7 +4899,7 @@ definitions:
RobotAccountUpdate: RobotAccountUpdate:
type: object type: object
properties: properties:
disable: disabled:
type: boolean type: boolean
description: The robot account is disable or enable description: The robot account is disable or enable
Permission: Permission:

View File

@ -29,6 +29,7 @@ This guide walks you through the fundamentals of using Harbor. You'll learn how
* [Working with Helm CLI](#working-with-helm-cli) * [Working with Helm CLI](#working-with-helm-cli)
* [Online Garbage Collection.](#online-garbage-collection) * [Online Garbage Collection.](#online-garbage-collection)
* [View build history.](#build-history) * [View build history.](#build-history)
* [Manage robot account of a project.](#robot-account)
## Role Based Access Control(RBAC) ## Role Based Access Control(RBAC)
@ -597,3 +598,42 @@ Build history make it easy to see the contents of a container image, find the co
In Harbor portal, enter your project, select the repository, click on the link of tag name you'd like to see its build history, the detail page will be opened. Then switch to `Build History` tab, you can see the build history information. In Harbor portal, enter your project, select the repository, click on the link of tag name you'd like to see its build history, the detail page will be opened. Then switch to `Build History` tab, you can see the build history information.
![build_ history](img/build_history.png) ![build_ history](img/build_history.png)
## Robot Account
Robot Accounts are accounts created by project admins that are intended for automated operations. They have the following limitations:
1, Robot Accounts cannot login Harbor portal
2, Robot Accounts can only perform `docker push`/`docker pull` operations with a token.
### Add a Robot Account
If you are a project admin, you can create a Robot Account by clicking "New Robot Account" in the `Robot Accounts` tab of a project, and enter a name, a description and permission.
![add_robot_account](img/robotaccount/add_robot_account.png)
![add_robot_account](img/robotaccount/add_robot_account_2.png)
> **NOTE:** The name will become `robot$<accountname>` and will be used to distinguish a robot account from a normal harbor user.
![copy_robot_account_token](img/robotaccount/copy_robot_account_token.png)
As Harbor doesn't store your account token, please make sure to copy it in the pop up dialog after creating, otherwise, there is no way to get it from Harbor.
### Configure duration of robot account
If you are a system admin, you can configure the robot account token duration in days.
![set_robot_account_token_duration](img/robotaccount/set_robot_account_token_duration.png)
### Authenticate with a robot account
To authenticate with a Robot Account, use `docker login` as below,
```
docker login harbor.io
Username: robot$accountname
Password: Thepasswordgeneratedbyprojectadmin
```
### Disable a robot account
If you are a project admin, you can disable a Robot Account by clicking "Disable Account" in the `Robot Accounts` tab of a project.
![disable_robot_account](img/robotaccount/disable_delete_robot_account.png)
### Delete a robot account
If you are a project admin, you can delete a Robot Account by clicking "Delete" in the `Robot Accounts` tab of a project.
![delete_robot_account](img/robotaccount/disable_delete_robot_account.png)

View File

@ -192,7 +192,7 @@ docker-compose up -d
protocol=http protocol=http
hostname=reg.mydomain.com hostname=reg.mydomain.com
if [ -n "$(grep '^[^#]*https:' ./harbor.yml)"] if [ -n "$(grep '^[^#]*https:' ./harbor.yml)" ]
then then
protocol=https protocol=https
fi fi

View File

@ -16,6 +16,9 @@ CREATE TRIGGER robot_update_time_at_modtime BEFORE UPDATE ON robot FOR EACH ROW
CREATE TABLE oidc_user ( CREATE TABLE oidc_user (
id SERIAL NOT NULL, id SERIAL NOT NULL,
user_id int NOT NULL, user_id int NOT NULL,
/*
Encoded secret
*/
secret varchar(255) NOT NULL, secret varchar(255) NOT NULL,
/* /*
Subject and Issuer Subject and Issuer
@ -24,9 +27,14 @@ CREATE TABLE oidc_user (
The sub (subject) and iss (issuer) Claims, used together, are the only Claims that an RP can rely upon as a stable identifier for the End-User The sub (subject) and iss (issuer) Claims, used together, are the only Claims that an RP can rely upon as a stable identifier for the End-User
*/ */
subiss varchar(255) NOT NULL, subiss varchar(255) NOT NULL,
/*
Encoded token
*/
token text,
creation_time timestamp default CURRENT_TIMESTAMP, creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP, update_time timestamp default CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES harbor_user(user_id),
UNIQUE (subiss) UNIQUE (subiss)
); );

View File

@ -261,8 +261,12 @@ services:
dns_search: . dns_search: .
ports: ports:
- {{http_port}}:80 - {{http_port}}:80
{% if protocol == 'https' %}
- {{https_port}}:443 - {{https_port}}:443
{% endif %}
{% if with_notary %}
- 4443:4443 - 4443:4443
{% endif %}
depends_on: depends_on:
- postgresql - postgresql
- registry - registry

View File

@ -37,3 +37,6 @@ notifications:
timeout: 3000ms timeout: 3000ms
threshold: 5 threshold: 5
backoff: 1s backoff: 1s
compatibility:
schema1:
enabled: true

View File

@ -1,7 +1,7 @@
FROM golang:1.11 FROM golang:1.11
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV DOCKER_BUILDTAGS include_oss include_gcs ENV BUILDTAGS include_oss include_gcs
WORKDIR $DISTRIBUTION_DIR WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR COPY . $DISTRIBUTION_DIR

View File

@ -24,6 +24,7 @@ import (
commonhttp "github.com/goharbor/harbor/src/common/http" commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"errors"
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
@ -48,68 +49,6 @@ func (b *BaseAPI) GetInt64FromPath(key string) (int64, error) {
return strconv.ParseInt(value, 10, 64) return strconv.ParseInt(value, 10, 64)
} }
// HandleNotFound ...
func (b *BaseAPI) HandleNotFound(text string) {
log.Info(text)
b.RenderError(http.StatusNotFound, text)
}
// HandleUnauthorized ...
func (b *BaseAPI) HandleUnauthorized() {
log.Info("unauthorized")
b.RenderError(http.StatusUnauthorized, "")
}
// HandleForbidden ...
func (b *BaseAPI) HandleForbidden(text string) {
log.Infof("forbidden: %s", text)
b.RenderError(http.StatusForbidden, text)
}
// HandleBadRequest ...
func (b *BaseAPI) HandleBadRequest(text string) {
log.Info(text)
b.RenderError(http.StatusBadRequest, text)
}
// HandleStatusPreconditionFailed ...
func (b *BaseAPI) HandleStatusPreconditionFailed(text string) {
log.Info(text)
b.RenderError(http.StatusPreconditionFailed, text)
}
// HandleConflict ...
func (b *BaseAPI) HandleConflict(text ...string) {
msg := ""
if len(text) > 0 {
msg = text[0]
}
log.Infof("conflict: %s", msg)
b.RenderError(http.StatusConflict, msg)
}
// HandleInternalServerError ...
func (b *BaseAPI) HandleInternalServerError(text string) {
log.Error(text)
b.RenderError(http.StatusInternalServerError, "")
}
// ParseAndHandleError : if the err is an instance of utils/error.Error,
// return the status code and the detail message contained in err, otherwise
// return 500
func (b *BaseAPI) ParseAndHandleError(text string, err error) {
if err == nil {
return
}
log.Errorf("%s: %v", text, err)
if e, ok := err.(*commonhttp.Error); ok {
b.RenderError(e.Code, e.Message)
return
}
b.RenderError(http.StatusInternalServerError, "")
}
// Render returns nil as it won't render template // Render returns nil as it won't render template
func (b *BaseAPI) Render() error { func (b *BaseAPI) Render() error {
return nil return nil
@ -120,23 +59,35 @@ func (b *BaseAPI) RenderError(code int, text string) {
http.Error(b.Ctx.ResponseWriter, text, code) http.Error(b.Ctx.ResponseWriter, text, code)
} }
// RenderFormattedError renders errors with well formatted style
func (b *BaseAPI) RenderFormattedError(errorCode int, errorMsg string) {
error := commonhttp.Error{
Code: errorCode,
Message: errorMsg,
}
formattedErrMsg := error.String()
log.Errorf("%s %s failed with error: %s", b.Ctx.Request.Method, b.Ctx.Request.URL.String(), formattedErrMsg)
b.RenderError(error.Code, formattedErrMsg)
}
// DecodeJSONReq decodes a json request // DecodeJSONReq decodes a json request
func (b *BaseAPI) DecodeJSONReq(v interface{}) { func (b *BaseAPI) DecodeJSONReq(v interface{}) error {
err := json.Unmarshal(b.Ctx.Input.CopyBody(1<<32), v) err := json.Unmarshal(b.Ctx.Input.CopyBody(1<<32), v)
if err != nil { if err != nil {
log.Errorf("Error while decoding the json request, error: %v, %v", log.Errorf("Error while decoding the json request, error: %v, %v",
err, string(b.Ctx.Input.CopyBody(1 << 32)[:])) err, string(b.Ctx.Input.CopyBody(1 << 32)[:]))
b.CustomAbort(http.StatusBadRequest, "Invalid json request") return errors.New("Invalid json request")
} }
return nil
} }
// Validate validates v if it implements interface validation.ValidFormer // Validate validates v if it implements interface validation.ValidFormer
func (b *BaseAPI) Validate(v interface{}) { func (b *BaseAPI) Validate(v interface{}) (bool, error) {
validator := validation.Validation{} validator := validation.Validation{}
isValid, err := validator.Valid(v) isValid, err := validator.Valid(v)
if err != nil { if err != nil {
log.Errorf("failed to validate: %v", err) log.Errorf("failed to validate: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) return false, err
} }
if !isValid { if !isValid {
@ -144,14 +95,17 @@ func (b *BaseAPI) Validate(v interface{}) {
for _, e := range validator.Errors { for _, e := range validator.Errors {
message += fmt.Sprintf("%s %s \n", e.Field, e.Message) message += fmt.Sprintf("%s %s \n", e.Field, e.Message)
} }
b.CustomAbort(http.StatusBadRequest, message) return false, errors.New(message)
} }
return true, nil
} }
// DecodeJSONReqAndValidate does both decoding and validation // DecodeJSONReqAndValidate does both decoding and validation
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) { func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) (bool, error) {
b.DecodeJSONReq(v) if err := b.DecodeJSONReq(v); err != nil {
b.Validate(v) return false, err
}
return b.Validate(v)
} }
// Redirect does redirection to resource URI with http header status code. // Redirect does redirection to resource URI with http header status code.
@ -163,18 +117,18 @@ func (b *BaseAPI) Redirect(statusCode int, resouceID string) {
} }
// GetIDFromURL checks the ID in request URL // GetIDFromURL checks the ID in request URL
func (b *BaseAPI) GetIDFromURL() int64 { func (b *BaseAPI) GetIDFromURL() (int64, error) {
idStr := b.Ctx.Input.Param(":id") idStr := b.Ctx.Input.Param(":id")
if len(idStr) == 0 { if len(idStr) == 0 {
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL") return 0, errors.New("invalid ID in URL")
} }
id, err := strconv.ParseInt(idStr, 10, 64) id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 { if err != nil || id <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL") return 0, errors.New("invalid ID in URL")
} }
return id return id, nil
} }
// SetPaginationHeader set"Link" and "X-Total-Count" header for pagination request // SetPaginationHeader set"Link" and "X-Total-Count" header for pagination request
@ -213,15 +167,15 @@ func (b *BaseAPI) SetPaginationHeader(total, page, pageSize int64) {
} }
// GetPaginationParams ... // GetPaginationParams ...
func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) { func (b *BaseAPI) GetPaginationParams() (page, pageSize int64, err error) {
page, err := b.GetInt64("page", 1) page, err = b.GetInt64("page", 1)
if err != nil || page <= 0 { if err != nil || page <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page") return 0, 0, errors.New("invalid page")
} }
pageSize, err = b.GetInt64("page_size", defaultPageSize) pageSize, err = b.GetInt64("page_size", defaultPageSize)
if err != nil || pageSize <= 0 { if err != nil || pageSize <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page_size") return 0, 0, errors.New("invalid page_size")
} }
if pageSize > maxPageSize { if pageSize > maxPageSize {
@ -229,5 +183,60 @@ func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
log.Debugf("the parameter page_size %d exceeds the max %d, set it to max", pageSize, maxPageSize) log.Debugf("the parameter page_size %d exceeds the max %d, set it to max", pageSize, maxPageSize)
} }
return page, pageSize return page, pageSize, nil
}
// ParseAndHandleError : if the err is an instance of utils/error.Error,
// return the status code and the detail message contained in err, otherwise
// return 500
func (b *BaseAPI) ParseAndHandleError(text string, err error) {
if err == nil {
return
}
log.Errorf("%s: %v", text, err)
if e, ok := err.(*commonhttp.Error); ok {
b.RenderFormattedError(e.Code, e.Message)
return
}
b.SendInternalServerError(errors.New(""))
}
// SendUnAuthorizedError sends unauthorized error to the client.
func (b *BaseAPI) SendUnAuthorizedError(err error) {
b.RenderFormattedError(http.StatusUnauthorized, err.Error())
}
// SendConflictError sends conflict error to the client.
func (b *BaseAPI) SendConflictError(err error) {
b.RenderFormattedError(http.StatusConflict, err.Error())
}
// SendNotFoundError sends not found error to the client.
func (b *BaseAPI) SendNotFoundError(err error) {
b.RenderFormattedError(http.StatusNotFound, err.Error())
}
// SendBadRequestError sends bad request error to the client.
func (b *BaseAPI) SendBadRequestError(err error) {
b.RenderFormattedError(http.StatusBadRequest, err.Error())
}
// SendInternalServerError sends internal server error to the client.
func (b *BaseAPI) SendInternalServerError(err error) {
b.RenderFormattedError(http.StatusInternalServerError, err.Error())
}
// SendForbiddenError sends forbidden error to the client.
func (b *BaseAPI) SendForbiddenError(err error) {
b.RenderFormattedError(http.StatusForbidden, err.Error())
}
// SendPreconditionFailedError sends forbidden error to the client.
func (b *BaseAPI) SendPreconditionFailedError(err error) {
b.RenderFormattedError(http.StatusPreconditionFailed, err.Error())
}
// SendStatusServiceUnavailableError sends forbidden error to the client.
func (b *BaseAPI) SendStatusServiceUnavailableError(err error) {
b.RenderFormattedError(http.StatusServiceUnavailable, err.Error())
} }

View File

@ -46,3 +46,13 @@ func (f *FileKeyProvider) Get(params map[string]interface{}) (string, error) {
} }
return string(b), nil return string(b), nil
} }
// PresetKeyProvider returns the preset key disregarding the parm, this is for testing only
type PresetKeyProvider struct {
Key string
}
// Get ...
func (p *PresetKeyProvider) Get(params map[string]interface{}) (string, error) {
return p.Key, nil
}

View File

@ -15,6 +15,7 @@
package config package config
import ( import (
"github.com/stretchr/testify/assert"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@ -42,3 +43,12 @@ func TestGetOfFileKeyProvider(t *testing.T) {
return return
} }
} }
func TestPresetKeyProvider(t *testing.T) {
kp := &PresetKeyProvider{
Key: "mykey",
}
k, err := kp.Get(nil)
assert.Nil(t, err)
assert.Equal(t, "mykey", k)
}

View File

@ -133,7 +133,7 @@ var (
{Name: common.HTTPAuthProxyEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}}, {Name: common.HTTPAuthProxyEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}}, {Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxySkipCertVerify, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}}, {Name: common.HTTPAuthProxyVerifyCert, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "true", ItemType: &BoolType{}},
{Name: common.HTTPAuthProxyAlwaysOnboard, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}}, {Name: common.HTTPAuthProxyAlwaysOnboard, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
{Name: common.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}}, {Name: common.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
@ -141,7 +141,7 @@ var (
{Name: common.OIDCCLientID, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}}, {Name: common.OIDCCLientID, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
{Name: common.OIDCClientSecret, Scope: UserScope, Group: OIDCGroup, ItemType: &PasswordType{}}, {Name: common.OIDCClientSecret, Scope: UserScope, Group: OIDCGroup, ItemType: &PasswordType{}},
{Name: common.OIDCScope, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}}, {Name: common.OIDCScope, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
{Name: common.OIDCSkipCertVerify, Scope: UserScope, Group: OIDCGroup, DefaultValue: "false", ItemType: &BoolType{}}, {Name: common.OIDCVerifyCert, Scope: UserScope, Group: OIDCGroup, DefaultValue: "true", ItemType: &BoolType{}},
{Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true}, {Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},

View File

@ -100,13 +100,13 @@ const (
UAAVerifyCert = "uaa_verify_cert" UAAVerifyCert = "uaa_verify_cert"
HTTPAuthProxyEndpoint = "http_authproxy_endpoint" HTTPAuthProxyEndpoint = "http_authproxy_endpoint"
HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint" HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint"
HTTPAuthProxySkipCertVerify = "http_authproxy_skip_cert_verify" HTTPAuthProxyVerifyCert = "http_authproxy_verify_cert"
HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard" HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard"
OIDCName = "oidc_name" OIDCName = "oidc_name"
OIDCEndpoint = "oidc_endpoint" OIDCEndpoint = "oidc_endpoint"
OIDCCLientID = "oidc_client_id" OIDCCLientID = "oidc_client_id"
OIDCClientSecret = "oidc_client_secret" OIDCClientSecret = "oidc_client_secret"
OIDCSkipCertVerify = "oidc_skip_cert_verify" OIDCVerifyCert = "oidc_verify_cert"
OIDCScope = "oidc_scope" OIDCScope = "oidc_scope"
DefaultClairEndpoint = "http://clair:6060" DefaultClairEndpoint = "http://clair:6060"

View File

@ -15,16 +15,26 @@
package http package http
import ( import (
"encoding/json"
"fmt" "fmt"
) )
// Error wrap HTTP status code and message as an error // Error wrap HTTP status code and message as an error
type Error struct { type Error struct {
Code int Code int `json:"code"`
Message string Message string `json:"message"`
} }
// Error ... // Error ...
func (e *Error) Error() string { func (e *Error) Error() string {
return fmt.Sprintf("http error: code %d, message %s", e.Code, e.Message) return fmt.Sprintf("http error: code %d, message %s", e.Code, e.Message)
} }
// String wraps the error msg to the well formatted error message
func (e *Error) String() string {
data, err := json.Marshal(&e)
if err != nil {
return e.Message
}
return string(data)
}

View File

@ -0,0 +1,17 @@
package http
import (
"github.com/stretchr/testify/assert"
"testing"
)
// Test case for error wrapping function.
func TestWrapError(t *testing.T) {
err := Error{
Code: 1,
Message: "test",
}
assert.Equal(t, err.String(), "{\"code\":1,\"message\":\"test\"}")
}

View File

@ -69,19 +69,19 @@ type Email struct {
type HTTPAuthProxy struct { type HTTPAuthProxy struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
TokenReviewEndpoint string `json:"tokenreivew_endpoint"` TokenReviewEndpoint string `json:"tokenreivew_endpoint"`
SkipCertVerify bool `json:"skip_cert_verify"` VerifyCert bool `json:"verify_cert"`
AlwaysOnBoard bool `json:"always_onboard"` AlwaysOnBoard bool `json:"always_onboard"`
} }
// OIDCSetting wraps the settings for OIDC auth endpoint // OIDCSetting wraps the settings for OIDC auth endpoint
type OIDCSetting struct { type OIDCSetting struct {
Name string `json:"name"` Name string `json:"name"`
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
SkipCertVerify bool `json:"skip_cert_verify"` VerifyCert bool `json:"verify_cert"`
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"` RedirectURL string `json:"redirect_url"`
Scope []string `json:"scope"` Scope []string `json:"scope"`
} }
// ConfigEntry ... // ConfigEntry ...

View File

@ -6,10 +6,14 @@ import (
// OIDCUser ... // OIDCUser ...
type OIDCUser struct { type OIDCUser struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"` ID int64 `orm:"pk;auto;column(id)" json:"id"`
UserID int `orm:"column(user_id)" json:"user_id"` UserID int `orm:"column(user_id)" json:"user_id"`
Secret string `orm:"column(secret)" json:"secret"` // encrypted secret
Secret string `orm:"column(secret)" json:"-"`
// secret in plain text
PlainSecret string `orm:"-" json:"secret"`
SubIss string `orm:"column(subiss)" json:"subiss"` SubIss string `orm:"column(subiss)" json:"subiss"`
Token string `orm:"column(token)" json:"-"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
} }

View File

@ -1,9 +1,9 @@
package token package token
import ( import (
"errors"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/pkg/errors"
) )
// RobotClaims implements the interface of jwt.Claims // RobotClaims implements the interface of jwt.Claims

View File

@ -41,14 +41,14 @@ type providerHelper struct {
} }
type endpoint struct { type endpoint struct {
url string url string
skipCertVerify bool VerifyCert bool
} }
func (p *providerHelper) get() (*gooidc.Provider, error) { func (p *providerHelper) get() (*gooidc.Provider, error) {
if p.instance.Load() != nil { if p.instance.Load() != nil {
s := p.setting.Load().(models.OIDCSetting) s := p.setting.Load().(models.OIDCSetting)
if s.Endpoint != p.ep.url || s.SkipCertVerify != p.ep.skipCertVerify { // relevant settings have changed, need to re-create provider. if s.Endpoint != p.ep.url || s.VerifyCert != p.ep.VerifyCert { // relevant settings have changed, need to re-create provider.
if err := p.create(); err != nil { if err := p.create(); err != nil {
return nil, err return nil, err
} }
@ -90,24 +90,15 @@ func (p *providerHelper) create() error {
return errors.New("the configuration is not loaded") return errors.New("the configuration is not loaded")
} }
s := p.setting.Load().(models.OIDCSetting) s := p.setting.Load().(models.OIDCSetting)
var client *http.Client ctx := clientCtx(context.Background(), s.VerifyCert)
if s.SkipCertVerify {
client = &http.Client{
Transport: insecureTransport,
}
} else {
client = &http.Client{}
}
ctx := context.Background()
gooidc.ClientContext(ctx, client)
provider, err := gooidc.NewProvider(ctx, s.Endpoint) provider, err := gooidc.NewProvider(ctx, s.Endpoint)
if err != nil { if err != nil {
return fmt.Errorf("failed to create OIDC provider, error: %v", err) return fmt.Errorf("failed to create OIDC provider, error: %v", err)
} }
p.instance.Store(provider) p.instance.Store(provider)
p.ep = endpoint{ p.ep = endpoint{
url: s.Endpoint, url: s.Endpoint,
skipCertVerify: s.SkipCertVerify, VerifyCert: s.VerifyCert,
} }
return nil return nil
} }
@ -170,6 +161,8 @@ func ExchangeToken(ctx context.Context, code string) (*Token, error) {
log.Errorf("Failed to get OAuth configuration, error: %v", err) log.Errorf("Failed to get OAuth configuration, error: %v", err)
return nil, err return nil, err
} }
setting := provider.setting.Load().(models.OIDCSetting)
ctx = clientCtx(ctx, setting.VerifyCert)
oauthToken, err := oauth.Exchange(ctx, code) oauthToken, err := oauth.Exchange(ctx, code)
if err != nil { if err != nil {
return nil, err return nil, err
@ -184,5 +177,36 @@ func VerifyToken(ctx context.Context, rawIDToken string) (*gooidc.IDToken, error
return nil, err return nil, err
} }
verifier := p.Verifier(&gooidc.Config{ClientID: provider.setting.Load().(models.OIDCSetting).ClientID}) verifier := p.Verifier(&gooidc.Config{ClientID: provider.setting.Load().(models.OIDCSetting).ClientID})
setting := provider.setting.Load().(models.OIDCSetting)
ctx = clientCtx(ctx, setting.VerifyCert)
return verifier.Verify(ctx, rawIDToken) return verifier.Verify(ctx, rawIDToken)
} }
func clientCtx(ctx context.Context, verifyCert bool) context.Context {
var client *http.Client
if !verifyCert {
client = &http.Client{
Transport: insecureTransport,
}
} else {
client = &http.Client{}
}
return gooidc.ClientContext(ctx, client)
}
// RefreshToken refreshes the token passed in parameter, and return the new token.
func RefreshToken(ctx context.Context, token *Token) (*Token, error) {
oauth, err := getOauthConf()
if err != nil {
log.Errorf("Failed to get OAuth configuration, error: %v", err)
return nil, err
}
setting := provider.setting.Load().(models.OIDCSetting)
ctx = clientCtx(ctx, setting.VerifyCert)
ts := oauth.TokenSource(ctx, token.Token)
t, err := ts.Token()
if err != nil {
return nil, err
}
return &Token{Token: t, IDToken: t.Extra("id_token").(string)}, nil
}

View File

@ -16,6 +16,7 @@ package oidc
import ( import (
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
config2 "github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,16 +29,17 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
conf := map[string]interface{}{ conf := map[string]interface{}{
common.OIDCName: "test", common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com", common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCSkipCertVerify: "false", common.OIDCVerifyCert: "true",
common.OIDCScope: "openid, profile, offline_access", common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client", common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret", common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test", common.ExtEndpoint: "https://harbor.test",
} }
kp := &config2.PresetKeyProvider{Key: "naa4JtarA1Zsc3uY"}
config.InitWithSettings(conf) config.InitWithSettings(conf, kp)
result := m.Run() result := m.Run()
if result != 0 { if result != 0 {
@ -71,13 +73,13 @@ func TestHelperGet(t *testing.T) {
assert.Equal(t, "https://oauth2.googleapis.com/token", p.Endpoint().TokenURL) assert.Equal(t, "https://oauth2.googleapis.com/token", p.Endpoint().TokenURL)
update := map[string]interface{}{ update := map[string]interface{}{
common.OIDCName: "test", common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com", common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCSkipCertVerify: "false", common.OIDCVerifyCert: "true",
common.OIDCScope: "openid, profile, offline_access", common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client", common.OIDCCLientID: "client",
common.OIDCClientSecret: "new-secret", common.OIDCClientSecret: "new-secret",
common.ExtEndpoint: "https://harbor.test", common.ExtEndpoint: "https://harbor.test",
} }
config.GetCfgManager().UpdateConfig(update) config.GetCfgManager().UpdateConfig(update)

View File

@ -0,0 +1,130 @@
package oidc
import (
"context"
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/pkg/errors"
"sync"
)
// SecretVerifyError wraps the different errors happened when verifying a secret for OIDC user. When seeing this error,
// the caller should consider this an authentication error.
type SecretVerifyError struct {
cause error
}
func (se *SecretVerifyError) Error() string {
return fmt.Sprintf("failed to verify the secret: %v", se.cause)
}
func verifyError(err error) error {
return &SecretVerifyError{err}
}
// SecretManager is the interface for store and verify the secret
type SecretManager interface {
// SetSecret sets the secret and token based on the ID of the user, when setting the secret the user has to be
// onboarded to Harbor DB.
SetSecret(userID int, secret string, token *Token) error
// VerifySecret verifies the secret and the token associated with it, it refreshes the token in the DB if it's
// refreshed during the verification
VerifySecret(ctx context.Context, userID int, secret string) error
}
type defaultManager struct {
sync.Mutex
key string
}
var m SecretManager = &defaultManager{}
func (dm *defaultManager) getEncryptKey() (string, error) {
if dm.key == "" {
dm.Lock()
defer dm.Unlock()
if dm.key == "" {
key, err := config.SecretKey()
if err != nil {
return "", err
}
dm.key = key
}
}
return dm.key, nil
}
// SetSecret sets the secret and token based on the ID of the user, when setting the secret the user has to be
// onboarded to Harbor DB.
func (dm *defaultManager) SetSecret(userID int, secret string, token *Token) error {
key, err := dm.getEncryptKey()
if err != nil {
return fmt.Errorf("failed to load the key for encryption/decryption %v", err)
}
oidcUser, err := dao.GetOIDCUserByUserID(userID)
if oidcUser == nil {
return fmt.Errorf("failed to get oidc user info, error: %v", err)
}
encSecret, _ := utils.ReversibleEncrypt(secret, key)
tb, _ := json.Marshal(token)
encToken, _ := utils.ReversibleEncrypt(string(tb), key)
oidcUser.Secret = encSecret
oidcUser.Token = encToken
return dao.UpdateOIDCUser(oidcUser)
}
// VerifySecret verifies the secret and the token associated with it, it tries to update the token in the DB if it's
// refreshed during the verification
func (dm *defaultManager) VerifySecret(ctx context.Context, userID int, secret string) error {
oidcUser, err := dao.GetOIDCUserByUserID(userID)
if err != nil {
return fmt.Errorf("failed to get oidc user info, error: %v", err)
}
if oidcUser == nil {
return fmt.Errorf("user is not onboarded as OIDC user")
}
key, err := dm.getEncryptKey()
if err != nil {
return fmt.Errorf("failed to load the key for encryption/decryption %v", err)
}
plainSecret, err := utils.ReversibleDecrypt(oidcUser.Secret, key)
if err != nil {
return fmt.Errorf("failed to decrypt secret from DB: %v", err)
}
if secret != plainSecret {
return verifyError(errors.New("secret mismatch"))
}
tokenStr, err := utils.ReversibleDecrypt(oidcUser.Token, key)
if err != nil {
return verifyError(err)
}
token := &Token{}
err = json.Unmarshal(([]byte)(tokenStr), token)
if err != nil {
return verifyError(err)
}
_, err = VerifyToken(ctx, token.IDToken)
if err == nil {
return nil
}
log.Infof("Failed to verify ID Token, error: %v, refreshing...", err)
t, err := RefreshToken(ctx, token)
if err != nil {
return verifyError(err)
}
err = dm.SetSecret(oidcUser.UserID, secret, t)
if err != nil {
log.Warningf("Failed to update the token in DB: %v, ignore this error.", err)
}
return nil
}
// VerifySecret verifies the secret and the token associated with it, it tries to update the token in the DB if it's
// refreshed during the verification
func VerifySecret(ctx context.Context, userID int, secret string) error {
return m.VerifySecret(ctx, userID, secret)
}

View File

@ -0,0 +1,32 @@
package oidc
import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSecretVerifyError(t *testing.T) {
sve := &SecretVerifyError{cause: fmt.Errorf("myerror")}
assert.Equal(t, "failed to verify the secret: myerror", sve.Error())
err := verifyError(fmt.Errorf("myerror"))
assert.Equal(t, sve, err)
}
func TestDefaultManagerGetEncryptKey(t *testing.T) {
d := &defaultManager{}
k, err := d.getEncryptKey()
assert.Nil(t, err)
assert.Equal(t, "naa4JtarA1Zsc3uY", k)
d2 := &defaultManager{key: "oldkey"}
k2, err := d2.getEncryptKey()
assert.Nil(t, err)
assert.Equal(t, "oldkey", k2)
}
func TestPkgVerifySecret(t *testing.T) {
SetHardcodeVerifierForTest("secret")
assert.Nil(t, VerifySecret(context.Background(), 1, "secret"))
assert.NotNil(t, VerifySecret(context.Background(), 1, "not-the-secret"))
}

View File

@ -0,0 +1,26 @@
package oidc
import "context"
import "errors"
// This is for testing only
type fakeVerifier struct {
secret string
}
func (fv *fakeVerifier) SetSecret(uid int, s string, t *Token) error {
return nil
}
func (fv *fakeVerifier) VerifySecret(ctx context.Context, userID int, secret string) error {
if secret != fv.secret {
return verifyError(errors.New("mismatch"))
}
return nil
}
// SetHardcodeVerifierForTest overwrite the default secret manager for testing.
// Be reminded this is for testing only.
func SetHardcodeVerifierForTest(s string) {
m = &fakeVerifier{s}
}

View File

@ -27,6 +27,7 @@ import (
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/api/models" "github.com/goharbor/harbor/src/core/api/models"
utils_core "github.com/goharbor/harbor/src/core/utils" utils_core "github.com/goharbor/harbor/src/core/utils"
"github.com/pkg/errors"
) )
// AJAPI manages the CRUD of admin job and its schedule, any API wants to handle manual and cron job like ScanAll and GC cloud reuse it. // AJAPI manages the CRUD of admin job and its schedule, any API wants to handle manual and cron job like ScanAll and GC cloud reuse it.
@ -42,7 +43,7 @@ func (aj *AJAPI) Prepare() {
// updateSchedule update a schedule of admin job. // updateSchedule update a schedule of admin job.
func (aj *AJAPI) updateSchedule(ajr models.AdminJobReq) { func (aj *AJAPI) updateSchedule(ajr models.AdminJobReq) {
if ajr.Schedule.Type == models.ScheduleManual { if ajr.Schedule.Type == models.ScheduleManual {
aj.HandleInternalServerError(fmt.Sprintf("Fail to update admin job schedule as wrong schedule type: %s.", ajr.Schedule.Type)) aj.SendInternalServerError((fmt.Errorf("fail to update admin job schedule as wrong schedule type: %s", ajr.Schedule.Type)))
return return
} }
@ -52,24 +53,24 @@ func (aj *AJAPI) updateSchedule(ajr models.AdminJobReq) {
} }
jobs, err := dao.GetAdminJobs(query) jobs, err := dao.GetAdminJobs(query)
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err)) aj.SendInternalServerError(err)
return return
} }
if len(jobs) != 1 { if len(jobs) != 1 {
aj.HandleInternalServerError("Fail to update admin job schedule as we found more than one schedule in system, please ensure that only one schedule left for your job .") aj.SendInternalServerError(errors.New("fail to update admin job schedule as we found more than one schedule in system, please ensure that only one schedule left for your job"))
return return
} }
// stop the scheduled job and remove it. // stop the scheduled job and remove it.
if err = utils_core.GetJobServiceClient().PostAction(jobs[0].UUID, common_job.JobActionStop); err != nil { if err = utils_core.GetJobServiceClient().PostAction(jobs[0].UUID, common_job.JobActionStop); err != nil {
if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound { if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound {
aj.HandleInternalServerError(fmt.Sprintf("%v", err)) aj.SendInternalServerError(err)
return return
} }
} }
if err = dao.DeleteAdminJob(jobs[0].ID); err != nil { if err = dao.DeleteAdminJob(jobs[0].ID); err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err)) aj.SendInternalServerError(err)
return return
} }
@ -85,17 +86,17 @@ func (aj *AJAPI) get(id int64) {
ID: id, ID: id,
}) })
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return return
} }
if len(jobs) == 0 { if len(jobs) == 0 {
aj.HandleNotFound("No admin job found.") aj.SendNotFoundError(errors.New("no admin job found"))
return return
} }
adminJobRep, err := convertToAdminJobRep(jobs[0]) adminJobRep, err := convertToAdminJobRep(jobs[0])
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to convert admin job response: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to convert admin job response: %v", err))
return return
} }
@ -107,7 +108,7 @@ func (aj *AJAPI) get(id int64) {
func (aj *AJAPI) list(name string) { func (aj *AJAPI) list(name string) {
jobs, err := dao.GetTop10AdminJobsOfName(name) jobs, err := dao.GetTop10AdminJobsOfName(name)
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return return
} }
@ -115,7 +116,7 @@ func (aj *AJAPI) list(name string) {
for _, job := range jobs { for _, job := range jobs {
AdminJobRep, err := convertToAdminJobRep(job) AdminJobRep, err := convertToAdminJobRep(job)
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to convert admin job response: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to convert admin job response: %v", err))
return return
} }
AdminJobReps = append(AdminJobReps, &AdminJobRep) AdminJobReps = append(AdminJobReps, &AdminJobRep)
@ -134,18 +135,18 @@ func (aj *AJAPI) getSchedule(name string) {
Kind: common_job.JobKindPeriodic, Kind: common_job.JobKindPeriodic,
}) })
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return return
} }
if len(jobs) > 1 { if len(jobs) > 1 {
aj.HandleInternalServerError("Get more than one scheduled admin job, make sure there has only one.") aj.SendInternalServerError(errors.New("get more than one scheduled admin job, make sure there has only one"))
return return
} }
if len(jobs) != 0 { if len(jobs) != 0 {
adminJobRep, err := convertToAdminJobRep(jobs[0]) adminJobRep, err := convertToAdminJobRep(jobs[0])
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to convert admin job response: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to convert admin job response: %v", err))
return return
} }
adminJobSchedule.Schedule = adminJobRep.Schedule adminJobSchedule.Schedule = adminJobRep.Schedule
@ -160,11 +161,13 @@ func (aj *AJAPI) getLog(id int64) {
job, err := dao.GetAdminJob(id) job, err := dao.GetAdminJob(id)
if err != nil { if err != nil {
log.Errorf("Failed to load job data for job: %d, error: %v", id, err) log.Errorf("Failed to load job data for job: %d, error: %v", id, err)
aj.CustomAbort(http.StatusInternalServerError, "Failed to get Job data") aj.SendInternalServerError(errors.New("Failed to get Job data"))
return
} }
if job == nil { if job == nil {
log.Errorf("Failed to get admin job: %d", id) log.Errorf("Failed to get admin job: %d", id)
aj.CustomAbort(http.StatusNotFound, "Failed to get Job") aj.SendNotFoundError(errors.New("Failed to get Job"))
return
} }
logBytes, err := utils_core.GetJobServiceClient().GetJobLog(job.UUID) logBytes, err := utils_core.GetJobServiceClient().GetJobLog(job.UUID)
@ -175,14 +178,14 @@ func (aj *AJAPI) getLog(id int64) {
id, httpErr.Code, httpErr.Message)) id, httpErr.Code, httpErr.Message))
return return
} }
aj.HandleInternalServerError(fmt.Sprintf("Failed to get job logs, uuid: %s, error: %v", job.UUID, err)) aj.SendInternalServerError(fmt.Errorf("Failed to get job logs, uuid: %s, error: %v", job.UUID, err))
return return
} }
aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes))) aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain") aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = aj.Ctx.ResponseWriter.Write(logBytes) _, err = aj.Ctx.ResponseWriter.Write(logBytes)
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("Failed to write job logs, uuid: %s, error: %v", job.UUID, err)) aj.SendInternalServerError(fmt.Errorf("Failed to write job logs, uuid: %s, error: %v", job.UUID, err))
} }
} }
@ -195,11 +198,11 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
Kind: common_job.JobKindPeriodic, Kind: common_job.JobKindPeriodic,
}) })
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err)) aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return return
} }
if len(jobs) != 0 { if len(jobs) != 0 {
aj.HandleStatusPreconditionFailed("Fail to set schedule for admin job as always had one, please delete it firstly then to re-schedule.") aj.SendPreconditionFailedError(errors.New("fail to set schedule for admin job as always had one, please delete it firstly then to re-schedule"))
return return
} }
} }
@ -210,7 +213,7 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
Cron: ajr.CronString(), Cron: ajr.CronString(),
}) })
if err != nil { if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err)) aj.SendInternalServerError(err)
return return
} }
ajr.ID = id ajr.ID = id
@ -224,14 +227,14 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
log.Debugf("Failed to delete admin job, err: %v", err) log.Debugf("Failed to delete admin job, err: %v", err)
} }
if httpErr, ok := err.(*common_http.Error); ok && httpErr.Code == http.StatusConflict { if httpErr, ok := err.(*common_http.Error); ok && httpErr.Code == http.StatusConflict {
aj.HandleConflict(fmt.Sprintf("Conflict when triggering %s, please try again later.", ajr.Name)) aj.SendConflictError(fmt.Errorf("conflict when triggering %s, please try again later", ajr.Name))
return return
} }
aj.HandleInternalServerError(fmt.Sprintf("%v", err)) aj.SendInternalServerError(err)
return return
} }
if err := dao.SetAdminJobUUID(id, uuid); err != nil { if err := dao.SetAdminJobUUID(id, uuid); err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err)) aj.SendInternalServerError(err)
return return
} }
} }

View File

@ -17,14 +17,14 @@ package api
import ( import (
"net/http" "net/http"
yaml "github.com/ghodss/yaml" "errors"
"github.com/ghodss/yaml"
"github.com/goharbor/harbor/src/common/api" "github.com/goharbor/harbor/src/common/api"
"github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter" "github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/core/promgr" "github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/utils"
) )
const ( const (
@ -54,55 +54,20 @@ func (b *BaseController) Prepare() {
ctx, err := filter.GetSecurityContext(b.Ctx.Request) ctx, err := filter.GetSecurityContext(b.Ctx.Request)
if err != nil { if err != nil {
log.Errorf("failed to get security context: %v", err) log.Errorf("failed to get security context: %v", err)
b.CustomAbort(http.StatusInternalServerError, "") b.SendInternalServerError(errors.New(""))
return
} }
b.SecurityCtx = ctx b.SecurityCtx = ctx
pm, err := filter.GetProjectManager(b.Ctx.Request) pm, err := filter.GetProjectManager(b.Ctx.Request)
if err != nil { if err != nil {
log.Errorf("failed to get project manager: %v", err) log.Errorf("failed to get project manager: %v", err)
b.CustomAbort(http.StatusInternalServerError, "") b.SendInternalServerError(errors.New(""))
return
} }
b.ProjectMgr = pm b.ProjectMgr = pm
} }
// RenderFormatedError renders errors with well formted style `{"error": "This is an error"}`
func (b *BaseController) RenderFormatedError(code int, err error) {
formatedErr := utils.WrapError(err)
log.Errorf("%s %s failed with error: %s", b.Ctx.Request.Method, b.Ctx.Request.URL.String(), formatedErr.Error())
b.RenderError(code, formatedErr.Error())
}
// SendUnAuthorizedError sends unauthorized error to the client.
func (b *BaseController) SendUnAuthorizedError(err error) {
b.RenderFormatedError(http.StatusUnauthorized, err)
}
// SendConflictError sends conflict error to the client.
func (b *BaseController) SendConflictError(err error) {
b.RenderFormatedError(http.StatusConflict, err)
}
// SendNotFoundError sends not found error to the client.
func (b *BaseController) SendNotFoundError(err error) {
b.RenderFormatedError(http.StatusNotFound, err)
}
// SendBadRequestError sends bad request error to the client.
func (b *BaseController) SendBadRequestError(err error) {
b.RenderFormatedError(http.StatusBadRequest, err)
}
// SendInternalServerError sends internal server error to the client.
func (b *BaseController) SendInternalServerError(err error) {
b.RenderFormatedError(http.StatusInternalServerError, err)
}
// SendForbiddenError sends forbidden error to the client.
func (b *BaseController) SendForbiddenError(err error) {
b.RenderFormatedError(http.StatusForbidden, err)
}
// WriteJSONData writes the JSON data to the client. // WriteJSONData writes the JSON data to the client.
func (b *BaseController) WriteJSONData(object interface{}) { func (b *BaseController) WriteJSONData(object interface{}) {
b.Data["json"] = object b.Data["json"] = object

View File

@ -61,7 +61,7 @@ func (cla *ChartLabelAPI) requireAccess(action rbac.Action) bool {
resource := rbac.NewProjectNamespace(cla.project.ProjectID).Resource(rbac.ResourceHelmChartVersionLabel) resource := rbac.NewProjectNamespace(cla.project.ProjectID).Resource(rbac.ResourceHelmChartVersionLabel)
if !cla.SecurityCtx.Can(action, resource) { if !cla.SecurityCtx.Can(action, resource) {
cla.HandleForbidden(cla.SecurityCtx.GetUsername()) cla.SendForbiddenError(errors.New(cla.SecurityCtx.GetUsername()))
return false return false
} }
@ -75,7 +75,10 @@ func (cla *ChartLabelAPI) MarkLabel() {
} }
l := &models.Label{} l := &models.Label{}
cla.DecodeJSONReq(l) if err := cla.DecodeJSONReq(l); err != nil {
cla.SendBadRequestError(err)
return
}
label, ok := cla.validate(l.ID, cla.project.ProjectID) label, ok := cla.validate(l.ID, cla.project.ProjectID)
if !ok { if !ok {

View File

@ -96,7 +96,7 @@ func (cra *ChartRepositoryAPI) requireAccess(action rbac.Action, subresource ...
if !cra.SecurityCtx.IsAuthenticated() { if !cra.SecurityCtx.IsAuthenticated() {
cra.SendUnAuthorizedError(errors.New("Unauthorized")) cra.SendUnAuthorizedError(errors.New("Unauthorized"))
} else { } else {
cra.HandleForbidden(cra.SecurityCtx.GetUsername()) cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
} }
return false return false
@ -114,7 +114,7 @@ func (cra *ChartRepositoryAPI) GetHealthStatus() {
} }
if !cra.SecurityCtx.IsSysAdmin() { if !cra.SecurityCtx.IsSysAdmin() {
cra.HandleForbidden(cra.SecurityCtx.GetUsername()) cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
return return
} }
@ -142,7 +142,7 @@ func (cra *ChartRepositoryAPI) GetIndex() {
} }
if !cra.SecurityCtx.IsSysAdmin() { if !cra.SecurityCtx.IsSysAdmin() {
cra.HandleForbidden(cra.SecurityCtx.GetUsername()) cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
return return
} }

View File

@ -16,9 +16,9 @@ package api
import ( import (
"fmt" "fmt"
"net/http"
"strings" "strings"
"errors"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/config" "github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/config/metadata" "github.com/goharbor/harbor/src/common/config/metadata"
@ -41,20 +41,20 @@ func (c *ConfigAPI) Prepare() {
c.BaseController.Prepare() c.BaseController.Prepare()
c.cfgManager = corecfg.GetCfgManager() c.cfgManager = corecfg.GetCfgManager()
if !c.SecurityCtx.IsAuthenticated() { if !c.SecurityCtx.IsAuthenticated() {
c.HandleUnauthorized() c.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
// Only internal container can access /api/internal/configurations // Only internal container can access /api/internal/configurations
if strings.EqualFold(c.Ctx.Request.RequestURI, "/api/internal/configurations") { if strings.EqualFold(c.Ctx.Request.RequestURI, "/api/internal/configurations") {
if _, ok := c.Ctx.Request.Context().Value(filter.SecurCtxKey).(*secret.SecurityContext); !ok { if _, ok := c.Ctx.Request.Context().Value(filter.SecurCtxKey).(*secret.SecurityContext); !ok {
c.HandleUnauthorized() c.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
} }
if !c.SecurityCtx.IsSysAdmin() && !c.SecurityCtx.IsSolutionUser() { if !c.SecurityCtx.IsSysAdmin() && !c.SecurityCtx.IsSolutionUser() {
c.HandleForbidden(c.SecurityCtx.GetUsername()) c.SendForbiddenError(errors.New(c.SecurityCtx.GetUsername()))
return return
} }
@ -71,7 +71,8 @@ func (c *ConfigAPI) Get() {
m, err := convertForGet(configs) m, err := convertForGet(configs)
if err != nil { if err != nil {
log.Errorf("failed to convert configurations: %v", err) log.Errorf("failed to convert configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) c.SendInternalServerError(errors.New(""))
return
} }
c.Data["json"] = m c.Data["json"] = m
@ -89,25 +90,33 @@ func (c *ConfigAPI) GetInternalConfig() {
// Put updates configurations // Put updates configurations
func (c *ConfigAPI) Put() { func (c *ConfigAPI) Put() {
m := map[string]interface{}{} m := map[string]interface{}{}
c.DecodeJSONReq(&m) if err := c.DecodeJSONReq(&m); err != nil {
c.SendBadRequestError(err)
return
}
err := c.cfgManager.Load() err := c.cfgManager.Load()
if err != nil { if err != nil {
log.Errorf("failed to get configurations: %v", err) log.Errorf("failed to get configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) c.SendInternalServerError(errors.New(""))
return
} }
isSysErr, err := c.validateCfg(m) isSysErr, err := c.validateCfg(m)
if err != nil { if err != nil {
if isSysErr { if isSysErr {
log.Errorf("failed to validate configurations: %v", err) log.Errorf("failed to validate configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) c.SendInternalServerError(errors.New(""))
return
} }
c.CustomAbort(http.StatusBadRequest, err.Error()) c.SendBadRequestError(err)
return
} }
if err := c.cfgManager.UpdateConfig(m); err != nil { if err := c.cfgManager.UpdateConfig(m); err != nil {
log.Errorf("failed to upload configurations: %v", err) log.Errorf("failed to upload configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) c.SendInternalServerError(errors.New(""))
return
} }
} }

View File

@ -15,8 +15,8 @@
package api package api
import ( import (
"errors"
"net" "net"
"net/http"
"strconv" "strconv"
"github.com/goharbor/harbor/src/common/utils/email" "github.com/goharbor/harbor/src/common/utils/email"
@ -37,12 +37,12 @@ type EmailAPI struct {
func (e *EmailAPI) Prepare() { func (e *EmailAPI) Prepare() {
e.BaseController.Prepare() e.BaseController.Prepare()
if !e.SecurityCtx.IsAuthenticated() { if !e.SecurityCtx.IsAuthenticated() {
e.HandleUnauthorized() e.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
if !e.SecurityCtx.IsSysAdmin() { if !e.SecurityCtx.IsSysAdmin() {
e.HandleForbidden(e.SecurityCtx.GetUsername()) e.SendForbiddenError(errors.New(e.SecurityCtx.GetUsername()))
return return
} }
} }
@ -57,8 +57,8 @@ func (e *EmailAPI) Ping() {
cfg, err := config.Email() cfg, err := config.Email()
if err != nil { if err != nil {
log.Errorf("failed to get email configurations: %v", err) log.Errorf("failed to get email configurations: %v", err)
e.CustomAbort(http.StatusInternalServerError, e.SendInternalServerError(err)
http.StatusText(http.StatusInternalServerError)) return
} }
host = cfg.Host host = cfg.Host
port = cfg.Port port = cfg.Port
@ -77,18 +77,22 @@ func (e *EmailAPI) Ping() {
Identity string `json:"email_identity"` Identity string `json:"email_identity"`
Insecure bool `json:"email_insecure"` Insecure bool `json:"email_insecure"`
}{} }{}
e.DecodeJSONReq(&settings) if err := e.DecodeJSONReq(&settings); err != nil {
e.SendBadRequestError(err)
return
}
if len(settings.Host) == 0 || settings.Port == nil { if len(settings.Host) == 0 || settings.Port == nil {
e.CustomAbort(http.StatusBadRequest, "empty host or port") e.SendBadRequestError(errors.New("empty host or port"))
return
} }
if settings.Password == nil { if settings.Password == nil {
cfg, err := config.Email() cfg, err := config.Email()
if err != nil { if err != nil {
log.Errorf("failed to get email configurations: %v", err) log.Errorf("failed to get email configurations: %v", err)
e.CustomAbort(http.StatusInternalServerError, e.SendInternalServerError(err)
http.StatusText(http.StatusInternalServerError)) return
} }
settings.Password = &cfg.Password settings.Password = &cfg.Password
@ -108,7 +112,7 @@ func (e *EmailAPI) Ping() {
password, pingEmailTimeout, ssl, insecure); err != nil { password, pingEmailTimeout, ssl, insecure); err != nil {
log.Errorf("failed to ping email server: %v", err) log.Errorf("failed to ping email server: %v", err)
// do not return any detail information of the error, or may cause SSRF security issue #3755 // do not return any detail information of the error, or may cause SSRF security issue #3755
e.RenderError(http.StatusBadRequest, "failed to ping email server") e.SendBadRequestError(errors.New("failed to ping email server"))
return return
} }
} }

View File

@ -15,7 +15,7 @@
package api package api
import ( import (
"net/http" "errors"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
@ -32,11 +32,11 @@ type InternalAPI struct {
func (ia *InternalAPI) Prepare() { func (ia *InternalAPI) Prepare() {
ia.BaseController.Prepare() ia.BaseController.Prepare()
if !ia.SecurityCtx.IsAuthenticated() { if !ia.SecurityCtx.IsAuthenticated() {
ia.HandleUnauthorized() ia.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
if !ia.SecurityCtx.IsSysAdmin() { if !ia.SecurityCtx.IsSysAdmin() {
ia.HandleForbidden(ia.SecurityCtx.GetUsername()) ia.SendForbiddenError(errors.New(ia.SecurityCtx.GetUsername()))
return return
} }
} }
@ -45,7 +45,7 @@ func (ia *InternalAPI) Prepare() {
func (ia *InternalAPI) SyncRegistry() { func (ia *InternalAPI) SyncRegistry() {
err := SyncRegistry(ia.ProjectMgr) err := SyncRegistry(ia.ProjectMgr)
if err != nil { if err != nil {
ia.HandleInternalServerError(err.Error()) ia.SendInternalServerError(err)
return return
} }
} }
@ -54,7 +54,8 @@ func (ia *InternalAPI) SyncRegistry() {
func (ia *InternalAPI) RenameAdmin() { func (ia *InternalAPI) RenameAdmin() {
if !dao.IsSuperUser(ia.SecurityCtx.GetUsername()) { if !dao.IsSuperUser(ia.SecurityCtx.GetUsername()) {
log.Errorf("User %s is not super user, not allow to rename admin.", ia.SecurityCtx.GetUsername()) log.Errorf("User %s is not super user, not allow to rename admin.", ia.SecurityCtx.GetUsername())
ia.CustomAbort(http.StatusForbidden, "") ia.SendForbiddenError(errors.New(ia.SecurityCtx.GetUsername()))
return
} }
newName := common.NewHarborAdminName newName := common.NewHarborAdminName
if err := dao.ChangeUserProfile(models.User{ if err := dao.ChangeUserProfile(models.User{
@ -62,7 +63,8 @@ func (ia *InternalAPI) RenameAdmin() {
Username: newName, Username: newName,
}, "username"); err != nil { }, "username"); err != nil {
log.Errorf("Failed to change admin's username, error: %v", err) log.Errorf("Failed to change admin's username, error: %v", err)
ia.CustomAbort(http.StatusInternalServerError, "Failed to rename admin user.") ia.SendInternalServerError(errors.New("failed to rename admin user"))
return
} }
log.Debugf("The super user has been renamed to: %s", newName) log.Debugf("The super user has been renamed to: %s", newName)
ia.DestroySession() ia.DestroySession()

View File

@ -15,6 +15,7 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -41,25 +42,25 @@ func (l *LabelAPI) Prepare() {
// POST, PUT, DELETE need login first // POST, PUT, DELETE need login first
if !l.SecurityCtx.IsAuthenticated() { if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized() l.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
if method == http.MethodPut || method == http.MethodDelete { if method == http.MethodPut || method == http.MethodDelete {
id, err := l.GetInt64FromPath(":id") id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 { if err != nil || id <= 0 {
l.HandleBadRequest("invalid label ID") l.SendBadRequestError(errors.New("invalid lable ID"))
return return
} }
label, err := dao.GetLabel(id) label, err := dao.GetLabel(id)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err)) l.SendInternalServerError(fmt.Errorf("failed to get label %d: %v", id, err))
return return
} }
if label == nil || label.Deleted { if label == nil || label.Deleted {
l.HandleNotFound(fmt.Sprintf("label %d not found", id)) l.SendNotFoundError(fmt.Errorf("label %d not found", id))
return return
} }
@ -83,9 +84,9 @@ func (l *LabelAPI) requireAccess(label *models.Label, action rbac.Action, subres
if !hasPermission { if !hasPermission {
if !l.SecurityCtx.IsAuthenticated() { if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized() l.SendUnAuthorizedError(errors.New("UnAuthorized"))
} else { } else {
l.HandleForbidden(l.SecurityCtx.GetUsername()) l.SendForbiddenError(errors.New(l.SecurityCtx.GetUsername()))
} }
return false return false
} }
@ -96,7 +97,12 @@ func (l *LabelAPI) requireAccess(label *models.Label, action rbac.Action, subres
// Post creates a label // Post creates a label
func (l *LabelAPI) Post() { func (l *LabelAPI) Post() {
label := &models.Label{} label := &models.Label{}
l.DecodeJSONReqAndValidate(label) isValid, err := l.DecodeJSONReqAndValidate(label)
if !isValid {
l.SendBadRequestError(err)
return
}
label.Level = common.LabelLevelUser label.Level = common.LabelLevelUser
switch label.Scope { switch label.Scope {
@ -105,12 +111,12 @@ func (l *LabelAPI) Post() {
case common.LabelScopeProject: case common.LabelScopeProject:
exist, err := l.ProjectMgr.Exists(label.ProjectID) exist, err := l.ProjectMgr.Exists(label.ProjectID)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %d: %v", l.SendInternalServerError(fmt.Errorf("failed to check the existence of project %d: %v",
label.ProjectID, err)) label.ProjectID, err))
return return
} }
if !exist { if !exist {
l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID)) l.SendNotFoundError(fmt.Errorf("project %d not found", label.ProjectID))
return return
} }
} }
@ -126,17 +132,17 @@ func (l *LabelAPI) Post() {
ProjectID: label.ProjectID, ProjectID: label.ProjectID,
}) })
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err)) l.SendInternalServerError(fmt.Errorf("failed to list labels: %v", err))
return return
} }
if len(labels) > 0 { if len(labels) > 0 {
l.HandleConflict() l.SendConflictError(errors.New("conflict label"))
return return
} }
id, err := dao.AddLabel(label) id, err := dao.AddLabel(label)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to create label: %v", err)) l.SendInternalServerError(fmt.Errorf("failed to create label: %v", err))
return return
} }
@ -147,18 +153,18 @@ func (l *LabelAPI) Post() {
func (l *LabelAPI) Get() { func (l *LabelAPI) Get() {
id, err := l.GetInt64FromPath(":id") id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 { if err != nil || id <= 0 {
l.HandleBadRequest(fmt.Sprintf("invalid label ID: %s", l.GetStringFromPath(":id"))) l.SendBadRequestError(fmt.Errorf("invalid label ID: %s", l.GetStringFromPath(":id")))
return return
} }
label, err := dao.GetLabel(id) label, err := dao.GetLabel(id)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err)) l.SendInternalServerError(fmt.Errorf("failed to get label %d: %v", id, err))
return return
} }
if label == nil || label.Deleted { if label == nil || label.Deleted {
l.HandleNotFound(fmt.Sprintf("label %d not found", id)) l.SendNotFoundError(fmt.Errorf("label %d not found", id))
return return
} }
@ -180,7 +186,7 @@ func (l *LabelAPI) List() {
scope := l.GetString("scope") scope := l.GetString("scope")
if scope != common.LabelScopeGlobal && scope != common.LabelScopeProject { if scope != common.LabelScopeGlobal && scope != common.LabelScopeProject {
l.HandleBadRequest(fmt.Sprintf("invalid scope: %s", scope)) l.SendBadRequestError(fmt.Errorf("invalid scope: %s", scope))
return return
} }
query.Scope = scope query.Scope = scope
@ -188,22 +194,22 @@ func (l *LabelAPI) List() {
if scope == common.LabelScopeProject { if scope == common.LabelScopeProject {
projectIDStr := l.GetString("project_id") projectIDStr := l.GetString("project_id")
if len(projectIDStr) == 0 { if len(projectIDStr) == 0 {
l.HandleBadRequest("project_id is required") l.SendBadRequestError(errors.New("project_id is required"))
return return
} }
projectID, err := strconv.ParseInt(projectIDStr, 10, 64) projectID, err := strconv.ParseInt(projectIDStr, 10, 64)
if err != nil || projectID <= 0 { if err != nil || projectID <= 0 {
l.HandleBadRequest(fmt.Sprintf("invalid project_id: %s", projectIDStr)) l.SendBadRequestError(fmt.Errorf("invalid project_id: %s", projectIDStr))
return return
} }
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceLabel) resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceLabel)
if !l.SecurityCtx.Can(rbac.ActionList, resource) { if !l.SecurityCtx.Can(rbac.ActionList, resource) {
if !l.SecurityCtx.IsAuthenticated() { if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized() l.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
l.HandleForbidden(l.SecurityCtx.GetUsername()) l.SendForbiddenError(errors.New(l.SecurityCtx.GetUsername()))
return return
} }
query.ProjectID = projectID query.ProjectID = projectID
@ -211,15 +217,19 @@ func (l *LabelAPI) List() {
total, err := dao.GetTotalOfLabels(query) total, err := dao.GetTotalOfLabels(query)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get total count of labels: %v", err)) l.SendInternalServerError(fmt.Errorf("failed to get total count of labels: %v", err))
return return
} }
query.Page, query.Size = l.GetPaginationParams() query.Page, query.Size, err = l.GetPaginationParams()
if err != nil {
l.SendBadRequestError(err)
return
}
labels, err := dao.ListLabels(query) labels, err := dao.ListLabels(query)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err)) l.SendInternalServerError(fmt.Errorf("failed to list labels: %v", err))
return return
} }
@ -235,7 +245,10 @@ func (l *LabelAPI) Put() {
} }
label := &models.Label{} label := &models.Label{}
l.DecodeJSONReq(label) if err := l.DecodeJSONReq(label); err != nil {
l.SendBadRequestError(err)
return
}
oldName := l.label.Name oldName := l.label.Name
@ -244,7 +257,13 @@ func (l *LabelAPI) Put() {
l.label.Description = label.Description l.label.Description = label.Description
l.label.Color = label.Color l.label.Color = label.Color
l.Validate(l.label) isValidate, err := l.Validate(l.label)
if !isValidate {
if err != nil {
l.SendBadRequestError(err)
return
}
}
if l.label.Name != oldName { if l.label.Name != oldName {
labels, err := dao.ListLabels(&models.LabelQuery{ labels, err := dao.ListLabels(&models.LabelQuery{
@ -254,17 +273,17 @@ func (l *LabelAPI) Put() {
ProjectID: l.label.ProjectID, ProjectID: l.label.ProjectID,
}) })
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err)) l.SendInternalServerError(fmt.Errorf("failed to list labels: %v", err))
return return
} }
if len(labels) > 0 { if len(labels) > 0 {
l.HandleConflict() l.SendConflictError(errors.New("conflict label"))
return return
} }
} }
if err := dao.UpdateLabel(l.label); err != nil { if err := dao.UpdateLabel(l.label); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to update label %d: %v", l.label.ID, err)) l.SendInternalServerError(fmt.Errorf("failed to update label %d: %v", l.label.ID, err))
return return
} }
@ -278,11 +297,11 @@ func (l *LabelAPI) Delete() {
id := l.label.ID id := l.label.ID
if err := dao.DeleteResourceLabelByLabel(id); err != nil { if err := dao.DeleteResourceLabelByLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete resource label mappings of label %d: %v", id, err)) l.SendInternalServerError(fmt.Errorf("failed to delete resource label mappings of label %d: %v", id, err))
return return
} }
if err := dao.DeleteLabel(id); err != nil { if err := dao.DeleteLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err)) l.SendInternalServerError(fmt.Errorf("failed to delete label %d: %v", id, err))
return return
} }
} }
@ -291,18 +310,18 @@ func (l *LabelAPI) Delete() {
func (l *LabelAPI) ListResources() { func (l *LabelAPI) ListResources() {
id, err := l.GetInt64FromPath(":id") id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 { if err != nil || id <= 0 {
l.HandleBadRequest("invalid label ID") l.SendBadRequestError(errors.New("invalid label ID"))
return return
} }
label, err := dao.GetLabel(id) label, err := dao.GetLabel(id)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err)) l.SendInternalServerError(fmt.Errorf("failed to get label %d: %v", id, err))
return return
} }
if label == nil || label.Deleted { if label == nil || label.Deleted {
l.HandleNotFound(fmt.Sprintf("label %d not found", id)) l.SendNotFoundError(fmt.Errorf("label %d not found", id))
return return
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/auth"
"errors"
goldap "gopkg.in/ldap.v2" goldap "gopkg.in/ldap.v2"
) )
@ -43,17 +44,17 @@ const (
func (l *LdapAPI) Prepare() { func (l *LdapAPI) Prepare() {
l.BaseController.Prepare() l.BaseController.Prepare()
if !l.SecurityCtx.IsAuthenticated() { if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized() l.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
if !l.SecurityCtx.IsSysAdmin() { if !l.SecurityCtx.IsSysAdmin() {
l.HandleForbidden(l.SecurityCtx.GetUsername()) l.SendForbiddenError(errors.New(l.SecurityCtx.GetUsername()))
return return
} }
ldapCfg, err := ldapUtils.LoadSystemLdapConfig() ldapCfg, err := ldapUtils.LoadSystemLdapConfig()
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("Can't load system configuration, error: %v", err)) l.SendInternalServerError(fmt.Errorf("Can't load system configuration, error: %v", err))
return return
} }
l.ldapConfig = ldapCfg l.ldapConfig = ldapCfg
@ -73,12 +74,16 @@ func (l *LdapAPI) Ping() {
ldapSession := *l.ldapConfig ldapSession := *l.ldapConfig
err = ldapSession.ConnectionTest() err = ldapSession.ConnectionTest()
} else { } else {
l.DecodeJSONReqAndValidate(&ldapConfs) isValid, err := l.DecodeJSONReqAndValidate(&ldapConfs)
if !isValid {
l.SendBadRequestError(err)
return
}
err = ldapUtils.ConnectionTestWithConfig(ldapConfs) err = ldapUtils.ConnectionTestWithConfig(ldapConfs)
} }
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("LDAP connect fail, error: %v", err)) l.SendInternalServerError(fmt.Errorf("LDAP connect fail, error: %v", err))
return return
} }
} }
@ -89,7 +94,7 @@ func (l *LdapAPI) Search() {
var ldapUsers []models.LdapUser var ldapUsers []models.LdapUser
ldapSession := *l.ldapConfig ldapSession := *l.ldapConfig
if err = ldapSession.Open(); err != nil { if err = ldapSession.Open(); err != nil {
l.HandleInternalServerError(fmt.Sprintf("Can't Open LDAP session, error: %v", err)) l.SendInternalServerError(fmt.Errorf("can't Open LDAP session, error: %v", err))
return return
} }
defer ldapSession.Close() defer ldapSession.Close()
@ -99,7 +104,7 @@ func (l *LdapAPI) Search() {
ldapUsers, err = ldapSession.SearchUser(searchName) ldapUsers, err = ldapSession.SearchUser(searchName)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("LDAP search fail, error: %v", err)) l.SendInternalServerError(fmt.Errorf("LDAP search fail, error: %v", err))
return return
} }
@ -113,18 +118,22 @@ func (l *LdapAPI) ImportUser() {
var ldapImportUsers models.LdapImportUser var ldapImportUsers models.LdapImportUser
var ldapFailedImportUsers []models.LdapFailedImportUser var ldapFailedImportUsers []models.LdapFailedImportUser
l.DecodeJSONReqAndValidate(&ldapImportUsers) isValid, err := l.DecodeJSONReqAndValidate(&ldapImportUsers)
if !isValid {
l.SendBadRequestError(err)
return
}
ldapFailedImportUsers, err := importUsers(ldapImportUsers.LdapUIDList, l.ldapConfig) ldapFailedImportUsers, err = importUsers(ldapImportUsers.LdapUIDList, l.ldapConfig)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("LDAP import user fail, error: %v", err)) l.SendInternalServerError(fmt.Errorf("LDAP import user fail, error: %v", err))
return return
} }
if len(ldapFailedImportUsers) > 0 { if len(ldapFailedImportUsers) > 0 {
// Some user require json format response. // Some user require json format response.
l.HandleNotFound("") l.SendNotFoundError(errors.New("ldap user is not found"))
l.Data["json"] = ldapFailedImportUsers l.Data["json"] = ldapFailedImportUsers
l.ServeJSON() l.ServeJSON()
return return
@ -206,23 +215,23 @@ func (l *LdapAPI) SearchGroup() {
if len(searchName) > 0 { if len(searchName) > 0 {
ldapGroups, err = ldapSession.SearchGroupByName(searchName) ldapGroups, err = ldapSession.SearchGroupByName(searchName)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("Can't search LDAP group by name, error: %v", err)) l.SendInternalServerError(fmt.Errorf("can't search LDAP group by name, error: %v", err))
return return
} }
} else if len(groupDN) > 0 { } else if len(groupDN) > 0 {
if _, err := goldap.ParseDN(groupDN); err != nil { if _, err := goldap.ParseDN(groupDN); err != nil {
l.HandleBadRequest(fmt.Sprintf("Invalid DN: %v", err)) l.SendBadRequestError(fmt.Errorf("invalid DN: %v", err))
return return
} }
ldapGroups, err = ldapSession.SearchGroupByDN(groupDN) ldapGroups, err = ldapSession.SearchGroupByDN(groupDN)
if err != nil { if err != nil {
// OpenLDAP usually return an error if DN is not found // OpenLDAP usually return an error if DN is not found
l.HandleNotFound(fmt.Sprintf("Search LDAP group fail, error: %v", err)) l.SendNotFoundError(fmt.Errorf("search LDAP group fail, error: %v", err))
return return
} }
} }
if len(ldapGroups) == 0 { if len(ldapGroups) == 0 {
l.HandleNotFound("No ldap group found") l.SendNotFoundError(errors.New("No ldap group found"))
return return
} }
l.Data["json"] = ldapGroups l.Data["json"] = ldapGroups

View File

@ -17,6 +17,7 @@ package api
import ( import (
"fmt" "fmt"
"errors"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
@ -33,7 +34,7 @@ type LogAPI struct {
func (l *LogAPI) Prepare() { func (l *LogAPI) Prepare() {
l.BaseController.Prepare() l.BaseController.Prepare()
if !l.SecurityCtx.IsAuthenticated() { if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized() l.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
l.username = l.SecurityCtx.GetUsername() l.username = l.SecurityCtx.GetUsername()
@ -42,7 +43,11 @@ func (l *LogAPI) Prepare() {
// Get returns the recent logs according to parameters // Get returns the recent logs according to parameters
func (l *LogAPI) Get() { func (l *LogAPI) Get() {
page, size := l.GetPaginationParams() page, size, err := l.GetPaginationParams()
if err != nil {
l.SendBadRequestError(err)
return
}
query := &models.LogQueryParam{ query := &models.LogQueryParam{
Username: l.GetString("username"), Username: l.GetString("username"),
Repository: l.GetString("repository"), Repository: l.GetString("repository"),
@ -58,7 +63,7 @@ func (l *LogAPI) Get() {
if len(timestamp) > 0 { if len(timestamp) > 0 {
t, err := utils.ParseTimeStamp(timestamp) t, err := utils.ParseTimeStamp(timestamp)
if err != nil { if err != nil {
l.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp)) l.SendBadRequestError(fmt.Errorf("invalid begin_timestamp: %s", timestamp))
return return
} }
query.BeginTime = t query.BeginTime = t
@ -68,7 +73,7 @@ func (l *LogAPI) Get() {
if len(timestamp) > 0 { if len(timestamp) > 0 {
t, err := utils.ParseTimeStamp(timestamp) t, err := utils.ParseTimeStamp(timestamp)
if err != nil { if err != nil {
l.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp)) l.SendBadRequestError(fmt.Errorf("invalid end_timestamp: %s", timestamp))
return return
} }
query.EndTime = t query.EndTime = t
@ -77,7 +82,7 @@ func (l *LogAPI) Get() {
if !l.isSysAdmin { if !l.isSysAdmin {
projects, err := l.SecurityCtx.GetMyProjects() projects, err := l.SecurityCtx.GetMyProjects()
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf( l.SendInternalServerError(fmt.Errorf(
"failed to get projects of user %s: %v", l.username, err)) "failed to get projects of user %s: %v", l.username, err))
return return
} }
@ -98,14 +103,14 @@ func (l *LogAPI) Get() {
total, err := dao.GetTotalOfAccessLogs(query) total, err := dao.GetTotalOfAccessLogs(query)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf( l.SendInternalServerError(fmt.Errorf(
"failed to get total of access logs: %v", err)) "failed to get total of access logs: %v", err))
return return
} }
logs, err := dao.GetAccessLogs(query) logs, err := dao.GetAccessLogs(query)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf( l.SendInternalServerError(fmt.Errorf(
"failed to get access logs: %v", err)) "failed to get access logs: %v", err))
return return
} }

View File

@ -21,6 +21,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"errors"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
@ -56,7 +57,7 @@ func (m *MetadataAPI) Prepare() {
} else { } else {
text += fmt.Sprintf("%d", id) text += fmt.Sprintf("%d", id)
} }
m.HandleBadRequest(text) m.SendBadRequestError(errors.New(text))
return return
} }
@ -67,7 +68,7 @@ func (m *MetadataAPI) Prepare() {
} }
if project == nil { if project == nil {
m.HandleNotFound(fmt.Sprintf("project %d not found", id)) m.SendNotFoundError(fmt.Errorf("project %d not found", id))
return return
} }
@ -78,11 +79,11 @@ func (m *MetadataAPI) Prepare() {
m.name = name m.name = name
metas, err := m.metaMgr.Get(project.ProjectID, name) metas, err := m.metaMgr.Get(project.ProjectID, name)
if err != nil { if err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to get metadata of project %d: %v", project.ProjectID, err)) m.SendInternalServerError(fmt.Errorf("failed to get metadata of project %d: %v", project.ProjectID, err))
return return
} }
if len(metas) == 0 { if len(metas) == 0 {
m.HandleNotFound(fmt.Sprintf("metadata %s of project %d not found", name, project.ProjectID)) m.SendNotFoundError(fmt.Errorf("metadata %s of project %d not found", name, project.ProjectID))
return return
} }
} }
@ -93,9 +94,9 @@ func (m *MetadataAPI) requireAccess(action rbac.Action) bool {
if !m.SecurityCtx.Can(action, resource) { if !m.SecurityCtx.Can(action, resource) {
if !m.SecurityCtx.IsAuthenticated() { if !m.SecurityCtx.IsAuthenticated() {
m.HandleUnauthorized() m.SendUnAuthorizedError(errors.New("Unauthorized"))
} else { } else {
m.HandleForbidden(m.SecurityCtx.GetUsername()) m.SendForbiddenError(errors.New(m.SecurityCtx.GetUsername()))
} }
return false return false
} }
@ -118,7 +119,7 @@ func (m *MetadataAPI) Get() {
} }
if err != nil { if err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to get metadata %s of project %d: %v", m.name, m.project.ProjectID, err)) m.SendInternalServerError(fmt.Errorf("failed to get metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
return return
} }
m.Data["json"] = metas m.Data["json"] = metas
@ -132,33 +133,36 @@ func (m *MetadataAPI) Post() {
} }
var metas map[string]string var metas map[string]string
m.DecodeJSONReq(&metas) if err := m.DecodeJSONReq(&metas); err != nil {
m.SendBadRequestError(err)
return
}
ms, err := validateProjectMetadata(metas) ms, err := validateProjectMetadata(metas)
if err != nil { if err != nil {
m.HandleBadRequest(err.Error()) m.SendBadRequestError(err)
return return
} }
if len(ms) != 1 { if len(ms) != 1 {
m.HandleBadRequest("invalid request: has no valid key/value pairs or has more than one valid key/value pairs") m.SendBadRequestError(errors.New("invalid request: has no valid key/value pairs or has more than one valid key/value pairs"))
return return
} }
keys := reflect.ValueOf(ms).MapKeys() keys := reflect.ValueOf(ms).MapKeys()
mts, err := m.metaMgr.Get(m.project.ProjectID, keys[0].String()) mts, err := m.metaMgr.Get(m.project.ProjectID, keys[0].String())
if err != nil { if err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to get metadata for project %d: %v", m.project.ProjectID, err)) m.SendInternalServerError(fmt.Errorf("failed to get metadata for project %d: %v", m.project.ProjectID, err))
return return
} }
if len(mts) != 0 { if len(mts) != 0 {
m.HandleConflict() m.SendConflictError(errors.New("conflict metadata"))
return return
} }
if err := m.metaMgr.Add(m.project.ProjectID, ms); err != nil { if err := m.metaMgr.Add(m.project.ProjectID, ms); err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to create metadata for project %d: %v", m.project.ProjectID, err)) m.SendInternalServerError(fmt.Errorf("failed to create metadata for project %d: %v", m.project.ProjectID, err))
return return
} }
@ -172,11 +176,14 @@ func (m *MetadataAPI) Put() {
} }
var metas map[string]string var metas map[string]string
m.DecodeJSONReq(&metas) if err := m.DecodeJSONReq(&metas); err != nil {
m.SendBadRequestError(err)
return
}
meta, exist := metas[m.name] meta, exist := metas[m.name]
if !exist { if !exist {
m.HandleBadRequest(fmt.Sprintf("must contains key %s", m.name)) m.SendBadRequestError(fmt.Errorf("must contains key %s", m.name))
return return
} }
@ -184,14 +191,14 @@ func (m *MetadataAPI) Put() {
m.name: meta, m.name: meta,
}) })
if err != nil { if err != nil {
m.HandleBadRequest(err.Error()) m.SendBadRequestError(err)
return return
} }
if err := m.metaMgr.Update(m.project.ProjectID, map[string]string{ if err := m.metaMgr.Update(m.project.ProjectID, map[string]string{
m.name: ms[m.name], m.name: ms[m.name],
}); err != nil { }); err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to update metadata %s of project %d: %v", m.name, m.project.ProjectID, err)) m.SendInternalServerError(fmt.Errorf("failed to update metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
return return
} }
} }
@ -203,7 +210,7 @@ func (m *MetadataAPI) Delete() {
} }
if err := m.metaMgr.Delete(m.project.ProjectID, m.name); err != nil { if err := m.metaMgr.Delete(m.project.ProjectID, m.name); err != nil {
m.HandleInternalServerError(fmt.Sprintf("failed to delete metadata %s of project %d: %v", m.name, m.project.ProjectID, err)) m.SendInternalServerError(fmt.Errorf("failed to delete metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
return return
} }
} }

View File

@ -28,6 +28,7 @@ import (
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
"errors"
"strconv" "strconv"
"time" "time"
) )
@ -59,7 +60,7 @@ func (p *ProjectAPI) Prepare() {
} else { } else {
text += fmt.Sprintf("%d", id) text += fmt.Sprintf("%d", id)
} }
p.HandleBadRequest(text) p.SendBadRequestError(errors.New(text))
return return
} }
@ -70,7 +71,7 @@ func (p *ProjectAPI) Prepare() {
} }
if project == nil { if project == nil {
p.HandleNotFound(fmt.Sprintf("project %d not found", id)) p.SendNotFoundError(fmt.Errorf("project %d not found", id))
return return
} }
@ -86,9 +87,10 @@ func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resou
if !p.SecurityCtx.Can(action, resource) { if !p.SecurityCtx.Can(action, resource) {
if !p.SecurityCtx.IsAuthenticated() { if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized() p.SendUnAuthorizedError(errors.New("Unauthorized"))
} else { } else {
p.HandleForbidden(p.SecurityCtx.GetUsername()) p.SendForbiddenError(errors.New(p.SecurityCtx.GetUsername()))
} }
return false return false
@ -100,7 +102,7 @@ func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resou
// Post ... // Post ...
func (p *ProjectAPI) Post() { func (p *ProjectAPI) Post() {
if !p.SecurityCtx.IsAuthenticated() { if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized() p.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
var onlyAdmin bool var onlyAdmin bool
@ -111,21 +113,25 @@ func (p *ProjectAPI) Post() {
onlyAdmin, err = config.OnlyAdminCreateProject() onlyAdmin, err = config.OnlyAdminCreateProject()
if err != nil { if err != nil {
log.Errorf("failed to determine whether only admin can create projects: %v", err) log.Errorf("failed to determine whether only admin can create projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) p.SendInternalServerError(fmt.Errorf("failed to determine whether only admin can create projects: %v", err))
return
} }
} }
if onlyAdmin && !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) { if onlyAdmin && !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
log.Errorf("Only sys admin can create project") log.Errorf("Only sys admin can create project")
p.RenderError(http.StatusForbidden, "Only system admin can create project") p.SendForbiddenError(errors.New("Only system admin can create project"))
return return
} }
var pro *models.ProjectRequest var pro *models.ProjectRequest
p.DecodeJSONReq(&pro) if err := p.DecodeJSONReq(&pro); err != nil {
p.SendBadRequestError(err)
return
}
err = validateProjectReq(pro) err = validateProjectReq(pro)
if err != nil { if err != nil {
log.Errorf("Invalid project request, error: %v", err) log.Errorf("Invalid project request, error: %v", err)
p.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) p.SendBadRequestError(fmt.Errorf("invalid request: %v", err))
return return
} }
@ -136,7 +142,7 @@ func (p *ProjectAPI) Post() {
return return
} }
if exist { if exist {
p.RenderError(http.StatusConflict, "") p.SendConflictError(errors.New("conflict project"))
return return
} }
@ -162,7 +168,7 @@ func (p *ProjectAPI) Post() {
UserID: 1, UserID: 1,
}) })
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to get the user 1: %v", err)) p.SendInternalServerError(fmt.Errorf("failed to get the user 1: %v", err))
return return
} }
owner = user.Username owner = user.Username
@ -175,7 +181,7 @@ func (p *ProjectAPI) Post() {
if err != nil { if err != nil {
if err == errutil.ErrDupProject { if err == errutil.ErrDupProject {
log.Debugf("conflict %s", pro.Name) log.Debugf("conflict %s", pro.Name)
p.RenderError(http.StatusConflict, "") p.SendConflictError(fmt.Errorf("conflict %s", pro.Name))
} else { } else {
p.ParseAndHandleError("failed to add project", err) p.ParseAndHandleError("failed to add project", err)
} }
@ -203,7 +209,7 @@ func (p *ProjectAPI) Post() {
func (p *ProjectAPI) Head() { func (p *ProjectAPI) Head() {
name := p.GetString("project_name") name := p.GetString("project_name")
if len(name) == 0 { if len(name) == 0 {
p.HandleBadRequest("project_name is needed") p.SendBadRequestError(errors.New("project_name is needed"))
return return
} }
@ -214,7 +220,7 @@ func (p *ProjectAPI) Head() {
} }
if project == nil { if project == nil {
p.HandleNotFound(fmt.Sprintf("project %s not found", name)) p.SendNotFoundError(fmt.Errorf("project %s not found", name))
return return
} }
} }
@ -239,12 +245,13 @@ func (p *ProjectAPI) Delete() {
result, err := p.deletable(p.project.ProjectID) result, err := p.deletable(p.project.ProjectID)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.SendInternalServerError(fmt.Errorf(
"failed to check the deletable of project %d: %v", p.project.ProjectID, err)) "failed to check the deletable of project %d: %v", p.project.ProjectID, err))
return return
} }
if !result.Deletable { if !result.Deletable {
p.CustomAbort(http.StatusPreconditionFailed, result.Message) p.SendPreconditionFailedError(errors.New(result.Message))
return
} }
if err = p.ProjectMgr.Delete(p.project.ProjectID); err != nil { if err = p.ProjectMgr.Delete(p.project.ProjectID); err != nil {
@ -274,7 +281,7 @@ func (p *ProjectAPI) Deletable() {
result, err := p.deletable(p.project.ProjectID) result, err := p.deletable(p.project.ProjectID)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.SendInternalServerError(fmt.Errorf(
"failed to check the deletable of project %d: %v", p.project.ProjectID, err)) "failed to check the deletable of project %d: %v", p.project.ProjectID, err))
return return
} }
@ -321,7 +328,11 @@ func (p *ProjectAPI) deletable(projectID int64) (*deletableResp, error) {
// List ... // List ...
func (p *ProjectAPI) List() { func (p *ProjectAPI) List() {
// query strings // query strings
page, size := p.GetPaginationParams() page, size, err := p.GetPaginationParams()
if err != nil {
p.SendBadRequestError(err)
return
}
query := &models.ProjectQueryParam{ query := &models.ProjectQueryParam{
Name: p.GetString("name"), Name: p.GetString("name"),
Owner: p.GetString("owner"), Owner: p.GetString("owner"),
@ -335,7 +346,7 @@ func (p *ProjectAPI) List() {
if len(public) > 0 { if len(public) > 0 {
pub, err := strconv.ParseBool(public) pub, err := strconv.ParseBool(public)
if err != nil { if err != nil {
p.HandleBadRequest(fmt.Sprintf("invalid public: %s", public)) p.SendBadRequestError(fmt.Errorf("invalid public: %s", public))
return return
} }
query.Public = &pub query.Public = &pub
@ -348,7 +359,7 @@ func (p *ProjectAPI) List() {
// not login, only get public projects // not login, only get public projects
pros, err := p.ProjectMgr.GetPublic() pros, err := p.ProjectMgr.GetPublic()
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to get public projects: %v", err)) p.SendInternalServerError(fmt.Errorf("failed to get public projects: %v", err))
return return
} }
projects = []*models.Project{} projects = []*models.Project{}
@ -360,13 +371,13 @@ func (p *ProjectAPI) List() {
// projects that the user is member of // projects that the user is member of
pros, err := p.ProjectMgr.GetPublic() pros, err := p.ProjectMgr.GetPublic()
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to get public projects: %v", err)) p.SendInternalServerError(fmt.Errorf("failed to get public projects: %v", err))
return return
} }
projects = append(projects, pros...) projects = append(projects, pros...)
mps, err := p.SecurityCtx.GetMyProjects() mps, err := p.SecurityCtx.GetMyProjects()
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to list projects: %v", err)) p.SendInternalServerError(fmt.Errorf("failed to list projects: %v", err))
return return
} }
projects = append(projects, mps...) projects = append(projects, mps...)
@ -416,7 +427,8 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
}) })
if err != nil { if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err) log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err)
p.CustomAbort(http.StatusInternalServerError, "") p.SendInternalServerError(errors.New(""))
return
} }
project.RepoCount = total project.RepoCount = total
@ -426,7 +438,8 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
count, err := chartController.GetCountOfCharts([]string{project.Name}) count, err := chartController.GetCountOfCharts([]string{project.Name})
if err != nil { if err != nil {
log.Errorf("Failed to get total of charts under project %s: %v", project.Name, err) log.Errorf("Failed to get total of charts under project %s: %v", project.Name, err)
p.CustomAbort(http.StatusInternalServerError, "") p.SendInternalServerError(errors.New(""))
return
} }
project.ChartCount = count project.ChartCount = count
@ -440,7 +453,10 @@ func (p *ProjectAPI) Put() {
} }
var req *models.ProjectRequest var req *models.ProjectRequest
p.DecodeJSONReq(&req) if err := p.DecodeJSONReq(&req); err != nil {
p.SendBadRequestError(err)
return
}
if err := p.ProjectMgr.Update(p.project.ProjectID, if err := p.ProjectMgr.Update(p.project.ProjectID,
&models.Project{ &models.Project{
@ -458,7 +474,11 @@ func (p *ProjectAPI) Logs() {
return return
} }
page, size := p.GetPaginationParams() page, size, err := p.GetPaginationParams()
if err != nil {
p.SendBadRequestError(err)
return
}
query := &models.LogQueryParam{ query := &models.LogQueryParam{
ProjectIDs: []int64{p.project.ProjectID}, ProjectIDs: []int64{p.project.ProjectID},
Username: p.GetString("username"), Username: p.GetString("username"),
@ -475,7 +495,7 @@ func (p *ProjectAPI) Logs() {
if len(timestamp) > 0 { if len(timestamp) > 0 {
t, err := utils.ParseTimeStamp(timestamp) t, err := utils.ParseTimeStamp(timestamp)
if err != nil { if err != nil {
p.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp)) p.SendBadRequestError(fmt.Errorf("invalid begin_timestamp: %s", timestamp))
return return
} }
query.BeginTime = t query.BeginTime = t
@ -485,7 +505,7 @@ func (p *ProjectAPI) Logs() {
if len(timestamp) > 0 { if len(timestamp) > 0 {
t, err := utils.ParseTimeStamp(timestamp) t, err := utils.ParseTimeStamp(timestamp)
if err != nil { if err != nil {
p.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp)) p.SendBadRequestError(fmt.Errorf("invalid end_timestamp: %s", timestamp))
return return
} }
query.EndTime = t query.EndTime = t
@ -493,14 +513,14 @@ func (p *ProjectAPI) Logs() {
total, err := dao.GetTotalOfAccessLogs(query) total, err := dao.GetTotalOfAccessLogs(query)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.SendInternalServerError(fmt.Errorf(
"failed to get total of access log: %v", err)) "failed to get total of access log: %v", err))
return return
} }
logs, err := dao.GetAccessLogs(query) logs, err := dao.GetAccessLogs(query)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.SendInternalServerError(fmt.Errorf(
"failed to get access log: %v", err)) "failed to get access log: %v", err))
return return
} }

View File

@ -50,7 +50,7 @@ func (pma *ProjectMemberAPI) Prepare() {
pma.BaseController.Prepare() pma.BaseController.Prepare()
if !pma.SecurityCtx.IsAuthenticated() { if !pma.SecurityCtx.IsAuthenticated() {
pma.HandleUnauthorized() pma.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
pid, err := pma.GetInt64FromPath(":pid") pid, err := pma.GetInt64FromPath(":pid")
@ -61,7 +61,7 @@ func (pma *ProjectMemberAPI) Prepare() {
} else { } else {
text += fmt.Sprintf("%d", pid) text += fmt.Sprintf("%d", pid)
} }
pma.HandleBadRequest(text) pma.SendBadRequestError(errors.New(text))
return return
} }
project, err := pma.ProjectMgr.Get(pid) project, err := pma.ProjectMgr.Get(pid)
@ -70,7 +70,7 @@ func (pma *ProjectMemberAPI) Prepare() {
return return
} }
if project == nil { if project == nil {
pma.HandleNotFound(fmt.Sprintf("project %d not found", pid)) pma.SendNotFoundError(fmt.Errorf("project %d not found", pid))
return return
} }
pma.project = project pma.project = project
@ -80,7 +80,7 @@ func (pma *ProjectMemberAPI) Prepare() {
log.Warningf("Failed to get pmid from path, error %v", err) log.Warningf("Failed to get pmid from path, error %v", err)
} }
if pmid <= 0 && (pma.Ctx.Input.IsPut() || pma.Ctx.Input.IsDelete()) { if pmid <= 0 && (pma.Ctx.Input.IsPut() || pma.Ctx.Input.IsDelete()) {
pma.HandleBadRequest(fmt.Sprintf("The project member id is invalid, pmid:%s", pma.GetStringFromPath(":pmid"))) pma.SendBadRequestError(fmt.Errorf("The project member id is invalid, pmid:%s", pma.GetStringFromPath(":pmid")))
return return
} }
pma.id = int(pmid) pma.id = int(pmid)
@ -91,9 +91,9 @@ func (pma *ProjectMemberAPI) requireAccess(action rbac.Action) bool {
if !pma.SecurityCtx.Can(action, resource) { if !pma.SecurityCtx.Can(action, resource) {
if !pma.SecurityCtx.IsAuthenticated() { if !pma.SecurityCtx.IsAuthenticated() {
pma.HandleUnauthorized() pma.SendUnAuthorizedError(errors.New("Unauthorized"))
} else { } else {
pma.HandleForbidden(pma.SecurityCtx.GetUsername()) pma.SendForbiddenError(errors.New(pma.SecurityCtx.GetUsername()))
} }
return false return false
@ -115,7 +115,7 @@ func (pma *ProjectMemberAPI) Get() {
entityname := pma.GetString("entityname") entityname := pma.GetString("entityname")
memberList, err := project.SearchMemberByName(projectID, entityname) memberList, err := project.SearchMemberByName(projectID, entityname)
if err != nil { if err != nil {
pma.HandleInternalServerError(fmt.Sprintf("Failed to query database for member list, error: %v", err)) pma.SendInternalServerError(fmt.Errorf("Failed to query database for member list, error: %v", err))
return return
} }
if len(memberList) > 0 { if len(memberList) > 0 {
@ -127,11 +127,11 @@ func (pma *ProjectMemberAPI) Get() {
queryMember.ID = pma.id queryMember.ID = pma.id
memberList, err := project.GetProjectMember(queryMember) memberList, err := project.GetProjectMember(queryMember)
if err != nil { if err != nil {
pma.HandleInternalServerError(fmt.Sprintf("Failed to query database for member list, error: %v", err)) pma.SendInternalServerError(fmt.Errorf("Failed to query database for member list, error: %v", err))
return return
} }
if len(memberList) == 0 { if len(memberList) == 0 {
pma.HandleNotFound(fmt.Sprintf("The project member does not exit, pmid:%v", pma.id)) pma.SendNotFoundError(fmt.Errorf("The project member does not exit, pmid:%v", pma.id))
return return
} }
@ -150,27 +150,30 @@ func (pma *ProjectMemberAPI) Post() {
} }
projectID := pma.project.ProjectID projectID := pma.project.ProjectID
var request models.MemberReq var request models.MemberReq
pma.DecodeJSONReq(&request) if err := pma.DecodeJSONReq(&request); err != nil {
pma.SendBadRequestError(err)
return
}
request.MemberGroup.LdapGroupDN = strings.TrimSpace(request.MemberGroup.LdapGroupDN) request.MemberGroup.LdapGroupDN = strings.TrimSpace(request.MemberGroup.LdapGroupDN)
pmid, err := AddProjectMember(projectID, request) pmid, err := AddProjectMember(projectID, request)
if err == auth.ErrorGroupNotExist || err == auth.ErrorUserNotExist { if err == auth.ErrorGroupNotExist || err == auth.ErrorUserNotExist {
pma.HandleNotFound(fmt.Sprintf("Failed to add project member, error: %v", err)) pma.SendNotFoundError(fmt.Errorf("Failed to add project member, error: %v", err))
return return
} else if err == auth.ErrDuplicateLDAPGroup { } else if err == auth.ErrDuplicateLDAPGroup {
pma.HandleConflict(fmt.Sprintf("Failed to add project member, already exist LDAP group or project member, groupDN:%v", request.MemberGroup.LdapGroupDN)) pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist LDAP group or project member, groupDN:%v", request.MemberGroup.LdapGroupDN))
return return
} else if err == ErrDuplicateProjectMember { } else if err == ErrDuplicateProjectMember {
pma.HandleConflict(fmt.Sprintf("Failed to add project member, already exist LDAP group or project member, groupMemberID:%v", request.MemberGroup.ID)) pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist LDAP group or project member, groupMemberID:%v", request.MemberGroup.ID))
return return
} else if err == ErrInvalidRole { } else if err == ErrInvalidRole {
pma.HandleBadRequest(fmt.Sprintf("Invalid role ID, role ID %v", request.Role)) pma.SendBadRequestError(fmt.Errorf("Invalid role ID, role ID %v", request.Role))
return return
} else if err == auth.ErrInvalidLDAPGroupDN { } else if err == auth.ErrInvalidLDAPGroupDN {
pma.HandleBadRequest(fmt.Sprintf("Invalid LDAP DN: %v", request.MemberGroup.LdapGroupDN)) pma.SendBadRequestError(fmt.Errorf("Invalid LDAP DN: %v", request.MemberGroup.LdapGroupDN))
return return
} else if err != nil { } else if err != nil {
pma.HandleInternalServerError(fmt.Sprintf("Failed to add project member, error: %v", err)) pma.SendInternalServerError(fmt.Errorf("Failed to add project member, error: %v", err))
return return
} }
pma.Redirect(http.StatusCreated, strconv.FormatInt(int64(pmid), 10)) pma.Redirect(http.StatusCreated, strconv.FormatInt(int64(pmid), 10))
@ -184,14 +187,17 @@ func (pma *ProjectMemberAPI) Put() {
pid := pma.project.ProjectID pid := pma.project.ProjectID
pmID := pma.id pmID := pma.id
var req models.Member var req models.Member
pma.DecodeJSONReq(&req) if err := pma.DecodeJSONReq(&req); err != nil {
pma.SendBadRequestError(err)
return
}
if req.Role < 1 || req.Role > 4 { if req.Role < 1 || req.Role > 4 {
pma.HandleBadRequest(fmt.Sprintf("Invalid role id %v", req.Role)) pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role))
return return
} }
err := project.UpdateProjectMemberRole(pmID, req.Role) err := project.UpdateProjectMemberRole(pmID, req.Role)
if err != nil { if err != nil {
pma.HandleInternalServerError(fmt.Sprintf("Failed to update DB to add project user role, project id: %d, pmid : %d, role id: %d", pid, pmID, req.Role)) pma.SendInternalServerError(fmt.Errorf("Failed to update DB to add project user role, project id: %d, pmid : %d, role id: %d", pid, pmID, req.Role))
return return
} }
} }
@ -204,7 +210,7 @@ func (pma *ProjectMemberAPI) Delete() {
pmid := pma.id pmid := pma.id
err := project.DeleteProjectMemberByID(pmid) err := project.DeleteProjectMemberByID(pmid)
if err != nil { if err != nil {
pma.HandleInternalServerError(fmt.Sprintf("Failed to delete project roles for user, project member id: %d, error: %v", pmid, err)) pma.SendInternalServerError(fmt.Errorf("Failed to delete project roles for user, project member id: %d, error: %v", pmid, err))
return return
} }
} }

View File

@ -15,7 +15,7 @@
package api package api
import ( import (
"fmt" "errors"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
@ -33,11 +33,11 @@ type GCAPI struct {
func (gc *GCAPI) Prepare() { func (gc *GCAPI) Prepare() {
gc.BaseController.Prepare() gc.BaseController.Prepare()
if !gc.SecurityCtx.IsAuthenticated() { if !gc.SecurityCtx.IsAuthenticated() {
gc.HandleUnauthorized() gc.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
if !gc.SecurityCtx.IsSysAdmin() { if !gc.SecurityCtx.IsSysAdmin() {
gc.HandleForbidden(gc.SecurityCtx.GetUsername()) gc.SendForbiddenError(errors.New(gc.SecurityCtx.GetUsername()))
return return
} }
} }
@ -58,7 +58,11 @@ func (gc *GCAPI) Prepare() {
// } // }
func (gc *GCAPI) Post() { func (gc *GCAPI) Post() {
ajr := models.AdminJobReq{} ajr := models.AdminJobReq{}
gc.DecodeJSONReqAndValidate(&ajr) isValid, err := gc.DecodeJSONReqAndValidate(&ajr)
if !isValid {
gc.SendBadRequestError(err)
return
}
ajr.Name = common_job.ImageGC ajr.Name = common_job.ImageGC
ajr.Parameters = map[string]interface{}{ ajr.Parameters = map[string]interface{}{
"redis_url_reg": os.Getenv("_REDIS_URL_REG"), "redis_url_reg": os.Getenv("_REDIS_URL_REG"),
@ -77,7 +81,11 @@ func (gc *GCAPI) Post() {
// } // }
func (gc *GCAPI) Put() { func (gc *GCAPI) Put() {
ajr := models.AdminJobReq{} ajr := models.AdminJobReq{}
gc.DecodeJSONReqAndValidate(&ajr) isValid, err := gc.DecodeJSONReqAndValidate(&ajr)
if !isValid {
gc.SendBadRequestError(err)
return
}
ajr.Name = common_job.ImageGC ajr.Name = common_job.ImageGC
gc.updateSchedule(ajr) gc.updateSchedule(ajr)
} }
@ -86,7 +94,7 @@ func (gc *GCAPI) Put() {
func (gc *GCAPI) GetGC() { func (gc *GCAPI) GetGC() {
id, err := gc.GetInt64FromPath(":id") id, err := gc.GetInt64FromPath(":id")
if err != nil { if err != nil {
gc.HandleInternalServerError(fmt.Sprintf("need to specify gc id")) gc.SendInternalServerError(errors.New("need to specify gc id"))
return return
} }
gc.get(id) gc.get(id)
@ -106,7 +114,7 @@ func (gc *GCAPI) Get() {
func (gc *GCAPI) GetLog() { func (gc *GCAPI) GetLog() {
id, err := gc.GetInt64FromPath(":id") id, err := gc.GetInt64FromPath(":id")
if err != nil { if err != nil {
gc.HandleBadRequest("invalid ID") gc.SendBadRequestError(errors.New("invalid ID"))
return return
} }
gc.getLog(id) gc.getLog(id)

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -28,12 +29,12 @@ type RegistryAPI struct {
func (t *RegistryAPI) Prepare() { func (t *RegistryAPI) Prepare() {
t.BaseController.Prepare() t.BaseController.Prepare()
if !t.SecurityCtx.IsAuthenticated() { if !t.SecurityCtx.IsAuthenticated() {
t.HandleUnauthorized() t.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
if !t.SecurityCtx.IsSysAdmin() { if !t.SecurityCtx.IsSysAdmin() {
t.HandleForbidden(t.SecurityCtx.GetUsername()) t.SendForbiddenError(errors.New(t.SecurityCtx.GetUsername()))
return return
} }
@ -59,12 +60,12 @@ func (t *RegistryAPI) Ping() {
if req.ID != nil { if req.ID != nil {
reg, err = t.manager.Get(*req.ID) reg, err = t.manager.Get(*req.ID)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", *req.ID, err)) t.SendInternalServerError(fmt.Errorf("failed to get registry %d: %v", *req.ID, err))
return return
} }
if reg == nil { if reg == nil {
t.HandleNotFound(fmt.Sprintf("registry %d not found", *req.ID)) t.SendNotFoundError(fmt.Errorf("registry %d not found", *req.ID))
return return
} }
} }
@ -74,7 +75,7 @@ func (t *RegistryAPI) Ping() {
if req.URL != nil { if req.URL != nil {
url, err := utils.ParseEndpoint(*req.URL) url, err := utils.ParseEndpoint(*req.URL)
if err != nil { if err != nil {
t.HandleBadRequest(err.Error()) t.SendBadRequestError(err)
return return
} }
@ -103,7 +104,7 @@ func (t *RegistryAPI) Ping() {
reg.Insecure = *req.Insecure reg.Insecure = *req.Insecure
} }
if len(reg.Type) == 0 || len(reg.URL) == 0 { if len(reg.Type) == 0 || len(reg.URL) == 0 {
t.HandleBadRequest("type or url cannot be empty") t.SendBadRequestError(errors.New("type or url cannot be empty"))
return return
} }
@ -111,15 +112,15 @@ func (t *RegistryAPI) Ping() {
if err != nil { if err != nil {
e, ok := err.(*common_http.Error) e, ok := err.(*common_http.Error)
if ok && e.Code == http.StatusUnauthorized { if ok && e.Code == http.StatusUnauthorized {
t.HandleBadRequest("invalid credential") t.SendBadRequestError(errors.New("invalid credential"))
return return
} }
t.HandleInternalServerError(fmt.Sprintf("failed to check health of registry %s: %v", reg.URL, err)) t.SendInternalServerError(fmt.Errorf("failed to check health of registry %s: %v", reg.URL, err))
return return
} }
if status != model.Healthy { if status != model.Healthy {
t.HandleBadRequest("") t.SendBadRequestError(errors.New(""))
return return
} }
return return
@ -127,17 +128,21 @@ func (t *RegistryAPI) Ping() {
// Get gets a registry by id. // Get gets a registry by id.
func (t *RegistryAPI) Get() { func (t *RegistryAPI) Get() {
id := t.GetIDFromURL() id, err := t.GetIDFromURL()
if err != nil {
t.SendBadRequestError(err)
return
}
r, err := t.manager.Get(id) r, err := t.manager.Get(id)
if err != nil { if err != nil {
log.Errorf("failed to get registry %d: %v", id, err) log.Errorf("failed to get registry %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
if r == nil { if r == nil {
t.HandleNotFound(fmt.Sprintf("registry %d not found", id)) t.SendNotFoundError(fmt.Errorf("registry %d not found", id))
return return
} }
@ -159,7 +164,7 @@ func (t *RegistryAPI) List() {
}) })
if err != nil { if err != nil {
log.Errorf("failed to list registries %s: %v", name, err) log.Errorf("failed to list registries %s: %v", name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
@ -178,34 +183,38 @@ func (t *RegistryAPI) List() {
// Post creates a registry // Post creates a registry
func (t *RegistryAPI) Post() { func (t *RegistryAPI) Post() {
r := &model.Registry{} r := &model.Registry{}
t.DecodeJSONReqAndValidate(r) isValid, err := t.DecodeJSONReqAndValidate(r)
if !isValid {
t.SendBadRequestError(err)
return
}
reg, err := t.manager.GetByName(r.Name) reg, err := t.manager.GetByName(r.Name)
if err != nil { if err != nil {
log.Errorf("failed to get registry %s: %v", r.Name, err) log.Errorf("failed to get registry %s: %v", r.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
if reg != nil { if reg != nil {
t.HandleConflict(fmt.Sprintf("name '%s' is already used", r.Name)) t.SendConflictError(fmt.Errorf("name '%s' is already used", r.Name))
return return
} }
status, err := registry.CheckHealthStatus(r) status, err := registry.CheckHealthStatus(r)
if err != nil { if err != nil {
t.HandleBadRequest(fmt.Sprintf("health check to registry %s failed: %v", r.URL, err)) t.SendBadRequestError(fmt.Errorf("health check to registry %s failed: %v", r.URL, err))
return return
} }
if status != model.Healthy { if status != model.Healthy {
t.HandleBadRequest(fmt.Sprintf("registry %s is unhealthy: %s", r.URL, status)) t.SendBadRequestError(fmt.Errorf("registry %s is unhealthy: %s", r.URL, status))
return return
} }
id, err := t.manager.Add(r) id, err := t.manager.Add(r)
if err != nil { if err != nil {
log.Errorf("Add registry '%s' error: %v", r.URL, err) log.Errorf("Add registry '%s' error: %v", r.URL, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
@ -214,22 +223,29 @@ func (t *RegistryAPI) Post() {
// Put updates a registry // Put updates a registry
func (t *RegistryAPI) Put() { func (t *RegistryAPI) Put() {
id := t.GetIDFromURL() id, err := t.GetIDFromURL()
if err != nil {
t.SendBadRequestError(err)
return
}
r, err := t.manager.Get(id) r, err := t.manager.Get(id)
if err != nil { if err != nil {
log.Errorf("Get registry by id %d error: %v", id, err) log.Errorf("Get registry by id %d error: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
if r == nil { if r == nil {
t.HandleNotFound(fmt.Sprintf("Registry %d not found", id)) t.SendNotFoundError(fmt.Errorf("Registry %d not found", id))
return return
} }
req := models.RegistryUpdateRequest{} req := models.RegistryUpdateRequest{}
t.DecodeJSONReq(&req) if err := t.DecodeJSONReq(&req); err != nil {
t.SendBadRequestError(err)
return
}
originalName := r.Name originalName := r.Name
@ -261,47 +277,51 @@ func (t *RegistryAPI) Put() {
reg, err := t.manager.GetByName(r.Name) reg, err := t.manager.GetByName(r.Name)
if err != nil { if err != nil {
log.Errorf("Get registry by name '%s' error: %v", r.Name, err) log.Errorf("Get registry by name '%s' error: %v", r.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
if reg != nil { if reg != nil {
t.HandleConflict("name is already used") t.SendConflictError(errors.New("name is already used"))
return return
} }
} }
status, err := registry.CheckHealthStatus(r) status, err := registry.CheckHealthStatus(r)
if err != nil { if err != nil {
t.HandleBadRequest(fmt.Sprintf("health check to registry %s failed: %v", r.URL, err)) t.SendBadRequestError(fmt.Errorf("health check to registry %s failed: %v", r.URL, err))
return return
} }
if status != model.Healthy { if status != model.Healthy {
t.HandleBadRequest(fmt.Sprintf("registry %s is unhealthy: %s", r.URL, status)) t.SendBadRequestError(fmt.Errorf("registry %s is unhealthy: %s", r.URL, status))
return return
} }
if err := t.manager.Update(r); err != nil { if err := t.manager.Update(r); err != nil {
log.Errorf("Update registry %d error: %v", id, err) log.Errorf("Update registry %d error: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.SendInternalServerError(err)
return return
} }
} }
// Delete deletes a registry // Delete deletes a registry
func (t *RegistryAPI) Delete() { func (t *RegistryAPI) Delete() {
id := t.GetIDFromURL() id, err := t.GetIDFromURL()
if err != nil {
t.SendBadRequestError(err)
return
}
registry, err := t.manager.Get(id) registry, err := t.manager.Get(id)
if err != nil { if err != nil {
msg := fmt.Sprintf("Get registry %d error: %v", id, err) msg := fmt.Sprintf("Get registry %d error: %v", id, err)
log.Error(msg) log.Error(msg)
t.HandleInternalServerError(msg) t.SendInternalServerError(errors.New(msg))
return return
} }
if registry == nil { if registry == nil {
t.HandleNotFound(fmt.Sprintf("Registry %d not found", id)) t.SendNotFoundError(fmt.Errorf("Registry %d not found", id))
return return
} }
@ -312,13 +332,13 @@ func (t *RegistryAPI) Delete() {
}, },
}...) }...)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("List replication policies with source registry %d error: %v", id, err)) t.SendInternalServerError(fmt.Errorf("List replication policies with source registry %d error: %v", id, err))
return return
} }
if total > 0 { if total > 0 {
msg := fmt.Sprintf("Can't delete registry %d, %d replication policies use it as source registry", id, total) msg := fmt.Sprintf("Can't delete registry %d, %d replication policies use it as source registry", id, total)
log.Error(msg) log.Error(msg)
t.HandleStatusPreconditionFailed(msg) t.SendPreconditionFailedError(errors.New(msg))
return return
} }
@ -329,20 +349,20 @@ func (t *RegistryAPI) Delete() {
}, },
}...) }...)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("List replication policies with destination registry %d error: %v", id, err)) t.SendInternalServerError(fmt.Errorf("List replication policies with destination registry %d error: %v", id, err))
return return
} }
if total > 0 { if total > 0 {
msg := fmt.Sprintf("Can't delete registry %d, %d replication policies use it as destination registry", id, total) msg := fmt.Sprintf("Can't delete registry %d, %d replication policies use it as destination registry", id, total)
log.Error(msg) log.Error(msg)
t.HandleStatusPreconditionFailed(msg) t.SendPreconditionFailedError(errors.New(msg))
return return
} }
if err := t.manager.Remove(id); err != nil { if err := t.manager.Remove(id); err != nil {
msg := fmt.Sprintf("Delete registry %d error: %v", id, err) msg := fmt.Sprintf("Delete registry %d error: %v", id, err)
log.Error(msg) log.Error(msg)
t.HandleInternalServerError(msg) t.SendPreconditionFailedError(errors.New(msg))
return return
} }
} }
@ -352,7 +372,7 @@ func (t *RegistryAPI) GetInfo() {
id, err := t.GetInt64FromPath(":id") id, err := t.GetInt64FromPath(":id")
// "0" is used for the ID of the local Harbor registry // "0" is used for the ID of the local Harbor registry
if err != nil || id < 0 { if err != nil || id < 0 {
t.HandleBadRequest(fmt.Sprintf("invalid registry ID %s", t.GetString(":id"))) t.SendBadRequestError(fmt.Errorf("invalid registry ID %s", t.GetString(":id")))
return return
} }
var registry *model.Registry var registry *model.Registry
@ -361,28 +381,28 @@ func (t *RegistryAPI) GetInfo() {
} else { } else {
registry, err = t.manager.Get(id) registry, err = t.manager.Get(id)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", id, err)) t.SendInternalServerError(fmt.Errorf("failed to get registry %d: %v", id, err))
return return
} }
if registry == nil { if registry == nil {
t.HandleNotFound(fmt.Sprintf("registry %d not found", id)) t.SendNotFoundError(fmt.Errorf("registry %d not found", id))
return return
} }
} }
factory, err := adapter.GetFactory(registry.Type) factory, err := adapter.GetFactory(registry.Type)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to get the adapter factory for registry type %s: %v", registry.Type, err)) t.SendInternalServerError(fmt.Errorf("failed to get the adapter factory for registry type %s: %v", registry.Type, err))
return return
} }
adp, err := factory(registry) adp, err := factory(registry)
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to create the adapter for registry %d: %v", registry.ID, err)) t.SendInternalServerError(fmt.Errorf("failed to create the adapter for registry %d: %v", registry.ID, err))
return return
} }
info, err := adp.Info() info, err := adp.Info()
if err != nil { if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to get registry info %d: %v", id, err)) t.SendInternalServerError(fmt.Errorf("failed to get registry info %d: %v", id, err))
return return
} }
t.WriteJSONData(process(info)) t.WriteJSONData(process(info))

View File

@ -15,6 +15,7 @@
package api package api
import ( import (
"errors"
"github.com/goharbor/harbor/src/replication/adapter" "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
) )
@ -29,10 +30,10 @@ func (r *ReplicationAdapterAPI) Prepare() {
r.BaseController.Prepare() r.BaseController.Prepare()
if !r.SecurityCtx.IsSysAdmin() { if !r.SecurityCtx.IsSysAdmin() {
if !r.SecurityCtx.IsAuthenticated() { if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized() r.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
r.HandleForbidden(r.SecurityCtx.GetUsername()) r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
return return
} }
} }

View File

@ -15,6 +15,7 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -37,10 +38,10 @@ func (r *ReplicationOperationAPI) Prepare() {
// we need to allow the jobservice to call the API // we need to allow the jobservice to call the API
if !(r.SecurityCtx.IsSysAdmin() || r.SecurityCtx.IsSolutionUser()) { if !(r.SecurityCtx.IsSysAdmin() || r.SecurityCtx.IsSolutionUser()) {
if !r.SecurityCtx.IsAuthenticated() { if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized() r.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
r.HandleForbidden(r.SecurityCtx.GetUsername()) r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
return return
} }
} }
@ -85,15 +86,22 @@ func (r *ReplicationOperationAPI) ListExecutions() {
if len(r.GetString("policy_id")) > 0 { if len(r.GetString("policy_id")) > 0 {
policyID, err := r.GetInt64("policy_id") policyID, err := r.GetInt64("policy_id")
if err != nil || policyID <= 0 { if err != nil || policyID <= 0 {
r.HandleBadRequest(fmt.Sprintf("invalid policy_id %s", r.GetString("policy_id"))) r.SendBadRequestError(fmt.Errorf("invalid policy_id %s", r.GetString("policy_id")))
return return
} }
query.PolicyID = policyID query.PolicyID = policyID
} }
query.Page, query.Size = r.GetPaginationParams() page, size, err := r.GetPaginationParams()
if err != nil {
r.SendBadRequestError(err)
return
}
query.Page = page
query.Size = size
total, executions, err := replication.OperationCtl.ListExecutions(query) total, executions, err := replication.OperationCtl.ListExecutions(query)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to list executions: %v", err)) r.SendInternalServerError(fmt.Errorf("failed to list executions: %v", err))
return return
} }
r.SetPaginationHeader(total, query.Page, query.Size) r.SetPaginationHeader(total, query.Page, query.Size)
@ -103,29 +111,33 @@ func (r *ReplicationOperationAPI) ListExecutions() {
// CreateExecution starts a replication // CreateExecution starts a replication
func (r *ReplicationOperationAPI) CreateExecution() { func (r *ReplicationOperationAPI) CreateExecution() {
execution := &models.Execution{} execution := &models.Execution{}
r.DecodeJSONReq(execution) if err := r.DecodeJSONReq(execution); err != nil {
r.SendBadRequestError(err)
return
}
policy, err := replication.PolicyCtl.Get(execution.PolicyID) policy, err := replication.PolicyCtl.Get(execution.PolicyID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get policy %d: %v", execution.PolicyID, err)) r.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", execution.PolicyID, err))
return return
} }
if policy == nil { if policy == nil {
r.HandleNotFound(fmt.Sprintf("policy %d not found", execution.PolicyID)) r.SendNotFoundError(fmt.Errorf("policy %d not found", execution.PolicyID))
return return
} }
if !policy.Enabled { if !policy.Enabled {
r.HandleBadRequest(fmt.Sprintf("the policy %d is disabled", execution.PolicyID)) r.SendBadRequestError(fmt.Errorf("the policy %d is disabled", execution.PolicyID))
return return
} }
if err = event.PopulateRegistries(replication.RegistryMgr, policy); err != nil { if err = event.PopulateRegistries(replication.RegistryMgr, policy); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to populate registries for policy %d: %v", execution.PolicyID, err)) r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", execution.PolicyID, err))
return return
} }
trigger := r.GetString("trigger", string(model.TriggerTypeManual)) trigger := r.GetString("trigger", string(model.TriggerTypeManual))
executionID, err := replication.OperationCtl.StartReplication(policy, nil, model.TriggerType(trigger)) executionID, err := replication.OperationCtl.StartReplication(policy, nil, model.TriggerType(trigger))
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to start replication for policy %d: %v", execution.PolicyID, err)) r.SendInternalServerError(fmt.Errorf("failed to start replication for policy %d: %v", execution.PolicyID, err))
return return
} }
r.Redirect(http.StatusCreated, strconv.FormatInt(executionID, 10)) r.Redirect(http.StatusCreated, strconv.FormatInt(executionID, 10))
@ -135,17 +147,17 @@ func (r *ReplicationOperationAPI) CreateExecution() {
func (r *ReplicationOperationAPI) GetExecution() { func (r *ReplicationOperationAPI) GetExecution() {
executionID, err := r.GetInt64FromPath(":id") executionID, err := r.GetInt64FromPath(":id")
if err != nil || executionID <= 0 { if err != nil || executionID <= 0 {
r.HandleBadRequest("invalid execution ID") r.SendBadRequestError(errors.New("invalid execution ID"))
return return
} }
execution, err := replication.OperationCtl.GetExecution(executionID) execution, err := replication.OperationCtl.GetExecution(executionID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get execution %d: %v", executionID, err)) r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
return return
} }
if execution == nil { if execution == nil {
r.HandleNotFound(fmt.Sprintf("execution %d not found", executionID)) r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
return return
} }
r.WriteJSONData(execution) r.WriteJSONData(execution)
@ -155,22 +167,22 @@ func (r *ReplicationOperationAPI) GetExecution() {
func (r *ReplicationOperationAPI) StopExecution() { func (r *ReplicationOperationAPI) StopExecution() {
executionID, err := r.GetInt64FromPath(":id") executionID, err := r.GetInt64FromPath(":id")
if err != nil || executionID <= 0 { if err != nil || executionID <= 0 {
r.HandleBadRequest("invalid execution ID") r.SendBadRequestError(errors.New("invalid execution ID"))
return return
} }
execution, err := replication.OperationCtl.GetExecution(executionID) execution, err := replication.OperationCtl.GetExecution(executionID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get execution %d: %v", executionID, err)) r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
return return
} }
if execution == nil { if execution == nil {
r.HandleNotFound(fmt.Sprintf("execution %d not found", executionID)) r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
return return
} }
if err := replication.OperationCtl.StopReplication(executionID); err != nil { if err := replication.OperationCtl.StopReplication(executionID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to stop execution %d: %v", executionID, err)) r.SendInternalServerError(fmt.Errorf("failed to stop execution %d: %v", executionID, err))
return return
} }
} }
@ -179,17 +191,17 @@ func (r *ReplicationOperationAPI) StopExecution() {
func (r *ReplicationOperationAPI) ListTasks() { func (r *ReplicationOperationAPI) ListTasks() {
executionID, err := r.GetInt64FromPath(":id") executionID, err := r.GetInt64FromPath(":id")
if err != nil || executionID <= 0 { if err != nil || executionID <= 0 {
r.HandleBadRequest("invalid execution ID") r.SendBadRequestError(errors.New("invalid execution ID"))
return return
} }
execution, err := replication.OperationCtl.GetExecution(executionID) execution, err := replication.OperationCtl.GetExecution(executionID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get execution %d: %v", executionID, err)) r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
return return
} }
if execution == nil { if execution == nil {
r.HandleNotFound(fmt.Sprintf("execution %d not found", executionID)) r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
return return
} }
@ -201,10 +213,16 @@ func (r *ReplicationOperationAPI) ListTasks() {
if len(status) > 0 { if len(status) > 0 {
query.Statuses = []string{status} query.Statuses = []string{status}
} }
query.Page, query.Size = r.GetPaginationParams() page, size, err := r.GetPaginationParams()
if err != nil {
r.SendBadRequestError(err)
return
}
query.Page = page
query.Size = size
total, tasks, err := replication.OperationCtl.ListTasks(query) total, tasks, err := replication.OperationCtl.ListTasks(query)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to list tasks: %v", err)) r.SendInternalServerError(fmt.Errorf("failed to list tasks: %v", err))
return return
} }
r.SetPaginationHeader(total, query.Page, query.Size) r.SetPaginationHeader(total, query.Page, query.Size)
@ -215,45 +233,45 @@ func (r *ReplicationOperationAPI) ListTasks() {
func (r *ReplicationOperationAPI) GetTaskLog() { func (r *ReplicationOperationAPI) GetTaskLog() {
executionID, err := r.GetInt64FromPath(":id") executionID, err := r.GetInt64FromPath(":id")
if err != nil || executionID <= 0 { if err != nil || executionID <= 0 {
r.HandleBadRequest("invalid execution ID") r.SendBadRequestError(errors.New("invalid execution ID"))
return return
} }
execution, err := replication.OperationCtl.GetExecution(executionID) execution, err := replication.OperationCtl.GetExecution(executionID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get execution %d: %v", executionID, err)) r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
return return
} }
if execution == nil { if execution == nil {
r.HandleNotFound(fmt.Sprintf("execution %d not found", executionID)) r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
return return
} }
taskID, err := r.GetInt64FromPath(":tid") taskID, err := r.GetInt64FromPath(":tid")
if err != nil || taskID <= 0 { if err != nil || taskID <= 0 {
r.HandleBadRequest("invalid task ID") r.SendBadRequestError(errors.New("invalid task ID"))
return return
} }
task, err := replication.OperationCtl.GetTask(taskID) task, err := replication.OperationCtl.GetTask(taskID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get task %d: %v", taskID, err)) r.SendInternalServerError(fmt.Errorf("failed to get task %d: %v", taskID, err))
return return
} }
if task == nil { if task == nil {
r.HandleNotFound(fmt.Sprintf("task %d not found", taskID)) r.SendNotFoundError(fmt.Errorf("task %d not found", taskID))
return return
} }
logBytes, err := replication.OperationCtl.GetTaskLog(taskID) logBytes, err := replication.OperationCtl.GetTaskLog(taskID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get log of task %d: %v", taskID, err)) r.SendInternalServerError(fmt.Errorf("failed to get log of task %d: %v", taskID, err))
return return
} }
r.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes))) r.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
r.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain") r.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = r.Ctx.ResponseWriter.Write(logBytes) _, err = r.Ctx.ResponseWriter.Write(logBytes)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to write log of task %d: %v", taskID, err)) r.SendInternalServerError(fmt.Errorf("failed to write log of task %d: %v", taskID, err))
return return
} }
} }

View File

@ -15,10 +15,12 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
common_model "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/dao/models" "github.com/goharbor/harbor/src/replication/dao/models"
"github.com/goharbor/harbor/src/replication/event" "github.com/goharbor/harbor/src/replication/event"
@ -38,30 +40,38 @@ func (r *ReplicationPolicyAPI) Prepare() {
r.BaseController.Prepare() r.BaseController.Prepare()
if !r.SecurityCtx.IsSysAdmin() { if !r.SecurityCtx.IsSysAdmin() {
if !r.SecurityCtx.IsAuthenticated() { if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized() r.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
r.HandleForbidden(r.SecurityCtx.GetUsername()) r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
return return
} }
} }
// List the replication policies // List the replication policies
func (r *ReplicationPolicyAPI) List() { func (r *ReplicationPolicyAPI) List() {
page, size, err := r.GetPaginationParams()
if err != nil {
r.SendInternalServerError(err)
return
}
// TODO: support more query // TODO: support more query
query := &model.PolicyQuery{ query := &model.PolicyQuery{
Name: r.GetString("name"), Name: r.GetString("name"),
Pagination: common_model.Pagination{
Page: page,
Size: size,
},
} }
query.Page, query.Size = r.GetPaginationParams()
total, policies, err := replication.PolicyCtl.List(query) total, policies, err := replication.PolicyCtl.List(query)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to list policies: %v", err)) r.SendInternalServerError(fmt.Errorf("failed to list policies: %v", err))
return return
} }
for _, policy := range policies { for _, policy := range policies {
if err = populateRegistries(replication.RegistryMgr, policy); err != nil { if err = populateRegistries(replication.RegistryMgr, policy); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to populate registries for policy %d: %v", policy.ID, err)) r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", policy.ID, err))
return return
} }
} }
@ -72,7 +82,11 @@ func (r *ReplicationPolicyAPI) List() {
// Create the replication policy // Create the replication policy
func (r *ReplicationPolicyAPI) Create() { func (r *ReplicationPolicyAPI) Create() {
policy := &model.Policy{} policy := &model.Policy{}
r.DecodeJSONReqAndValidate(policy) isValid, err := r.DecodeJSONReqAndValidate(policy)
if !isValid {
r.SendBadRequestError(err)
return
}
if !r.validateName(policy) { if !r.validateName(policy) {
return return
@ -83,7 +97,7 @@ func (r *ReplicationPolicyAPI) Create() {
id, err := replication.PolicyCtl.Create(policy) id, err := replication.PolicyCtl.Create(policy)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to create the policy: %v", err)) r.SendInternalServerError(fmt.Errorf("failed to create the policy: %v", err))
return return
} }
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10)) r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
@ -93,11 +107,11 @@ func (r *ReplicationPolicyAPI) Create() {
func (r *ReplicationPolicyAPI) validateName(policy *model.Policy) bool { func (r *ReplicationPolicyAPI) validateName(policy *model.Policy) bool {
p, err := replication.PolicyCtl.GetByName(policy.Name) p, err := replication.PolicyCtl.GetByName(policy.Name)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get policy %s: %v", policy.Name, err)) r.SendInternalServerError(fmt.Errorf("failed to get policy %s: %v", policy.Name, err))
return false return false
} }
if p != nil { if p != nil {
r.HandleConflict(fmt.Sprintf("policy %s already exists", policy.Name)) r.SendConflictError(fmt.Errorf("policy %s already exists", policy.Name))
return false return false
} }
return true return true
@ -113,11 +127,11 @@ func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool {
} }
registry, err := replication.RegistryMgr.Get(registryID) registry, err := replication.RegistryMgr.Get(registryID)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", registryID, err)) r.SendConflictError(fmt.Errorf("failed to get registry %d: %v", registryID, err))
return false return false
} }
if registry == nil { if registry == nil {
r.HandleNotFound(fmt.Sprintf("registry %d not found", registryID)) r.SendNotFoundError(fmt.Errorf("registry %d not found", registryID))
return false return false
} }
return true return true
@ -127,21 +141,21 @@ func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool {
func (r *ReplicationPolicyAPI) Get() { func (r *ReplicationPolicyAPI) Get() {
id, err := r.GetInt64FromPath(":id") id, err := r.GetInt64FromPath(":id")
if id <= 0 || err != nil { if id <= 0 || err != nil {
r.HandleBadRequest("invalid policy ID") r.SendBadRequestError(errors.New("invalid policy ID"))
return return
} }
policy, err := replication.PolicyCtl.Get(id) policy, err := replication.PolicyCtl.Get(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get the policy %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
return return
} }
if policy == nil { if policy == nil {
r.HandleNotFound(fmt.Sprintf("policy %d not found", id)) r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
return return
} }
if err = populateRegistries(replication.RegistryMgr, policy); err != nil { if err = populateRegistries(replication.RegistryMgr, policy); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to populate registries for policy %d: %v", policy.ID, err)) r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", policy.ID, err))
return return
} }
@ -152,22 +166,27 @@ func (r *ReplicationPolicyAPI) Get() {
func (r *ReplicationPolicyAPI) Update() { func (r *ReplicationPolicyAPI) Update() {
id, err := r.GetInt64FromPath(":id") id, err := r.GetInt64FromPath(":id")
if id <= 0 || err != nil { if id <= 0 || err != nil {
r.HandleBadRequest("invalid policy ID") r.SendBadRequestError(errors.New("invalid policy ID"))
return return
} }
originalPolicy, err := replication.PolicyCtl.Get(id) originalPolicy, err := replication.PolicyCtl.Get(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get the policy %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
return return
} }
if originalPolicy == nil { if originalPolicy == nil {
r.HandleNotFound(fmt.Sprintf("policy %d not found", id)) r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
return return
} }
policy := &model.Policy{} policy := &model.Policy{}
r.DecodeJSONReqAndValidate(policy) isValid, err := r.DecodeJSONReqAndValidate(policy)
if !isValid {
r.SendBadRequestError(err)
return
}
if policy.Name != originalPolicy.Name && if policy.Name != originalPolicy.Name &&
!r.validateName(policy) { !r.validateName(policy) {
return return
@ -179,7 +198,7 @@ func (r *ReplicationPolicyAPI) Update() {
policy.ID = id policy.ID = id
if err := replication.PolicyCtl.Update(policy); err != nil { if err := replication.PolicyCtl.Update(policy); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to update the policy %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to update the policy %d: %v", id, err))
return return
} }
} }
@ -188,17 +207,17 @@ func (r *ReplicationPolicyAPI) Update() {
func (r *ReplicationPolicyAPI) Delete() { func (r *ReplicationPolicyAPI) Delete() {
id, err := r.GetInt64FromPath(":id") id, err := r.GetInt64FromPath(":id")
if id <= 0 || err != nil { if id <= 0 || err != nil {
r.HandleBadRequest("invalid policy ID") r.SendBadRequestError(errors.New("invalid policy ID"))
return return
} }
policy, err := replication.PolicyCtl.Get(id) policy, err := replication.PolicyCtl.Get(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get the policy %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
return return
} }
if policy == nil { if policy == nil {
r.HandleNotFound(fmt.Sprintf("policy %d not found", id)) r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
return return
} }
@ -206,19 +225,19 @@ func (r *ReplicationPolicyAPI) Delete() {
PolicyID: id, PolicyID: id,
}) })
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get the executions of policy %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to get the executions of policy %d: %v", id, err))
return return
} }
for _, execution := range executions { for _, execution := range executions {
if execution.Status == models.ExecutionStatusInProgress { if execution.Status == models.ExecutionStatusInProgress {
r.HandleStatusPreconditionFailed(fmt.Sprintf("the policy %d has running executions, can not be deleted", id)) r.SendInternalServerError(fmt.Errorf("the policy %d has running executions, can not be deleted", id))
return return
} }
} }
if err := replication.PolicyCtl.Remove(id); err != nil { if err := replication.PolicyCtl.Remove(id); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the policy %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to delete the policy %d: %v", id, err))
return return
} }
} }

View File

@ -24,6 +24,7 @@ import (
"strings" "strings"
"time" "time"
"errors"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
@ -110,13 +111,13 @@ type manifestResp struct {
func (ra *RepositoryAPI) Get() { func (ra *RepositoryAPI) Get() {
projectID, err := ra.GetInt64("project_id") projectID, err := ra.GetInt64("project_id")
if err != nil || projectID <= 0 { if err != nil || projectID <= 0 {
ra.HandleBadRequest(fmt.Sprintf("invalid project_id %s", ra.GetString("project_id"))) ra.SendBadRequestError(fmt.Errorf("invalid project_id %s", ra.GetString("project_id")))
return return
} }
labelID, err := ra.GetInt64("label_id", 0) labelID, err := ra.GetInt64("label_id", 0)
if err != nil { if err != nil {
ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id"))) ra.SendBadRequestError(fmt.Errorf("invalid label_id: %s", ra.GetString("label_id")))
return return
} }
@ -128,17 +129,17 @@ func (ra *RepositoryAPI) Get() {
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("project %d not found", projectID)) ra.SendNotFoundError(fmt.Errorf("project %d not found", projectID))
return return
} }
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository) resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) { if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
@ -147,19 +148,24 @@ func (ra *RepositoryAPI) Get() {
Name: ra.GetString("q"), Name: ra.GetString("q"),
LabelID: labelID, LabelID: labelID,
} }
query.Page, query.Size = ra.GetPaginationParams() query.Page, query.Size, err = ra.GetPaginationParams()
if err != nil {
ra.SendBadRequestError(err)
return
}
query.Sort = ra.GetString("sort") query.Sort = ra.GetString("sort")
total, err := dao.GetTotalOfRepositories(query) total, err := dao.GetTotalOfRepositories(query)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get total of repositories of project %d: %v", ra.SendInternalServerError(fmt.Errorf("failed to get total of repositories of project %d: %v",
projectID, err)) projectID, err))
return return
} }
repositories, err := getRepositories(query) repositories, err := getRepositories(query)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository: %v", err)) ra.SendInternalServerError(fmt.Errorf("failed to get repository: %v", err))
return return
} }
@ -240,25 +246,26 @@ func (ra *RepositoryAPI) Delete() {
} }
if project == nil { if project == nil {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName)) ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return return
} }
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
resource := rbac.NewProjectNamespace(project.ProjectID).Resource(rbac.ResourceRepository) resource := rbac.NewProjectNamespace(project.ProjectID).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionDelete, resource) { if !ra.SecurityCtx.Can(rbac.ActionDelete, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
rc, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName) rc, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil { if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err) log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.SendInternalServerError(errors.New("internal error"))
return
} }
tags := []string{} tags := []string{}
@ -266,18 +273,13 @@ func (ra *RepositoryAPI) Delete() {
if len(tag) == 0 { if len(tag) == 0 {
tagList, err := rc.ListTag() tagList, err := rc.ListTag()
if err != nil { if err != nil {
log.Errorf("error occurred while listing tags of %s: %v", repoName, err) ra.ParseAndHandleError(fmt.Sprintf("error occurred while listing tags of %s", repoName), err)
return
if regErr, ok := err.(*commonhttp.Error); ok {
ra.CustomAbort(regErr.Code, regErr.Message)
}
ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
// TODO remove the logic if the bug of registry is fixed // TODO remove the logic if the bug of registry is fixed
if len(tagList) == 0 { if len(tagList) == 0 {
ra.HandleNotFound(fmt.Sprintf("no tags found for repository %s", repoName)) ra.SendNotFoundError(fmt.Errorf("no tags found for repository %s", repoName))
return return
} }
@ -289,7 +291,7 @@ func (ra *RepositoryAPI) Delete() {
if config.WithNotary() { if config.WithNotary() {
signedTags, err := getSignatures(ra.SecurityCtx.GetUsername(), repoName) signedTags, err := getSignatures(ra.SecurityCtx.GetUsername(), repoName)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf( ra.SendInternalServerError(fmt.Errorf(
"failed to get signatures for repository %s: %v", repoName, err)) "failed to get signatures for repository %s: %v", repoName, err))
return return
} }
@ -298,12 +300,14 @@ func (ra *RepositoryAPI) Delete() {
digest, _, err := rc.ManifestExist(t) digest, _, err := rc.ManifestExist(t)
if err != nil { if err != nil {
log.Errorf("Failed to Check the digest of tag: %s, error: %v", t, err.Error()) log.Errorf("Failed to Check the digest of tag: %s, error: %v", t, err.Error())
ra.CustomAbort(http.StatusInternalServerError, err.Error()) ra.SendInternalServerError(err)
return
} }
log.Debugf("Tag: %s, digest: %s", t, digest) log.Debugf("Tag: %s, digest: %s", t, digest)
if _, ok := signedTags[digest]; ok { if _, ok := signedTags[digest]; ok {
log.Errorf("Found signed tag, repostory: %s, tag: %s, deletion will be canceled", repoName, t) log.Errorf("Found signed tag, repostory: %s, tag: %s, deletion will be canceled", repoName, t)
ra.CustomAbort(http.StatusPreconditionFailed, fmt.Sprintf("tag %s is signed", t)) ra.SendPreconditionFailedError(fmt.Errorf("tag %s is signed", t))
return
} }
} }
} }
@ -311,7 +315,7 @@ func (ra *RepositoryAPI) Delete() {
for _, t := range tags { for _, t := range tags {
image := fmt.Sprintf("%s:%s", repoName, t) image := fmt.Sprintf("%s:%s", repoName, t)
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil { if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of image %s: %v", image, err)) ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
return return
} }
if err = rc.DeleteTag(t); err != nil { if err = rc.DeleteTag(t); err != nil {
@ -319,11 +323,9 @@ func (ra *RepositoryAPI) Delete() {
if regErr.Code == http.StatusNotFound { if regErr.Code == http.StatusNotFound {
continue continue
} }
log.Errorf("failed to delete tag %s: %v", t, err)
ra.CustomAbort(regErr.Code, regErr.Message)
} }
log.Errorf("error occurred while deleting tag %s:%s: %v", repoName, t, err) ra.ParseAndHandleError(fmt.Sprintf("failed to delete tag %s", t), err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") return
} }
log.Infof("delete tag: %s:%s", repoName, t) log.Infof("delete tag: %s:%s", repoName, t)
@ -363,12 +365,13 @@ func (ra *RepositoryAPI) Delete() {
exist, err := repositoryExist(repoName, rc) exist, err := repositoryExist(repoName, rc)
if err != nil { if err != nil {
log.Errorf("failed to check the existence of repository %s: %v", repoName, err) log.Errorf("failed to check the existence of repository %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "") ra.SendInternalServerError(fmt.Errorf("failed to check the existence of repository %s: %v", repoName, err))
return
} }
if !exist { if !exist {
repository, err := dao.GetRepositoryByName(repoName) repository, err := dao.GetRepositoryByName(repoName)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v", repoName, err)) ra.SendInternalServerError(fmt.Errorf("failed to get repository %s: %v", repoName, err))
return return
} }
if repository == nil { if repository == nil {
@ -378,13 +381,14 @@ func (ra *RepositoryAPI) Delete() {
if err = dao.DeleteLabelsOfResource(common.ResourceTypeRepository, if err = dao.DeleteLabelsOfResource(common.ResourceTypeRepository,
strconv.FormatInt(repository.RepositoryID, 10)); err != nil { strconv.FormatInt(repository.RepositoryID, 10)); err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of repository %s: %v", ra.SendInternalServerError(fmt.Errorf("failed to delete labels of repository %s: %v",
repoName, err)) repoName, err))
return return
} }
if err = dao.DeleteRepository(repoName); err != nil { if err = dao.DeleteRepository(repoName); err != nil {
log.Errorf("failed to delete repository %s: %v", repoName, err) log.Errorf("failed to delete repository %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "") ra.SendInternalServerError(fmt.Errorf("failed to delete repository %s: %v", repoName, err))
return
} }
} }
} }
@ -395,38 +399,38 @@ func (ra *RepositoryAPI) GetTag() {
tag := ra.GetString(":tag") tag := ra.GetString(":tag")
exist, _, err := ra.checkExistence(repository, tag) exist, _, err := ra.checkExistence(repository, tag)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of resource, error: %v", err)) ra.SendInternalServerError(fmt.Errorf("failed to check the existence of resource, error: %v", err))
return return
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("resource: %s:%s not found", repository, tag)) ra.SendNotFoundError(fmt.Errorf("resource: %s:%s not found", repository, tag))
return return
} }
project, _ := utils.ParseRepository(repository) project, _ := utils.ParseRepository(repository)
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTag) resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTag)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
client, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository) client, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to initialize the client for %s: %v", ra.SendInternalServerError(fmt.Errorf("failed to initialize the client for %s: %v",
repository, err)) repository, err))
return return
} }
_, exist, err = client.ManifestExist(tag) _, exist, err = client.ManifestExist(tag)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of %s:%s: %v", repository, tag, err)) ra.SendInternalServerError(fmt.Errorf("failed to check the existence of %s:%s: %v", repository, tag, err))
return return
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("%s not found", tag)) ra.SendNotFoundError(fmt.Errorf("%s not found", tag))
return return
} }
@ -439,38 +443,41 @@ func (ra *RepositoryAPI) GetTag() {
// Retag tags an existing image to another tag in this repo, the source image is specified by request body. // Retag tags an existing image to another tag in this repo, the source image is specified by request body.
func (ra *RepositoryAPI) Retag() { func (ra *RepositoryAPI) Retag() {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
repoName := ra.GetString(":splat") repoName := ra.GetString(":splat")
project, repo := utils.ParseRepository(repoName) project, repo := utils.ParseRepository(repoName)
if !utils.ValidateRepo(repo) { if !utils.ValidateRepo(repo) {
ra.HandleBadRequest(fmt.Sprintf("invalid repo '%s'", repo)) ra.SendBadRequestError(fmt.Errorf("invalid repo '%s'", repo))
return return
} }
request := models.RetagRequest{} request := models.RetagRequest{}
ra.DecodeJSONReq(&request) if err := ra.DecodeJSONReq(&request); err != nil {
ra.SendBadRequestError(err)
return
}
srcImage, err := models.ParseImage(request.SrcImage) srcImage, err := models.ParseImage(request.SrcImage)
if err != nil { if err != nil {
ra.HandleBadRequest(fmt.Sprintf("invalid src image string '%s', should in format '<project>/<repo>:<tag>'", request.SrcImage)) ra.SendBadRequestError(fmt.Errorf("invalid src image string '%s', should in format '<project>/<repo>:<tag>'", request.SrcImage))
return return
} }
if !utils.ValidateTag(request.Tag) { if !utils.ValidateTag(request.Tag) {
ra.HandleBadRequest(fmt.Sprintf("invalid tag '%s'", request.Tag)) ra.SendBadRequestError(fmt.Errorf("invalid tag '%s'", request.Tag))
return return
} }
// Check whether source image exists // Check whether source image exists
exist, _, err := ra.checkExistence(fmt.Sprintf("%s/%s", srcImage.Project, srcImage.Repo), srcImage.Tag) exist, _, err := ra.checkExistence(fmt.Sprintf("%s/%s", srcImage.Project, srcImage.Repo), srcImage.Tag)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("check existence of %s error: %v", request.SrcImage, err)) ra.SendInternalServerError(fmt.Errorf("check existence of %s error: %v", request.SrcImage, err))
return return
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("image %s not exist", request.SrcImage)) ra.SendNotFoundError(fmt.Errorf("image %s not exist", request.SrcImage))
return return
} }
@ -481,7 +488,7 @@ func (ra *RepositoryAPI) Retag() {
return return
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", project)) ra.SendNotFoundError(fmt.Errorf("project %s not found", project))
return return
} }
@ -489,11 +496,11 @@ func (ra *RepositoryAPI) Retag() {
if !request.Override { if !request.Override {
exist, _, err := ra.checkExistence(repoName, request.Tag) exist, _, err := ra.checkExistence(repoName, request.Tag)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("check existence of %s:%s error: %v", repoName, request.Tag, err)) ra.SendInternalServerError(fmt.Errorf("check existence of %s:%s error: %v", repoName, request.Tag, err))
return return
} }
if exist { if exist {
ra.HandleConflict(fmt.Sprintf("tag '%s' already existed for '%s'", request.Tag, repoName)) ra.SendConflictError(fmt.Errorf("tag '%s' already existed for '%s'", request.Tag, repoName))
return return
} }
} }
@ -502,7 +509,7 @@ func (ra *RepositoryAPI) Retag() {
srcResource := rbac.NewProjectNamespace(srcImage.Project).Resource(rbac.ResourceRepository) srcResource := rbac.NewProjectNamespace(srcImage.Project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionPull, srcResource) { if !ra.SecurityCtx.Can(rbac.ActionPull, srcResource) {
log.Errorf("user has no read permission to project '%s'", srcImage.Project) log.Errorf("user has no read permission to project '%s'", srcImage.Project)
ra.HandleForbidden(fmt.Sprintf("%s has no read permission to project %s", ra.SecurityCtx.GetUsername(), srcImage.Project)) ra.SendForbiddenError(fmt.Errorf("%s has no read permission to project %s", ra.SecurityCtx.GetUsername(), srcImage.Project))
return return
} }
@ -510,7 +517,7 @@ func (ra *RepositoryAPI) Retag() {
destResource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository) destResource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionPush, destResource) { if !ra.SecurityCtx.Can(rbac.ActionPush, destResource) {
log.Errorf("user has no write permission to project '%s'", project) log.Errorf("user has no write permission to project '%s'", project)
ra.HandleForbidden(fmt.Sprintf("%s has no write permission to project %s", ra.SecurityCtx.GetUsername(), project)) ra.SendForbiddenError(fmt.Errorf("%s has no write permission to project %s", ra.SecurityCtx.GetUsername(), project))
return return
} }
@ -520,7 +527,7 @@ func (ra *RepositoryAPI) Retag() {
Repo: repo, Repo: repo,
Tag: request.Tag, Tag: request.Tag,
}); err != nil { }); err != nil {
ra.HandleInternalServerError(fmt.Sprintf("%v", err)) ra.SendInternalServerError(fmt.Errorf("%v", err))
} }
} }
@ -529,7 +536,7 @@ func (ra *RepositoryAPI) GetTags() {
repoName := ra.GetString(":splat") repoName := ra.GetString(":splat")
labelID, err := ra.GetInt64("label_id", 0) labelID, err := ra.GetInt64("label_id", 0)
if err != nil { if err != nil {
ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id"))) ra.SendBadRequestError(fmt.Errorf("invalid label_id: %s", ra.GetString("label_id")))
return return
} }
@ -542,29 +549,30 @@ func (ra *RepositoryAPI) GetTags() {
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName)) ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return return
} }
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTag) resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTag)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) { if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
client, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName) client, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil { if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err) log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.SendInternalServerError(errors.New("internal error"))
return
} }
tags, err := client.ListTag() tags, err := client.ListTag()
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get tag of %s: %v", repoName, err)) ra.SendInternalServerError(fmt.Errorf("failed to get tag of %s: %v", repoName, err))
return return
} }
@ -575,7 +583,7 @@ func (ra *RepositoryAPI) GetTags() {
ResourceType: common.ResourceTypeImage, ResourceType: common.ResourceTypeImage,
}) })
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to list resource labels: %v", err)) ra.SendInternalServerError(fmt.Errorf("failed to list resource labels: %v", err))
return return
} }
labeledTags := map[string]struct{}{} labeledTags := map[string]struct{}{}
@ -738,7 +746,7 @@ func (ra *RepositoryAPI) GetManifests() {
} }
if version != "v1" && version != "v2" { if version != "v1" && version != "v2" {
ra.HandleBadRequest("version should be v1 or v2") ra.SendBadRequestError(errors.New("version should be v1 or v2"))
return return
} }
@ -751,36 +759,32 @@ func (ra *RepositoryAPI) GetManifests() {
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName)) ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return return
} }
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagManifest) resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagManifest)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
rc, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName) rc, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil { if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err) log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.SendInternalServerError(errors.New("internal error"))
return
} }
manifest, err := getManifest(rc, tag, version) manifest, err := getManifest(rc, tag, version)
if err != nil { if err != nil {
log.Errorf("error occurred while getting manifest of %s:%s: %v", repoName, tag, err) ra.ParseAndHandleError(fmt.Sprintf("error occurred while getting manifest of %s:%s", repoName, tag), err)
return
if regErr, ok := err.(*commonhttp.Error); ok {
ra.CustomAbort(regErr.Code, regErr.Message)
}
ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
ra.Data["json"] = manifest ra.Data["json"] = manifest
@ -833,7 +837,7 @@ func getManifest(client *registry.Repository,
func (ra *RepositoryAPI) GetTopRepos() { func (ra *RepositoryAPI) GetTopRepos() {
count, err := ra.GetInt("count", 10) count, err := ra.GetInt("count", 10)
if err != nil || count <= 0 { if err != nil || count <= 0 {
ra.HandleBadRequest(fmt.Sprintf("invalid count: %s", ra.GetString("count"))) ra.SendBadRequestError(fmt.Errorf("invalid count: %s", ra.GetString("count")))
return return
} }
@ -846,7 +850,7 @@ func (ra *RepositoryAPI) GetTopRepos() {
if ra.SecurityCtx.IsAuthenticated() { if ra.SecurityCtx.IsAuthenticated() {
list, err := ra.SecurityCtx.GetMyProjects() list, err := ra.SecurityCtx.GetMyProjects()
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get projects which the user %s is a member of: %v", ra.SendInternalServerError(fmt.Errorf("failed to get projects which the user %s is a member of: %v",
ra.SecurityCtx.GetUsername(), err)) ra.SecurityCtx.GetUsername(), err))
return return
} }
@ -860,7 +864,8 @@ func (ra *RepositoryAPI) GetTopRepos() {
repos, err := dao.GetTopRepos(projectIDs, count) repos, err := dao.GetTopRepos(projectIDs, count)
if err != nil { if err != nil {
log.Errorf("failed to get top repos: %v", err) log.Errorf("failed to get top repos: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal server error") ra.SendInternalServerError(errors.New("internal server error"))
return
} }
ra.Data["json"] = assembleReposInParallel(repos) ra.Data["json"] = assembleReposInParallel(repos)
@ -872,35 +877,38 @@ func (ra *RepositoryAPI) Put() {
name := ra.GetString(":splat") name := ra.GetString(":splat")
repository, err := dao.GetRepositoryByName(name) repository, err := dao.GetRepositoryByName(name)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v", name, err)) ra.SendInternalServerError(fmt.Errorf("failed to get repository %s: %v", name, err))
return return
} }
if repository == nil { if repository == nil {
ra.HandleNotFound(fmt.Sprintf("repository %s not found", name)) ra.SendNotFoundError(fmt.Errorf("repository %s not found", name))
return return
} }
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
project, _ := utils.ParseRepository(name) project, _ := utils.ParseRepository(name)
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository) resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionUpdate, resource) { if !ra.SecurityCtx.Can(rbac.ActionUpdate, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
desc := struct { desc := struct {
Description string `json:"description"` Description string `json:"description"`
}{} }{}
ra.DecodeJSONReq(&desc) if err := ra.DecodeJSONReq(&desc); err != nil {
ra.SendBadRequestError(err)
return
}
repository.Description = desc.Description repository.Description = desc.Description
if err = dao.UpdateRepository(*repository); err != nil { if err = dao.UpdateRepository(*repository); err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to update repository %s: %v", name, err)) ra.SendInternalServerError(fmt.Errorf("failed to update repository %s: %v", name, err))
return return
} }
} }
@ -918,17 +926,17 @@ func (ra *RepositoryAPI) GetSignatures() {
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName)) ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return return
} }
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository) resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
@ -936,7 +944,8 @@ func (ra *RepositoryAPI) GetSignatures() {
ra.SecurityCtx.GetUsername(), repoName) ra.SecurityCtx.GetUsername(), repoName)
if err != nil { if err != nil {
log.Errorf("Error while fetching signature from notary: %v", err) log.Errorf("Error while fetching signature from notary: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.SendInternalServerError(errors.New("internal error"))
return
} }
ra.Data["json"] = targets ra.Data["json"] = targets
ra.ServeJSON() ra.ServeJSON()
@ -946,7 +955,7 @@ func (ra *RepositoryAPI) GetSignatures() {
func (ra *RepositoryAPI) ScanImage() { func (ra *RepositoryAPI) ScanImage() {
if !config.WithClair() { if !config.WithClair() {
log.Warningf("Harbor is not deployed with Clair, scan is disabled.") log.Warningf("Harbor is not deployed with Clair, scan is disabled.")
ra.RenderError(http.StatusServiceUnavailable, "") ra.SendInternalServerError(errors.New("harbor is not deployed with Clair, scan is disabled"))
return return
} }
repoName := ra.GetString(":splat") repoName := ra.GetString(":splat")
@ -959,23 +968,23 @@ func (ra *RepositoryAPI) ScanImage() {
return return
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName)) ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return return
} }
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob) resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
if !ra.SecurityCtx.Can(rbac.ActionCreate, resource) { if !ra.SecurityCtx.Can(rbac.ActionCreate, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
err = coreutils.TriggerImageScan(repoName, tag) err = coreutils.TriggerImageScan(repoName, tag)
if err != nil { if err != nil {
log.Errorf("Error while calling job service to trigger image scan: %v", err) log.Errorf("Error while calling job service to trigger image scan: %v", err)
ra.HandleInternalServerError("Failed to scan image, please check log for details") ra.SendInternalServerError(errors.New("Failed to scan image, please check log for details"))
return return
} }
} }
@ -984,18 +993,18 @@ func (ra *RepositoryAPI) ScanImage() {
func (ra *RepositoryAPI) VulnerabilityDetails() { func (ra *RepositoryAPI) VulnerabilityDetails() {
if !config.WithClair() { if !config.WithClair() {
log.Warningf("Harbor is not deployed with Clair, it's not impossible to get vulnerability details.") log.Warningf("Harbor is not deployed with Clair, it's not impossible to get vulnerability details.")
ra.RenderError(http.StatusServiceUnavailable, "") ra.SendInternalServerError(errors.New("harbor is not deployed with Clair, it's not impossible to get vulnerability details"))
return return
} }
repository := ra.GetString(":splat") repository := ra.GetString(":splat")
tag := ra.GetString(":tag") tag := ra.GetString(":tag")
exist, digest, err := ra.checkExistence(repository, tag) exist, digest, err := ra.checkExistence(repository, tag)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of resource, error: %v", err)) ra.SendInternalServerError(fmt.Errorf("failed to check the existence of resource, error: %v", err))
return return
} }
if !exist { if !exist {
ra.HandleNotFound(fmt.Sprintf("resource: %s:%s not found", repository, tag)) ra.SendNotFoundError(fmt.Errorf("resource: %s:%s not found", repository, tag))
return return
} }
project, _ := utils.ParseRepository(repository) project, _ := utils.ParseRepository(repository)
@ -1003,16 +1012,16 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTagVulnerability) resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTagVulnerability)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) { if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() { if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized() ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
ra.HandleForbidden(ra.SecurityCtx.GetUsername()) ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return return
} }
res := []*models.VulnerabilityItem{} res := []*models.VulnerabilityItem{}
overview, err := dao.GetImgScanOverview(digest) overview, err := dao.GetImgScanOverview(digest)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get the scan overview, error: %v", err)) ra.SendInternalServerError(fmt.Errorf("failed to get the scan overview, error: %v", err))
return return
} }
if overview != nil && len(overview.DetailsKey) > 0 { if overview != nil && len(overview.DetailsKey) > 0 {
@ -1020,7 +1029,7 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
log.Debugf("The key for getting details: %s", overview.DetailsKey) log.Debugf("The key for getting details: %s", overview.DetailsKey)
details, err := clairClient.GetResult(overview.DetailsKey) details, err := clairClient.GetResult(overview.DetailsKey)
if err != nil { if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("Failed to get scan details from Clair, error: %v", err)) ra.SendInternalServerError(fmt.Errorf("Failed to get scan details from Clair, error: %v", err))
return return
} }
res = transformVulnerabilities(details) res = transformVulnerabilities(details)

View File

@ -114,7 +114,10 @@ func (r *RepositoryLabelAPI) isValidLabelReq() bool {
} }
l := &models.Label{} l := &models.Label{}
r.DecodeJSONReq(l) if err := r.DecodeJSONReq(l); err != nil {
r.SendBadRequestError(err)
return false
}
label, ok := r.validate(l.ID, p.ProjectID) label, ok := r.validate(l.ID, p.ProjectID)
if !ok { if !ok {

View File

@ -15,6 +15,7 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
@ -41,7 +42,7 @@ func (r *RobotAPI) Prepare() {
method := r.Ctx.Request.Method method := r.Ctx.Request.Method
if !r.SecurityCtx.IsAuthenticated() { if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized() r.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
@ -53,7 +54,7 @@ func (r *RobotAPI) Prepare() {
} else { } else {
errMsg = "invalid project ID: " + fmt.Sprintf("%d", pid) errMsg = "invalid project ID: " + fmt.Sprintf("%d", pid)
} }
r.HandleBadRequest(errMsg) r.SendBadRequestError(errors.New(errMsg))
return return
} }
project, err := r.ProjectMgr.Get(pid) project, err := r.ProjectMgr.Get(pid)
@ -62,7 +63,7 @@ func (r *RobotAPI) Prepare() {
return return
} }
if project == nil { if project == nil {
r.HandleNotFound(fmt.Sprintf("project %d not found", pid)) r.SendNotFoundError(fmt.Errorf("project %d not found", pid))
return return
} }
r.project = project r.project = project
@ -70,18 +71,18 @@ func (r *RobotAPI) Prepare() {
if method == http.MethodPut || method == http.MethodDelete { if method == http.MethodPut || method == http.MethodDelete {
id, err := r.GetInt64FromPath(":id") id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 { if err != nil || id <= 0 {
r.HandleBadRequest("invalid robot ID") r.SendBadRequestError(errors.New("invalid robot ID"))
return return
} }
robot, err := dao.GetRobotByID(id) robot, err := dao.GetRobotByID(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to get robot %d: %v", id, err))
return return
} }
if robot == nil { if robot == nil {
r.HandleNotFound(fmt.Sprintf("robot %d not found", id)) r.SendNotFoundError(fmt.Errorf("robot %d not found", id))
return return
} }
@ -92,7 +93,7 @@ func (r *RobotAPI) Prepare() {
func (r *RobotAPI) requireAccess(action rbac.Action) bool { func (r *RobotAPI) requireAccess(action rbac.Action) bool {
resource := rbac.NewProjectNamespace(r.project.ProjectID).Resource(rbac.ResourceRobot) resource := rbac.NewProjectNamespace(r.project.ProjectID).Resource(rbac.ResourceRobot)
if !r.SecurityCtx.Can(action, resource) { if !r.SecurityCtx.Can(action, resource) {
r.HandleForbidden(r.SecurityCtx.GetUsername()) r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
return false return false
} }
@ -106,10 +107,14 @@ func (r *RobotAPI) Post() {
} }
var robotReq models.RobotReq var robotReq models.RobotReq
if err := r.DecodeJSONReq(&robotReq); err != nil {
r.SendBadRequestError(err)
return
}
// Token duration in minutes // Token duration in minutes
tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute
expiresAt := time.Now().UTC().Add(tokenDuration).Unix() expiresAt := time.Now().UTC().Add(tokenDuration).Unix()
r.DecodeJSONReq(&robotReq)
createdName := common.RobotPrefix + robotReq.Name createdName := common.RobotPrefix + robotReq.Name
// first to add a robot account, and get its id. // first to add a robot account, and get its id.
@ -122,10 +127,10 @@ func (r *RobotAPI) Post() {
id, err := dao.AddRobot(&robot) id, err := dao.AddRobot(&robot)
if err != nil { if err != nil {
if err == dao.ErrDupRows { if err == dao.ErrDupRows {
r.HandleConflict() r.SendConflictError(errors.New("conflict robot account"))
return return
} }
r.HandleInternalServerError(fmt.Sprintf("failed to create robot account: %v", err)) r.SendInternalServerError(fmt.Errorf("failed to create robot account: %v", err))
return return
} }
@ -133,20 +138,20 @@ func (r *RobotAPI) Post() {
// token is not stored in the database. // token is not stored in the database.
jwtToken, err := token.New(id, r.project.ProjectID, expiresAt, robotReq.Access) jwtToken, err := token.New(id, r.project.ProjectID, expiresAt, robotReq.Access)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err)) r.SendInternalServerError(fmt.Errorf("failed to valid parameters to generate token for robot account, %v", err))
err := dao.DeleteRobot(id) err := dao.DeleteRobot(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to delete the robot account: %d, %v", id, err))
} }
return return
} }
rawTk, err := jwtToken.Raw() rawTk, err := jwtToken.Raw()
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to sign token for robot account, %v", err)) r.SendInternalServerError(fmt.Errorf("failed to sign token for robot account, %v", err))
err := dao.DeleteRobot(id) err := dao.DeleteRobot(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to delete the robot account: %d, %v", id, err))
} }
return return
} }
@ -172,14 +177,18 @@ func (r *RobotAPI) List() {
count, err := dao.CountRobot(&query) count, err := dao.CountRobot(&query)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to list robots on project: %d, %v", r.project.ProjectID, err)) r.SendInternalServerError(fmt.Errorf("failed to list robots on project: %d, %v", r.project.ProjectID, err))
return
}
query.Page, query.Size, err = r.GetPaginationParams()
if err != nil {
r.SendBadRequestError(err)
return return
} }
query.Page, query.Size = r.GetPaginationParams()
robots, err := dao.ListRobots(&query) robots, err := dao.ListRobots(&query)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robots %v", err)) r.SendInternalServerError(fmt.Errorf("failed to get robots %v", err))
return return
} }
@ -196,17 +205,17 @@ func (r *RobotAPI) Get() {
id, err := r.GetInt64FromPath(":id") id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 { if err != nil || id <= 0 {
r.HandleBadRequest(fmt.Sprintf("invalid robot ID: %s", r.GetStringFromPath(":id"))) r.SendBadRequestError(fmt.Errorf("invalid robot ID: %s", r.GetStringFromPath(":id")))
return return
} }
robot, err := dao.GetRobotByID(id) robot, err := dao.GetRobotByID(id)
if err != nil { if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err)) r.SendInternalServerError(fmt.Errorf("failed to get robot %d: %v", id, err))
return return
} }
if robot == nil { if robot == nil {
r.HandleNotFound(fmt.Sprintf("robot %d not found", id)) r.SendNotFoundError(fmt.Errorf("robot %d not found", id))
return return
} }
@ -221,11 +230,16 @@ func (r *RobotAPI) Put() {
} }
var robotReq models.RobotReq var robotReq models.RobotReq
r.DecodeJSONReqAndValidate(&robotReq) isValid, err := r.DecodeJSONReqAndValidate(&robotReq)
if !isValid {
r.SendBadRequestError(err)
return
}
r.robot.Disabled = robotReq.Disabled r.robot.Disabled = robotReq.Disabled
if err := dao.UpdateRobot(r.robot); err != nil { if err := dao.UpdateRobot(r.robot); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to update robot %d: %v", r.robot.ID, err)) r.SendInternalServerError(fmt.Errorf("failed to update robot %d: %v", r.robot.ID, err))
return return
} }
@ -238,7 +252,7 @@ func (r *RobotAPI) Delete() {
} }
if err := dao.DeleteRobot(r.robot.ID); err != nil { if err := dao.DeleteRobot(r.robot.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete robot %d: %v", r.robot.ID, err)) r.SendInternalServerError(fmt.Errorf("failed to delete robot %d: %v", r.robot.ID, err))
return return
} }
} }

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"errors"
"net/http" "net/http"
"strconv" "strconv"
@ -20,15 +21,15 @@ func (sc *ScanAllAPI) Prepare() {
sc.BaseController.Prepare() sc.BaseController.Prepare()
if !config.WithClair() { if !config.WithClair() {
log.Warningf("Harbor is not deployed with Clair, it's not possible to scan images.") log.Warningf("Harbor is not deployed with Clair, it's not possible to scan images.")
sc.RenderError(http.StatusServiceUnavailable, "") sc.SendStatusServiceUnavailableError(errors.New(""))
return return
} }
if !sc.SecurityCtx.IsAuthenticated() { if !sc.SecurityCtx.IsAuthenticated() {
sc.HandleUnauthorized() sc.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
if !sc.SecurityCtx.IsSysAdmin() { if !sc.SecurityCtx.IsSysAdmin() {
sc.HandleForbidden(sc.SecurityCtx.GetUsername()) sc.SendForbiddenError(errors.New(sc.SecurityCtx.GetUsername()))
return return
} }
} }
@ -49,7 +50,11 @@ func (sc *ScanAllAPI) Prepare() {
// } // }
func (sc *ScanAllAPI) Post() { func (sc *ScanAllAPI) Post() {
ajr := models.AdminJobReq{} ajr := models.AdminJobReq{}
sc.DecodeJSONReqAndValidate(&ajr) isValid, err := sc.DecodeJSONReqAndValidate(&ajr)
if !isValid {
sc.SendBadRequestError(err)
return
}
ajr.Name = common_job.ImageScanAllJob ajr.Name = common_job.ImageScanAllJob
sc.submit(&ajr) sc.submit(&ajr)
sc.Redirect(http.StatusCreated, strconv.FormatInt(ajr.ID, 10)) sc.Redirect(http.StatusCreated, strconv.FormatInt(ajr.ID, 10))
@ -65,7 +70,11 @@ func (sc *ScanAllAPI) Post() {
// } // }
func (sc *ScanAllAPI) Put() { func (sc *ScanAllAPI) Put() {
ajr := models.AdminJobReq{} ajr := models.AdminJobReq{}
sc.DecodeJSONReqAndValidate(&ajr) isValid, err := sc.DecodeJSONReqAndValidate(&ajr)
if !isValid {
sc.SendBadRequestError(err)
return
}
ajr.Name = common_job.ImageScanAllJob ajr.Name = common_job.ImageScanAllJob
sc.updateSchedule(ajr) sc.updateSchedule(ajr)
} }

View File

@ -16,11 +16,11 @@ package api
import ( import (
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/utils" "github.com/goharbor/harbor/src/core/utils"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -39,12 +39,12 @@ type ScanJobAPI struct {
func (sj *ScanJobAPI) Prepare() { func (sj *ScanJobAPI) Prepare() {
sj.BaseController.Prepare() sj.BaseController.Prepare()
if !sj.SecurityCtx.IsAuthenticated() { if !sj.SecurityCtx.IsAuthenticated() {
sj.HandleUnauthorized() sj.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
id, err := sj.GetInt64FromPath(":id") id, err := sj.GetInt64FromPath(":id")
if err != nil { if err != nil {
sj.HandleBadRequest("invalid ID") sj.SendBadRequestError(errors.New("invalid ID"))
return return
} }
sj.jobID = id sj.jobID = id
@ -52,14 +52,16 @@ func (sj *ScanJobAPI) Prepare() {
data, err := dao.GetScanJob(id) data, err := dao.GetScanJob(id)
if err != nil { if err != nil {
log.Errorf("Failed to load job data for job: %d, error: %v", id, err) log.Errorf("Failed to load job data for job: %d, error: %v", id, err)
sj.CustomAbort(http.StatusInternalServerError, "Failed to get Job data") sj.SendInternalServerError(errors.New("Failed to get Job data"))
return
} }
projectName := strings.SplitN(data.Repository, "/", 2)[0] projectName := strings.SplitN(data.Repository, "/", 2)[0]
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob) resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
if !sj.SecurityCtx.Can(rbac.ActionRead, resource) { if !sj.SecurityCtx.Can(rbac.ActionRead, resource) {
log.Errorf("User does not have read permission for project: %s", projectName) log.Errorf("User does not have read permission for project: %s", projectName)
sj.HandleForbidden(sj.SecurityCtx.GetUsername()) sj.SendForbiddenError(errors.New(sj.SecurityCtx.GetUsername()))
return
} }
sj.projectName = projectName sj.projectName = projectName
sj.jobUUID = data.UUID sj.jobUUID = data.UUID
@ -69,20 +71,14 @@ func (sj *ScanJobAPI) Prepare() {
func (sj *ScanJobAPI) GetLog() { func (sj *ScanJobAPI) GetLog() {
logBytes, err := utils.GetJobServiceClient().GetJobLog(sj.jobUUID) logBytes, err := utils.GetJobServiceClient().GetJobLog(sj.jobUUID)
if err != nil { if err != nil {
if httpErr, ok := err.(*common_http.Error); ok { sj.ParseAndHandleError(fmt.Sprintf("Failed to get job logs, uuid: %s, error: %v", sj.jobUUID, err), err)
sj.RenderError(httpErr.Code, "")
log.Errorf(fmt.Sprintf("failed to get log of job %d: %d %s",
sj.jobID, httpErr.Code, httpErr.Message))
return
}
sj.HandleInternalServerError(fmt.Sprintf("Failed to get job logs, uuid: %s, error: %v", sj.jobUUID, err))
return return
} }
sj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes))) sj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
sj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain") sj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = sj.Ctx.ResponseWriter.Write(logBytes) _, err = sj.Ctx.ResponseWriter.Write(logBytes)
if err != nil { if err != nil {
sj.HandleInternalServerError(fmt.Sprintf("Failed to write job logs, uuid: %s, error: %v", sj.jobUUID, err)) sj.SendInternalServerError(fmt.Errorf("Failed to write job logs, uuid: %s, error: %v", sj.jobUUID, err))
} }
} }

View File

@ -16,7 +16,6 @@ package api
import ( import (
"fmt" "fmt"
"net/http"
"strings" "strings"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
@ -69,7 +68,7 @@ func (s *SearchAPI) Get() {
if isAuthenticated { if isAuthenticated {
mys, err := s.SecurityCtx.GetMyProjects() mys, err := s.SecurityCtx.GetMyProjects()
if err != nil { if err != nil {
s.HandleInternalServerError(fmt.Sprintf( s.SendInternalServerError(fmt.Errorf(
"failed to get projects: %v", err)) "failed to get projects: %v", err))
return return
} }
@ -111,7 +110,8 @@ func (s *SearchAPI) Get() {
}) })
if err != nil { if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err) log.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err)
s.CustomAbort(http.StatusInternalServerError, "") s.SendInternalServerError(fmt.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err))
return
} }
p.RepoCount = total p.RepoCount = total
@ -122,7 +122,8 @@ func (s *SearchAPI) Get() {
repositoryResult, err := filterRepositories(projects, keyword) repositoryResult, err := filterRepositories(projects, keyword)
if err != nil { if err != nil {
log.Errorf("failed to filter repositories: %v", err) log.Errorf("failed to filter repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "") s.SendInternalServerError(fmt.Errorf("failed to filter repositories: %v", err))
return
} }
result := &searchResult{ result := &searchResult{
@ -139,7 +140,9 @@ func (s *SearchAPI) Get() {
chartResults, err := searchHandler(keyword, proNames) chartResults, err := searchHandler(keyword, proNames)
if err != nil { if err != nil {
log.Errorf("failed to filter charts: %v", err) log.Errorf("failed to filter charts: %v", err)
s.CustomAbort(http.StatusInternalServerError, err.Error()) s.SendInternalServerError(err)
return
} }
result.Chart = &chartResults result.Chart = &chartResults

View File

@ -15,8 +15,8 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"net/http"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
@ -48,7 +48,7 @@ type StatisticAPI struct {
func (s *StatisticAPI) Prepare() { func (s *StatisticAPI) Prepare() {
s.BaseController.Prepare() s.BaseController.Prepare()
if !s.SecurityCtx.IsAuthenticated() { if !s.SecurityCtx.IsAuthenticated() {
s.HandleUnauthorized() s.SendUnAuthorizedError(errors.New("UnAuthorized"))
return return
} }
s.username = s.SecurityCtx.GetUsername() s.username = s.SecurityCtx.GetUsername()
@ -76,7 +76,8 @@ func (s *StatisticAPI) Get() {
}) })
if err != nil { if err != nil {
log.Errorf("failed to get total of public repositories: %v", err) log.Errorf("failed to get total of public repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "") s.SendInternalServerError(fmt.Errorf("failed to get total of public repositories: %v", err))
return
} }
statistic[PubRC] = n statistic[PubRC] = n
} }
@ -85,7 +86,8 @@ func (s *StatisticAPI) Get() {
result, err := s.ProjectMgr.List(nil) result, err := s.ProjectMgr.List(nil)
if err != nil { if err != nil {
log.Errorf("failed to get total of projects: %v", err) log.Errorf("failed to get total of projects: %v", err)
s.CustomAbort(http.StatusInternalServerError, "") s.SendInternalServerError(fmt.Errorf("failed to get total of projects: %v", err))
return
} }
statistic[TPC] = result.Total statistic[TPC] = result.Total
statistic[PriPC] = result.Total - statistic[PubPC] statistic[PriPC] = result.Total - statistic[PubPC]
@ -93,7 +95,8 @@ func (s *StatisticAPI) Get() {
n, err := dao.GetTotalOfRepositories() n, err := dao.GetTotalOfRepositories()
if err != nil { if err != nil {
log.Errorf("failed to get total of repositories: %v", err) log.Errorf("failed to get total of repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "") s.SendInternalServerError(fmt.Errorf("failed to get total of repositories: %v", err))
return
} }
statistic[TRC] = n statistic[TRC] = n
statistic[PriRC] = n - statistic[PubRC] statistic[PriRC] = n - statistic[PubRC]
@ -124,7 +127,7 @@ func (s *StatisticAPI) Get() {
ProjectIDs: ids, ProjectIDs: ids,
}) })
if err != nil { if err != nil {
s.HandleInternalServerError(fmt.Sprintf( s.SendInternalServerError(fmt.Errorf(
"failed to get total of repositories for user %s: %v", "failed to get total of repositories for user %s: %v",
s.username, err)) s.username, err))
return return

View File

@ -15,12 +15,14 @@
package api package api
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"sync" "sync"
"fmt"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
clairdao "github.com/goharbor/harbor/src/common/dao/clair" clairdao "github.com/goharbor/harbor/src/common/dao/clair"
@ -105,28 +107,24 @@ type GeneralInfo struct {
WithChartMuseum bool `json:"with_chartmuseum"` WithChartMuseum bool `json:"with_chartmuseum"`
} }
// validate for validating user if an admin. // GetVolumeInfo gets specific volume storage info.
func (sia *SystemInfoAPI) validate() { func (sia *SystemInfoAPI) GetVolumeInfo() {
if !sia.SecurityCtx.IsAuthenticated() { if !sia.SecurityCtx.IsAuthenticated() {
sia.HandleUnauthorized() sia.SendUnAuthorizedError(errors.New("UnAuthorized"))
sia.StopRun() return
} }
if !sia.SecurityCtx.IsSysAdmin() { if !sia.SecurityCtx.IsSysAdmin() {
sia.HandleForbidden(sia.SecurityCtx.GetUsername()) sia.SendForbiddenError(errors.New(sia.SecurityCtx.GetUsername()))
sia.StopRun() return
} }
}
// GetVolumeInfo gets specific volume storage info.
func (sia *SystemInfoAPI) GetVolumeInfo() {
sia.validate()
systeminfo.Init() systeminfo.Init()
capacity, err := imagestorage.GlobalDriver.Cap() capacity, err := imagestorage.GlobalDriver.Cap()
if err != nil { if err != nil {
log.Errorf("failed to get capacity: %v", err) log.Errorf("failed to get capacity: %v", err)
sia.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) sia.SendInternalServerError(fmt.Errorf("failed to get capacity: %v", err))
return
} }
systemInfo := SystemInfo{ systemInfo := SystemInfo{
HarborStorage: Storage{ HarborStorage: Storage{
@ -147,10 +145,12 @@ func (sia *SystemInfoAPI) GetCert() {
http.ServeFile(sia.Ctx.ResponseWriter, sia.Ctx.Request, defaultRootCert) http.ServeFile(sia.Ctx.ResponseWriter, sia.Ctx.Request, defaultRootCert)
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
log.Error("No certificate found.") log.Error("No certificate found.")
sia.CustomAbort(http.StatusNotFound, "No certificate found.") sia.SendNotFoundError(errors.New("no certificate found"))
return
} else { } else {
log.Errorf("Unexpected error: %v", err) log.Errorf("Unexpected error: %v", err)
sia.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) sia.SendInternalServerError(fmt.Errorf("Unexpected error: %v", err))
return
} }
} }
@ -159,7 +159,8 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
cfg, err := config.GetSystemCfg() cfg, err := config.GetSystemCfg()
if err != nil { if err != nil {
log.Errorf("Error occurred getting config: %v", err) log.Errorf("Error occurred getting config: %v", err)
sia.CustomAbort(http.StatusInternalServerError, "Unexpected error") sia.SendInternalServerError(fmt.Errorf("Unexpected error: %v", err))
return
} }
var registryURL string var registryURL string
if l := strings.Split(cfg[common.ExtEndpoint].(string), "://"); len(l) > 1 { if l := strings.Split(cfg[common.ExtEndpoint].(string), "://"); len(l) > 1 {

View File

@ -15,6 +15,7 @@
package api package api
import ( import (
"errors"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
@ -55,7 +56,8 @@ func (ua *UserAPI) Prepare() {
mode, err := config.AuthMode() mode, err := config.AuthMode()
if err != nil { if err != nil {
log.Errorf("failed to get auth mode: %v", err) log.Errorf("failed to get auth mode: %v", err)
ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) ua.SendInternalServerError(errors.New(""))
return
} }
ua.AuthMode = mode ua.AuthMode = mode
@ -63,7 +65,8 @@ func (ua *UserAPI) Prepare() {
self, err := config.SelfRegistration() self, err := config.SelfRegistration()
if err != nil { if err != nil {
log.Errorf("failed to get self registration: %v", err) log.Errorf("failed to get self registration: %v", err)
ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) ua.SendInternalServerError(errors.New(""))
return
} }
ua.SelfRegistration = self ua.SelfRegistration = self
@ -72,7 +75,7 @@ func (ua *UserAPI) Prepare() {
if ua.Ctx.Input.IsPost() { if ua.Ctx.Input.IsPost() {
return return
} }
ua.HandleUnauthorized() ua.SendUnAuthorizedError(errors.New("UnAuthorize"))
return return
} }
@ -80,7 +83,7 @@ func (ua *UserAPI) Prepare() {
Username: ua.SecurityCtx.GetUsername(), Username: ua.SecurityCtx.GetUsername(),
}) })
if err != nil { if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get user %s: %v", ua.SendInternalServerError(fmt.Errorf("failed to get user %s: %v",
ua.SecurityCtx.GetUsername(), err)) ua.SecurityCtx.GetUsername(), err))
return return
} }
@ -94,17 +97,20 @@ func (ua *UserAPI) Prepare() {
ua.userID, err = strconv.Atoi(id) ua.userID, err = strconv.Atoi(id)
if err != nil { if err != nil {
log.Errorf("Invalid user id, error: %v", err) log.Errorf("Invalid user id, error: %v", err)
ua.CustomAbort(http.StatusBadRequest, "Invalid user Id") ua.SendBadRequestError(errors.New("invalid user Id"))
return
} }
userQuery := models.User{UserID: ua.userID} userQuery := models.User{UserID: ua.userID}
u, err := dao.GetUser(userQuery) u, err := dao.GetUser(userQuery)
if err != nil { if err != nil {
log.Errorf("Error occurred in GetUser, error: %v", err) log.Errorf("Error occurred in GetUser, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
if u == nil { if u == nil {
log.Errorf("User with Id: %d does not exist", ua.userID) log.Errorf("User with Id: %d does not exist", ua.userID)
ua.CustomAbort(http.StatusNotFound, "") ua.SendNotFoundError(errors.New(""))
return
} }
} }
@ -118,19 +124,28 @@ func (ua *UserAPI) Get() {
u, err := dao.GetUser(userQuery) u, err := dao.GetUser(userQuery)
if err != nil { if err != nil {
log.Errorf("Error occurred in GetUser, error: %v", err) log.Errorf("Error occurred in GetUser, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(err)
return
} }
u.Password = "" u.Password = ""
if ua.userID == ua.currentUserID { if ua.userID == ua.currentUserID {
u.HasAdminRole = ua.SecurityCtx.IsSysAdmin() u.HasAdminRole = ua.SecurityCtx.IsSysAdmin()
} }
if ua.AuthMode == common.OIDCAuth {
o, err := ua.getOIDCUserInfo()
if err != nil {
ua.SendInternalServerError(err)
return
}
u.OIDCUserMeta = o
}
ua.Data["json"] = u ua.Data["json"] = u
ua.ServeJSON() ua.ServeJSON()
return return
} }
log.Errorf("Current user, id: %d does not have admin role, can not view other user's detail", ua.currentUserID) log.Errorf("Current user, id: %d does not have admin role, can not view other user's detail", ua.currentUserID)
ua.RenderError(http.StatusForbidden, "User does not have admin role") ua.SendForbiddenError(errors.New("user does not have admin role"))
return return
} }
@ -138,11 +153,16 @@ func (ua *UserAPI) Get() {
func (ua *UserAPI) List() { func (ua *UserAPI) List() {
if !ua.IsAdmin { if !ua.IsAdmin {
log.Errorf("Current user, id: %d does not have admin role, can not list users", ua.currentUserID) log.Errorf("Current user, id: %d does not have admin role, can not list users", ua.currentUserID)
ua.RenderError(http.StatusForbidden, "User does not have admin role") ua.SendForbiddenError(errors.New("user does not have admin role"))
return
}
page, size, err := ua.GetPaginationParams()
if err != nil {
ua.SendBadRequestError(err)
return return
} }
page, size := ua.GetPaginationParams()
query := &models.UserQuery{ query := &models.UserQuery{
Username: ua.GetString("username"), Username: ua.GetString("username"),
Email: ua.GetString("email"), Email: ua.GetString("email"),
@ -154,13 +174,13 @@ func (ua *UserAPI) List() {
total, err := dao.GetTotalOfUsers(query) total, err := dao.GetTotalOfUsers(query)
if err != nil { if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get total of users: %v", err)) ua.SendInternalServerError(fmt.Errorf("failed to get total of users: %v", err))
return return
} }
users, err := dao.ListUsers(query) users, err := dao.ListUsers(query)
if err != nil { if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get users: %v", err)) ua.SendInternalServerError(fmt.Errorf("failed to get users: %v", err))
return return
} }
@ -171,7 +191,11 @@ func (ua *UserAPI) List() {
// Search ... // Search ...
func (ua *UserAPI) Search() { func (ua *UserAPI) Search() {
page, size := ua.GetPaginationParams() page, size, err := ua.GetPaginationParams()
if err != nil {
ua.SendBadRequestError(err)
return
}
query := &models.UserQuery{ query := &models.UserQuery{
Username: ua.GetString("username"), Username: ua.GetString("username"),
Email: ua.GetString("email"), Email: ua.GetString("email"),
@ -183,13 +207,13 @@ func (ua *UserAPI) Search() {
total, err := dao.GetTotalOfUsers(query) total, err := dao.GetTotalOfUsers(query)
if err != nil { if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get total of users: %v", err)) ua.SendInternalServerError(fmt.Errorf("failed to get total of users: %v", err))
return return
} }
users, err := dao.ListUsers(query) users, err := dao.ListUsers(query)
if err != nil { if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get users: %v", err)) ua.SendInternalServerError(fmt.Errorf("failed to get users: %v", err))
return return
} }
@ -206,42 +230,49 @@ func (ua *UserAPI) Search() {
// Put ... // Put ...
func (ua *UserAPI) Put() { func (ua *UserAPI) Put() {
if !ua.modifiable() { if !ua.modifiable() {
ua.RenderError(http.StatusForbidden, fmt.Sprintf("User with ID %d cannot be modified", ua.userID)) ua.SendForbiddenError(fmt.Errorf("User with ID %d cannot be modified", ua.userID))
return return
} }
user := models.User{UserID: ua.userID} user := models.User{UserID: ua.userID}
ua.DecodeJSONReq(&user) if err := ua.DecodeJSONReq(&user); err != nil {
ua.SendBadRequestError(err)
return
}
err := commonValidate(user) err := commonValidate(user)
if err != nil { if err != nil {
log.Warningf("Bad request in change user profile: %v", err) log.Warningf("Bad request in change user profile: %v", err)
ua.RenderError(http.StatusBadRequest, "change user profile error:"+err.Error()) ua.SendBadRequestError(fmt.Errorf("change user profile error:" + err.Error()))
return return
} }
userQuery := models.User{UserID: ua.userID} userQuery := models.User{UserID: ua.userID}
u, err := dao.GetUser(userQuery) u, err := dao.GetUser(userQuery)
if err != nil { if err != nil {
log.Errorf("Error occurred in GetUser, error: %v", err) log.Errorf("Error occurred in GetUser, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
if u == nil { if u == nil {
log.Errorf("User with Id: %d does not exist", ua.userID) log.Errorf("User with Id: %d does not exist", ua.userID)
ua.CustomAbort(http.StatusNotFound, "") ua.SendNotFoundError(errors.New(""))
return
} }
if u.Email != user.Email { if u.Email != user.Email {
emailExist, err := dao.UserExists(user, "email") emailExist, err := dao.UserExists(user, "email")
if err != nil { if err != nil {
log.Errorf("Error occurred in change user profile: %v", err) log.Errorf("Error occurred in change user profile: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
if emailExist { if emailExist {
log.Warning("email has already been used!") log.Warning("email has already been used!")
ua.RenderError(http.StatusConflict, "email has already been used!") ua.SendConflictError(errors.New("email has already been used"))
return return
} }
} }
if err := dao.ChangeUserProfile(user); err != nil { if err := dao.ChangeUserProfile(user); err != nil {
log.Errorf("Failed to update user profile, error: %v", err) log.Errorf("Failed to update user profile, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, err.Error()) ua.SendInternalServerError(err)
return
} }
} }
@ -249,16 +280,21 @@ func (ua *UserAPI) Put() {
func (ua *UserAPI) Post() { func (ua *UserAPI) Post() {
if !(ua.AuthMode == common.DBAuth) { if !(ua.AuthMode == common.DBAuth) {
ua.CustomAbort(http.StatusForbidden, "") ua.SendForbiddenError(errors.New(""))
return
} }
if !(ua.SelfRegistration || ua.IsAdmin) { if !(ua.SelfRegistration || ua.IsAdmin) {
log.Warning("Registration can only be used by admin role user when self-registration is off.") log.Warning("Registration can only be used by admin role user when self-registration is off.")
ua.CustomAbort(http.StatusForbidden, "") ua.SendForbiddenError(errors.New(""))
return
} }
user := models.User{} user := models.User{}
ua.DecodeJSONReq(&user) if err := ua.DecodeJSONReq(&user); err != nil {
ua.SendBadRequestError(err)
return
}
err := validate(user) err := validate(user)
if err != nil { if err != nil {
log.Warningf("Bad request in Register: %v", err) log.Warningf("Bad request in Register: %v", err)
@ -268,27 +304,30 @@ func (ua *UserAPI) Post() {
userExist, err := dao.UserExists(user, "username") userExist, err := dao.UserExists(user, "username")
if err != nil { if err != nil {
log.Errorf("Error occurred in Register: %v", err) log.Errorf("Error occurred in Register: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
if userExist { if userExist {
log.Warning("username has already been used!") log.Warning("username has already been used!")
ua.RenderError(http.StatusConflict, "username has already been used!") ua.SendConflictError(errors.New("username has already been used"))
return return
} }
emailExist, err := dao.UserExists(user, "email") emailExist, err := dao.UserExists(user, "email")
if err != nil { if err != nil {
log.Errorf("Error occurred in change user profile: %v", err) log.Errorf("Error occurred in change user profile: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
if emailExist { if emailExist {
log.Warning("email has already been used!") log.Warning("email has already been used!")
ua.RenderError(http.StatusConflict, "email has already been used!") ua.SendConflictError(errors.New("email has already been used"))
return return
} }
userID, err := dao.Register(user) userID, err := dao.Register(user)
if err != nil { if err != nil {
log.Errorf("Error occurred in Register: %v", err) log.Errorf("Error occurred in Register: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10)) ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10))
@ -297,7 +336,7 @@ func (ua *UserAPI) Post() {
// Delete ... // Delete ...
func (ua *UserAPI) Delete() { func (ua *UserAPI) Delete() {
if !ua.IsAdmin || ua.AuthMode != common.DBAuth || ua.userID == 1 || ua.currentUserID == ua.userID { if !ua.IsAdmin || ua.AuthMode != common.DBAuth || ua.userID == 1 || ua.currentUserID == ua.userID {
ua.RenderError(http.StatusForbidden, fmt.Sprintf("User with ID: %d cannot be removed, auth mode: %s, current user ID: %d", ua.userID, ua.AuthMode, ua.currentUserID)) ua.SendForbiddenError(fmt.Errorf("User with ID: %d cannot be removed, auth mode: %s, current user ID: %d", ua.userID, ua.AuthMode, ua.currentUserID))
return return
} }
@ -305,7 +344,7 @@ func (ua *UserAPI) Delete() {
err = dao.DeleteUser(ua.userID) err = dao.DeleteUser(ua.userID)
if err != nil { if err != nil {
log.Errorf("Failed to delete data from database, error: %v", err) log.Errorf("Failed to delete data from database, error: %v", err)
ua.RenderError(http.StatusInternalServerError, "Failed to delete User") ua.SendInternalServerError(errors.New("failed to delete User"))
return return
} }
} }
@ -313,43 +352,46 @@ func (ua *UserAPI) Delete() {
// ChangePassword handles PUT to /api/users/{}/password // ChangePassword handles PUT to /api/users/{}/password
func (ua *UserAPI) ChangePassword() { func (ua *UserAPI) ChangePassword() {
if !ua.modifiable() { if !ua.modifiable() {
ua.RenderError(http.StatusForbidden, fmt.Sprintf("User with ID: %d is not modifiable", ua.userID)) ua.SendForbiddenError(fmt.Errorf("User with ID: %d is not modifiable", ua.userID))
return return
} }
changePwdOfOwn := ua.userID == ua.currentUserID changePwdOfOwn := ua.userID == ua.currentUserID
var req passwordReq var req passwordReq
ua.DecodeJSONReq(&req) if err := ua.DecodeJSONReq(&req); err != nil {
ua.SendBadRequestError(err)
return
}
if changePwdOfOwn && len(req.OldPassword) == 0 { if changePwdOfOwn && len(req.OldPassword) == 0 {
ua.HandleBadRequest("empty old_password") ua.SendBadRequestError(errors.New("empty old_password"))
return return
} }
if len(req.NewPassword) == 0 { if len(req.NewPassword) == 0 {
ua.HandleBadRequest("empty new_password") ua.SendBadRequestError(errors.New("empty new_password"))
return return
} }
user, err := dao.GetUser(models.User{UserID: ua.userID}) user, err := dao.GetUser(models.User{UserID: ua.userID})
if err != nil { if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get user %d: %v", ua.userID, err)) ua.SendInternalServerError(fmt.Errorf("failed to get user %d: %v", ua.userID, err))
return return
} }
if user == nil { if user == nil {
ua.HandleNotFound(fmt.Sprintf("user %d not found", ua.userID)) ua.SendNotFoundError(fmt.Errorf("user %d not found", ua.userID))
return return
} }
if changePwdOfOwn { if changePwdOfOwn {
if user.Password != utils.Encrypt(req.OldPassword, user.Salt) { if user.Password != utils.Encrypt(req.OldPassword, user.Salt) {
log.Info("incorrect old_password") log.Info("incorrect old_password")
ua.RenderError(http.StatusForbidden, "incorrect old_password") ua.SendForbiddenError(errors.New("incorrect old_password"))
return return
} }
} }
if user.Password == utils.Encrypt(req.NewPassword, user.Salt) { if user.Password == utils.Encrypt(req.NewPassword, user.Salt) {
ua.HandleBadRequest("the new password can not be same with the old one") ua.SendBadRequestError(errors.New("the new password can not be same with the old one"))
return return
} }
@ -358,7 +400,7 @@ func (ua *UserAPI) ChangePassword() {
Password: req.NewPassword, Password: req.NewPassword,
} }
if err = dao.ChangeUserPassword(updatedUser); err != nil { if err = dao.ChangeUserPassword(updatedUser); err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to change password of user %d: %v", ua.userID, err)) ua.SendInternalServerError(fmt.Errorf("failed to change password of user %d: %v", ua.userID, err))
return return
} }
} }
@ -371,10 +413,14 @@ func (ua *UserAPI) ToggleUserAdminRole() {
return return
} }
userQuery := models.User{UserID: ua.userID} userQuery := models.User{UserID: ua.userID}
ua.DecodeJSONReq(&userQuery) if err := ua.DecodeJSONReq(&userQuery); err != nil {
ua.SendBadRequestError(err)
return
}
if err := dao.ToggleUserAdminRole(userQuery.UserID, userQuery.HasAdminRole); err != nil { if err := dao.ToggleUserAdminRole(userQuery.UserID, userQuery.HasAdminRole); err != nil {
log.Errorf("Error occurred in ToggleUserAdminRole: %v", err) log.Errorf("Error occurred in ToggleUserAdminRole: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") ua.SendInternalServerError(errors.New("internal error"))
return
} }
} }
@ -429,6 +475,25 @@ func (ua *UserAPI) ListUserPermissions() {
return return
} }
func (ua *UserAPI) getOIDCUserInfo() (*models.OIDCUser, error) {
key, err := config.SecretKey()
if err != nil {
return nil, err
}
o, err := dao.GetOIDCUserByUserID(ua.userID)
if err != nil || o == nil {
return nil, err
}
if len(o.Secret) > 0 {
p, err := utils.ReversibleDecrypt(o.Secret, key)
if err != nil {
return nil, err
}
o.PlainSecret = p
}
return o, nil
}
// modifiable returns whether the modify is allowed based on current auth mode and context // modifiable returns whether the modify is allowed based on current auth mode and context
func (ua *UserAPI) modifiable() bool { func (ua *UserAPI) modifiable() bool {
if ua.AuthMode == common.DBAuth { if ua.AuthMode == common.DBAuth {

View File

@ -20,6 +20,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"errors"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao/group" "github.com/goharbor/harbor/src/common/dao/group"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
@ -42,7 +43,7 @@ const (
func (uga *UserGroupAPI) Prepare() { func (uga *UserGroupAPI) Prepare() {
uga.BaseController.Prepare() uga.BaseController.Prepare()
if !uga.SecurityCtx.IsAuthenticated() { if !uga.SecurityCtx.IsAuthenticated() {
uga.HandleUnauthorized() uga.SendUnAuthorizedError(errors.New("Unauthorized"))
return return
} }
@ -51,13 +52,13 @@ func (uga *UserGroupAPI) Prepare() {
log.Warningf("failed to parse user group id, error: %v", err) log.Warningf("failed to parse user group id, error: %v", err)
} }
if ugid <= 0 && (uga.Ctx.Input.IsPut() || uga.Ctx.Input.IsDelete()) { if ugid <= 0 && (uga.Ctx.Input.IsPut() || uga.Ctx.Input.IsDelete()) {
uga.HandleBadRequest(fmt.Sprintf("invalid user group ID: %s", uga.GetStringFromPath(":ugid"))) uga.SendBadRequestError(fmt.Errorf("invalid user group ID: %s", uga.GetStringFromPath(":ugid")))
return return
} }
uga.id = int(ugid) uga.id = int(ugid)
// Common user can create/update, only harbor admin can delete user group. // Common user can create/update, only harbor admin can delete user group.
if uga.Ctx.Input.IsDelete() && !uga.SecurityCtx.IsSysAdmin() { if uga.Ctx.Input.IsDelete() && !uga.SecurityCtx.IsSysAdmin() {
uga.HandleForbidden(uga.SecurityCtx.GetUsername()) uga.SendForbiddenError(errors.New(uga.SecurityCtx.GetUsername()))
return return
} }
} }
@ -71,7 +72,7 @@ func (uga *UserGroupAPI) Get() {
query := models.UserGroup{GroupType: common.LdapGroupType} // Current query LDAP group only query := models.UserGroup{GroupType: common.LdapGroupType} // Current query LDAP group only
userGroupList, err := group.QueryUserGroup(query) userGroupList, err := group.QueryUserGroup(query)
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Failed to query database for user group list, error: %v", err)) uga.SendInternalServerError(fmt.Errorf("failed to query database for user group list, error: %v", err))
return return
} }
if len(userGroupList) > 0 { if len(userGroupList) > 0 {
@ -81,11 +82,11 @@ func (uga *UserGroupAPI) Get() {
// return a specific user group // return a specific user group
userGroup, err := group.GetUserGroup(ID) userGroup, err := group.GetUserGroup(ID)
if userGroup == nil { if userGroup == nil {
uga.HandleNotFound("The user group does not exist.") uga.SendNotFoundError(errors.New("the user group does not exist"))
return return
} }
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Failed to query database for user group list, error: %v", err)) uga.SendInternalServerError(fmt.Errorf("failed to query database for user group list, error: %v", err))
return return
} }
uga.Data["json"] = userGroup uga.Data["json"] = userGroup
@ -96,43 +97,47 @@ func (uga *UserGroupAPI) Get() {
// Post ... Create User Group // Post ... Create User Group
func (uga *UserGroupAPI) Post() { func (uga *UserGroupAPI) Post() {
userGroup := models.UserGroup{} userGroup := models.UserGroup{}
uga.DecodeJSONReq(&userGroup) if err := uga.DecodeJSONReq(&userGroup); err != nil {
uga.SendBadRequestError(err)
return
}
userGroup.ID = 0 userGroup.ID = 0
userGroup.GroupType = common.LdapGroupType userGroup.GroupType = common.LdapGroupType
userGroup.LdapGroupDN = strings.TrimSpace(userGroup.LdapGroupDN) userGroup.LdapGroupDN = strings.TrimSpace(userGroup.LdapGroupDN)
userGroup.GroupName = strings.TrimSpace(userGroup.GroupName) userGroup.GroupName = strings.TrimSpace(userGroup.GroupName)
if len(userGroup.GroupName) == 0 { if len(userGroup.GroupName) == 0 {
uga.HandleBadRequest(userNameEmptyMsg) uga.SendBadRequestError(errors.New(userNameEmptyMsg))
return return
} }
query := models.UserGroup{GroupType: userGroup.GroupType, LdapGroupDN: userGroup.LdapGroupDN} query := models.UserGroup{GroupType: userGroup.GroupType, LdapGroupDN: userGroup.LdapGroupDN}
result, err := group.QueryUserGroup(query) result, err := group.QueryUserGroup(query)
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Error occurred in add user group, error: %v", err)) uga.SendInternalServerError(fmt.Errorf("error occurred in add user group, error: %v", err))
return return
} }
if len(result) > 0 { if len(result) > 0 {
uga.HandleConflict("Error occurred in add user group, duplicate user group exist.") uga.SendConflictError(errors.New("error occurred in add user group, duplicate user group exist"))
return return
} }
// User can not add ldap group when the ldap server is offline // User can not add ldap group when the ldap server is offline
ldapGroup, err := auth.SearchGroup(userGroup.LdapGroupDN) ldapGroup, err := auth.SearchGroup(userGroup.LdapGroupDN)
if err == ldap.ErrNotFound || ldapGroup == nil { if err == ldap.ErrNotFound || ldapGroup == nil {
uga.HandleNotFound(fmt.Sprintf("LDAP Group DN is not found: DN:%v", userGroup.LdapGroupDN)) uga.SendNotFoundError(fmt.Errorf("LDAP Group DN is not found: DN:%v", userGroup.LdapGroupDN))
return return
} }
if err == ldap.ErrDNSyntax { if err == ldap.ErrDNSyntax {
uga.HandleBadRequest(fmt.Sprintf("Invalid DN syntax. DN: %v", userGroup.LdapGroupDN)) uga.SendBadRequestError(fmt.Errorf("invalid DN syntax. DN: %v", userGroup.LdapGroupDN))
return return
} }
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Error occurred in search user group. error: %v", err)) uga.SendInternalServerError(fmt.Errorf("Error occurred in search user group. error: %v", err))
return return
} }
groupID, err := group.AddUserGroup(userGroup) groupID, err := group.AddUserGroup(userGroup)
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Error occurred in add user group, error: %v", err)) uga.SendInternalServerError(fmt.Errorf("Error occurred in add user group, error: %v", err))
return return
} }
uga.Redirect(http.StatusCreated, strconv.FormatInt(int64(groupID), 10)) uga.Redirect(http.StatusCreated, strconv.FormatInt(int64(groupID), 10))
@ -141,18 +146,21 @@ func (uga *UserGroupAPI) Post() {
// Put ... Only support update name // Put ... Only support update name
func (uga *UserGroupAPI) Put() { func (uga *UserGroupAPI) Put() {
userGroup := models.UserGroup{} userGroup := models.UserGroup{}
uga.DecodeJSONReq(&userGroup) if err := uga.DecodeJSONReq(&userGroup); err != nil {
uga.SendBadRequestError(err)
return
}
ID := uga.id ID := uga.id
userGroup.GroupName = strings.TrimSpace(userGroup.GroupName) userGroup.GroupName = strings.TrimSpace(userGroup.GroupName)
if len(userGroup.GroupName) == 0 { if len(userGroup.GroupName) == 0 {
uga.HandleBadRequest(userNameEmptyMsg) uga.SendBadRequestError(errors.New(userNameEmptyMsg))
return return
} }
userGroup.GroupType = common.LdapGroupType userGroup.GroupType = common.LdapGroupType
log.Debugf("Updated user group %v", userGroup) log.Debugf("Updated user group %v", userGroup)
err := group.UpdateUserGroupName(ID, userGroup.GroupName) err := group.UpdateUserGroupName(ID, userGroup.GroupName)
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Error occurred in update user group, error: %v", err)) uga.SendInternalServerError(fmt.Errorf("Error occurred in update user group, error: %v", err))
return return
} }
return return
@ -162,7 +170,7 @@ func (uga *UserGroupAPI) Put() {
func (uga *UserGroupAPI) Delete() { func (uga *UserGroupAPI) Delete() {
err := group.DeleteUserGroup(uga.id) err := group.DeleteUserGroup(uga.id)
if err != nil { if err != nil {
uga.HandleInternalServerError(fmt.Sprintf("Error occurred in update user group, error: %v", err)) uga.SendInternalServerError(fmt.Errorf("Error occurred in update user group, error: %v", err))
return return
} }
return return

View File

@ -94,18 +94,11 @@ func (a *Auth) PostAuthenticate(u *models.User) error {
return a.OnBoardUser(u) return a.OnBoardUser(u)
} }
// SearchUser - TODO: Remove this workaround when #6767 is fixed. // SearchUser returns nil as authproxy does not have such capability.
// When the flag is set it always return the default model without searching // When AlwaysOnboard is set it always return the default model.
func (a *Auth) SearchUser(username string) (*models.User, error) { func (a *Auth) SearchUser(username string) (*models.User, error) {
a.ensure() var u *models.User
var queryCondition = models.User{ if a.AlwaysOnboard {
Username: username,
}
u, err := dao.GetUser(queryCondition)
if err != nil {
return nil, err
}
if a.AlwaysOnboard && u == nil {
u = &models.User{Username: username} u = &models.User{Username: username}
if err := a.fillInModel(u); err != nil { if err := a.fillInModel(u); err != nil {
return nil, err return nil, err
@ -138,7 +131,7 @@ func (a *Auth) ensure() error {
return err return err
} }
a.Endpoint = setting.Endpoint a.Endpoint = setting.Endpoint
a.SkipCertVerify = setting.SkipCertVerify a.SkipCertVerify = !setting.VerifyCert
a.AlwaysOnboard = setting.AlwaysOnBoard a.AlwaysOnboard = setting.AlwaysOnBoard
} }
if a.client == nil { if a.client == nil {

View File

@ -80,11 +80,14 @@ func Init() error {
return nil return nil
} }
// InitWithSettings init config with predefined configs // InitWithSettings init config with predefined configs, and optionally overwrite the keyprovider
func InitWithSettings(cfgs map[string]interface{}) { func InitWithSettings(cfgs map[string]interface{}, kp ...comcfg.KeyProvider) {
Init() Init()
cfgMgr = comcfg.NewInMemoryManager() cfgMgr = comcfg.NewInMemoryManager()
cfgMgr.UpdateConfig(cfgs) cfgMgr.UpdateConfig(cfgs)
if len(kp) > 0 {
keyProvider = kp[0]
}
} }
func initKeyProvider() { func initKeyProvider() {
@ -473,7 +476,7 @@ func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
return &models.HTTPAuthProxy{ return &models.HTTPAuthProxy{
Endpoint: cfgMgr.Get(common.HTTPAuthProxyEndpoint).GetString(), Endpoint: cfgMgr.Get(common.HTTPAuthProxyEndpoint).GetString(),
TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(), TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(),
SkipCertVerify: cfgMgr.Get(common.HTTPAuthProxySkipCertVerify).GetBool(), VerifyCert: cfgMgr.Get(common.HTTPAuthProxyVerifyCert).GetBool(),
AlwaysOnBoard: cfgMgr.Get(common.HTTPAuthProxyAlwaysOnboard).GetBool(), AlwaysOnBoard: cfgMgr.Get(common.HTTPAuthProxyAlwaysOnboard).GetBool(),
}, nil }, nil
@ -493,12 +496,12 @@ func OIDCSetting() (*models.OIDCSetting, error) {
} }
return &models.OIDCSetting{ return &models.OIDCSetting{
Name: cfgMgr.Get(common.OIDCName).GetString(), Name: cfgMgr.Get(common.OIDCName).GetString(),
Endpoint: cfgMgr.Get(common.OIDCEndpoint).GetString(), Endpoint: cfgMgr.Get(common.OIDCEndpoint).GetString(),
SkipCertVerify: cfgMgr.Get(common.OIDCSkipCertVerify).GetBool(), VerifyCert: cfgMgr.Get(common.OIDCVerifyCert).GetBool(),
ClientID: cfgMgr.Get(common.OIDCCLientID).GetString(), ClientID: cfgMgr.Get(common.OIDCCLientID).GetString(),
ClientSecret: cfgMgr.Get(common.OIDCClientSecret).GetString(), ClientSecret: cfgMgr.Get(common.OIDCClientSecret).GetString(),
RedirectURL: extEndpoint + common.OIDCCallbackPath, RedirectURL: extEndpoint + common.OIDCCallbackPath,
Scope: scope, Scope: scope,
}, nil }, nil
} }

View File

@ -228,36 +228,36 @@ func TestConfigureValue_GetMap(t *testing.T) {
func TestHTTPAuthProxySetting(t *testing.T) { func TestHTTPAuthProxySetting(t *testing.T) {
m := map[string]interface{}{ m := map[string]interface{}{
common.HTTPAuthProxyAlwaysOnboard: "true", common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxySkipCertVerify: "true", common.HTTPAuthProxyVerifyCert: "true",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix", common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
} }
InitWithSettings(m) InitWithSettings(m)
v, e := HTTPAuthProxySetting() v, e := HTTPAuthProxySetting()
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, *v, models.HTTPAuthProxy{ assert.Equal(t, *v, models.HTTPAuthProxy{
Endpoint: "https://auth.proxy/suffix", Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true, AlwaysOnBoard: true,
SkipCertVerify: true, VerifyCert: true,
}) })
} }
func TestOIDCSetting(t *testing.T) { func TestOIDCSetting(t *testing.T) {
m := map[string]interface{}{ m := map[string]interface{}{
common.OIDCName: "test", common.OIDCName: "test",
common.OIDCEndpoint: "https://oidc.test", common.OIDCEndpoint: "https://oidc.test",
common.OIDCSkipCertVerify: "true", common.OIDCVerifyCert: "true",
common.OIDCScope: "openid, profile", common.OIDCScope: "openid, profile",
common.OIDCCLientID: "client", common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret", common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test", common.ExtEndpoint: "https://harbor.test",
} }
InitWithSettings(m) InitWithSettings(m)
v, e := OIDCSetting() v, e := OIDCSetting()
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "test", v.Name) assert.Equal(t, "test", v.Name)
assert.Equal(t, "https://oidc.test", v.Endpoint) assert.Equal(t, "https://oidc.test", v.Endpoint)
assert.True(t, v.SkipCertVerify) assert.True(t, v.VerifyCert)
assert.Equal(t, "client", v.ClientID) assert.Equal(t, "client", v.ClientID)
assert.Equal(t, "secret", v.ClientSecret) assert.Equal(t, "secret", v.ClientSecret)
assert.Equal(t, "https://harbor.test/c/oidc/callback", v.RedirectURL) assert.Equal(t, "https://harbor.test/c/oidc/callback", v.RedirectURL)

View File

@ -35,6 +35,8 @@ import (
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
) )
const userKey = "user"
// CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ... // CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ...
type CommonController struct { type CommonController struct {
beego.Controller beego.Controller
@ -69,7 +71,7 @@ func (cc *CommonController) Login() {
if user == nil { if user == nil {
cc.CustomAbort(http.StatusUnauthorized, "") cc.CustomAbort(http.StatusUnauthorized, "")
} }
cc.SetSession("user", *user) cc.SetSession(userKey, *user)
} }
// LogOut Habor UI // LogOut Habor UI

View File

@ -21,6 +21,7 @@ import (
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/oidc" "github.com/goharbor/harbor/src/common/utils/oidc"
"github.com/goharbor/harbor/src/core/api" "github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
@ -29,14 +30,19 @@ import (
"strings" "strings"
) )
const idTokenKey = "oidc_id_token" const tokenKey = "oidc_token"
const stateKey = "oidc_state" const stateKey = "oidc_state"
const userInfoKey = "oidc_user_info"
// OIDCController handles requests for OIDC login, callback and user onboard // OIDCController handles requests for OIDC login, callback and user onboard
type OIDCController struct { type OIDCController struct {
api.BaseController api.BaseController
} }
type onboardReq struct {
Username string `json:"username"`
}
type oidcUserData struct { type oidcUserData struct {
Issuer string `json:"iss"` Issuer string `json:"iss"`
Subject string `json:"sub"` Subject string `json:"sub"`
@ -47,7 +53,8 @@ type oidcUserData struct {
// Prepare include public code path for call request handler of OIDCController // Prepare include public code path for call request handler of OIDCController
func (oc *OIDCController) Prepare() { func (oc *OIDCController) Prepare() {
if mode, _ := config.AuthMode(); mode != common.OIDCAuth { if mode, _ := config.AuthMode(); mode != common.OIDCAuth {
oc.CustomAbort(http.StatusPreconditionFailed, fmt.Sprintf("Auth Mode: %s is not OIDC based.", mode)) oc.SendPreconditionFailedError(fmt.Errorf("Auth Mode: %s is not OIDC based", mode))
return
} }
} }
@ -56,7 +63,7 @@ func (oc *OIDCController) RedirectLogin() {
state := utils.GenerateRandomString() state := utils.GenerateRandomString()
url, err := oidc.AuthCodeURL(state) url, err := oidc.AuthCodeURL(state)
if err != nil { if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
return return
} }
oc.SetSession(stateKey, state) oc.SetSession(stateKey, state)
@ -68,66 +75,101 @@ func (oc *OIDCController) RedirectLogin() {
// kick off onboard if needed. // kick off onboard if needed.
func (oc *OIDCController) Callback() { func (oc *OIDCController) Callback() {
if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) { if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) {
oc.RenderError(http.StatusBadRequest, "State mismatch.") oc.SendBadRequestError(errors.New("State mismatch"))
return return
} }
code := oc.Ctx.Request.URL.Query().Get("code") code := oc.Ctx.Request.URL.Query().Get("code")
ctx := oc.Ctx.Request.Context() ctx := oc.Ctx.Request.Context()
token, err := oidc.ExchangeToken(ctx, code) token, err := oidc.ExchangeToken(ctx, code)
if err != nil { if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
return return
} }
idToken, err := oidc.VerifyToken(ctx, token.IDToken) idToken, err := oidc.VerifyToken(ctx, token.IDToken)
if err != nil { if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
return return
} }
d := &oidcUserData{} d := &oidcUserData{}
err = idToken.Claims(d) err = idToken.Claims(d)
if err != nil { if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
return return
} }
ouDataStr, err := json.Marshal(d) ouDataStr, err := json.Marshal(d)
if err != nil { if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
return return
} }
oc.SetSession(idTokenKey, string(ouDataStr)) u, err := dao.GetUserBySubIss(d.Subject, d.Issuer)
// TODO: check and trigger onboard popup or redirect user to project page if err != nil {
oc.Data["json"] = d oc.SendInternalServerError(err)
oc.ServeFormatted() return
}
tokenBytes, err := json.Marshal(token)
if err != nil {
oc.SendInternalServerError(err)
return
}
oc.SetSession(tokenKey, tokenBytes)
if u == nil {
oc.SetSession(userInfoKey, string(ouDataStr))
oc.Controller.Redirect("/oidc-onboard", http.StatusFound)
} else {
oc.SetSession(userKey, *u)
oc.Controller.Redirect("/", http.StatusFound)
}
} }
// Onboard handles the request to onboard an user authenticated via OIDC provider // Onboard handles the request to onboard an user authenticated via OIDC provider
func (oc *OIDCController) Onboard() { func (oc *OIDCController) Onboard() {
u := &onboardReq{}
username := oc.GetString("username") if err := oc.DecodeJSONReq(u); err != nil {
oc.SendBadRequestError(err)
return
}
username := u.Username
if utils.IsIllegalLength(username, 1, 255) { if utils.IsIllegalLength(username, 1, 255) {
oc.RenderFormatedError(http.StatusBadRequest, errors.New("username with illegal length")) oc.SendBadRequestError(errors.New("username with illegal length"))
return return
} }
if utils.IsContainIllegalChar(username, []string{",", "~", "#", "$", "%"}) { if utils.IsContainIllegalChar(username, []string{",", "~", "#", "$", "%"}) {
oc.RenderFormatedError(http.StatusBadRequest, errors.New("username contains illegal characters")) oc.SendBadRequestError(errors.New("username contains illegal characters"))
return return
} }
idTokenStr := oc.GetSession(idTokenKey) userInfoStr, ok := oc.GetSession(userInfoKey).(string)
d := &oidcUserData{} if !ok {
err := json.Unmarshal([]byte(idTokenStr.(string)), &d) oc.SendBadRequestError(errors.New("Failed to get OIDC user info from session"))
return
}
log.Debugf("User info string: %s\n", userInfoStr)
tb, ok := oc.GetSession(tokenKey).([]byte)
if !ok {
oc.SendBadRequestError(errors.New("Failed to get OIDC token from session"))
return
}
s, t, err := secretAndToken(tb)
if err != nil { if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
return
}
d := &oidcUserData{}
err = json.Unmarshal([]byte(userInfoStr), &d)
if err != nil {
oc.SendInternalServerError(err)
return return
} }
oidcUser := models.OIDCUser{ oidcUser := models.OIDCUser{
SubIss: d.Subject + d.Issuer, SubIss: d.Subject + d.Issuer,
// TODO: get secret with secret manager. Secret: s,
Secret: utils.GenerateRandomString(), Token: t,
} }
var email string email := d.Email
if d.Email == "" { if email == "" {
email = utils.GenerateRandomString() + "@harbor.com" email = utils.GenerateRandomString() + "@harbor.com"
} }
user := models.User{ user := models.User{
@ -139,12 +181,32 @@ func (oc *OIDCController) Onboard() {
err = dao.OnBoardOIDCUser(&user) err = dao.OnBoardOIDCUser(&user)
if err != nil { if err != nil {
if strings.Contains(err.Error(), dao.ErrDupUser.Error()) { if strings.Contains(err.Error(), dao.ErrDupUser.Error()) {
oc.RenderFormatedError(http.StatusConflict, err) oc.RenderError(http.StatusConflict, "Duplicate username")
return return
} }
oc.RenderFormatedError(http.StatusInternalServerError, err) oc.SendInternalServerError(err)
oc.DelSession(userInfoKey)
return return
} }
oc.Controller.Redirect(config.GetPortalURL(), http.StatusMovedPermanently) user.OIDCUserMeta = nil
oc.SetSession(userKey, user)
oc.DelSession(userInfoKey)
}
func secretAndToken(tokenBytes []byte) (string, string, error) {
key, err := config.SecretKey()
if err != nil {
return "", "", err
}
token, err := utils.ReversibleEncrypt((string)(tokenBytes), key)
if err != nil {
return "", "", err
}
str := utils.GenerateRandomString()
secret, err := utils.ReversibleEncrypt(str, key)
if err != nil {
return "", "", err
}
return secret, token, nil
} }

View File

@ -17,6 +17,7 @@ package filter
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common/utils/oidc"
"net/http" "net/http"
"regexp" "regexp"
@ -110,6 +111,7 @@ func Init() {
// standalone // standalone
reqCtxModifiers = []ReqCtxModifier{ reqCtxModifiers = []ReqCtxModifier{
&secretReqCtxModifier{config.SecretStore}, &secretReqCtxModifier{config.SecretStore},
&oidcCliReqCtxModifier{},
&authProxyReqCtxModifier{}, &authProxyReqCtxModifier{},
&robotAuthReqCtxModifier{}, &robotAuthReqCtxModifier{},
&basicAuthReqCtxModifier{}, &basicAuthReqCtxModifier{},
@ -205,6 +207,47 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
return true return true
} }
type oidcCliReqCtxModifier struct{}
func (oc *oidcCliReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
path := ctx.Request.URL.Path
if path != "/service/token" || strings.HasPrefix(path, "/chartrepo/") {
log.Debug("OIDC CLI modifer only handles request by docker CLI or helm CLI")
return false
}
authMode, err := config.AuthMode()
if err != nil {
log.Errorf("fail to get auth mode, %v", err)
return false
}
if authMode != common.OIDCAuth {
return false
}
username, secret, ok := ctx.Request.BasicAuth()
if !ok {
return false
}
user, err := dao.GetUser(models.User{
Username: username,
})
if err != nil {
log.Errorf("Failed to get user: %v", err)
return false
}
if user == nil {
return false
}
if err := oidc.VerifySecret(ctx.Request.Context(), user.UserID, secret); err != nil {
log.Errorf("Failed to verify secret: %v", err)
return false
}
pm := config.GlobalProjectMgr
sc := local.NewSecurityContext(user, pm)
setSecurCtxAndPM(ctx.Request, sc, pm)
return true
}
type authProxyReqCtxModifier struct{} type authProxyReqCtxModifier struct{}
func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool { func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
@ -249,7 +292,7 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
}, },
BearerToken: proxyPwd, BearerToken: proxyPwd,
TLSClientConfig: rest.TLSClientConfig{ TLSClientConfig: rest.TLSClientConfig{
Insecure: httpAuthProxyConf.SkipCertVerify, Insecure: !httpAuthProxyConf.VerifyCert,
}, },
} }
authClient, err := rest.RESTClientFor(authClientCfg) authClient, err := rest.RESTClientFor(authClientCfg)
@ -307,7 +350,6 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
return false return false
} }
log.Debug("using local database project manager")
pm := config.GlobalProjectMgr pm := config.GlobalProjectMgr
log.Debug("creating local database security context for auth proxy...") log.Debug("creating local database security context for auth proxy...")
securCtx := local.NewSecurityContext(user, pm) securCtx := local.NewSecurityContext(user, pm)

View File

@ -16,6 +16,8 @@ package filter
import ( import (
"context" "context"
"github.com/goharbor/harbor/src/common/utils/oidc"
"github.com/stretchr/testify/require"
"log" "log"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -28,6 +30,7 @@ import (
"github.com/astaxie/beego" "github.com/astaxie/beego"
beegoctx "github.com/astaxie/beego/context" beegoctx "github.com/astaxie/beego/context"
"github.com/astaxie/beego/session" "github.com/astaxie/beego/session"
config2 "github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
commonsecret "github.com/goharbor/harbor/src/common/secret" commonsecret "github.com/goharbor/harbor/src/common/secret"
@ -118,6 +121,53 @@ func TestSecretReqCtxModifier(t *testing.T) {
assert.NotNil(t, projectManager(ctx)) assert.NotNil(t, projectManager(ctx))
} }
func TestOIDCCliReqCtxModifier(t *testing.T) {
conf := map[string]interface{}{
common.AUTHMode: common.OIDCAuth,
common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCVerifyCert: "true",
common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test",
}
kp := &config2.PresetKeyProvider{Key: "naa4JtarA1Zsc3uY"}
config.InitWithSettings(conf, kp)
modifier := &oidcCliReqCtxModifier{}
req1, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/api/projects/", nil)
require.Nil(t, err)
ctx1, err := newContext(req1)
require.Nil(t, err)
assert.False(t, modifier.Modify(ctx1))
req2, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/service/token", nil)
require.Nil(t, err)
ctx2, err := newContext(req2)
require.Nil(t, err)
assert.False(t, modifier.Modify(ctx2))
username := "oidcModiferTester"
password := "oidcSecret"
u := &models.User{
Username: username,
Email: "testtest@test.org",
Password: "12345678",
}
id, err := dao.Register(*u)
require.Nil(t, err)
oidc.SetHardcodeVerifierForTest(password)
req3, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/service/token", nil)
require.Nil(t, err)
req3.SetBasicAuth(username, password)
ctx3, err := newContext(req3)
assert.True(t, modifier.Modify(ctx3))
o := dao.GetOrmer()
_, err = o.Delete(&models.User{UserID: int(id)})
assert.Nil(t, err)
}
func TestRobotReqCtxModifier(t *testing.T) { func TestRobotReqCtxModifier(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, req, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/api/projects/", nil) "http://127.0.0.1/api/projects/", nil)
@ -135,7 +185,7 @@ func TestRobotReqCtxModifier(t *testing.T) {
assert.False(t, modified) assert.False(t, modified)
} }
func TestAutoProxyReqCtxModifier(t *testing.T) { func TestAuthProxyReqCtxModifier(t *testing.T) {
server, err := fiter_test.NewAuthProxyTestServer() server, err := fiter_test.NewAuthProxyTestServer()
assert.Nil(t, err) assert.Nil(t, err)
@ -143,7 +193,7 @@ func TestAutoProxyReqCtxModifier(t *testing.T) {
c := map[string]interface{}{ c := map[string]interface{}{
common.HTTPAuthProxyAlwaysOnboard: "true", common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxySkipCertVerify: "true", common.HTTPAuthProxyVerifyCert: "false",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix", common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
common.HTTPAuthProxyTokenReviewEndpoint: server.URL, common.HTTPAuthProxyTokenReviewEndpoint: server.URL,
common.AUTHMode: common.HTTPAuth, common.AUTHMode: common.HTTPAuth,
@ -155,7 +205,7 @@ func TestAutoProxyReqCtxModifier(t *testing.T) {
assert.Equal(t, *v, models.HTTPAuthProxy{ assert.Equal(t, *v, models.HTTPAuthProxy{
Endpoint: "https://auth.proxy/suffix", Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true, AlwaysOnBoard: true,
SkipCertVerify: true, VerifyCert: false,
TokenReviewEndpoint: server.URL, TokenReviewEndpoint: server.URL,
}) })

View File

@ -75,12 +75,12 @@ func (h *Handler) HandleAdminJob() {
log.Infof("received admin job status update event: job-%d, status-%s", h.id, h.status) log.Infof("received admin job status update event: job-%d, status-%s", h.id, h.status)
// create the mapping relationship between the jobs in database and jobservice // create the mapping relationship between the jobs in database and jobservice
if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil { if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil {
h.HandleInternalServerError(err.Error()) h.SendInternalServerError(err)
return return
} }
if err := dao.UpdateAdminJobStatus(h.id, h.status); err != nil { if err := dao.UpdateAdminJobStatus(h.id, h.status); err != nil {
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status) log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
h.HandleInternalServerError(err.Error()) h.SendInternalServerError(err)
return return
} }
} }

View File

@ -78,7 +78,7 @@ func (h *Handler) HandleScan() {
log.Debugf("received san job status update event: job-%d, status-%s", h.id, h.status) log.Debugf("received san job status update event: job-%d, status-%s", h.id, h.status)
if err := dao.UpdateScanJobStatus(h.id, h.status); err != nil { if err := dao.UpdateScanJobStatus(h.id, h.status); err != nil {
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status) log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
h.HandleInternalServerError(err.Error()) h.SendInternalServerError(err)
return return
} }
} }
@ -88,7 +88,7 @@ func (h *Handler) HandleReplicationScheduleJob() {
log.Debugf("received replication schedule job status update event: schedule-job-%d, status-%s", h.id, h.status) log.Debugf("received replication schedule job status update event: schedule-job-%d, status-%s", h.id, h.status)
if err := scheduler.UpdateStatus(h.id, h.status); err != nil { if err := scheduler.UpdateStatus(h.id, h.status); err != nil {
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status) log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
h.HandleInternalServerError(err.Error()) h.SendInternalServerError(err)
return return
} }
} }

View File

@ -1,27 +0,0 @@
package utils
import (
"encoding/json"
"errors"
)
// WrapErrorMessage wraps the error msg to the well formated error message `{ "error": "The error message" }`
func WrapErrorMessage(msg string) string {
errBody := make(map[string]string, 1)
errBody["error"] = msg
data, err := json.Marshal(&errBody)
if err != nil {
return msg
}
return string(data)
}
// WrapError wraps the error to the well formated error `{ "error": "The error message" }`
func WrapError(err error) error {
if err == nil {
return nil
}
return errors.New(WrapErrorMessage(err.Error()))
}

View File

@ -1,33 +0,0 @@
package utils
import (
"encoding/json"
"errors"
"testing"
)
// Test case for error wrapping function.
func TestWrapError(t *testing.T) {
if WrapError(nil) != nil {
t.Fatal("expect nil error but got a non-nil one")
}
err := errors.New("mock error")
formatedErr := WrapError(err)
if formatedErr == nil {
t.Fatal("expect non-nil error but got nil")
}
jsonErr := formatedErr.Error()
structuredErr := make(map[string]string, 1)
if e := json.Unmarshal([]byte(jsonErr), &structuredErr); e != nil {
t.Fatal("expect nil error but got a non-nil one when doing error converting")
}
if msg, ok := structuredErr["error"]; !ok {
t.Fatal("expect an 'error' filed but missing")
} else {
if msg != "mock error" {
t.Fatalf("expect error message '%s' but got '%s'", "mock error", msg)
}
}
}

View File

@ -89,13 +89,14 @@ export class Configuration {
scan_all_policy: ComplexValueItem; scan_all_policy: ComplexValueItem;
read_only: BoolValueItem; read_only: BoolValueItem;
http_authproxy_endpoint?: StringValueItem; http_authproxy_endpoint?: StringValueItem;
http_authproxy_skip_cert_verify?: BoolValueItem; http_authproxy_tokenreview_endpoint?: StringValueItem;
http_authproxy_verify_cert?: BoolValueItem;
http_authproxy_always_onboard?: BoolValueItem; http_authproxy_always_onboard?: BoolValueItem;
oidc_name?: StringValueItem; oidc_name?: StringValueItem;
oidc_endpoint?: StringValueItem; oidc_endpoint?: StringValueItem;
oidc_client_id?: StringValueItem; oidc_client_id?: StringValueItem;
oidc_client_secret?: StringValueItem; oidc_client_secret?: StringValueItem;
oidc_skip_cert_verify?: BoolValueItem; oidc_verify_cert?: BoolValueItem;
oidc_scope?: StringValueItem; oidc_scope?: StringValueItem;
public constructor() { public constructor() {
this.auth_mode = new StringValueItem("db_auth", true); this.auth_mode = new StringValueItem("db_auth", true);
@ -139,13 +140,14 @@ export class Configuration {
}, true); }, true);
this.read_only = new BoolValueItem(false, true); this.read_only = new BoolValueItem(false, true);
this.http_authproxy_endpoint = new StringValueItem("", true); this.http_authproxy_endpoint = new StringValueItem("", true);
this.http_authproxy_skip_cert_verify = new BoolValueItem(false, true); this.http_authproxy_tokenreview_endpoint = new StringValueItem("", true);
this.http_authproxy_verify_cert = new BoolValueItem(false, true);
this.http_authproxy_always_onboard = new BoolValueItem(false, true); this.http_authproxy_always_onboard = new BoolValueItem(false, true);
this.oidc_name = new StringValueItem('', true); this.oidc_name = new StringValueItem('', true);
this.oidc_endpoint = new StringValueItem('', true); this.oidc_endpoint = new StringValueItem('', true);
this.oidc_client_id = new StringValueItem('', true); this.oidc_client_id = new StringValueItem('', true);
this.oidc_client_secret = new StringValueItem('', true); this.oidc_client_secret = new StringValueItem('', true);
this.oidc_skip_cert_verify = new BoolValueItem(false, true); this.oidc_verify_cert = new BoolValueItem(false, true);
this.oidc_scope = new StringValueItem('', true); this.oidc_scope = new StringValueItem('', true);
} }
} }

View File

@ -33,7 +33,7 @@
</select> </select>
</div> </div>
<span [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }} :</span> <span [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }} :</span>
<div [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM"> <div [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM" class="cron-input">
<label for="targetCron" aria-haspopup="true" role="tooltip" [class.invalid]="dateInvalid" class="tooltip tooltip-validation tooltip-md tooltip-top-right cron-label"> <label for="targetCron" aria-haspopup="true" role="tooltip" [class.invalid]="dateInvalid" class="tooltip tooltip-validation tooltip-md tooltip-top-right cron-label">
<input type="text" (blur)="blurInvalid()" (input)="inputInvalid()" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="form-control" <input type="text" (blur)="blurInvalid()" (input)="inputInvalid()" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="form-control"
[(ngModel)]="cronString"> [(ngModel)]="cronString">

View File

@ -37,6 +37,12 @@
width: 100px; width: 100px;
} }
.cron-input {
position: absolute;
display: inline-block;
width: 270px;
}
.cron-label { .cron-label {
width: 195px; width: 195px;
} }

View File

@ -198,7 +198,7 @@ describe('RepositoryComponentGridview (inline template)', () => {
}); });
})); }));
// Will fail after upgrade to angular 6. todo: need to fix it. // Will fail after upgrade to angular 6. todo: need to fix it.
xit('should filter data by keyword', async(() => { it('should filter data by keyword', async(() => {
fixtureRepo.whenStable().then(() => { fixtureRepo.whenStable().then(() => {
fixtureRepo.detectChanges(); fixtureRepo.detectChanges();

View File

@ -51,6 +51,19 @@
</span> </span>
</label> </label>
</div> </div>
<div class="form-group form-group-override" *ngIf="account.oidc_user_meta">
<label for="cli_password" aria-haspopup="true" class="form-group-label-override"><span class="label-inner-text">{{'PROFILE.CLI_PASSWORD' | translate}}</span>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="20"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="md" *clrIfOpen>
<span> {{'PROFILE.CLI_PASSWORD_TIP' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip></label>
<input type="password" name="cli_password" disabled [ngModel]="'account.oidc_user_meta.secret'" size="33">
<div class="rename-tool">
<hbr-copy-input #copyInput (onCopySuccess)="onSuccess($event)" (onCopyError)="onCpError($event)" iconMode="true" defaultValue="{{account.oidc_user_meta.secret}}"></hbr-copy-input>
</div>
</div>
</section> </section>
</form> </form>
</div> </div>

View File

@ -11,4 +11,7 @@ clr-modal {
position: relative; position: relative;
bottom: 9px; bottom: 9px;
} }
.label-inner-text{
margin: 0;
}
} }

View File

@ -22,7 +22,7 @@ import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.com
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service"; import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { SearchTriggerService } from "../../base/global-search/search-trigger.service"; import { SearchTriggerService } from "../../base/global-search/search-trigger.service";
import { CommonRoutes } from "../../shared/shared.const"; import { CommonRoutes } from "../../shared/shared.const";
import { CopyInputComponent } from "@harbor/ui";
@Component({ @Component({
selector: "account-settings-modal", selector: "account-settings-modal",
templateUrl: "account-settings-modal.component.html", templateUrl: "account-settings-modal.component.html",
@ -48,6 +48,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
accountFormRef: NgForm; accountFormRef: NgForm;
@ViewChild("accountSettingsFrom") accountForm: NgForm; @ViewChild("accountSettingsFrom") accountForm: NgForm;
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent; @ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
@ViewChild("copyInput") copyInput: CopyInputComponent;
constructor( constructor(
private session: SessionService, private session: SessionService,
@ -320,4 +321,10 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.inlineAlert.close(); this.inlineAlert.close();
this.opened = false; this.opened = false;
} }
onSuccess(event) {
this.inlineAlert.showInlineSuccess({message: 'PROFILE.COPY_SUCCESS'});
}
onError(event) {
this.inlineAlert.showInlineError({message: 'PROFILE.COPY_ERROR'});
}
} }

View File

@ -18,7 +18,6 @@ import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module'; import { RepositoryModule } from '../repository/repository.module';
import { SignInComponent } from './sign-in/sign-in.component';
import { PasswordSettingComponent } from './password-setting/password-setting.component'; import { PasswordSettingComponent } from './password-setting/password-setting.component';
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component'; import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
import { SignUpComponent } from './sign-up/sign-up.component'; import { SignUpComponent } from './sign-up/sign-up.component';
@ -36,7 +35,6 @@ import { PasswordSettingService } from './password-setting/password-setting.serv
RepositoryModule RepositoryModule
], ],
declarations: [ declarations: [
SignInComponent,
PasswordSettingComponent, PasswordSettingComponent,
AccountSettingsModalComponent, AccountSettingsModalComponent,
SignUpComponent, SignUpComponent,
@ -44,10 +42,11 @@ import { PasswordSettingService } from './password-setting/password-setting.serv
ResetPasswordComponent, ResetPasswordComponent,
SignUpPageComponent], SignUpPageComponent],
exports: [ exports: [
SignInComponent,
PasswordSettingComponent, PasswordSettingComponent,
AccountSettingsModalComponent, AccountSettingsModalComponent,
ForgotPasswordComponent,
ResetPasswordComponent, ResetPasswordComponent,
SignUpComponent,
SignUpPageComponent], SignUpPageComponent],
providers: [PasswordSettingService] providers: [PasswordSettingService]

View File

@ -19,6 +19,7 @@ import { BaseModule } from './base/base.module';
import { HarborRoutingModule } from './harbor-routing.module'; import { HarborRoutingModule } from './harbor-routing.module';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from './shared/shared.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
import { SignInModule } from './sign-in/sign-in.module';
import { ConfigurationModule } from './config/config.module'; import { ConfigurationModule } from './config/config.module';
import { DeveloperCenterModule } from './dev-center/dev-center.module'; import { DeveloperCenterModule } from './dev-center/dev-center.module';
import { registerLocaleData } from '@angular/common'; import { registerLocaleData } from '@angular/common';
@ -63,6 +64,7 @@ export function getCurrentLanguage(translateService: TranslateService) {
SharedModule, SharedModule,
BaseModule, BaseModule,
AccountModule, AccountModule,
SignInModule,
HarborRoutingModule, HarborRoutingModule,
ConfigurationModule, ConfigurationModule,
DeveloperCenterModule, DeveloperCenterModule,

View File

@ -46,7 +46,7 @@ import { SearchTriggerService } from './global-search/search-trigger.service';
HarborShellComponent, HarborShellComponent,
SearchResultComponent, SearchResultComponent,
], ],
exports: [ HarborShellComponent ], exports: [ HarborShellComponent, NavigatorComponent, SearchResultComponent ],
providers: [SearchTriggerService] providers: [SearchTriggerService]
}) })
export class BaseModule { export class BaseModule {

View File

@ -287,13 +287,26 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="http_authproxy_skip_cert_verify" <label for="http_authproxy_tokenreview_endpoint" class="required">{{'CONFIG.HTTP_AUTH.TOKEN_REVIEW' | translate}}</label>
<label for="http_authproxy_tokenreview_endpoint" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="httpAuthproxyTokenReviewInput.invalid && (httpAuthproxyTokenReviewInput.dirty || httpAuthproxyTokenReviewInput.touched)">
<input type="text" #httpAuthproxyTokenReviewInput="ngModel" pattern="^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.*?)*$"
required id="http_authproxy_tokenreview_endpoint" name="http_authproxy_tokenreview_endpoint" size="35"
[(ngModel)]="currentConfig.http_authproxy_tokenreview_endpoint.value" [disabled]="!currentConfig.http_authproxy_tokenreview_endpoint.editable">
<span class="tooltip-content">
{{'TOOLTIP.ENDPOINT_FORMAT' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="http_authproxy_verify_cert"
class="required">{{'CONFIG.HTTP_AUTH.VERIFY_CERT' | translate}}</label> class="required">{{'CONFIG.HTTP_AUTH.VERIFY_CERT' | translate}}</label>
<clr-checkbox-wrapper> <clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="http_authproxy_skip_cert_verify" <input type="checkbox" clrCheckbox name="http_authproxy_verify_cert"
id="http_authproxy_skip_cert_verify" id="http_authproxy_verify_cert"
[(ngModel)]="currentConfig.http_authproxy_skip_cert_verify.value" [(ngModel)]="currentConfig.http_authproxy_verify_cert.value"
[disabled]="!currentConfig.http_authproxy_skip_cert_verify.editable" /> [disabled]="!currentConfig.http_authproxy_verify_cert.editable" />
</clr-checkbox-wrapper> </clr-checkbox-wrapper>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -390,16 +403,16 @@
</a> </a>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="oidc_skip_cert_verify">{{'CONFIG.OIDC.OIDCSKIPCERTVERIFY' | translate}}</label> <label for="oidc_verify_cert">{{'CONFIG.OIDC.OIDC_VERIFYCERT' | translate}}</label>
<clr-checkbox-wrapper> <clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="oidc_skip_cert_verify" id="oidc_skip_cert_verify" <input type="checkbox" clrCheckbox name="oidc_verify_cert" id="oidc_verify_cert"
[disabled]="disabled(currentConfig.oidc_skip_cert_verify)" [disabled]="disabled(currentConfig.oidc_verify_cert)"
[(ngModel)]="currentConfig.oidc_skip_cert_verify.value" /> [(ngModel)]="currentConfig.oidc_verify_cert.value" />
</clr-checkbox-wrapper> </clr-checkbox-wrapper>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" <a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1px"> class="tooltip tooltip-lg tooltip-top-right top-1px">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon> <clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'TOOLTIP.OIDC_SKIPCERTVERIFY' | translate}}</span> <span class="tooltip-content">{{'TOOLTIP.OIDC_VERIFYCERT' | translate}}</span>
</a> </a>
</div> </div>
</section> </section>
@ -412,4 +425,4 @@
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" <button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn"
[disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button> [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideLDAPTestingSpinner"></span> <span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideLDAPTestingSpinner"></span>
</div> </div>

View File

@ -53,7 +53,6 @@ export class ConfigurationAuthComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) { if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig); this.originalConfig = clone(this.currentConfig);
} }
@ -153,9 +152,7 @@ export class ConfigurationAuthComponent implements OnChanges {
|| prop === 'auth_mode' || prop === 'auth_mode'
|| prop === 'project_creattion_restriction' || prop === 'project_creattion_restriction'
|| prop === 'self_registration' || prop === 'self_registration'
|| prop === 'http_authproxy_endpoint' || prop.startsWith('http_')
|| prop === 'http_authproxy_skip_cert_verify'
|| prop === 'http_authproxy_always_onboard'
) { ) {
changes[prop] = allChanges[prop]; changes[prop] = allChanges[prop];
} }

View File

@ -28,7 +28,7 @@ import { GcPageComponent } from './gc-page/gc-page.component';
import { VulnerabilityPageComponent } from './vulnerability-page/vulnerability-page.component'; import { VulnerabilityPageComponent } from './vulnerability-page/vulnerability-page.component';
import { UserComponent } from './user/user.component'; import { UserComponent } from './user/user.component';
import { SignInComponent } from './account/sign-in/sign-in.component'; import { SignInComponent } from './sign-in/sign-in.component';
import { ResetPasswordComponent } from './account/password-setting/reset-password/reset-password.component'; import { ResetPasswordComponent } from './account/password-setting/reset-password/reset-password.component';
import { GroupComponent } from './group/group.component'; import { GroupComponent } from './group/group.component';
@ -69,17 +69,18 @@ const harborRoutes: Routes = [
component: OidcOnboardComponent, component: OidcOnboardComponent,
canActivate: [OidcGuard, SignInGuard] canActivate: [OidcGuard, SignInGuard]
}, },
{
path: 'harbor/sign-in',
component: SignInComponent,
canActivate: [ SignInGuard]
},
{ {
path: 'harbor', path: 'harbor',
component: HarborShellComponent, component: HarborShellComponent,
// canActivate: [AuthCheckGuard],
canActivateChild: [AuthCheckGuard], canActivateChild: [AuthCheckGuard],
children: [ children: [
{ path: '', redirectTo: 'sign-in', pathMatch: 'full' }, { path: '', redirectTo: 'projects', pathMatch: 'full' },
{
path: 'sign-in',
component: SignInComponent,
canActivate: [SignInGuard]
},
{ {
path: 'projects', path: 'projects',
component: ProjectComponent component: ProjectComponent

View File

@ -54,7 +54,7 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
} }
this.searchTrigger.closeSearch(true); this.searchTrigger.closeSearch(true);
return new Observable( observer => { return new Observable(observer => {
let queryParams = route.queryParams; let queryParams = route.queryParams;
if (queryParams) { if (queryParams) {
if (queryParams[AdmiralQueryParamKey]) { if (queryParams[AdmiralQueryParamKey]) {
@ -72,25 +72,27 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
let user = this.authService.getCurrentUser(); let user = this.authService.getCurrentUser();
if (!user) { if (!user) {
this.authService.retrieveUser() this.authService.retrieveUser()
.subscribe(() => observer.next(true) .subscribe(() => {
, error => { return observer.next(true);
// If is guest, skip it }
if (this.isGuest(route, state)) { , error => {
return observer.next(true); // If is guest, skip it
} if (this.isGuest(route, state)) {
// Session retrieving failed then redirect to sign-in return observer.next(true);
// no matter what status code is. }
// Please pay attention that route 'HARBOR_ROOT' and 'EMBEDDED_SIGN_IN' support anonymous user // Session retrieving failed then redirect to sign-in
if (state.url !== CommonRoutes.HARBOR_ROOT && !state.url.startsWith(CommonRoutes.EMBEDDED_SIGN_IN)) { // no matter what status code is.
let navigatorExtra: NavigationExtras = { // Please pay attention that route 'HARBOR_ROOT' and 'EMBEDDED_SIGN_IN' support anonymous user
queryParams: { "redirect_url": state.url } if (!state.url.startsWith(CommonRoutes.EMBEDDED_SIGN_IN)) {
}; let navigatorExtra: NavigationExtras = {
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN], navigatorExtra); queryParams: { "redirect_url": state.url }
return observer.next(false); };
} else { this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN], navigatorExtra);
return observer.next(true); return observer.next(false);
} } else {
}); return observer.next(true);
}
});
} else { } else {
return observer.next(true); return observer.next(true);
} }

View File

@ -21,4 +21,13 @@ export class SessionUser {
role_id?: number; role_id?: number;
has_admin_role?: boolean; has_admin_role?: boolean;
comment: string; comment: string;
oidc_user_meta?: OidcUserMeta;
}
export class OidcUserMeta {
id: number;
user_id: number;
secret: string;
subiss: string;
creation_time: Date;
update_time: Date;
} }

View File

@ -1,3 +1,6 @@
<clr-main-container><global-message [isAppLevel]="true"></global-message>
<navigator></navigator>
<search-result></search-result>
<div class="login-wrapper" <div class="login-wrapper"
[ngStyle]="{'background-image': customLoginBgImg? 'url(static/images/' + customLoginBgImg + ')': ''}"> [ngStyle]="{'background-image': customLoginBgImg? 'url(static/images/' + customLoginBgImg + ')': ''}">
<form #signInForm="ngForm" class="login"> <form #signInForm="ngForm" class="login">
@ -37,8 +40,8 @@
<div [class.visibility-hidden]="!isError" class="error active"> <div [class.visibility-hidden]="!isError" class="error active">
{{ 'SIGN_IN.INVALID_MSG' | translate }} {{ 'SIGN_IN.INVALID_MSG' | translate }}
</div> </div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" <button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()"
(click)="signIn()" id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button> id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()" <a href="javascript:void(0)" class="signup" (click)="signUp()"
*ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a> *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div> </div>
@ -51,5 +54,6 @@
<top-repo class="repo-container"></top-repo> <top-repo class="repo-container"></top-repo>
</div> </div>
</div> </div>
</clr-main-container>
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up> <sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>
<forgot-password #forgotPwdDialog></forgot-password> <forgot-password #forgotPwdDialog></forgot-password>

View File

@ -55,7 +55,6 @@
.login-wrapper { .login-wrapper {
flex-wrap: wrap; flex-wrap: wrap;
margin-top:-20px;
.login { .login {
background:transparent; background:transparent;
} }

View File

@ -16,19 +16,19 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Input, ViewChild, AfterViewChecked } from '@angular/core'; import { Input, ViewChild, AfterViewChecked } from '@angular/core';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../shared/session.service';
import { SignInCredential } from '../../shared/sign-in-credential'; import { SignInCredential } from '../shared/sign-in-credential';
import { SignUpComponent } from '../sign-up/sign-up.component'; import { SignUpComponent } from '../account/sign-up/sign-up.component';
import { CommonRoutes } from '../../shared/shared.const'; import { CommonRoutes } from '../shared/shared.const';
import { ForgotPasswordComponent } from '../password-setting/forgot-password/forgot-password.component'; import { ForgotPasswordComponent } from '../account/password-setting/forgot-password/forgot-password.component';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../app-config.service';
import { AppConfig } from '../../app-config'; import { AppConfig } from '../app-config';
import { User } from '../../user/user'; import { User } from '../user/user';
import { CookieService, CookieOptions } from 'ngx-cookie'; import { CookieService, CookieOptions } from 'ngx-cookie';
import { SkinableConfig } from "../../skinable-config.service"; import { SkinableConfig } from "../skinable-config.service";
// Define status flags for signing in states // Define status flags for signing in states
export const signInStatusNormal = 0; export const signInStatusNormal = 0;
@ -271,4 +271,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
forgotPassword(): void { forgotPassword(): void {
this.forgotPwdDialog.open(); this.forgotPwdDialog.open();
} }
} }

View File

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SignInComponent } from './sign-in.component';
import { AccountModule } from '../account/account.module';
import { BaseModule } from '../base/base.module';
import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module';
@NgModule({
declarations: [
SignInComponent,
],
imports: [
CommonModule,
AccountModule,
SharedModule,
BaseModule,
RepositoryModule
]
})
export class SignInModule { }

View File

@ -15,8 +15,8 @@ import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http'; import { Http, URLSearchParams } from '@angular/http';
// import 'rxjs/add/operator/toPromise'; // import 'rxjs/add/operator/toPromise';
import { SignInCredential } from '../../shared/sign-in-credential'; import { SignInCredential } from '../shared/sign-in-credential';
import {HTTP_FORM_OPTIONS} from "../../shared/shared.utils"; import {HTTP_FORM_OPTIONS} from "../shared/shared.utils";
import { map, catchError } from "rxjs/operators"; import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs"; import { Observable, throwError as observableThrowError } from "rxjs";
const signInUrl = '/c/login'; const signInUrl = '/c/login';

View File

@ -74,12 +74,12 @@
"EMPTY": "Name is required", "EMPTY": "Name is required",
"NONEMPTY": "Can't be empty", "NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.", "REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.", "ENDPOINT_FORMAT": "Endpoint must start with HTTP:// or HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.", "OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS://.",
"OIDC_NAME": "The name of the OIDC provider.", "OIDC_NAME": "The name of the OIDC provider.",
"OIDC_ENDPOINT": "The URL of an OIDC-complaint server.", "OIDC_ENDPOINT": "The URL of an OIDC-complaint server.",
"OIDC_SCOPE": "The scope sent to OIDC server during authentication. It has to contain “openid”, and “offline_access”. If you are using google, please remove “offline_access” from this field.", "OIDC_SCOPE": "The scope sent to OIDC server during authentication. It has to contain “openid”, and “offline_access”. If you are using google, please remove “offline_access” from this field.",
"OIDC_SKIPCERTVERIFY": "Check this box if your OIDC server is hosted via self-signed certificate." "OIDC_VERIFYCERT": "Uncheck this box if your OIDC server is hosted via self-signed certificate."
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
"CURRENT_PWD": "Enter current password", "CURRENT_PWD": "Enter current password",
@ -102,7 +102,11 @@
"ADMIN_RENAME_BUTTON": "Change username", "ADMIN_RENAME_BUTTON": "Change username",
"ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.", "ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.",
"RENAME_SUCCESS": "Rename success!", "RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone." "RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone.",
"CLI_PASSWORD": "CLI secret",
"CLI_PASSWORD_TIP": "You can use this cli secret as password when using docker/helm cli to access Harbor.",
"COPY_SUCCESS": "copy success",
"COPY_ERROR": "copy failed"
}, },
"CHANGE_PWD": { "CHANGE_PWD": {
"TITLE": "Change Password", "TITLE": "Change Password",
@ -705,7 +709,7 @@
"FILTER": "LDAP Filter", "FILTER": "LDAP Filter",
"UID": "LDAP UID", "UID": "LDAP UID",
"SCOPE": "LDAP Scope", "SCOPE": "LDAP Scope",
"VERIFY_CERT": "LDAP Verify Cert", "VERIFY_CERT": "LDAP Verify Certificate",
"LDAP_GROUP_BASE_DN": "LDAP Group Base DN", "LDAP_GROUP_BASE_DN": "LDAP Group Base DN",
"LDAP_GROUP_BASE_DN_INFO": "The base DN from which to look up a group in LDAP/AD.", "LDAP_GROUP_BASE_DN_INFO": "The base DN from which to look up a group in LDAP/AD.",
"LDAP_GROUP_FILTER": "LDAP Group Filter", "LDAP_GROUP_FILTER": "LDAP Group Filter",
@ -728,16 +732,17 @@
}, },
"HTTP_AUTH": { "HTTP_AUTH": {
"ENDPOINT": "Server Endpoint", "ENDPOINT": "Server Endpoint",
"TOKEN_REVIEW": "Token Review Endpoint",
"ALWAYS_ONBOARD": "Always Onboard", "ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication Verify Cert" "VERIFY_CERT": "Verify Certificate"
}, },
"OIDC": { "OIDC": {
"OIDC_PROVIDER": "OIDC Provider", "OIDC_PROVIDER": "OIDC Provider Name",
"ENDPOINT": "OIDC Endpoint", "ENDPOINT": "OIDC Endpoint",
"CLIENT_ID": "OIDC Client ID", "CLIENT_ID": "OIDC Client ID",
"CLIENTSECRET": "OIDC Client Secret", "CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Scope", "SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "OIDC Skip Verifying Certificate", "OIDC_VERIFYCERT": "Verify Certificate",
"OIDC_SETNAME": "Set OIDC Username", "OIDC_SETNAME": "Set OIDC Username",
"OIDC_SETNAMECONTENT": "You must create a Harbor username the first time when authenticating via a third party(OIDC).This will be used within Harbor to be associated with projects, roles, etc.", "OIDC_SETNAMECONTENT": "You must create a Harbor username the first time when authenticating via a third party(OIDC).This will be used within Harbor to be associated with projects, roles, etc.",
"OIDC_USERNAME": "Username" "OIDC_USERNAME": "Username"

View File

@ -74,12 +74,12 @@
"EMPTY": "Name is required", "EMPTY": "Name is required",
"NONEMPTY": "Can't be empty", "NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.", "REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.", "ENDPOINT_FORMAT": "Endpoint must start with HTTP:// or HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.", "OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS://.",
"OIDC_NAME": "El nombre de la OIDC proveedor.", "OIDC_NAME": "El nombre de la OIDC proveedor.",
"OIDC_ENDPOINT": "La dirección URL de un servidor OIDC denuncia.", "OIDC_ENDPOINT": "La dirección URL de un servidor OIDC denuncia.",
"OIDC_SCOPE": "El ámbito de aplicación enviada a OIDC Server durante la autenticación.Tiene que contener 'Openid', y 'offline_access'.Si usted esta usando Google, por favor quitar 'offline_access' de este campo", "OIDC_SCOPE": "El ámbito de aplicación enviada a OIDC Server durante la autenticación.Tiene que contener 'Openid', y 'offline_access'.Si usted esta usando Google, por favor quitar 'offline_access' de este campo",
"OIDC_SKIPCERTVERIFY": "Marque esta casilla si tu OIDC servidor está alojado a través de certificado autofirmado." "OIDC_VERIFYCERT": "Desmarque esta casilla si tu OIDC servidor está alojado a través de certificado autofirmado."
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
"CURRENT_PWD": "Introduzca la contraseña actual", "CURRENT_PWD": "Introduzca la contraseña actual",
@ -102,7 +102,11 @@
"ADMIN_RENAME_BUTTON": "Change username", "ADMIN_RENAME_BUTTON": "Change username",
"ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.", "ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.",
"RENAME_SUCCESS": "Rename success!", "RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone." "RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone.",
"CLI_PASSWORD": "CLI secreto",
"CLI_PASSWORD_TIP": "Puede utilizar este generador CLI secreto como utilizando Docker / Helm CLI para acceder a puerto.",
"COPY_SUCCESS": "Copiar el éxito",
"COPY_ERROR": "Copia no"
}, },
"CHANGE_PWD": { "CHANGE_PWD": {
"TITLE": "Cambiar contraseña", "TITLE": "Cambiar contraseña",
@ -727,6 +731,7 @@
}, },
"HTTP_AUTH": { "HTTP_AUTH": {
"ENDPOINT": "Server Endpoint", "ENDPOINT": "Server Endpoint",
"TOKEN_REVIEW": "Review Endpoint De Token",
"ALWAYS_ONBOARD": "Always Onboard", "ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication Verify Cert" "VERIFY_CERT": "Authentication Verify Cert"
}, },
@ -736,7 +741,7 @@
"CLIENT_ID": "ID de cliente OIDC", "CLIENT_ID": "ID de cliente OIDC",
"CLIENTSECRET": "OIDC Client Secret", "CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Ámbito", "SCOPE": "OIDC Ámbito",
"OIDCSKIPCERTVERIFY": "OIDC Skip Verificar certificado", "OIDC_VERIFYCERT": "Verificar certificado",
"OIDC_SETNAME": "Set OIDC nombre de usuario", "OIDC_SETNAME": "Set OIDC nombre de usuario",
"OIDC_SETNAMECONTENT": "Usted debe crear un Harbor nombre de usuario la primera vez cuando la autenticación a través de un tercero (OIDC). Esta será usada en Harbor para ser asociados con proyectos, funciones, etc.", "OIDC_SETNAMECONTENT": "Usted debe crear un Harbor nombre de usuario la primera vez cuando la autenticación a través de un tercero (OIDC). Esta será usada en Harbor para ser asociados con proyectos, funciones, etc.",
"OIDC_USERNAME": "Usuario" "OIDC_USERNAME": "Usuario"

View File

@ -61,12 +61,12 @@
"USER_EXISTING": "Le nom d'utilisateur est déjà utilisé.", "USER_EXISTING": "Le nom d'utilisateur est déjà utilisé.",
"NONEMPTY": "Can't be empty", "NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.", "REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.", "ENDPOINT_FORMAT": "Endpoint must start with HTTP:// or HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.", "OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS://.",
"OIDC_NAME": "le nom du fournisseur de oidc.", "OIDC_NAME": "le nom du fournisseur de oidc.",
"OIDC_ENDPOINT": "l'url d'un serveur oidc plainte.", "OIDC_ENDPOINT": "l'url d'un serveur oidc plainte.",
"OIDC_SCOPE": "le champ envoyés au serveur au cours oidc l'authentification.il doit contenir 'openid', et 'offline_access'.si vous utilisez google, veuillez supprimer 'offline_access' dans ce domaine", "OIDC_SCOPE": "le champ envoyés au serveur au cours oidc l'authentification.il doit contenir 'openid', et 'offline_access'.si vous utilisez google, veuillez supprimer 'offline_access' dans ce domaine",
"OIDC_SKIPCERTVERIFY": "cocher cette case si votre oidc serveur est accueilli par auto - certificat signé." "OIDC_VERIFYCERT": "cocher cette case si votre oidc serveur est accueilli par auto - certificat signé."
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
"CURRENT_PWD": "Entrez le mot de passe actuel", "CURRENT_PWD": "Entrez le mot de passe actuel",
@ -89,7 +89,11 @@
"ADMIN_RENAME_BUTTON": "Change username", "ADMIN_RENAME_BUTTON": "Change username",
"ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.", "ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.",
"RENAME_SUCCESS": "Rename success!", "RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone." "RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone.",
"CLI_PASSWORD": "CLI secret",
"CLI_PASSWORD_TIP": "vous pouvez utiliser ce cli secret comme mot de passe quand utiliser docker / barre l'accès à harbor.",
"COPY_SUCCESS": "copie de succès",
"COPY_ERROR": "copie a échoué"
}, },
"CHANGE_PWD": { "CHANGE_PWD": {
"TITLE": "Modifier le mot de passe", "TITLE": "Modifier le mot de passe",
@ -692,7 +696,8 @@
}, },
"HTTP_AUTH": { "HTTP_AUTH": {
"ENDPOINT": "serveur paramètre", "ENDPOINT": "serveur paramètre",
"ALWAYS_ONBOARD": "Always Onboard", "TOKEN_REVIEW": "examen symbolique paramètre",
"ALWAYS_ONBOARD": "always onboard",
"VERIFY_CERT": "authentification vérifier cert" "VERIFY_CERT": "authentification vérifier cert"
}, },
"OIDC": { "OIDC": {
@ -701,7 +706,7 @@
"CLIENT_ID": "no d'identification du client OIDC", "CLIENT_ID": "no d'identification du client OIDC",
"CLIENTSECRET": "OIDC Client Secret", "CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Scope", "SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "Certificat OIDC skip vérifier", "OIDC_VERIFYCERT": "Certificat vérifier",
"OIDC_SETNAME": "Ensemble OIDC nom d'utilisateur", "OIDC_SETNAME": "Ensemble OIDC nom d'utilisateur",
"OIDC_SETNAMECONTENT": "vous devez créer un Harbor identifiant la première fois lors de la vérification par une tierce partie (oidc). il sera utilisé au sein de port à être associés aux projets, des rôles, etc.", "OIDC_SETNAMECONTENT": "vous devez créer un Harbor identifiant la première fois lors de la vérification par une tierce partie (oidc). il sera utilisé au sein de port à être associés aux projets, des rôles, etc.",
"OIDC_USERNAME": "d'utilisateur" "OIDC_USERNAME": "d'utilisateur"

View File

@ -72,12 +72,12 @@
"USER_EXISTING": "Nome de usuário já está em uso.", "USER_EXISTING": "Nome de usuário já está em uso.",
"RULE_USER_EXISTING": "Nome já em uso.", "RULE_USER_EXISTING": "Nome já em uso.",
"EMPTY": "Nome é obrigatório", "EMPTY": "Nome é obrigatório",
"ENDPOINT_FORMAT": "Avaliação deve começar por HTTP Ou HTTPS.", "ENDPOINT_FORMAT": "Avaliação deve começar por HTTP:// Ou HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Avaliação deve começar por HTTPS.", "OIDC_ENDPOIT_FORMAT": "Avaliação deve começar por HTTPS://.",
"OIDC_NAME": "O Nome do prestador de oidc.", "OIDC_NAME": "O Nome do prestador de oidc.",
"OIDC_ENDPOINT": "A URL de um servidor oidc denúncia.", "OIDC_ENDPOINT": "A URL de um servidor oidc denúncia.",
"OIDC_SCOPE": "O âmbito de aplicação enviada Ao servidor oidc Durante a autenticação.TEM que conter 'openid' e 'offline_access'.Se você está usando o Google, por favor remova 'offline_access' desse Campo.", "OIDC_SCOPE": "O âmbito de aplicação enviada Ao servidor oidc Durante a autenticação.TEM que conter 'openid' e 'offline_access'.Se você está usando o Google, por favor remova 'offline_access' desse Campo.",
"OIDC_SKIPCERTVERIFY": "Assinale esta opção se o SEU servidor está hospedado oidc via self - signed certificate." "OIDC_VERIFYCERT": "Desmarque esta opção se o SEU servidor está hospedado oidc via self - signed certificate."
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
"CURRENT_PWD": "Insira a senha atual", "CURRENT_PWD": "Insira a senha atual",
@ -100,7 +100,11 @@
"ADMIN_RENAME_BUTTON": "Alterar nome de usuário", "ADMIN_RENAME_BUTTON": "Alterar nome de usuário",
"ADMIN_RENAME_TIP": "Selecione o botão para alterar o nome de usuário para \"admin@harbor.local\". Essa operação não pode ser desfeita.", "ADMIN_RENAME_TIP": "Selecione o botão para alterar o nome de usuário para \"admin@harbor.local\". Essa operação não pode ser desfeita.",
"RENAME_SUCCESS": "Renomeado com sucesso!", "RENAME_SUCCESS": "Renomeado com sucesso!",
"RENAME_CONFIRM_INFO": "Atenção, alterar o nome para admin@harbor.local não pode ser desfeito." "RENAME_CONFIRM_INFO": "Atenção, alterar o nome para admin@harbor.local não pode ser desfeito.",
"CLI_PASSWORD": "Segredo CLI",
"CLI_PASSWORD_TIP": "Você Pode USAR este Segredo de clitóris Como senha Ao USAR clitóris de estivador /leme para acessar Harbor.",
"COPY_SUCCESS": "SUCESSO de cópia",
"COPY_ERROR": "Cópia falhou"
}, },
"CHANGE_PWD": { "CHANGE_PWD": {
"TITLE": "Alterar Senha", "TITLE": "Alterar Senha",
@ -720,7 +724,8 @@
"VERIFY_CERT": "Verificar certificado UAA" "VERIFY_CERT": "Verificar certificado UAA"
}, },
"HTTP_AUTH": { "HTTP_AUTH": {
"ENDPOINT": "server endpoint", "ENDPOINT": "Server endpoint",
"TOKEN_REVIEW": "Ponto final do Token Review",
"ALWAYS_ONBOARD": "Sempre Onboard", "ALWAYS_ONBOARD": "Sempre Onboard",
"VERIFY_CERT": "Verificar certificado de Authentication" "VERIFY_CERT": "Verificar certificado de Authentication"
}, },
@ -730,7 +735,7 @@
"CLIENT_ID": "ID de cliente OIDC", "CLIENT_ID": "ID de cliente OIDC",
"CLIENTSECRET": "OIDC Client Secret", "CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "Escopo OIDC", "SCOPE": "Escopo OIDC",
"OIDCSKIPCERTVERIFY": "OIDC Skip Verificar Certificado", "OIDC_VERIFYCERT": "Verificar Certificado",
"OIDC_SETNAME": "Definir o Utilizador OIDC", "OIDC_SETNAME": "Definir o Utilizador OIDC",
"OIDC_SETNAMECONTENT": "Você deve Criar um Nome de usuário do Porto a primeira vez que autenticar através de um terceiro (OIDC). Isto será usado Dentro de Harbor para ser associado a projetos, papéis, etc.", "OIDC_SETNAMECONTENT": "Você deve Criar um Nome de usuário do Porto a primeira vez que autenticar através de um terceiro (OIDC). Isto será usado Dentro de Harbor para ser associado a projetos, papéis, etc.",
"OIDC_USERNAME": "Utilizador" "OIDC_USERNAME": "Utilizador"

View File

@ -73,12 +73,12 @@
"RULE_USER_EXISTING": "名称已经存在。", "RULE_USER_EXISTING": "名称已经存在。",
"EMPTY": "名称为必填项", "EMPTY": "名称为必填项",
"NONEMPTY": "不能为空", "NONEMPTY": "不能为空",
"ENDPOINT_FORMAT": "Endpoint必须以http或https开头。", "ENDPOINT_FORMAT": "Endpoint必须以http://或https://开头。",
"OIDC_ENDPOIT_FORMAT": "Endpoint必须以https开头。", "OIDC_ENDPOIT_FORMAT": "Endpoint必须以https://开头。",
"OIDC_NAME": "OIDC提供商的名称.", "OIDC_NAME": "OIDC提供商的名称.",
"OIDC_ENDPOINT": "OIDC服务器的地址.", "OIDC_ENDPOINT": "OIDC服务器的地址.",
"OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google请从此字段中删除“脱机访问”。", "OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google请从此字段中删除“脱机访问”。",
"OIDC_SKIPCERTVERIFY": "如果您的OIDC服务器是通过自签名证书托管的选中此框。" "OIDC_VERIFYCERT": "如果您的OIDC服务器是通过自签名证书托管的取消选中此框。"
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码", "CURRENT_PWD": "输入当前密码",
@ -101,7 +101,11 @@
"ADMIN_RENAME_TIP": "单击将用户名改为 \"admin@harbor.local\", 注意这个操作是无法撤销的", "ADMIN_RENAME_TIP": "单击将用户名改为 \"admin@harbor.local\", 注意这个操作是无法撤销的",
"RENAME_SUCCESS": "用户名更改成功!", "RENAME_SUCCESS": "用户名更改成功!",
"ADMIN_RENAME_BUTTON": "更改用户名", "ADMIN_RENAME_BUTTON": "更改用户名",
"RENAME_CONFIRM_INFO": "更改用户名为admin@harbor.local是无法撤销的, 你确定更改吗?" "RENAME_CONFIRM_INFO": "更改用户名为admin@harbor.local是无法撤销的, 你确定更改吗?",
"CLI_PASSWORD": "CLI密码",
"CLI_PASSWORD_TIP": "使用docker/helm cli访问Harbor时可以使用此cli密码作为密码。",
"COPY_SUCCESS": "复制成功",
"COPY_ERROR": "复制失败"
}, },
"CHANGE_PWD": { "CHANGE_PWD": {
"TITLE": "修改密码", "TITLE": "修改密码",
@ -726,6 +730,7 @@
}, },
"HTTP_AUTH": { "HTTP_AUTH": {
"ENDPOINT": "Server Endpoint", "ENDPOINT": "Server Endpoint",
"TOKEN_REVIEW": "Token Review Endpoint",
"ALWAYS_ONBOARD": "Always Onboard", "ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication验证证书" "VERIFY_CERT": "Authentication验证证书"
}, },
@ -735,7 +740,7 @@
"CLIENT_ID": "OIDC 客户端标识", "CLIENT_ID": "OIDC 客户端标识",
"CLIENTSECRET": "OIDC 客户端密码", "CLIENTSECRET": "OIDC 客户端密码",
"SCOPE": "OIDC Scope", "SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "OIDC 验证证书", "OIDC_VERIFYCERT": "验证证书",
"OIDC_SETNAME": "设置OIDC用户名", "OIDC_SETNAME": "设置OIDC用户名",
"OIDC_SETNAMECONTENT": "在通过第三方OIDC进行身份验证时您必须第一次创建一个Harbor用户名。这将在端口中用于与项目、角色等关联。", "OIDC_SETNAMECONTENT": "在通过第三方OIDC进行身份验证时您必须第一次创建一个Harbor用户名。这将在端口中用于与项目、角色等关联。",
"OIDC_USERNAME": "用户名" "OIDC_USERNAME": "用户名"

View File

@ -15,6 +15,12 @@ def is_member_exist_in_project(members, member_user_name, expected_member_role_i
return True return True
return result return result
def get_member_id_by_name(members, member_user_name):
for member in members:
if member.entity_name == member_user_name:
return member.id
return None
class Project(base.Base): class Project(base.Base):
def create_project(self, name=None, metadata=None, expect_status_code = 201, expect_response_body = None, **kwargs): def create_project(self, name=None, metadata=None, expect_status_code = 201, expect_response_body = None, **kwargs):
if name is None: if name is None:
@ -131,6 +137,14 @@ class Project(base.Base):
base._assert_status_code(200, status_code) base._assert_status_code(200, status_code)
return data return data
def get_project_member_id(self, project_id, member_user_name, **kwargs):
members = self.get_project_members(project_id, **kwargs)
result = get_member_id_by_name(list(members), member_user_name)
if result == None:
raise Exception(r"Failed to get member id of member {} in project {}.".format(member_user_name, project_id))
else:
return result
def check_project_member_not_exist(self, project_id, member_user_name, **kwargs): def check_project_member_not_exist(self, project_id, member_user_name, **kwargs):
members = self.get_project_members(project_id, **kwargs) members = self.get_project_members(project_id, **kwargs)
result = is_member_exist_in_project(list(members), member_user_name) result = is_member_exist_in_project(list(members), member_user_name)

View File

@ -4,6 +4,7 @@ import time
import base import base
import swagger_client import swagger_client
from docker_api import DockerAPI from docker_api import DockerAPI
from swagger_client.rest import ApiException
def pull_harbor_image(registry, username, password, image, tag, expected_error_message = None): def pull_harbor_image(registry, username, password, image, tag, expected_error_message = None):
_docker_api = DockerAPI() _docker_api = DockerAPI()
@ -94,7 +95,7 @@ class Repository(base.Base):
time.sleep(5) time.sleep(5)
timeout_count = timeout_count - 1 timeout_count = timeout_count - 1
if (timeout_count == 0): if (timeout_count == 0):
break break
_tag = self.get_tag(repo_name, tag, **kwargs) _tag = self.get_tag(repo_name, tag, **kwargs)
if _tag.name == tag and _tag.scan_overview !=None: if _tag.name == tag and _tag.scan_overview !=None:
if _tag.scan_overview.scan_status == expected_scan_status: if _tag.scan_overview.scan_status == expected_scan_status:
@ -118,4 +119,20 @@ class Repository(base.Base):
if each_sign.tag == tag and len(each_sign.hashes["sha256"]) == 44: if each_sign.tag == tag and len(each_sign.hashes["sha256"]) == 44:
print "sha256:", len(each_sign.hashes["sha256"]) print "sha256:", len(each_sign.hashes["sha256"])
return return
raise Exception(r"Signature of {}:{} is not exist!".format(repo_name, tag)) raise Exception(r"Signature of {}:{} is not exist!".format(repo_name, tag))
def retag_image(self, repo_name, tag, src_image, override=True, expect_status_code = 200, expect_response_body = None, **kwargs):
client = self._get_client(**kwargs)
request = swagger_client.RetagReq(tag=tag, src_image=src_image, override=override)
try:
data, status_code, _ = client.repositories_repo_name_tags_post_with_http_info(repo_name, request)
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None:
base._assert_status_body(expect_response_body, e.body)
return
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(200, status_code)
return data

View File

@ -59,7 +59,7 @@ class TestProjects(unittest.TestCase):
#3. Create a new project(PA) by user(UA), and fail to create a new project; #3. Create a new project(PA) by user(UA), and fail to create a new project;
self.project.create_project(metadata = {"public": "false"}, expect_status_code = 403, self.project.create_project(metadata = {"public": "false"}, expect_status_code = 403,
expect_response_body = "Only system admin can create project", **TestProjects.USER_edit_project_creation_CLIENT) expect_response_body = "{\"code\":403,\"message\":\"Only system admin can create project\"}", **TestProjects.USER_edit_project_creation_CLIENT)
#4. Set project creation to "everyone"; #4. Set project creation to "everyone";
self.conf.set_configurations_of_project_creation_restriction("everyone", **ADMIN_CLIENT) self.conf.set_configurations_of_project_creation_restriction("everyone", **ADMIN_CLIENT)

View File

@ -0,0 +1,127 @@
from __future__ import absolute_import
import unittest
from library.base import _assert_status_code
from testutils import ADMIN_CLIENT
from testutils import harbor_server
from testutils import TEARDOWN
from library.project import Project
from library.user import User
from library.repository import Repository
from library.repository import push_image_to_project
from library.repository import pull_harbor_image
class TestProjects(unittest.TestCase):
@classmethod
def setUpClass(self):
project = Project()
self.project= project
user = User()
self.user= user
repo = Repository()
self.repo= repo
@classmethod
def tearDownClass(self):
print "Case completed"
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def test_ClearData(self):
#1. Delete repository(RA);
self.repo.delete_repoitory(TestProjects.src_repo_name, **TestProjects.USER_RETAG_CLIENT)
#2. Delete repository by retag;
self.repo.delete_repoitory(TestProjects.dst_repo_name, **TestProjects.USER_RETAG_CLIENT)
#3. Delete project(PA);
self.project.delete_project(TestProjects.project_src_repo_id, **TestProjects.USER_RETAG_CLIENT)
self.project.delete_project(TestProjects.project_dst_repo_id, **TestProjects.USER_RETAG_CLIENT)
#4. Delete user(UA).
self.user.delete_user(TestProjects.user_retag_id, **ADMIN_CLIENT)
def testRetag(self):
"""
Test case:
Retag Image
Test step and expected result:
1. Create a new user(UA);
2. Create a new project(PA) by user(UA);
3. Create a new project(PB) by user(UA);
4. Update role of user-retag as guest member of project(PB);
5. Create a new repository(RA) in project(PA) by user(UA);
6. Get repository in project(PA), there should be one repository which was created by user(UA);
7. Get repository(RA)'s image tag detail information;
8. Retag image in project(PA) to project(PB), it should be forbidden;
9. Update role of user-retag as admin member of project(PB);
10. Retag image in project(PA) to project(PB), it should be successful;
11. Get repository(RB)'s image tag detail information;
12. Read digest of retaged image, it must be the same with the image in repository(RA);
13. Pull image from project(PB) by user_retag, it must be successful;
Tear down:
1. Delete repository(RA);
2. Delete repository by retag;
3. Delete project(PA);
4. Delete user(UA).
"""
url = ADMIN_CLIENT["endpoint"]
user_retag_password = "Aa123456"
pull_tag_name = "latest"
dst_repo_sub_name = "repo"
dst_tag_name = "test_tag"
#1. Create a new user(UA);
TestProjects.user_retag_id, user_retag_name = self.user.create_user(user_password = user_retag_password, **ADMIN_CLIENT)
TestProjects.USER_RETAG_CLIENT=dict(endpoint = url, username = user_retag_name, password = user_retag_password)
#2. Create a new project(PA) by user(UA);
TestProjects.project_src_repo_id, project_src_repo_name = self.project.create_project(metadata = {"public": "false"}, **TestProjects.USER_RETAG_CLIENT)
#3. Create a new project(PB) by user(UA);
TestProjects.project_dst_repo_id, project_dst_repo_name = self.project.create_project(metadata = {"public": "false"}, **TestProjects.USER_RETAG_CLIENT)
retag_member_id = self.project.get_project_member_id(TestProjects.project_dst_repo_id, user_retag_name, **TestProjects.USER_RETAG_CLIENT)
#4. Update role of user-retag as guest member of project(PB);
self.project.update_project_member_role(TestProjects.project_dst_repo_id, retag_member_id, 3, **ADMIN_CLIENT)
#5. Create a new repository(RA) in project(PA) by user(UA);
TestProjects.src_repo_name, tag_name = push_image_to_project(project_src_repo_name, harbor_server, 'admin', 'Harbor12345', "hello-world", pull_tag_name)
#6. Get repository in project(PA), there should be one repository which was created by user(UA);
src_repo_data = self.repo.get_repository(TestProjects.project_src_repo_id, **TestProjects.USER_RETAG_CLIENT)
_assert_status_code(TestProjects.src_repo_name, src_repo_data[0].name)
#7. Get repository(RA)'s image tag detail information;
src_tag_data = self.repo.get_tag(TestProjects.src_repo_name, tag_name, **TestProjects.USER_RETAG_CLIENT)
TestProjects.dst_repo_name = project_dst_repo_name+"/"+ dst_repo_sub_name
#8. Retag image in project(PA) to project(PB), it should be forbidden;
self.repo.retag_image(TestProjects.dst_repo_name, dst_tag_name, TestProjects.src_repo_name+":"+src_tag_data.digest, expect_status_code=403, **TestProjects.USER_RETAG_CLIENT)
#9. Update role of user-retag as admin member of project(PB);
self.project.update_project_member_role(TestProjects.project_dst_repo_id, retag_member_id, 1, **ADMIN_CLIENT)
#10. Retag image in project(PA) to project(PB), it should be successful;
self.repo.retag_image(TestProjects.dst_repo_name, dst_tag_name, TestProjects.src_repo_name+":"+src_tag_data.digest, **TestProjects.USER_RETAG_CLIENT)
#11. Get repository(RB)'s image tag detail information;
dst_tag_data = self.repo.get_tag(TestProjects.dst_repo_name, dst_tag_name, **TestProjects.USER_RETAG_CLIENT)
#12. Read digest of retaged image, it must be the same with the image in repository(RA);
self.assertEqual(src_tag_data.digest, dst_tag_data.digest)
#13. Pull image from project(PB) by user_retag, it must be successful;"
pull_harbor_image(harbor_server, user_retag_name, user_retag_password, TestProjects.dst_repo_name, dst_tag_name)
if __name__ == '__main__':
unittest.main()

View File

@ -16,7 +16,7 @@
Documentation This resource provides any keywords related to the Harbor private registry appliance Documentation This resource provides any keywords related to the Harbor private registry appliance
*** Variables *** *** Variables ***
${sign_up_for_an_account_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/sign-in/div/form/div[1]/a ${sign_up_for_an_account_xpath} /html/body/harbor-app/sign-in/clr-main-container/div/form/div[1]/a
${sign_up_button_xpath} //a[@class='signup'] ${sign_up_button_xpath} //a[@class='signup']
${username_xpath} //*[@id='username'] ${username_xpath} //*[@id='username']
${email_xpath} //*[@id='email'] ${email_xpath} //*[@id='email']

View File

@ -0,0 +1,21 @@
# Copyright Project Harbor Authors
#
# 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
*** Settings ***
Documentation This resource provides any keywords related to public
*** Variables ***
${delete_btn} //clr-modal//button[contains(.,'DELETE')]
${delete_btn_2} //button[contains(.,'Delete')]

View File

@ -67,34 +67,27 @@ Multi-delete Object
Multi-delete User Multi-delete User
[Arguments] @{obj} [Arguments] @{obj}
:For ${obj} in @{obj} :For ${obj} in @{obj}
\ Click Element //clr-dg-row[contains(.,'${obj}')]//label \ Retry Element Click //clr-dg-row[contains(.,'${obj}')]//label
Sleep 1 Retry Element Click ${member_action_xpath}
Click Element ${member_action_xpath} Retry Element Click //clr-dropdown/clr-dropdown-menu/button[2]
Sleep 1 Retry Double Keywords When Error Retry Element Click ${delete_btn} Retry Wait Until Page Not Contains Element ${delete_btn}
Click Element //clr-dropdown/clr-dropdown-menu/button[2]
Sleep 2
Click Element //clr-modal//button[contains(.,'DELETE')]
Sleep 3
Multi-delete Member Multi-delete Member
[Arguments] @{obj} [Arguments] @{obj}
:For ${obj} in @{obj} :For ${obj} in @{obj}
\ Click Element //clr-dg-row[contains(.,'${obj}')]//label \ Retry Element Click //clr-dg-row[contains(.,'${obj}')]//label
Sleep 1 Retry Element Click ${member_action_xpath}
Click Element ${member_action_xpath} Retry Element Click ${delete_action_xpath}
Sleep 1 Retry Double Keywords When Error Retry Element Click ${delete_btn} Retry Wait Until Page Not Contains Element ${delete_btn}
Click Element ${delete_action_xpath}
Sleep 2
Click Element //clr-modal//button[contains(.,'DELETE')]
Sleep 3
Multi-delete Object Without Confirmation Multi-delete Object Without Confirmation
[Arguments] @{obj} [Arguments] @{obj}
:For ${obj} in @{obj} :For ${obj} in @{obj}
\ Click Element //clr-dg-row[contains(.,'${obj}')]//label \ Retry Element Click //clr-dg-row[contains(.,'${obj}')]//label
Sleep 1 Retry Double Keywords When Error Retry Element Click ${delete_btn_2} Retry Wait Until Page Not Contains Element ${delete_btn_2}
Click Element //button[contains(.,'Delete')]
Sleep 3
Select All On Current Page Object Select All On Current Page Object
Click Element //div[@class='datagrid-head']//label Retry Element Click //div[@class='datagrid-head']//label

View File

@ -28,6 +28,7 @@ Resource VCH-Util.robot
Resource Drone-Util.robot Resource Drone-Util.robot
Resource Github-Util.robot Resource Github-Util.robot
Resource Harbor-Util.robot Resource Harbor-Util.robot
Resource Harbor-Pages/Public_Elements.robot
Resource Harbor-Pages/HomePage.robot Resource Harbor-Pages/HomePage.robot
Resource Harbor-Pages/HomePage_Elements.robot Resource Harbor-Pages/HomePage_Elements.robot
Resource Harbor-Pages/Project.robot Resource Harbor-Pages/Project.robot

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