Merge pull request #7419 from wy65701436/replication_ng

Replication ng
This commit is contained in:
Wenkai Yin 2019-04-17 18:57:18 +08:00 committed by GitHub
commit 4ca482d6d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 1873 additions and 941 deletions

View File

@ -98,7 +98,7 @@ VERSIONFILENAME=UIVERSION
PREPARE_VERSION_NAME=versions
#versions
REGISTRYVERSION=v2.7.1
REGISTRYVERSION=v2.7.1-patch-2819
NGINXVERSION=$(VERSIONTAG)
NOTARYVERSION=v0.6.1
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:
type: object
properties:
disable:
disabled:
type: boolean
description: The robot account is disable or enable
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)
* [Online Garbage Collection.](#online-garbage-collection)
* [View build history.](#build-history)
* [Manage robot account of a project.](#robot-account)
## 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.
![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
hostname=reg.mydomain.com
if [ -n "$(grep '^[^#]*https:' ./harbor.yml)"]
if [ -n "$(grep '^[^#]*https:' ./harbor.yml)" ]
then
protocol=https
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 (
id SERIAL NOT NULL,
user_id int NOT NULL,
/*
Encoded secret
*/
secret varchar(255) NOT NULL,
/*
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
*/
subiss varchar(255) NOT NULL,
/*
Encoded token
*/
token text,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES harbor_user(user_id),
UNIQUE (subiss)
);

View File

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

View File

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

View File

@ -1,7 +1,7 @@
FROM golang:1.11
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
COPY . $DISTRIBUTION_DIR

View File

@ -24,6 +24,7 @@ import (
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/utils/log"
"errors"
"github.com/astaxie/beego"
)
@ -48,68 +49,6 @@ func (b *BaseAPI) GetInt64FromPath(key string) (int64, error) {
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
func (b *BaseAPI) Render() error {
return nil
@ -120,23 +59,35 @@ func (b *BaseAPI) RenderError(code int, text string) {
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
func (b *BaseAPI) DecodeJSONReq(v interface{}) {
func (b *BaseAPI) DecodeJSONReq(v interface{}) error {
err := json.Unmarshal(b.Ctx.Input.CopyBody(1<<32), v)
if err != nil {
log.Errorf("Error while decoding the json request, error: %v, %v",
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
func (b *BaseAPI) Validate(v interface{}) {
func (b *BaseAPI) Validate(v interface{}) (bool, error) {
validator := validation.Validation{}
isValid, err := validator.Valid(v)
if err != nil {
log.Errorf("failed to validate: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return false, err
}
if !isValid {
@ -144,14 +95,17 @@ func (b *BaseAPI) Validate(v interface{}) {
for _, e := range validator.Errors {
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
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) {
b.DecodeJSONReq(v)
b.Validate(v)
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) (bool, error) {
if err := b.DecodeJSONReq(v); err != nil {
return false, err
}
return b.Validate(v)
}
// 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
func (b *BaseAPI) GetIDFromURL() int64 {
func (b *BaseAPI) GetIDFromURL() (int64, error) {
idStr := b.Ctx.Input.Param(":id")
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)
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
@ -213,15 +167,15 @@ func (b *BaseAPI) SetPaginationHeader(total, page, pageSize int64) {
}
// GetPaginationParams ...
func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
page, err := b.GetInt64("page", 1)
func (b *BaseAPI) GetPaginationParams() (page, pageSize int64, err error) {
page, err = b.GetInt64("page", 1)
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)
if err != nil || pageSize <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page_size")
return 0, 0, errors.New("invalid page_size")
}
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)
}
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
}
// 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
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"testing"
@ -42,3 +43,12 @@ func TestGetOfFileKeyProvider(t *testing.T) {
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.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.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
@ -141,7 +141,7 @@ var (
{Name: common.OIDCCLientID, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
{Name: common.OIDCClientSecret, Scope: UserScope, Group: OIDCGroup, ItemType: &PasswordType{}},
{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_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"
HTTPAuthProxyEndpoint = "http_authproxy_endpoint"
HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint"
HTTPAuthProxySkipCertVerify = "http_authproxy_skip_cert_verify"
HTTPAuthProxyVerifyCert = "http_authproxy_verify_cert"
HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard"
OIDCName = "oidc_name"
OIDCEndpoint = "oidc_endpoint"
OIDCCLientID = "oidc_client_id"
OIDCClientSecret = "oidc_client_secret"
OIDCSkipCertVerify = "oidc_skip_cert_verify"
OIDCVerifyCert = "oidc_verify_cert"
OIDCScope = "oidc_scope"
DefaultClairEndpoint = "http://clair:6060"

View File

@ -15,16 +15,26 @@
package http
import (
"encoding/json"
"fmt"
)
// Error wrap HTTP status code and message as an error
type Error struct {
Code int
Message string
Code int `json:"code"`
Message string `json:"message"`
}
// Error ...
func (e *Error) Error() string {
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 {
Endpoint string `json:"endpoint"`
TokenReviewEndpoint string `json:"tokenreivew_endpoint"`
SkipCertVerify bool `json:"skip_cert_verify"`
VerifyCert bool `json:"verify_cert"`
AlwaysOnBoard bool `json:"always_onboard"`
}
// OIDCSetting wraps the settings for OIDC auth endpoint
type OIDCSetting struct {
Name string `json:"name"`
Endpoint string `json:"endpoint"`
SkipCertVerify bool `json:"skip_cert_verify"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
Scope []string `json:"scope"`
Name string `json:"name"`
Endpoint string `json:"endpoint"`
VerifyCert bool `json:"verify_cert"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
Scope []string `json:"scope"`
}
// ConfigEntry ...

View File

@ -6,10 +6,14 @@ import (
// OIDCUser ...
type OIDCUser struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
UserID int `orm:"column(user_id)" json:"user_id"`
Secret string `orm:"column(secret)" json:"secret"`
ID int64 `orm:"pk;auto;column(id)" json:"id"`
UserID int `orm:"column(user_id)" json:"user_id"`
// encrypted secret
Secret string `orm:"column(secret)" json:"-"`
// secret in plain text
PlainSecret string `orm:"-" json:"secret"`
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"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}

View File

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

View File

@ -41,14 +41,14 @@ type providerHelper struct {
}
type endpoint struct {
url string
skipCertVerify bool
url string
VerifyCert bool
}
func (p *providerHelper) get() (*gooidc.Provider, error) {
if p.instance.Load() != nil {
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 {
return nil, err
}
@ -90,24 +90,15 @@ func (p *providerHelper) create() error {
return errors.New("the configuration is not loaded")
}
s := p.setting.Load().(models.OIDCSetting)
var client *http.Client
if s.SkipCertVerify {
client = &http.Client{
Transport: insecureTransport,
}
} else {
client = &http.Client{}
}
ctx := context.Background()
gooidc.ClientContext(ctx, client)
ctx := clientCtx(context.Background(), s.VerifyCert)
provider, err := gooidc.NewProvider(ctx, s.Endpoint)
if err != nil {
return fmt.Errorf("failed to create OIDC provider, error: %v", err)
}
p.instance.Store(provider)
p.ep = endpoint{
url: s.Endpoint,
skipCertVerify: s.SkipCertVerify,
url: s.Endpoint,
VerifyCert: s.VerifyCert,
}
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)
return nil, err
}
setting := provider.setting.Load().(models.OIDCSetting)
ctx = clientCtx(ctx, setting.VerifyCert)
oauthToken, err := oauth.Exchange(ctx, code)
if err != nil {
return nil, err
@ -184,5 +177,36 @@ func VerifyToken(ctx context.Context, rawIDToken string) (*gooidc.IDToken, error
return nil, err
}
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)
}
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 (
"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/core/config"
"github.com/stretchr/testify/assert"
@ -28,16 +29,17 @@ import (
func TestMain(m *testing.M) {
conf := map[string]interface{}{
common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCSkipCertVerify: "false",
common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test",
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)
config.InitWithSettings(conf, kp)
result := m.Run()
if result != 0 {
@ -71,13 +73,13 @@ func TestHelperGet(t *testing.T) {
assert.Equal(t, "https://oauth2.googleapis.com/token", p.Endpoint().TokenURL)
update := map[string]interface{}{
common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCSkipCertVerify: "false",
common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "new-secret",
common.ExtEndpoint: "https://harbor.test",
common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCVerifyCert: "true",
common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "new-secret",
common.ExtEndpoint: "https://harbor.test",
}
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/core/api/models"
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.
@ -42,7 +43,7 @@ func (aj *AJAPI) Prepare() {
// updateSchedule update a schedule of admin job.
func (aj *AJAPI) updateSchedule(ajr models.AdminJobReq) {
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
}
@ -52,24 +53,24 @@ func (aj *AJAPI) updateSchedule(ajr models.AdminJobReq) {
}
jobs, err := dao.GetAdminJobs(query)
if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err))
aj.SendInternalServerError(err)
return
}
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
}
// stop the scheduled job and remove it.
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 {
aj.HandleInternalServerError(fmt.Sprintf("%v", err))
aj.SendInternalServerError(err)
return
}
}
if err = dao.DeleteAdminJob(jobs[0].ID); err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err))
aj.SendInternalServerError(err)
return
}
@ -85,17 +86,17 @@ func (aj *AJAPI) get(id int64) {
ID: id,
})
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
}
if len(jobs) == 0 {
aj.HandleNotFound("No admin job found.")
aj.SendNotFoundError(errors.New("no admin job found"))
return
}
adminJobRep, err := convertToAdminJobRep(jobs[0])
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
}
@ -107,7 +108,7 @@ func (aj *AJAPI) get(id int64) {
func (aj *AJAPI) list(name string) {
jobs, err := dao.GetTop10AdminJobsOfName(name)
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
}
@ -115,7 +116,7 @@ func (aj *AJAPI) list(name string) {
for _, job := range jobs {
AdminJobRep, err := convertToAdminJobRep(job)
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
}
AdminJobReps = append(AdminJobReps, &AdminJobRep)
@ -134,18 +135,18 @@ func (aj *AJAPI) getSchedule(name string) {
Kind: common_job.JobKindPeriodic,
})
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
}
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
}
if len(jobs) != 0 {
adminJobRep, err := convertToAdminJobRep(jobs[0])
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
}
adminJobSchedule.Schedule = adminJobRep.Schedule
@ -160,11 +161,13 @@ func (aj *AJAPI) getLog(id int64) {
job, err := dao.GetAdminJob(id)
if err != nil {
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 {
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)
@ -175,14 +178,14 @@ func (aj *AJAPI) getLog(id int64) {
id, httpErr.Code, httpErr.Message))
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
}
aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = aj.Ctx.ResponseWriter.Write(logBytes)
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,
})
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
}
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
}
}
@ -210,7 +213,7 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
Cron: ajr.CronString(),
})
if err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err))
aj.SendInternalServerError(err)
return
}
ajr.ID = id
@ -224,14 +227,14 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
log.Debugf("Failed to delete admin job, err: %v", err)
}
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
}
aj.HandleInternalServerError(fmt.Sprintf("%v", err))
aj.SendInternalServerError(err)
return
}
if err := dao.SetAdminJobUUID(id, uuid); err != nil {
aj.HandleInternalServerError(fmt.Sprintf("%v", err))
aj.SendInternalServerError(err)
return
}
}

