mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 04:35:16 +01:00
Merge branch 'master' of https://github.com/goharbor/harbor into project-quota-dev
This commit is contained in:
commit
2292954a31
@ -3478,6 +3478,37 @@ paths:
|
|||||||
description: The robot account is not found.
|
description: The robot account is not found.
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
|
'/system/oidc/ping':
|
||||||
|
post:
|
||||||
|
summary: Test the OIDC endpoint.
|
||||||
|
description: Test the OIDC endpoint, the setting of the endpoint is provided in the request. This API can only
|
||||||
|
be called by system admin.
|
||||||
|
tags:
|
||||||
|
- Products
|
||||||
|
- System
|
||||||
|
parameters:
|
||||||
|
- name: endpoint
|
||||||
|
in: body
|
||||||
|
description: Request body for OIDC endpoint to be tested.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: The URL of OIDC endpoint to be tested.
|
||||||
|
verify_cert:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the certificate should be verified
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ping succeeded. The OIDC endpoint is valid.
|
||||||
|
'400':
|
||||||
|
description: The ping failed
|
||||||
|
'401':
|
||||||
|
description: User need to log in first.
|
||||||
|
'403':
|
||||||
|
description: User does not have permission to call this API
|
||||||
'/system/CVEWhitelist':
|
'/system/CVEWhitelist':
|
||||||
get:
|
get:
|
||||||
summary: Get the system level whitelist of CVE.
|
summary: Get the system level whitelist of CVE.
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package chartserver
|
package chartserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -49,11 +49,13 @@ func NewChartClient(credential *Credential) *ChartClient { // Create http client
|
|||||||
func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
||||||
response, err := cc.sendRequest(addr, http.MethodGet, nil)
|
response, err := cc.sendRequest(addr, http.MethodGet, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "get content failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(response.Body)
|
content, err := ioutil.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "Read response body error")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
@ -61,6 +63,7 @@ func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
|||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
text, err := extractError(content)
|
text, err := extractError(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "Extract content error failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, &commonhttp.Error{
|
return nil, &commonhttp.Error{
|
||||||
@ -106,7 +109,8 @@ func (cc *ChartClient) sendRequest(addr string, method string, body io.Reader) (
|
|||||||
|
|
||||||
fullURI, err := url.Parse(addr)
|
fullURI, err := url.Parse(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid url: %s", err.Error())
|
err = errors.Wrap(err, "Invalid url")
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
request, err := http.NewRequest(method, addr, body)
|
request, err := http.NewRequest(method, addr, body)
|
||||||
@ -121,7 +125,7 @@ func (cc *ChartClient) sendRequest(addr string, method string, body io.Reader) (
|
|||||||
|
|
||||||
response, err := cc.httpClient.Do(request)
|
response, err := cc.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hlog.Errorf("%s '%s' failed with error: %s", method, fullURI.Path, err)
|
err = errors.Wrap(err, fmt.Sprintf("send request %s %s failed", method, fullURI.Path))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,19 +2,17 @@ package chartserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
"github.com/ghodss/yaml"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/replication"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
rep_event "github.com/goharbor/harbor/src/replication/event"
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/pkg/errors"
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListCharts gets the chart list under the namespace
|
// ListCharts gets the chart list under the namespace
|
||||||
|
@ -20,12 +20,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego"
|
||||||
"github.com/astaxie/beego/validation"
|
"github.com/astaxie/beego/validation"
|
||||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"errors"
|
|
||||||
"github.com/astaxie/beego"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -144,11 +144,7 @@ func UpdateUserGroupName(id int, groupName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnBoardUserGroup will check if a usergroup exists in usergroup table, if not insert the usergroup and
|
func onBoardCommonUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttributes ...string) error {
|
||||||
// put the id in the pointer of usergroup model, if it does exist, return the usergroup's profile.
|
|
||||||
// This is used for ldap and uaa authentication, such the usergroup can have an ID in Harbor.
|
|
||||||
// the keyAttribute and combinedKeyAttribute are key columns used to check duplicate usergroup in harbor
|
|
||||||
func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttributes ...string) error {
|
|
||||||
g.LdapGroupDN = utils.TrimLower(g.LdapGroupDN)
|
g.LdapGroupDN = utils.TrimLower(g.LdapGroupDN)
|
||||||
|
|
||||||
o := dao.GetOrmer()
|
o := dao.GetOrmer()
|
||||||
@ -172,3 +168,12 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnBoardUserGroup will check if a usergroup exists in usergroup table, if not insert the usergroup and
|
||||||
|
// put the id in the pointer of usergroup model, if it does exist, return the usergroup's profile.
|
||||||
|
func OnBoardUserGroup(g *models.UserGroup) error {
|
||||||
|
if g.GroupType == common.LDAPGroupType {
|
||||||
|
return onBoardCommonUserGroup(g, "LdapGroupDN", "GroupType")
|
||||||
|
}
|
||||||
|
return onBoardCommonUserGroup(g, "GroupName", "GroupType")
|
||||||
|
}
|
||||||
|
@ -256,7 +256,7 @@ func TestOnBoardUserGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := OnBoardUserGroup(tt.args.g, "LdapGroupDN", "GroupType"); (err != nil) != tt.wantErr {
|
if err := OnBoardUserGroup(tt.args.g); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("OnBoardUserGroup() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("OnBoardUserGroup() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -206,3 +206,19 @@ func RefreshToken(ctx context.Context, token *Token) (*Token, error) {
|
|||||||
}
|
}
|
||||||
return &Token{Token: *t, IDToken: it}, nil
|
return &Token{Token: *t, IDToken: it}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conn wraps connection info of an OIDC endpoint
|
||||||
|
type Conn struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
VerifyCert bool `json:"verify_cert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEndpoint tests whether the endpoint is a valid OIDC endpoint.
|
||||||
|
// The nil return value indicates the success of the test
|
||||||
|
func TestEndpoint(conn Conn) error {
|
||||||
|
|
||||||
|
// gooidc will try to call the discovery api when creating the provider and that's all we need to check
|
||||||
|
ctx := clientCtx(context.Background(), conn.VerifyCert)
|
||||||
|
_, err := gooidc.NewProvider(ctx, conn.URL)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -97,3 +97,16 @@ func TestAuthCodeURL(t *testing.T) {
|
|||||||
assert.Equal(t, "offline", q.Get("access_type"))
|
assert.Equal(t, "offline", q.Get("access_type"))
|
||||||
assert.False(t, strings.Contains(q.Get("scope"), "offline_access"))
|
assert.False(t, strings.Contains(q.Get("scope"), "offline_access"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTestEndpoint(t *testing.T) {
|
||||||
|
c1 := Conn{
|
||||||
|
URL: googleEndpoint,
|
||||||
|
VerifyCert: true,
|
||||||
|
}
|
||||||
|
c2 := Conn{
|
||||||
|
URL: "https://www.baidu.com",
|
||||||
|
VerifyCert: false,
|
||||||
|
}
|
||||||
|
assert.Nil(t, TestEndpoint(c1))
|
||||||
|
assert.NotNil(t, TestEndpoint(c2))
|
||||||
|
}
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"errors"
|
|
||||||
"github.com/ghodss/yaml"
|
"github.com/ghodss/yaml"
|
||||||
"github.com/goharbor/harbor/src/common/api"
|
"github.com/goharbor/harbor/src/common/api"
|
||||||
"github.com/goharbor/harbor/src/common/security"
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
|
@ -145,6 +145,7 @@ func init() {
|
|||||||
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
|
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
|
||||||
beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post")
|
beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post")
|
||||||
beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put")
|
beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put")
|
||||||
|
beego.Router("/api/system/oidc/ping", &OIDCAPI{}, "post:Ping")
|
||||||
|
|
||||||
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
||||||
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
|
56
src/core/api/oidc.go
Normal file
56
src/core/api/oidc.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OIDCAPI handles the requests to /api/system/oidc/xxx
|
||||||
|
type OIDCAPI struct {
|
||||||
|
BaseController
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare validates the request initially
|
||||||
|
func (oa *OIDCAPI) Prepare() {
|
||||||
|
oa.BaseController.Prepare()
|
||||||
|
if !oa.SecurityCtx.IsAuthenticated() {
|
||||||
|
oa.SendUnAuthorizedError(errors.New("unauthorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !oa.SecurityCtx.IsSysAdmin() {
|
||||||
|
msg := "only system admin has permission to access this API"
|
||||||
|
log.Errorf(msg)
|
||||||
|
oa.SendForbiddenError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping will handles the request to test connection to OIDC endpoint
|
||||||
|
func (oa *OIDCAPI) Ping() {
|
||||||
|
var c oidc.Conn
|
||||||
|
if err := oa.DecodeJSONReq(&c); err != nil {
|
||||||
|
log.Error("Failed to decode JSON request.")
|
||||||
|
oa.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := oidc.TestEndpoint(c); err != nil {
|
||||||
|
log.Errorf("Failed to verify connection: %+v, err: %v", c, err)
|
||||||
|
oa.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
69
src/core/api/oidc_test.go
Normal file
69
src/core/api/oidc_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/oidc"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOIDCAPI_Ping(t *testing.T) {
|
||||||
|
url := "/api/system/oidc/ping"
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
{ // 401
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
bodyJSON: oidc.Conn{},
|
||||||
|
url: url,
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{ // 403
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
bodyJSON: oidc.Conn{},
|
||||||
|
url: url,
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{ // 400
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
bodyJSON: oidc.Conn{
|
||||||
|
URL: "https://www.baidu.com",
|
||||||
|
VerifyCert: true,
|
||||||
|
},
|
||||||
|
url: url,
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{ // 200
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
bodyJSON: oidc.Conn{
|
||||||
|
URL: "https://accounts.google.com",
|
||||||
|
VerifyCert: true,
|
||||||
|
},
|
||||||
|
url: url,
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
@ -18,6 +18,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
@ -27,10 +29,7 @@ import (
|
|||||||
errutil "github.com/goharbor/harbor/src/common/utils/error"
|
errutil "github.com/goharbor/harbor/src/common/utils/error"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type deletableResp struct {
|
type deletableResp struct {
|
||||||
@ -232,7 +231,10 @@ func (p *ProjectAPI) Get() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.populateProperties(p.project)
|
err := p.populateProperties(p.project)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("populate project poroperties failed with : %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
p.Data["json"] = p.project
|
p.Data["json"] = p.project
|
||||||
p.ServeJSON()
|
p.ServeJSON()
|
||||||
@ -402,15 +404,17 @@ func (p *ProjectAPI) List() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, project := range result.Projects {
|
for _, project := range result.Projects {
|
||||||
p.populateProperties(project)
|
err = p.populateProperties(project)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("populate project properties failed %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.SetPaginationHeader(result.Total, page, size)
|
p.SetPaginationHeader(result.Total, page, size)
|
||||||
p.Data["json"] = result.Projects
|
p.Data["json"] = result.Projects
|
||||||
p.ServeJSON()
|
p.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectAPI) populateProperties(project *models.Project) {
|
func (p *ProjectAPI) populateProperties(project *models.Project) error {
|
||||||
if p.SecurityCtx.IsAuthenticated() {
|
if p.SecurityCtx.IsAuthenticated() {
|
||||||
roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
|
roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
|
||||||
if len(roles) != 0 {
|
if len(roles) != 0 {
|
||||||
@ -427,9 +431,8 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
|
|||||||
ProjectIDs: []int64{project.ProjectID},
|
ProjectIDs: []int64{project.ProjectID},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err)
|
err = errors.Wrap(err, fmt.Sprintf("get repo count of project %d failed", project.ProjectID))
|
||||||
p.SendInternalServerError(errors.New(""))
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
project.RepoCount = total
|
project.RepoCount = total
|
||||||
@ -438,13 +441,13 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
|
|||||||
if config.WithChartMuseum() {
|
if config.WithChartMuseum() {
|
||||||
count, err := chartController.GetCountOfCharts([]string{project.Name})
|
count, err := chartController.GetCountOfCharts([]string{project.Name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to get total of charts under project %s: %v", project.Name, err)
|
err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", project.ProjectID))
|
||||||
p.SendInternalServerError(errors.New(""))
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
project.ChartCount = count
|
project.ChartCount = count
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put ...
|
// Put ...
|
||||||
|
@ -17,6 +17,7 @@ package authproxy
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -190,12 +191,14 @@ func (a *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) {
|
|||||||
// OnBoardGroup create user group entity in Harbor DB, altGroupName is not used.
|
// OnBoardGroup create user group entity in Harbor DB, altGroupName is not used.
|
||||||
func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
|
func (a *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
|
||||||
// if group name provided, on board the user group
|
// if group name provided, on board the user group
|
||||||
userGroup := &models.UserGroup{GroupName: u.GroupName, GroupType: common.HTTPGroupType}
|
if len(u.GroupName) == 0 {
|
||||||
err := group.OnBoardUserGroup(u, "GroupName", "GroupType")
|
return errors.New("Should provide a group name")
|
||||||
|
}
|
||||||
|
u.GroupType = common.HTTPGroupType
|
||||||
|
err := group.OnBoardUserGroup(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.ID = userGroup.ID
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
|
mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"})
|
||||||
defer mockSvr.Close()
|
defer mockSvr.Close()
|
||||||
|
defer dao.ExecuteBatchSQL([]string{"delete from user_group where group_name='OnBoardTest'"})
|
||||||
a = &Auth{
|
a = &Auth{
|
||||||
Endpoint: mockSvr.URL + "/test/login",
|
Endpoint: mockSvr.URL + "/test/login",
|
||||||
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
|
TokenReviewEndpoint: mockSvr.URL + "/test/tokenreview",
|
||||||
@ -50,10 +51,17 @@ func TestMain(m *testing.M) {
|
|||||||
// So it won't require mocking the cfgManager
|
// So it won't require mocking the cfgManager
|
||||||
settingTimeStamp: time.Now(),
|
settingTimeStamp: time.Now(),
|
||||||
}
|
}
|
||||||
|
cfgMap := cut.GetUnitTestConfig()
|
||||||
conf := map[string]interface{}{
|
conf := map[string]interface{}{
|
||||||
common.HTTPAuthProxyEndpoint: a.Endpoint,
|
common.HTTPAuthProxyEndpoint: a.Endpoint,
|
||||||
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
|
common.HTTPAuthProxyTokenReviewEndpoint: a.TokenReviewEndpoint,
|
||||||
common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify,
|
common.HTTPAuthProxyVerifyCert: !a.SkipCertVerify,
|
||||||
|
common.PostGreSQLSSLMode: cfgMap[common.PostGreSQLSSLMode],
|
||||||
|
common.PostGreSQLUsername: cfgMap[common.PostGreSQLUsername],
|
||||||
|
common.PostGreSQLPort: cfgMap[common.PostGreSQLPort],
|
||||||
|
common.PostGreSQLHOST: cfgMap[common.PostGreSQLHOST],
|
||||||
|
common.PostGreSQLPassword: cfgMap[common.PostGreSQLPassword],
|
||||||
|
common.PostGreSQLDatabase: cfgMap[common.PostGreSQLDatabase],
|
||||||
}
|
}
|
||||||
|
|
||||||
config.InitWithSettings(conf)
|
config.InitWithSettings(conf)
|
||||||
@ -174,3 +182,19 @@ func TestAuth_PostAuthenticate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuth_OnBoardGroup(t *testing.T) {
|
||||||
|
input := &models.UserGroup{
|
||||||
|
GroupName: "OnBoardTest",
|
||||||
|
GroupType: common.HTTPGroupType,
|
||||||
|
}
|
||||||
|
a.OnBoardGroup(input, "")
|
||||||
|
|
||||||
|
assert.True(t, input.ID > 0, "The OnBoardGroup should have a valid group ID")
|
||||||
|
|
||||||
|
emptyGroup := &models.UserGroup{}
|
||||||
|
err := a.OnBoardGroup(emptyGroup, "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Empty user group should failed to OnBoard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -214,7 +214,7 @@ func (l *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error {
|
|||||||
if len(userGroupList) > 0 {
|
if len(userGroupList) > 0 {
|
||||||
return auth.ErrDuplicateLDAPGroup
|
return auth.ErrDuplicateLDAPGroup
|
||||||
}
|
}
|
||||||
return group.OnBoardUserGroup(u, "LdapGroupDN", "GroupType")
|
return group.OnBoardUserGroup(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser
|
// PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser
|
||||||
|
@ -97,6 +97,7 @@ func initRouters() {
|
|||||||
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
|
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
|
||||||
beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post")
|
beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post")
|
||||||
beego.Router("/api/system/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put")
|
beego.Router("/api/system/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put")
|
||||||
|
beego.Router("/api/system/oidc/ping", &api.OIDCAPI{}, "post:Ping")
|
||||||
|
|
||||||
beego.Router("/api/logs", &api.LogAPI{})
|
beego.Router("/api/logs", &api.LogAPI{})
|
||||||
|
|
||||||
|
@ -21,18 +21,10 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"github.com/goharbor/harbor/src/jobservice/common/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
proxyEnvHTTP = "http_proxy"
|
|
||||||
proxyEnvHTTPS = "https_proxy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client for handling the hook events
|
// Client for handling the hook events
|
||||||
@ -60,19 +52,7 @@ func NewClient(ctx context.Context) Client {
|
|||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ResponseHeaderTimeout: 10 * time.Second,
|
ResponseHeaderTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
}
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
|
||||||
// Get the http/https proxies
|
|
||||||
proxyAddr, ok := os.LookupEnv(proxyEnvHTTP)
|
|
||||||
if !ok {
|
|
||||||
proxyAddr, ok = os.LookupEnv(proxyEnvHTTPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok && !utils.IsEmptyStr(proxyAddr) {
|
|
||||||
proxyURL, err := url.Parse(proxyAddr)
|
|
||||||
if err == nil {
|
|
||||||
transport.Proxy = http.ProxyURL(proxyURL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
|
@ -99,6 +99,7 @@
|
|||||||
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
|
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-modal" *ngIf="showAddModal">
|
<div class="add-modal" *ngIf="showAddModal">
|
||||||
|
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4" shape="window-close"></clr-icon>
|
||||||
<div>
|
<div>
|
||||||
<clr-textarea-container>
|
<clr-textarea-container>
|
||||||
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
|
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
|
||||||
|
@ -11,17 +11,17 @@
|
|||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
<clr-dropdown *ngIf="isClairDBFullyReady && showScanningNamespaces" class="clr-dropdown-override">
|
<clr-dropdown *ngIf="isClairDBFullyReady && showScanningNamespaces" class="clr-dropdown-override">
|
||||||
<button class="btn btn-link btn-font" clrDropdownToggle>
|
<button class="btn btn-link btn-font" clrDropdownToggle>
|
||||||
{{ updatedTimestamp | date:'MM/dd/y HH:mm:ss' }} AM
|
{{ updatedTimestamp | date:'short' }}
|
||||||
<clr-icon shape="caret down"></clr-icon>
|
<clr-icon shape="caret down"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
<clr-dropdown-menu [clrPosition]="'bottom-right'" class="dropdown-namespace">
|
<clr-dropdown-menu [clrPosition]="'bottom-right'" class="dropdown-namespace">
|
||||||
<div *ngFor="let nt of namespaceTimestamps" class="namespace">
|
<div *ngFor="let nt of namespaceTimestamps" class="namespace">
|
||||||
<span class="label label-info">{{nt.namespace}}</span>
|
<span class="label label-info">{{nt.namespace}}</span>
|
||||||
<span>{{ convertToLocalTime(nt.last_update) | date:'MM/dd/y HH:mm:ss'}} AM</span>
|
<span>{{ convertToLocalTime(nt.last_update) | date:'short'}} </span>
|
||||||
</div>
|
</div>
|
||||||
</clr-dropdown-menu>
|
</clr-dropdown-menu>
|
||||||
</clr-dropdown>
|
</clr-dropdown>
|
||||||
<span *ngIf="isClairDBFullyReady && !showScanningNamespaces">{{ updatedTimestamp | date:'MM/dd/y HH:mm:ss' }} AM</span>
|
<span *ngIf="isClairDBFullyReady && !showScanningNamespaces">{{ updatedTimestamp | date:'short' }} </span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<cron-selection #CronScheduleComponent [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection>
|
<cron-selection #CronScheduleComponent [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection>
|
||||||
|
@ -109,6 +109,7 @@
|
|||||||
class="btn btn-link ml-1">{{'CVE_WHITELIST.ADD_SYSTEM'|translate}}</button>
|
class="btn btn-link ml-1">{{'CVE_WHITELIST.ADD_SYSTEM'|translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-modal" *ngIf="showAddModal && !isUseSystemWhitelist()">
|
<div class="add-modal" *ngIf="showAddModal && !isUseSystemWhitelist()">
|
||||||
|
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4" shape="window-close"></clr-icon>
|
||||||
<div>
|
<div>
|
||||||
<clr-textarea-container>
|
<clr-textarea-container>
|
||||||
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
|
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
|
||||||
|
@ -44,42 +44,46 @@
|
|||||||
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
|
{{'ROBOT_ACCOUNT.PERMISSIONS' | translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col">
|
<div class="clr-col p-0">
|
||||||
<div class="form-group padding-left-120">
|
<table class="table table-noborder m-0 w-90">
|
||||||
<label>{{'ROBOT_ACCOUNT.PERMISSIONS_IMAGE' | translate}}</label>
|
<tr>
|
||||||
<div class="radio-inline">
|
<th></th>
|
||||||
<input type="radio" name="image-permission"
|
<th class="left">{{'ROBOT_ACCOUNT.PUSH' | translate}}</th>
|
||||||
id="image-permission-pull"
|
<th class="left">{{'ROBOT_ACCOUNT.PULL' | translate}}</th>
|
||||||
value="pull"
|
</tr>
|
||||||
[(ngModel)]="imagePermission">
|
<tr>
|
||||||
<label for="image-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
|
<td class="left">
|
||||||
</div>
|
<span>{{'ROBOT_ACCOUNT.PERMISSIONS_IMAGE' | translate}}</span>
|
||||||
<div class="radio-inline">
|
<clr-tooltip>
|
||||||
<input type="radio" name="image-permission"
|
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
|
||||||
id="image-permission-push-and-pull"
|
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
|
||||||
value="push-and-pull"
|
<span>{{'ROBOT_ACCOUNT.PULL_IS_MUST' | translate}}</span>
|
||||||
[(ngModel)]="imagePermission">
|
</clr-tooltip-content>
|
||||||
<label for="image-permission-push-and-pull">{{'ROBOT_ACCOUNT.PUSH' | translate}}
|
</clr-tooltip>
|
||||||
& {{'ROBOT_ACCOUNT.PULL' | translate}}</label>
|
</td>
|
||||||
</div>
|
<td>
|
||||||
</div>
|
<input type="checkbox" name="image-permission-push"
|
||||||
<div class="form-group padding-left-120">
|
[(ngModel)]="imagePermissionPush" clrCheckbox>
|
||||||
<label>{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</label>
|
</td>
|
||||||
<div class="checkbox-inline">
|
<td class="clr-form-control-disabled">
|
||||||
<input type="checkbox" id="helm-permission-push"
|
<input disabled type="checkbox" name="image-permission-pull"
|
||||||
[checked]="robot.access.isPushChart"
|
[(ngModel)]="imagePermissionPull" clrCheckbox>
|
||||||
[(ngModel)]="robot.access.isPushChart"
|
</td>
|
||||||
name="helm-permission">
|
</tr>
|
||||||
<label for="helm-permission-push">{{'ROBOT_ACCOUNT.PUSH' | translate}}</label>
|
<tr>
|
||||||
</div>
|
<td class="left">{{'ROBOT_ACCOUNT.PERMISSIONS_HELMCHART' | translate}}</td>
|
||||||
<div class="checkbox-inline">
|
<td>
|
||||||
<input type="checkbox" id="helm-permission-pull"
|
<input type="checkbox"
|
||||||
[checked]="robot.access.isPullChart"
|
[(ngModel)]="robot.access.isPushChart"
|
||||||
[(ngModel)]="robot.access.isPullChart"
|
name="helm-permission" clrCheckbox>
|
||||||
name="helm-permission">
|
</td>
|
||||||
<label for="helm-permission-pull">{{'ROBOT_ACCOUNT.PULL' | translate}}</label>
|
<td>
|
||||||
</div>
|
<input type="checkbox"
|
||||||
</div>
|
[(ngModel)]="robot.access.isPullChart"
|
||||||
|
name="helm-permission" clrCheckbox>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -42,5 +42,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.padding-left-120{
|
.padding-left-120{
|
||||||
padding-left: 120px;
|
padding-left: 126px;
|
||||||
|
}
|
||||||
|
.w-90{
|
||||||
|
width: 90%;
|
||||||
}
|
}
|
@ -38,7 +38,8 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
|||||||
robotNameChecker: Subject<string> = new Subject<string>();
|
robotNameChecker: Subject<string> = new Subject<string>();
|
||||||
nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
||||||
robotForm: NgForm;
|
robotForm: NgForm;
|
||||||
imagePermission: string = "push-and-pull";
|
imagePermissionPush: boolean = true;
|
||||||
|
imagePermissionPull: boolean = true;
|
||||||
@Input() projectId: number;
|
@Input() projectId: number;
|
||||||
@Input() projectName: string;
|
@Input() projectName: string;
|
||||||
@Output() create = new EventEmitter<boolean>();
|
@Output() create = new EventEmitter<boolean>();
|
||||||
@ -99,6 +100,8 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
|||||||
this.robot.name = "";
|
this.robot.name = "";
|
||||||
this.robot.description = "";
|
this.robot.description = "";
|
||||||
this.addRobotOpened = true;
|
this.addRobotOpened = true;
|
||||||
|
this.imagePermissionPush = true;
|
||||||
|
this.imagePermissionPull = true;
|
||||||
this.isRobotNameValid = true;
|
this.isRobotNameValid = true;
|
||||||
this.robot = new Robot();
|
this.robot = new Robot();
|
||||||
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME";
|
||||||
@ -118,12 +121,12 @@ export class AddRobotComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// set value to robot.access.isPullImage and robot.access.isPushOrPullImage when submit
|
// set value to robot.access.isPullImage and robot.access.isPushOrPullImage when submit
|
||||||
if ( this.imagePermission === 'pull' ) {
|
if ( this.imagePermissionPush && this.imagePermissionPull) {
|
||||||
this.robot.access.isPullImage = true;
|
|
||||||
this.robot.access.isPushOrPullImage = false;
|
|
||||||
} else {
|
|
||||||
this.robot.access.isPullImage = false;
|
this.robot.access.isPullImage = false;
|
||||||
this.robot.access.isPushOrPullImage = true;
|
this.robot.access.isPushOrPullImage = true;
|
||||||
|
} else {
|
||||||
|
this.robot.access.isPullImage = true;
|
||||||
|
this.robot.access.isPushOrPullImage = false;
|
||||||
}
|
}
|
||||||
this.isSubmitOnGoing = true;
|
this.isSubmitOnGoing = true;
|
||||||
this.robotService
|
this.robotService
|
||||||
|
@ -322,7 +322,8 @@
|
|||||||
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
||||||
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
||||||
"DELETION_TITLE": "Confirm removal of robot accounts",
|
"DELETION_TITLE": "Confirm removal of robot accounts",
|
||||||
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?"
|
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?",
|
||||||
|
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
|
||||||
},
|
},
|
||||||
"GROUP": {
|
"GROUP": {
|
||||||
"GROUP": "Group",
|
"GROUP": "Group",
|
||||||
|
@ -323,7 +323,8 @@
|
|||||||
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
||||||
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
||||||
"DELETION_TITLE": "Confirm removal of robot accounts",
|
"DELETION_TITLE": "Confirm removal of robot accounts",
|
||||||
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?"
|
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?",
|
||||||
|
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
|
||||||
},
|
},
|
||||||
"GROUP": {
|
"GROUP": {
|
||||||
"GROUP": "Group",
|
"GROUP": "Group",
|
||||||
|
@ -315,7 +315,8 @@
|
|||||||
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
||||||
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
||||||
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ",
|
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ",
|
||||||
"DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?"
|
"DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?",
|
||||||
|
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
|
||||||
},
|
},
|
||||||
"GROUP": {
|
"GROUP": {
|
||||||
"Group": "Group",
|
"Group": "Group",
|
||||||
|
@ -320,7 +320,8 @@
|
|||||||
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
"CREATED_SUCCESS": "Created '{{param}}' successfully.",
|
||||||
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
|
||||||
"DELETION_TITLE": "Confirmar a remoção do robô Contas",
|
"DELETION_TITLE": "Confirmar a remoção do robô Contas",
|
||||||
"DELETION_SUMMARY": "Você quer remover a regra {{param}}?"
|
"DELETION_SUMMARY": "Você quer remover a regra {{param}}?",
|
||||||
|
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified."
|
||||||
},
|
},
|
||||||
"GROUP": {
|
"GROUP": {
|
||||||
"GROUP": "Grupo",
|
"GROUP": "Grupo",
|
||||||
|
@ -321,7 +321,8 @@
|
|||||||
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功。",
|
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功。",
|
||||||
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
|
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
|
||||||
"DELETION_TITLE": "删除账户确认",
|
"DELETION_TITLE": "删除账户确认",
|
||||||
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?"
|
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?",
|
||||||
|
"PULL_IS_MUST" : "拉取权限默认选中且不可修改。"
|
||||||
},
|
},
|
||||||
"GROUP": {
|
"GROUP": {
|
||||||
"GROUP": "组",
|
"GROUP": "组",
|
||||||
|
Loading…
Reference in New Issue
Block a user