mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-30 04:28:17 +01:00
commit
4ca482d6d2
2
Makefile
2
Makefile
@ -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
|
||||
|
BIN
docs/img/robotaccount/add_robot_account.png
Normal file
BIN
docs/img/robotaccount/add_robot_account.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
BIN
docs/img/robotaccount/add_robot_account_2.png
Normal file
BIN
docs/img/robotaccount/add_robot_account_2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
docs/img/robotaccount/copy_robot_account_token.png
Normal file
BIN
docs/img/robotaccount/copy_robot_account_token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
docs/img/robotaccount/disable_delete_robot_account.png
Normal file
BIN
docs/img/robotaccount/disable_delete_robot_account.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
BIN
docs/img/robotaccount/set_robot_account_token_duration.png
Normal file
BIN
docs/img/robotaccount/set_robot_account_token_duration.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
@ -4899,7 +4899,7 @@ definitions:
|
||||
RobotAccountUpdate:
|
||||
type: object
|
||||
properties:
|
||||
disable:
|
||||
disabled:
|
||||
type: boolean
|
||||
description: The robot account is disable or enable
|
||||
Permission:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
@ -37,3 +37,6 @@ notifications:
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
||||
compatibility:
|
||||
schema1:
|
||||
enabled: true
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
17
src/common/http/error_test.go
Normal file
17
src/common/http/error_test.go
Normal 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\"}")
|
||||
|
||||
}
|
@ -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 ...
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
130
src/common/utils/oidc/secret.go
Normal file
130
src/common/utils/oidc/secret.go
Normal 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)
|
||||
}
|
32
src/common/utils/oidc/secret_test.go
Normal file
32
src/common/utils/oidc/secret_test.go
Normal 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"))
|
||||
}
|
26
src/common/utils/oidc/testutils.go
Normal file
26
src/common/utils/oidc/testutils.go
Normal 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}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()))
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -37,6 +37,12 @@
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.cron-input {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
.cron-label {
|
||||
width: 195px;
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -11,4 +11,7 @@ clr-modal {
|
||||
position: relative;
|
||||
bottom: 9px;
|
||||
}
|
||||
.label-inner-text{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -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'});
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
@ -55,7 +55,6 @@
|
||||
|
||||
.login-wrapper {
|
||||
flex-wrap: wrap;
|
||||
margin-top:-20px;
|
||||
.login {
|
||||
background:transparent;
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
22
src/portal/src/app/sign-in/sign-in.module.ts
Normal file
22
src/portal/src/app/sign-in/sign-in.module.ts
Normal 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 { }
|
@ -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';
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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": "dé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"
|
||||
|
@ -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"
|
||||
|
@ -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": "用户名"
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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)
|
||||
|
127
tests/apitests/python/test_retag.py
Normal file
127
tests/apitests/python/test_retag.py
Normal 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()
|
||||
|
@ -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']
|
||||
|
21
tests/resources/Harbor-Pages/Public_Elements.robot
Normal file
21
tests/resources/Harbor-Pages/Public_Elements.robot
Normal 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')]
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user