View File

@ -17,14 +17,14 @@ package api
import (
"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/security"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/utils"
)
const (
@ -54,55 +54,20 @@ func (b *BaseController) Prepare() {
ctx, err := filter.GetSecurityContext(b.Ctx.Request)
if err != nil {
log.Errorf("failed to get security context: %v", err)
b.CustomAbort(http.StatusInternalServerError, "")
b.SendInternalServerError(errors.New(""))
return
}
b.SecurityCtx = ctx
pm, err := filter.GetProjectManager(b.Ctx.Request)
if err != nil {
log.Errorf("failed to get project manager: %v", err)
b.CustomAbort(http.StatusInternalServerError, "")
b.SendInternalServerError(errors.New(""))
return
}
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.
func (b *BaseController) WriteJSONData(object interface{}) {
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)
if !cla.SecurityCtx.Can(action, resource) {
cla.HandleForbidden(cla.SecurityCtx.GetUsername())
cla.SendForbiddenError(errors.New(cla.SecurityCtx.GetUsername()))
return false
}
@ -75,7 +75,10 @@ func (cla *ChartLabelAPI) MarkLabel() {
}
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)
if !ok {

View File

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

View File

@ -16,9 +16,9 @@ package api
import (
"fmt"
"net/http"
"strings"
"errors"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/config/metadata"
@ -41,20 +41,20 @@ func (c *ConfigAPI) Prepare() {
c.BaseController.Prepare()
c.cfgManager = corecfg.GetCfgManager()
if !c.SecurityCtx.IsAuthenticated() {
c.HandleUnauthorized()
c.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
// Only internal container can access /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 {
c.HandleUnauthorized()
c.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
}
if !c.SecurityCtx.IsSysAdmin() && !c.SecurityCtx.IsSolutionUser() {
c.HandleForbidden(c.SecurityCtx.GetUsername())
c.SendForbiddenError(errors.New(c.SecurityCtx.GetUsername()))
return
}
@ -71,7 +71,8 @@ func (c *ConfigAPI) Get() {
m, err := convertForGet(configs)
if err != nil {
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
@ -89,25 +90,33 @@ func (c *ConfigAPI) GetInternalConfig() {
// Put updates configurations
func (c *ConfigAPI) Put() {
m := map[string]interface{}{}
c.DecodeJSONReq(&m)
if err := c.DecodeJSONReq(&m); err != nil {
c.SendBadRequestError(err)
return
}
err := c.cfgManager.Load()
if err != nil {
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)
if err != nil {
if isSysErr {
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 {
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
import (
"errors"
"net"
"net/http"
"strconv"
"github.com/goharbor/harbor/src/common/utils/email"
@ -37,12 +37,12 @@ type EmailAPI struct {
func (e *EmailAPI) Prepare() {
e.BaseController.Prepare()
if !e.SecurityCtx.IsAuthenticated() {
e.HandleUnauthorized()
e.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
if !e.SecurityCtx.IsSysAdmin() {
e.HandleForbidden(e.SecurityCtx.GetUsername())
e.SendForbiddenError(errors.New(e.SecurityCtx.GetUsername()))
return
}
}
@ -57,8 +57,8 @@ func (e *EmailAPI) Ping() {
cfg, err := config.Email()
if err != nil {
log.Errorf("failed to get email configurations: %v", err)
e.CustomAbort(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
e.SendInternalServerError(err)
return
}
host = cfg.Host
port = cfg.Port
@ -77,18 +77,22 @@ func (e *EmailAPI) Ping() {
Identity string `json:"email_identity"`
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 {
e.CustomAbort(http.StatusBadRequest, "empty host or port")
e.SendBadRequestError(errors.New("empty host or port"))
return
}
if settings.Password == nil {
cfg, err := config.Email()
if err != nil {
log.Errorf("failed to get email configurations: %v", err)
e.CustomAbort(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
e.SendInternalServerError(err)
return
}
settings.Password = &cfg.Password
@ -108,7 +112,7 @@ func (e *EmailAPI) Ping() {
password, pingEmailTimeout, ssl, insecure); err != nil {
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
e.RenderError(http.StatusBadRequest, "failed to ping email server")
e.SendBadRequestError(errors.New("failed to ping email server"))
return
}
}

View File

@ -15,7 +15,7 @@
package api
import (
"net/http"
"errors"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
@ -32,11 +32,11 @@ type InternalAPI struct {
func (ia *InternalAPI) Prepare() {
ia.BaseController.Prepare()
if !ia.SecurityCtx.IsAuthenticated() {
ia.HandleUnauthorized()
ia.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
if !ia.SecurityCtx.IsSysAdmin() {
ia.HandleForbidden(ia.SecurityCtx.GetUsername())
ia.SendForbiddenError(errors.New(ia.SecurityCtx.GetUsername()))
return
}
}
@ -45,7 +45,7 @@ func (ia *InternalAPI) Prepare() {
func (ia *InternalAPI) SyncRegistry() {
err := SyncRegistry(ia.ProjectMgr)
if err != nil {
ia.HandleInternalServerError(err.Error())
ia.SendInternalServerError(err)
return
}
}
@ -54,7 +54,8 @@ func (ia *InternalAPI) SyncRegistry() {
func (ia *InternalAPI) RenameAdmin() {
if !dao.IsSuperUser(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
if err := dao.ChangeUserProfile(models.User{
@ -62,7 +63,8 @@ func (ia *InternalAPI) RenameAdmin() {
Username: newName,
}, "username"); err != nil {
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)
ia.DestroySession()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ func (pma *ProjectMemberAPI) Prepare() {
pma.BaseController.Prepare()
if !pma.SecurityCtx.IsAuthenticated() {
pma.HandleUnauthorized()
pma.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
pid, err := pma.GetInt64FromPath(":pid")
@ -61,7 +61,7 @@ func (pma *ProjectMemberAPI) Prepare() {
} else {
text += fmt.Sprintf("%d", pid)
}
pma.HandleBadRequest(text)
pma.SendBadRequestError(errors.New(text))
return
}
project, err := pma.ProjectMgr.Get(pid)
@ -70,7 +70,7 @@ func (pma *ProjectMemberAPI) Prepare() {
return
}
if project == nil {
pma.HandleNotFound(fmt.Sprintf("project %d not found", pid))
pma.SendNotFoundError(fmt.Errorf("project %d not found", pid))
return
}
pma.project = project
@ -80,7 +80,7 @@ func (pma *ProjectMemberAPI) Prepare() {
log.Warningf("Failed to get pmid from path, error %v", err)
}
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
}
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.IsAuthenticated() {
pma.HandleUnauthorized()
pma.SendUnAuthorizedError(errors.New("Unauthorized"))
} else {
pma.HandleForbidden(pma.SecurityCtx.GetUsername())
pma.SendForbiddenError(errors.New(pma.SecurityCtx.GetUsername()))
}
return false
@ -115,7 +115,7 @@ func (pma *ProjectMemberAPI) Get() {
entityname := pma.GetString("entityname")
memberList, err := project.SearchMemberByName(projectID, entityname)
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
}
if len(memberList) > 0 {
@ -127,11 +127,11 @@ func (pma *ProjectMemberAPI) Get() {
queryMember.ID = pma.id
memberList, err := project.GetProjectMember(queryMember)
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
}
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
}
@ -150,27 +150,30 @@ func (pma *ProjectMemberAPI) Post() {
}
projectID := pma.project.ProjectID
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)
pmid, err := AddProjectMember(projectID, request)
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
} 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
} 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
} 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
} 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
} 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
}
pma.Redirect(http.StatusCreated, strconv.FormatInt(int64(pmid), 10))
@ -184,14 +187,17 @@ func (pma *ProjectMemberAPI) Put() {
pid := pma.project.ProjectID
pmID := pma.id
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 {
pma.HandleBadRequest(fmt.Sprintf("Invalid role id %v", req.Role))
pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role))
return
}
err := project.UpdateProjectMemberRole(pmID, req.Role)
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
}
}
@ -204,7 +210,7 @@ func (pma *ProjectMemberAPI) Delete() {
pmid := pma.id
err := project.DeleteProjectMemberByID(pmid)
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ import (
"strings"
"time"
"errors"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/common"
@ -110,13 +111,13 @@ type manifestResp struct {
func (ra *RepositoryAPI) Get() {
projectID, err := ra.GetInt64("project_id")
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
}
labelID, err := ra.GetInt64("label_id", 0)
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
}
@ -128,17 +129,17 @@ func (ra *RepositoryAPI) Get() {
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %d not found", projectID))
ra.SendNotFoundError(fmt.Errorf("project %d not found", projectID))
return
}
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
@ -147,19 +148,24 @@ func (ra *RepositoryAPI) Get() {
Name: ra.GetString("q"),
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")
total, err := dao.GetTotalOfRepositories(query)
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))
return
}
repositories, err := getRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository: %v", err))
ra.SendInternalServerError(fmt.Errorf("failed to get repository: %v", err))
return
}
@ -240,25 +246,26 @@ func (ra *RepositoryAPI) Delete() {
}
if project == nil {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName))
ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return
}
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
resource := rbac.NewProjectNamespace(project.ProjectID).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionDelete, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
rc, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
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{}
@ -266,18 +273,13 @@ func (ra *RepositoryAPI) Delete() {
if len(tag) == 0 {
tagList, err := rc.ListTag()
if err != nil {
log.Errorf("error occurred while listing tags of %s: %v", repoName, err)
if regErr, ok := err.(*commonhttp.Error); ok {
ra.CustomAbort(regErr.Code, regErr.Message)
}
ra.CustomAbort(http.StatusInternalServerError, "internal error")
ra.ParseAndHandleError(fmt.Sprintf("error occurred while listing tags of %s", repoName), err)
return
}
// TODO remove the logic if the bug of registry is fixed
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
}
@ -289,7 +291,7 @@ func (ra *RepositoryAPI) Delete() {
if config.WithNotary() {
signedTags, err := getSignatures(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf(
ra.SendInternalServerError(fmt.Errorf(
"failed to get signatures for repository %s: %v", repoName, err))
return
}
@ -298,12 +300,14 @@ func (ra *RepositoryAPI) Delete() {
digest, _, err := rc.ManifestExist(t)
if err != nil {
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)
if _, ok := signedTags[digest]; ok {
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 {
image := fmt.Sprintf("%s:%s", repoName, t)
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
}
if err = rc.DeleteTag(t); err != nil {
@ -319,11 +323,9 @@ func (ra *RepositoryAPI) Delete() {
if regErr.Code == http.StatusNotFound {
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.CustomAbort(http.StatusInternalServerError, "internal error")
ra.ParseAndHandleError(fmt.Sprintf("failed to delete tag %s", t), err)
return
}
log.Infof("delete tag: %s:%s", repoName, t)
@ -363,12 +365,13 @@ func (ra *RepositoryAPI) Delete() {
exist, err := repositoryExist(repoName, rc)
if err != nil {
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 {
repository, err := dao.GetRepositoryByName(repoName)
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
}
if repository == nil {
@ -378,13 +381,14 @@ func (ra *RepositoryAPI) Delete() {
if err = dao.DeleteLabelsOfResource(common.ResourceTypeRepository,
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))
return
}
if err = dao.DeleteRepository(repoName); err != nil {
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")
exist, _, err := ra.checkExistence(repository, tag)
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
}
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
}
project, _ := utils.ParseRepository(repository)
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTag)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
client, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository)
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))
return
}
_, exist, err = client.ManifestExist(tag)
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
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("%s not found", tag))
ra.SendNotFoundError(fmt.Errorf("%s not found", tag))
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.
func (ra *RepositoryAPI) Retag() {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
repoName := ra.GetString(":splat")
project, repo := utils.ParseRepository(repoName)
if !utils.ValidateRepo(repo) {
ra.HandleBadRequest(fmt.Sprintf("invalid repo '%s'", repo))
ra.SendBadRequestError(fmt.Errorf("invalid repo '%s'", repo))
return
}
request := models.RetagRequest{}
ra.DecodeJSONReq(&request)
if err := ra.DecodeJSONReq(&request); err != nil {
ra.SendBadRequestError(err)
return
}
srcImage, err := models.ParseImage(request.SrcImage)
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
}
if !utils.ValidateTag(request.Tag) {
ra.HandleBadRequest(fmt.Sprintf("invalid tag '%s'", request.Tag))
ra.SendBadRequestError(fmt.Errorf("invalid tag '%s'", request.Tag))
return
}
// Check whether source image exists
exist, _, err := ra.checkExistence(fmt.Sprintf("%s/%s", srcImage.Project, srcImage.Repo), srcImage.Tag)
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
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("image %s not exist", request.SrcImage))
ra.SendNotFoundError(fmt.Errorf("image %s not exist", request.SrcImage))
return
}
@ -481,7 +488,7 @@ func (ra *RepositoryAPI) Retag() {
return
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", project))
ra.SendNotFoundError(fmt.Errorf("project %s not found", project))
return
}
@ -489,11 +496,11 @@ func (ra *RepositoryAPI) Retag() {
if !request.Override {
exist, _, err := ra.checkExistence(repoName, request.Tag)
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
}
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
}
}
@ -502,7 +509,7 @@ func (ra *RepositoryAPI) Retag() {
srcResource := rbac.NewProjectNamespace(srcImage.Project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionPull, srcResource) {
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
}
@ -510,7 +517,7 @@ func (ra *RepositoryAPI) Retag() {
destResource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionPush, destResource) {
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
}
@ -520,7 +527,7 @@ func (ra *RepositoryAPI) Retag() {
Repo: repo,
Tag: request.Tag,
}); 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")
labelID, err := ra.GetInt64("label_id", 0)
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
}
@ -542,29 +549,30 @@ func (ra *RepositoryAPI) GetTags() {
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName))
ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return
}
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTag)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
client, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
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()
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
}
@ -575,7 +583,7 @@ func (ra *RepositoryAPI) GetTags() {
ResourceType: common.ResourceTypeImage,
})
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
}
labeledTags := map[string]struct{}{}
@ -738,7 +746,7 @@ func (ra *RepositoryAPI) GetManifests() {
}
if version != "v1" && version != "v2" {
ra.HandleBadRequest("version should be v1 or v2")
ra.SendBadRequestError(errors.New("version should be v1 or v2"))
return
}
@ -751,36 +759,32 @@ func (ra *RepositoryAPI) GetManifests() {
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName))
ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return
}
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagManifest)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
rc, err := coreutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
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)
if err != nil {
log.Errorf("error occurred while getting manifest of %s:%s: %v", repoName, tag, err)
if regErr, ok := err.(*commonhttp.Error); ok {
ra.CustomAbort(regErr.Code, regErr.Message)
}
ra.CustomAbort(http.StatusInternalServerError, "internal error")
ra.ParseAndHandleError(fmt.Sprintf("error occurred while getting manifest of %s:%s", repoName, tag), err)
return
}
ra.Data["json"] = manifest
@ -833,7 +837,7 @@ func getManifest(client *registry.Repository,
func (ra *RepositoryAPI) GetTopRepos() {
count, err := ra.GetInt("count", 10)
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
}
@ -846,7 +850,7 @@ func (ra *RepositoryAPI) GetTopRepos() {
if ra.SecurityCtx.IsAuthenticated() {
list, err := ra.SecurityCtx.GetMyProjects()
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))
return
}
@ -860,7 +864,8 @@ func (ra *RepositoryAPI) GetTopRepos() {
repos, err := dao.GetTopRepos(projectIDs, count)
if err != nil {
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)
@ -872,35 +877,38 @@ func (ra *RepositoryAPI) Put() {
name := ra.GetString(":splat")
repository, err := dao.GetRepositoryByName(name)
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
}
if repository == nil {
ra.HandleNotFound(fmt.Sprintf("repository %s not found", name))
ra.SendNotFoundError(fmt.Errorf("repository %s not found", name))
return
}
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
project, _ := utils.ParseRepository(name)
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionUpdate, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
desc := struct {
Description string `json:"description"`
}{}
ra.DecodeJSONReq(&desc)
if err := ra.DecodeJSONReq(&desc); err != nil {
ra.SendBadRequestError(err)
return
}
repository.Description = desc.Description
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
}
}
@ -918,17 +926,17 @@ func (ra *RepositoryAPI) GetSignatures() {
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName))
ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return
}
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository)
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
@ -936,7 +944,8 @@ func (ra *RepositoryAPI) GetSignatures() {
ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
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.ServeJSON()
@ -946,7 +955,7 @@ func (ra *RepositoryAPI) GetSignatures() {
func (ra *RepositoryAPI) ScanImage() {
if !config.WithClair() {
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
}
repoName := ra.GetString(":splat")
@ -959,23 +968,23 @@ func (ra *RepositoryAPI) ScanImage() {
return
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %s not found", projectName))
ra.SendNotFoundError(fmt.Errorf("project %s not found", projectName))
return
}
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
if !ra.SecurityCtx.Can(rbac.ActionCreate, resource) {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
err = coreutils.TriggerImageScan(repoName, tag)
if err != nil {
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
}
}
@ -984,18 +993,18 @@ func (ra *RepositoryAPI) ScanImage() {
func (ra *RepositoryAPI) VulnerabilityDetails() {
if !config.WithClair() {
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
}
repository := ra.GetString(":splat")
tag := ra.GetString(":tag")
exist, digest, err := ra.checkExistence(repository, tag)
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
}
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
}
project, _ := utils.ParseRepository(repository)
@ -1003,16 +1012,16 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTagVulnerability)
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
return
}
res := []*models.VulnerabilityItem{}
overview, err := dao.GetImgScanOverview(digest)
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
}
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)
details, err := clairClient.GetResult(overview.DetailsKey)
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
}
res = transformVulnerabilities(details)

View File

@ -114,7 +114,10 @@ func (r *RepositoryLabelAPI) isValidLabelReq() bool {
}
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)
if !ok {

View File

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

View File

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

View File

@ -16,11 +16,11 @@ package api
import (
"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/utils/log"
"github.com/goharbor/harbor/src/core/utils"
"errors"
"fmt"
"net/http"
"strconv"
@ -39,12 +39,12 @@ type ScanJobAPI struct {
func (sj *ScanJobAPI) Prepare() {
sj.BaseController.Prepare()
if !sj.SecurityCtx.IsAuthenticated() {
sj.HandleUnauthorized()
sj.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
id, err := sj.GetInt64FromPath(":id")
if err != nil {
sj.HandleBadRequest("invalid ID")
sj.SendBadRequestError(errors.New("invalid ID"))
return
}
sj.jobID = id
@ -52,14 +52,16 @@ func (sj *ScanJobAPI) Prepare() {
data, err := dao.GetScanJob(id)
if err != nil {
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]
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
if !sj.SecurityCtx.Can(rbac.ActionRead, resource) {
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.jobUUID = data.UUID
@ -69,20 +71,14 @@ func (sj *ScanJobAPI) Prepare() {
func (sj *ScanJobAPI) GetLog() {
logBytes, err := utils.GetJobServiceClient().GetJobLog(sj.jobUUID)
if err != nil {
if httpErr, ok := err.(*common_http.Error); ok {
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))
sj.ParseAndHandleError(fmt.Sprintf("Failed to get job logs, uuid: %s, error: %v", sj.jobUUID, err), err)
return
}
sj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
sj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = sj.Ctx.ResponseWriter.Write(logBytes)
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 (
"fmt"
"net/http"
"strings"
"github.com/goharbor/harbor/src/common"
@ -69,7 +68,7 @@ func (s *SearchAPI) Get() {
if isAuthenticated {
mys, err := s.SecurityCtx.GetMyProjects()
if err != nil {
s.HandleInternalServerError(fmt.Sprintf(
s.SendInternalServerError(fmt.Errorf(
"failed to get projects: %v", err))
return
}
@ -111,7 +110,8 @@ func (s *SearchAPI) Get() {
})
if err != nil {
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
@ -122,7 +122,8 @@ func (s *SearchAPI) Get() {
repositoryResult, err := filterRepositories(projects, keyword)
if err != nil {
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{
@ -139,7 +140,9 @@ func (s *SearchAPI) Get() {
chartResults, err := searchHandler(keyword, proNames)
if err != nil {
log.Errorf("failed to filter charts: %v", err)
s.CustomAbort(http.StatusInternalServerError, err.Error())
s.SendInternalServerError(err)
return
}
result.Chart = &chartResults

View File

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

View File

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

View File

@ -15,6 +15,7 @@
package api
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
@ -55,7 +56,8 @@ func (ua *UserAPI) Prepare() {
mode, err := config.AuthMode()
if err != nil {
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
@ -63,7 +65,8 @@ func (ua *UserAPI) Prepare() {
self, err := config.SelfRegistration()
if err != nil {
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
@ -72,7 +75,7 @@ func (ua *UserAPI) Prepare() {
if ua.Ctx.Input.IsPost() {
return
}
ua.HandleUnauthorized()
ua.SendUnAuthorizedError(errors.New("UnAuthorize"))
return
}
@ -80,7 +83,7 @@ func (ua *UserAPI) Prepare() {
Username: ua.SecurityCtx.GetUsername(),
})
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))
return
}
@ -94,17 +97,20 @@ func (ua *UserAPI) Prepare() {
ua.userID, err = strconv.Atoi(id)
if err != nil {
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}
u, err := dao.GetUser(userQuery)
if err != nil {
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 {
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)
if err != nil {
log.Errorf("Error occurred in GetUser, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
ua.SendInternalServerError(err)
return
}
u.Password = ""
if ua.userID == ua.currentUserID {
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.ServeJSON()
return
}
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
}
@ -138,11 +153,16 @@ func (ua *UserAPI) Get() {
func (ua *UserAPI) List() {
if !ua.IsAdmin {
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
}
page, size := ua.GetPaginationParams()
query := &models.UserQuery{
Username: ua.GetString("username"),
Email: ua.GetString("email"),
@ -154,13 +174,13 @@ func (ua *UserAPI) List() {
total, err := dao.GetTotalOfUsers(query)
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
}
users, err := dao.ListUsers(query)
if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get users: %v", err))
ua.SendInternalServerError(fmt.Errorf("failed to get users: %v", err))
return
}
@ -171,7 +191,11 @@ func (ua *UserAPI) List() {
// Search ...
func (ua *UserAPI) Search() {
page, size := ua.GetPaginationParams()
page, size, err := ua.GetPaginationParams()
if err != nil {
ua.SendBadRequestError(err)
return
}
query := &models.UserQuery{
Username: ua.GetString("username"),
Email: ua.GetString("email"),
@ -183,13 +207,13 @@ func (ua *UserAPI) Search() {
total, err := dao.GetTotalOfUsers(query)
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
}
users, err := dao.ListUsers(query)
if err != nil {
ua.HandleInternalServerError(fmt.Sprintf("failed to get users: %v", err))
ua.SendInternalServerError(fmt.Errorf("failed to get users: %v", err))
return
}
@ -206,42 +230,49 @@ func (ua *UserAPI) Search() {
// Put ...
func (ua *UserAPI) Put() {
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
}
user := models.User{UserID: ua.userID}
ua.DecodeJSONReq(&user)
if err := ua.DecodeJSONReq(&user); err != nil {
ua.SendBadRequestError(err)
return
}
err := commonValidate(user)
if err != nil {
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
}
userQuery := models.User{UserID: ua.userID}
u, err := dao.GetUser(userQuery)
if err != nil {
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 {
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 {
emailExist, err := dao.UserExists(user, "email")
if err != nil {
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 {
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
}
}
if err := dao.ChangeUserProfile(user); err != nil {
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() {
if !(ua.AuthMode == common.DBAuth) {
ua.CustomAbort(http.StatusForbidden, "")
ua.SendForbiddenError(errors.New(""))
return
}
if !(ua.SelfRegistration || ua.IsAdmin) {
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{}
ua.DecodeJSONReq(&user)
if err := ua.DecodeJSONReq(&user); err != nil {
ua.SendBadRequestError(err)
return
}
err := validate(user)
if err != nil {
log.Warningf("Bad request in Register: %v", err)
@ -268,27 +304,30 @@ func (ua *UserAPI) Post() {
userExist, err := dao.UserExists(user, "username")
if err != nil {
log.Errorf("Error occurred in Register: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
ua.SendInternalServerError(errors.New("internal error"))
return
}
if userExist {
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
}
emailExist, err := dao.UserExists(user, "email")
if err != nil {
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 {
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
}
userID, err := dao.Register(user)
if err != nil {
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))
@ -297,7 +336,7 @@ func (ua *UserAPI) Post() {
// Delete ...
func (ua *UserAPI) Delete() {
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
}
@ -305,7 +344,7 @@ func (ua *UserAPI) Delete() {
err = dao.DeleteUser(ua.userID)
if err != nil {
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
}
}
@ -313,43 +352,46 @@ func (ua *UserAPI) Delete() {
// ChangePassword handles PUT to /api/users/{}/password
func (ua *UserAPI) ChangePassword() {
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
}
changePwdOfOwn := ua.userID == ua.currentUserID
var req passwordReq
ua.DecodeJSONReq(&req)
if err := ua.DecodeJSONReq(&req); err != nil {
ua.SendBadRequestError(err)
return
}
if changePwdOfOwn && len(req.OldPassword) == 0 {
ua.HandleBadRequest("empty old_password")
ua.SendBadRequestError(errors.New("empty old_password"))
return
}
if len(req.NewPassword) == 0 {
ua.HandleBadRequest("empty new_password")
ua.SendBadRequestError(errors.New("empty new_password"))
return
}
user, err := dao.GetUser(models.User{UserID: ua.userID})
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
}
if user == nil {
ua.HandleNotFound(fmt.Sprintf("user %d not found", ua.userID))
ua.SendNotFoundError(fmt.Errorf("user %d not found", ua.userID))
return
}
if changePwdOfOwn {
if user.Password != utils.Encrypt(req.OldPassword, user.Salt) {
log.Info("incorrect old_password")
ua.RenderError(http.StatusForbidden, "incorrect old_password")
ua.SendForbiddenError(errors.New("incorrect old_password"))
return
}
}
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
}
@ -358,7 +400,7 @@ func (ua *UserAPI) ChangePassword() {
Password: req.NewPassword,
}
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
}
}
@ -371,10 +413,14 @@ func (ua *UserAPI) ToggleUserAdminRole() {
return
}
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 {
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
}
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
func (ua *UserAPI) modifiable() bool {
if ua.AuthMode == common.DBAuth {

View File

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

View File

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

View File

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

View File

@ -228,36 +228,36 @@ func TestConfigureValue_GetMap(t *testing.T) {
func TestHTTPAuthProxySetting(t *testing.T) {
m := map[string]interface{}{
common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxySkipCertVerify: "true",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxyVerifyCert: "true",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
}
InitWithSettings(m)
v, e := HTTPAuthProxySetting()
assert.Nil(t, e)
assert.Equal(t, *v, models.HTTPAuthProxy{
Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true,
SkipCertVerify: true,
Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true,
VerifyCert: true,
})
}
func TestOIDCSetting(t *testing.T) {
m := map[string]interface{}{
common.OIDCName: "test",
common.OIDCEndpoint: "https://oidc.test",
common.OIDCSkipCertVerify: "true",
common.OIDCScope: "openid, profile",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test",
common.OIDCName: "test",
common.OIDCEndpoint: "https://oidc.test",
common.OIDCVerifyCert: "true",
common.OIDCScope: "openid, profile",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test",
}
InitWithSettings(m)
v, e := OIDCSetting()
assert.Nil(t, e)
assert.Equal(t, "test", v.Name)
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, "secret", v.ClientSecret)
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"
)
const userKey = "user"
// CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ...
type CommonController struct {
beego.Controller
@ -69,7 +71,7 @@ func (cc *CommonController) Login() {
if user == nil {
cc.CustomAbort(http.StatusUnauthorized, "")
}
cc.SetSession("user", *user)
cc.SetSession(userKey, *user)
}
// LogOut Habor UI

View File

@ -21,6 +21,7 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"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/core/api"
"github.com/goharbor/harbor/src/core/config"
@ -29,14 +30,19 @@ import (
"strings"
)
const idTokenKey = "oidc_id_token"
const tokenKey = "oidc_token"
const stateKey = "oidc_state"
const userInfoKey = "oidc_user_info"
// OIDCController handles requests for OIDC login, callback and user onboard
type OIDCController struct {
api.BaseController
}
type onboardReq struct {
Username string `json:"username"`
}
type oidcUserData struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
@ -47,7 +53,8 @@ type oidcUserData struct {
// Prepare include public code path for call request handler of OIDCController
func (oc *OIDCController) Prepare() {
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()
url, err := oidc.AuthCodeURL(state)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
return
}
oc.SetSession(stateKey, state)
@ -68,66 +75,101 @@ func (oc *OIDCController) RedirectLogin() {
// kick off onboard if needed.
func (oc *OIDCController) Callback() {
if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) {
oc.RenderError(http.StatusBadRequest, "State mismatch.")
oc.SendBadRequestError(errors.New("State mismatch"))
return
}
code := oc.Ctx.Request.URL.Query().Get("code")
ctx := oc.Ctx.Request.Context()
token, err := oidc.ExchangeToken(ctx, code)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
return
}
idToken, err := oidc.VerifyToken(ctx, token.IDToken)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
return
}
d := &oidcUserData{}
err = idToken.Claims(d)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
return
}
ouDataStr, err := json.Marshal(d)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
return
}
oc.SetSession(idTokenKey, string(ouDataStr))
// TODO: check and trigger onboard popup or redirect user to project page
oc.Data["json"] = d
oc.ServeFormatted()
u, err := dao.GetUserBySubIss(d.Subject, d.Issuer)
if err != nil {
oc.SendInternalServerError(err)
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
func (oc *OIDCController) Onboard() {
username := oc.GetString("username")
u := &onboardReq{}
if err := oc.DecodeJSONReq(u); err != nil {
oc.SendBadRequestError(err)
return
}
username := u.Username
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
}
if utils.IsContainIllegalChar(username, []string{",", "~", "#", "$", "%"}) {
oc.RenderFormatedError(http.StatusBadRequest, errors.New("username contains illegal characters"))
oc.SendBadRequestError(errors.New("username contains illegal characters"))
return
}
idTokenStr := oc.GetSession(idTokenKey)
d := &oidcUserData{}
err := json.Unmarshal([]byte(idTokenStr.(string)), &d)
userInfoStr, ok := oc.GetSession(userInfoKey).(string)
if !ok {
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 {
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
return
}
d := &oidcUserData{}
err = json.Unmarshal([]byte(userInfoStr), &d)
if err != nil {
oc.SendInternalServerError(err)
return
}
oidcUser := models.OIDCUser{
SubIss: d.Subject + d.Issuer,
// TODO: get secret with secret manager.
Secret: utils.GenerateRandomString(),
Secret: s,
Token: t,
}
var email string
if d.Email == "" {
email := d.Email
if email == "" {
email = utils.GenerateRandomString() + "@harbor.com"
}
user := models.User{
@ -139,12 +181,32 @@ func (oc *OIDCController) Onboard() {
err = dao.OnBoardOIDCUser(&user)
if err != nil {
if strings.Contains(err.Error(), dao.ErrDupUser.Error()) {
oc.RenderFormatedError(http.StatusConflict, err)
oc.RenderError(http.StatusConflict, "Duplicate username")
return
}
oc.RenderFormatedError(http.StatusInternalServerError, err)
oc.SendInternalServerError(err)
oc.DelSession(userInfoKey)
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 (
"context"
"fmt"
"github.com/goharbor/harbor/src/common/utils/oidc"
"net/http"
"regexp"
@ -110,6 +111,7 @@ func Init() {
// standalone
reqCtxModifiers = []ReqCtxModifier{
&secretReqCtxModifier{config.SecretStore},
&oidcCliReqCtxModifier{},
&authProxyReqCtxModifier{},
&robotAuthReqCtxModifier{},
&basicAuthReqCtxModifier{},
@ -205,6 +207,47 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
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{}
func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
@ -249,7 +292,7 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
},
BearerToken: proxyPwd,
TLSClientConfig: rest.TLSClientConfig{
Insecure: httpAuthProxyConf.SkipCertVerify,
Insecure: !httpAuthProxyConf.VerifyCert,
},
}
authClient, err := rest.RESTClientFor(authClientCfg)
@ -307,7 +350,6 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
return false
}
log.Debug("using local database project manager")
pm := config.GlobalProjectMgr
log.Debug("creating local database security context for auth proxy...")
securCtx := local.NewSecurityContext(user, pm)

View File

@ -16,6 +16,8 @@ package filter
import (
"context"
"github.com/goharbor/harbor/src/common/utils/oidc"
"github.com/stretchr/testify/require"
"log"
"net/http"
"net/http/httptest"
@ -28,6 +30,7 @@ import (
"github.com/astaxie/beego"
beegoctx "github.com/astaxie/beego/context"
"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/models"
commonsecret "github.com/goharbor/harbor/src/common/secret"
@ -118,6 +121,53 @@ func TestSecretReqCtxModifier(t *testing.T) {
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) {
req, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/api/projects/", nil)
@ -135,7 +185,7 @@ func TestRobotReqCtxModifier(t *testing.T) {
assert.False(t, modified)
}
func TestAutoProxyReqCtxModifier(t *testing.T) {
func TestAuthProxyReqCtxModifier(t *testing.T) {
server, err := fiter_test.NewAuthProxyTestServer()
assert.Nil(t, err)
@ -143,7 +193,7 @@ func TestAutoProxyReqCtxModifier(t *testing.T) {
c := map[string]interface{}{
common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxySkipCertVerify: "true",
common.HTTPAuthProxyVerifyCert: "false",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
common.HTTPAuthProxyTokenReviewEndpoint: server.URL,
common.AUTHMode: common.HTTPAuth,
@ -155,7 +205,7 @@ func TestAutoProxyReqCtxModifier(t *testing.T) {
assert.Equal(t, *v, models.HTTPAuthProxy{
Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true,
SkipCertVerify: true,
VerifyCert: false,
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)
// create the mapping relationship between the jobs in database and jobservice
if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil {
h.HandleInternalServerError(err.Error())
h.SendInternalServerError(err)
return
}
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)
h.HandleInternalServerError(err.Error())
h.SendInternalServerError(err)
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)
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)
h.HandleInternalServerError(err.Error())
h.SendInternalServerError(err)
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)
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)
h.HandleInternalServerError(err.Error())
h.SendInternalServerError(err)
return
}
}
@ -98,7 +98,7 @@ func (h *Handler) HandleReplicationTask() {
log.Debugf("received replication task status update event: task-%d, status-%s", h.id, h.status)
if err := hook.UpdateTask(replication.OperationCtl, h.id, h.rawStatus); err != nil {
log.Errorf("Failed to update replication task status, id: %d, status: %s", h.id, h.status)
h.HandleInternalServerError(err.Error())
h.SendInternalServerError(err)
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;
read_only: BoolValueItem;
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;
oidc_name?: StringValueItem;
oidc_endpoint?: StringValueItem;
oidc_client_id?: StringValueItem;
oidc_client_secret?: StringValueItem;
oidc_skip_cert_verify?: BoolValueItem;
oidc_verify_cert?: BoolValueItem;
oidc_scope?: StringValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
@ -139,13 +140,14 @@ export class Configuration {
}, true);
this.read_only = new BoolValueItem(false, 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.oidc_name = new StringValueItem('', true);
this.oidc_endpoint = new StringValueItem('', true);
this.oidc_client_id = 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);
}
}

View File

@ -33,7 +33,7 @@
</select>
</div>
<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">
<input type="text" (blur)="blurInvalid()" (input)="inputInvalid()" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="form-control"
[(ngModel)]="cronString">

View File

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

View File

@ -198,7 +198,7 @@ describe('RepositoryComponentGridview (inline template)', () => {
});
}));
// 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.detectChanges();

View File

@ -51,6 +51,19 @@
</span>
</label>
</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>
</form>
</div>

View File

@ -11,4 +11,7 @@ clr-modal {
position: relative;
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 { SearchTriggerService } from "../../base/global-search/search-trigger.service";
import { CommonRoutes } from "../../shared/shared.const";
import { CopyInputComponent } from "@harbor/ui";
@Component({
selector: "account-settings-modal",
templateUrl: "account-settings-modal.component.html",
@ -48,6 +48,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
accountFormRef: NgForm;
@ViewChild("accountSettingsFrom") accountForm: NgForm;
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
@ViewChild("copyInput") copyInput: CopyInputComponent;
constructor(
private session: SessionService,
@ -320,4 +321,10 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.inlineAlert.close();
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 { RepositoryModule } from '../repository/repository.module';
import { SignInComponent } from './sign-in/sign-in.component';
import { PasswordSettingComponent } from './password-setting/password-setting.component';
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
import { SignUpComponent } from './sign-up/sign-up.component';
@ -36,7 +35,6 @@ import { PasswordSettingService } from './password-setting/password-setting.serv
RepositoryModule
],
declarations: [
SignInComponent,
PasswordSettingComponent,
AccountSettingsModalComponent,
SignUpComponent,
@ -44,10 +42,11 @@ import { PasswordSettingService } from './password-setting/password-setting.serv
ResetPasswordComponent,
SignUpPageComponent],
exports: [
SignInComponent,
PasswordSettingComponent,
AccountSettingsModalComponent,
ForgotPasswordComponent,
ResetPasswordComponent,
SignUpComponent,
SignUpPageComponent],
providers: [PasswordSettingService]

View File

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

View File

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

View File

@ -287,13 +287,26 @@
</label>
</div>
<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>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="http_authproxy_skip_cert_verify"
id="http_authproxy_skip_cert_verify"
[(ngModel)]="currentConfig.http_authproxy_skip_cert_verify.value"
[disabled]="!currentConfig.http_authproxy_skip_cert_verify.editable" />
<input type="checkbox" clrCheckbox name="http_authproxy_verify_cert"
id="http_authproxy_verify_cert"
[(ngModel)]="currentConfig.http_authproxy_verify_cert.value"
[disabled]="!currentConfig.http_authproxy_verify_cert.editable" />
</clr-checkbox-wrapper>
</div>
<div class="form-group">
@ -390,16 +403,16 @@
</a>
</div>
<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>
<input type="checkbox" clrCheckbox name="oidc_skip_cert_verify" id="oidc_skip_cert_verify"
[disabled]="disabled(currentConfig.oidc_skip_cert_verify)"
[(ngModel)]="currentConfig.oidc_skip_cert_verify.value" />
<input type="checkbox" clrCheckbox name="oidc_verify_cert" id="oidc_verify_cert"
[disabled]="disabled(currentConfig.oidc_verify_cert)"
[(ngModel)]="currentConfig.oidc_verify_cert.value" />
</clr-checkbox-wrapper>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1px">
<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>
</div>
</section>

View File

@ -53,7 +53,6 @@ export class ConfigurationAuthComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig);
}
@ -153,9 +152,7 @@ export class ConfigurationAuthComponent implements OnChanges {
|| prop === 'auth_mode'
|| prop === 'project_creattion_restriction'
|| prop === 'self_registration'
|| prop === 'http_authproxy_endpoint'
|| prop === 'http_authproxy_skip_cert_verify'
|| prop === 'http_authproxy_always_onboard'
|| prop.startsWith('http_')
) {
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 { 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 { GroupComponent } from './group/group.component';
@ -69,17 +69,18 @@ const harborRoutes: Routes = [
component: OidcOnboardComponent,
canActivate: [OidcGuard, SignInGuard]
},
{
path: 'harbor/sign-in',
component: SignInComponent,
canActivate: [ SignInGuard]
},
{
path: 'harbor',
component: HarborShellComponent,
// canActivate: [AuthCheckGuard],
canActivateChild: [AuthCheckGuard],
children: [
{ path: '', redirectTo: 'sign-in', pathMatch: 'full' },
{
path: 'sign-in',
component: SignInComponent,
canActivate: [SignInGuard]
},
{ path: '', redirectTo: 'projects', pathMatch: 'full' },
{
path: 'projects',
component: ProjectComponent

View File

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

View File

@ -21,4 +21,13 @@ export class SessionUser {
role_id?: number;
has_admin_role?: boolean;
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"
[ngStyle]="{'background-image': customLoginBgImg? 'url(static/images/' + customLoginBgImg + ')': ''}">
<form #signInForm="ngForm" class="login">
@ -37,8 +40,8 @@
<div [class.visibility-hidden]="!isError" class="error active">
{{ 'SIGN_IN.INVALID_MSG' | translate }}
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary"
(click)="signIn()" id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()"
id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()"
*ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
@ -51,5 +54,6 @@
<top-repo class="repo-container"></top-repo>
</div>
</div>
</clr-main-container>
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>
<forgot-password #forgotPwdDialog></forgot-password>

View File

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

View File

@ -16,19 +16,19 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Input, ViewChild, AfterViewChecked } from '@angular/core';
import { NgForm } from '@angular/forms';
import { SessionService } from '../../shared/session.service';
import { SignInCredential } from '../../shared/sign-in-credential';
import { SessionService } from '../shared/session.service';
import { SignInCredential } from '../shared/sign-in-credential';
import { SignUpComponent } from '../sign-up/sign-up.component';
import { CommonRoutes } from '../../shared/shared.const';
import { ForgotPasswordComponent } from '../password-setting/forgot-password/forgot-password.component';
import { SignUpComponent } from '../account/sign-up/sign-up.component';
import { CommonRoutes } from '../shared/shared.const';
import { ForgotPasswordComponent } from '../account/password-setting/forgot-password/forgot-password.component';
import { AppConfigService } from '../../app-config.service';
import { AppConfig } from '../../app-config';
import { User } from '../../user/user';
import { AppConfigService } from '../app-config.service';
import { AppConfig } from '../app-config';
import { User } from '../user/user';
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
export const signInStatusNormal = 0;
@ -271,4 +271,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
forgotPassword(): void {
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 'rxjs/add/operator/toPromise';
import { SignInCredential } from '../../shared/sign-in-credential';
import {HTTP_FORM_OPTIONS} from "../../shared/shared.utils";
import { SignInCredential } from '../shared/sign-in-credential';
import {HTTP_FORM_OPTIONS} from "../shared/shared.utils";
import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs";
const signInUrl = '/c/login';

View File

@ -74,12 +74,12 @@
"EMPTY": "Name is required",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP:// or HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS://.",
"OIDC_NAME": "The name of the OIDC provider.",
"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_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": {
"CURRENT_PWD": "Enter current password",
@ -102,7 +102,11 @@
"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.",
"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": {
"TITLE": "Change Password",
@ -705,7 +709,7 @@
"FILTER": "LDAP Filter",
"UID": "LDAP UID",
"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_INFO": "The base DN from which to look up a group in LDAP/AD.",
"LDAP_GROUP_FILTER": "LDAP Group Filter",
@ -728,16 +732,17 @@
},
"HTTP_AUTH": {
"ENDPOINT": "Server Endpoint",
"TOKEN_REVIEW": "Token Review Endpoint",
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication Verify Cert"
"VERIFY_CERT": "Verify Certificate"
},
"OIDC": {
"OIDC_PROVIDER": "OIDC Provider",
"OIDC_PROVIDER": "OIDC Provider Name",
"ENDPOINT": "OIDC Endpoint",
"CLIENT_ID": "OIDC Client ID",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "OIDC Skip Verifying Certificate",
"OIDC_VERIFYCERT": "Verify Certificate",
"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_USERNAME": "Username"

View File

@ -74,12 +74,12 @@
"EMPTY": "Name is required",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP:// or HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS://.",
"OIDC_NAME": "El nombre de la OIDC proveedor.",
"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_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": {
"CURRENT_PWD": "Introduzca la contraseña actual",
@ -102,7 +102,11 @@
"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.",
"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": {
"TITLE": "Cambiar contraseña",
@ -727,6 +731,7 @@
},
"HTTP_AUTH": {
"ENDPOINT": "Server Endpoint",
"TOKEN_REVIEW": "Review Endpoint De Token",
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication Verify Cert"
},
@ -736,7 +741,7 @@
"CLIENT_ID": "ID de cliente OIDC",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Ámbito",
"OIDCSKIPCERTVERIFY": "OIDC Skip Verificar certificado",
"OIDC_VERIFYCERT": "Verificar certificado",
"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_USERNAME": "Usuario"

View File

@ -61,12 +61,12 @@
"USER_EXISTING": "Le nom d'utilisateur est déjà utilisé.",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP:// or HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS://.",
"OIDC_NAME": "le nom du fournisseur de oidc.",
"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_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": {
"CURRENT_PWD": "Entrez le mot de passe actuel",
@ -89,7 +89,11 @@
"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.",
"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": {
"TITLE": "Modifier le mot de passe",
@ -692,7 +696,8 @@
},
"HTTP_AUTH": {
"ENDPOINT": "serveur paramètre",
"ALWAYS_ONBOARD": "Always Onboard",
"TOKEN_REVIEW": "examen symbolique paramètre",
"ALWAYS_ONBOARD": "always onboard",
"VERIFY_CERT": "authentification vérifier cert"
},
"OIDC": {
@ -701,7 +706,7 @@
"CLIENT_ID": "no d'identification du client OIDC",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "Certificat OIDC skip vérifier",
"OIDC_VERIFYCERT": "Certificat vérifier",
"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_USERNAME": "d'utilisateur"

View File

@ -72,12 +72,12 @@
"USER_EXISTING": "Nome de usuário já está em uso.",
"RULE_USER_EXISTING": "Nome já em uso.",
"EMPTY": "Nome é obrigatório",
"ENDPOINT_FORMAT": "Avaliação deve começar por HTTP Ou HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Avaliação deve começar por HTTPS.",
"ENDPOINT_FORMAT": "Avaliação deve começar por HTTP:// Ou HTTPS://.",
"OIDC_ENDPOIT_FORMAT": "Avaliação deve começar por HTTPS://.",
"OIDC_NAME": "O Nome do prestador de oidc.",
"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_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": {
"CURRENT_PWD": "Insira a senha atual",
@ -100,7 +100,11 @@
"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.",
"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": {
"TITLE": "Alterar Senha",
@ -720,7 +724,8 @@
"VERIFY_CERT": "Verificar certificado UAA"
},
"HTTP_AUTH": {
"ENDPOINT": "server endpoint",
"ENDPOINT": "Server endpoint",
"TOKEN_REVIEW": "Ponto final do Token Review",
"ALWAYS_ONBOARD": "Sempre Onboard",
"VERIFY_CERT": "Verificar certificado de Authentication"
},
@ -730,7 +735,7 @@
"CLIENT_ID": "ID de cliente OIDC",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "Escopo OIDC",
"OIDCSKIPCERTVERIFY": "OIDC Skip Verificar Certificado",
"OIDC_VERIFYCERT": "Verificar Certificado",
"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_USERNAME": "Utilizador"

View File

@ -73,12 +73,12 @@
"RULE_USER_EXISTING": "名称已经存在。",
"EMPTY": "名称为必填项",
"NONEMPTY": "不能为空",
"ENDPOINT_FORMAT": "Endpoint必须以http或https开头。",
"OIDC_ENDPOIT_FORMAT": "Endpoint必须以https开头。",
"ENDPOINT_FORMAT": "Endpoint必须以http://或https://开头。",
"OIDC_ENDPOIT_FORMAT": "Endpoint必须以https://开头。",
"OIDC_NAME": "OIDC提供商的名称.",
"OIDC_ENDPOINT": "OIDC服务器的地址.",
"OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google请从此字段中删除“脱机访问”。",
"OIDC_SKIPCERTVERIFY": "如果您的OIDC服务器是通过自签名证书托管的选中此框。"
"OIDC_VERIFYCERT": "如果您的OIDC服务器是通过自签名证书托管的取消选中此框。"
},
"PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码",
@ -101,7 +101,11 @@
"ADMIN_RENAME_TIP": "单击将用户名改为 \"admin@harbor.local\", 注意这个操作是无法撤销的",
"RENAME_SUCCESS": "用户名更改成功!",
"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": {
"TITLE": "修改密码",
@ -726,6 +730,7 @@
},
"HTTP_AUTH": {
"ENDPOINT": "Server Endpoint",
"TOKEN_REVIEW": "Token Review Endpoint",
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication验证证书"
},
@ -735,7 +740,7 @@
"CLIENT_ID": "OIDC 客户端标识",
"CLIENTSECRET": "OIDC 客户端密码",
"SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "OIDC 验证证书",
"OIDC_VERIFYCERT": "验证证书",
"OIDC_SETNAME": "设置OIDC用户名",
"OIDC_SETNAMECONTENT": "在通过第三方OIDC进行身份验证时您必须第一次创建一个Harbor用户名。这将在端口中用于与项目、角色等关联。",
"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 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):
def create_project(self, name=None, metadata=None, expect_status_code = 201, expect_response_body = None, **kwargs):
if name is None:
@ -131,6 +137,14 @@ class Project(base.Base):
base._assert_status_code(200, status_code)
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):
members = self.get_project_members(project_id, **kwargs)
result = is_member_exist_in_project(list(members), member_user_name)

View File

@ -4,6 +4,7 @@ import time
import base
import swagger_client
from docker_api import DockerAPI
from swagger_client.rest import ApiException
def pull_harbor_image(registry, username, password, image, tag, expected_error_message = None):
_docker_api = DockerAPI()
@ -119,3 +120,19 @@ class Repository(base.Base):
print "sha256:", len(each_sign.hashes["sha256"])
return
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;
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";
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
*** 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']
${username_xpath} //*[@id='username']
${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
[Arguments] @{obj}
:For ${obj} in @{obj}
\ Click Element //clr-dg-row[contains(.,'${obj}')]//label
Sleep 1
Click Element ${member_action_xpath}
Sleep 1
Click Element //clr-dropdown/clr-dropdown-menu/button[2]
Sleep 2
Click Element //clr-modal//button[contains(.,'DELETE')]
Sleep 3
\ Retry Element Click //clr-dg-row[contains(.,'${obj}')]//label
Retry Element Click ${member_action_xpath}
Retry Element Click //clr-dropdown/clr-dropdown-menu/button[2]
Retry Double Keywords When Error Retry Element Click ${delete_btn} Retry Wait Until Page Not Contains Element ${delete_btn}
Multi-delete Member
[Arguments] @{obj}
:For ${obj} in @{obj}
\ Click Element //clr-dg-row[contains(.,'${obj}')]//label
Sleep 1
Click Element ${member_action_xpath}
Sleep 1
Click Element ${delete_action_xpath}
Sleep 2
Click Element //clr-modal//button[contains(.,'DELETE')]
Sleep 3
\ Retry Element Click //clr-dg-row[contains(.,'${obj}')]//label
Retry Element Click ${member_action_xpath}
Retry Element Click ${delete_action_xpath}
Retry Double Keywords When Error Retry Element Click ${delete_btn} Retry Wait Until Page Not Contains Element ${delete_btn}
Multi-delete Object Without Confirmation
[Arguments] @{obj}
:For ${obj} in @{obj}
\ Click Element //clr-dg-row[contains(.,'${obj}')]//label
Sleep 1
Click Element //button[contains(.,'Delete')]
Sleep 3
\ Retry Element Click //clr-dg-row[contains(.,'${obj}')]//label
Retry Double Keywords When Error Retry Element Click ${delete_btn_2} Retry Wait Until Page Not Contains Element ${delete_btn_2}
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 Github-Util.robot
Resource Harbor-Util.robot
Resource Harbor-Pages/Public_Elements.robot
Resource Harbor-Pages/HomePage.robot
Resource Harbor-Pages/HomePage_Elements.robot
Resource Harbor-Pages/Project.robot

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