Merge pull request #12707 from heww/gen-project-apis

refactor(api): generate project apis by go-swagger
This commit is contained in:
He Weiwei 2020-08-16 00:56:23 +08:00 committed by GitHub
commit 0921beaf4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2083 additions and 1870 deletions

View File

@ -310,7 +310,7 @@ endif
SWAGGER_IMAGENAME=goharbor/swagger
SWAGGER_VERSION=v0.21.0
SWAGGER=$(DOCKERCMD) run --rm -u $(shell id -u):$(shell id -g) -v $(BUILDPATH):$(BUILDPATH) -w $(BUILDPATH) ${SWAGGER_IMAGENAME}:${SWAGGER_VERSION}
SWAGGER_GENERATE_SERVER=${SWAGGER} generate server --template-dir=$(TOOLSPATH)/swagger/templates --exclude-main
SWAGGER_GENERATE_SERVER=${SWAGGER} generate server --template-dir=$(TOOLSPATH)/swagger/templates --exclude-main --additional-initialism=CVE
SWAGGER_IMAGE_BUILD_CMD=${DOCKERBUILD} -f ${TOOLSPATH}/swagger/Dockerfile --build-arg SWAGGER_VERSION=${SWAGGER_VERSION} -t ${SWAGGER_IMAGENAME}:$(SWAGGER_VERSION) .
SWAGGER_IMAGENAME:

View File

@ -53,214 +53,6 @@ paths:
$ref: '#/definitions/Search'
'500':
description: Unexpected internal errors.
/projects:
get:
summary: List projects
description: |
This endpoint returns all projects created by Harbor, and can be filtered by project name.
parameters:
- name: name
in: query
description: The name of project.
required: false
type: string
- name: public
in: query
description: The project is public or private.
required: false
type: boolean
format: int32
- name: owner
in: query
description: The name of project owner.
required: false
type: string
- name: page
in: query
type: integer
format: int32
required: false
description: 'The page number, default is 1.'
- name: page_size
in: query
type: integer
format: int32
required: false
description: 'The size of per page, default is 10, maximum is 100.'
tags:
- Products
responses:
'200':
description: Return all matched projects.
schema:
type: array
items:
$ref: '#/definitions/Project'
headers:
X-Total-Count:
description: The total count of projects
type: integer
Link:
description: Link refers to the previous page and next page
type: string
'401':
description: User need to log in first.
'500':
description: Internal errors.
head:
summary: Check if the project name user provided already exists.
description: |
This endpoint is used to check if the project name user provided already exist.
parameters:
- name: project_name
in: query
description: Project name for checking exists.
required: true
type: string
tags:
- Products
responses:
'200':
description: Project name exists.
'404':
description: Project name does not exist.
'500':
description: Unexpected internal errors.
post:
summary: Create a new project.
description: |
This endpoint is for user to create a new project.
parameters:
- name: project
in: body
description: New created project.
required: true
schema:
$ref: '#/definitions/ProjectReq'
tags:
- Products
responses:
'201':
description: Project created successfully.
'400':
description: Unsatisfied with constraints of the project creation.
'401':
description: User need to log in first.
'409':
description: Project name already exists.
'415':
$ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
'/projects/{project_id}':
get:
summary: Return specific project detail information
description: |
This endpoint returns specific project information by project ID.
parameters:
- name: project_id
in: path
description: Project ID for filtering results.
required: true
type: integer
format: int64
tags:
- Products
responses:
'200':
description: Return matched project information.
schema:
$ref: '#/definitions/Project'
'401':
description: User need to log in first.
'500':
description: Internal errors.
put:
summary: Update properties for a selected project.
description: |
This endpoint is aimed to update the properties of a project.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Selected project ID.
- name: project
in: body
required: true
schema:
$ref: '#/definitions/ProjectReq'
description: Updates of project.
tags:
- Products
responses:
'200':
description: Updated project properties successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User does not have permission to the project.
'404':
description: Project ID does not exist.
'500':
description: Unexpected internal errors.
delete:
summary: Delete project by projectID
description: |
This endpoint is aimed to delete project by project ID.
parameters:
- name: project_id
in: path
description: Project ID of project which will be deleted.
required: true
type: integer
format: int64
tags:
- Products
responses:
'200':
description: Project is deleted successfully.
'400':
description: Invalid project id.
'403':
description: User need to log in first.
'404':
description: Project does not exist.
'412':
description: 'Project contains policies, can not be deleted.'
'500':
description: Internal errors.
'/projects/{project_id}/summary':
get:
summary: Get summary of the project.
description: Get summary of the project.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID
tags:
- Products
responses:
'200':
description: Get summary of the project successfully.
schema:
$ref: '#/definitions/ProjectSummary'
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User does not have permission to get summary of the project.
'404':
description: Project ID does not exist.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/metadatas':
get:
summary: Get project metadata.

View File

@ -20,6 +20,213 @@ security:
- basic: []
- {}
paths:
/projects:
get:
summary: List projects
description: This endpoint returns projects created by Harbor.
tags:
- project
operationId: listProjects
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
- name: name
in: query
description: The name of project.
required: false
type: string
- name: public
in: query
description: The project is public or private.
required: false
type: boolean
format: int32
- name: owner
in: query
description: The name of project owner.
required: false
type: string
responses:
'200':
description: Return all matched projects.
schema:
type: array
items:
$ref: '#/definitions/Project'
headers:
X-Total-Count:
description: The total count of projects
type: integer
Link:
description: Link refers to the previous page and next page
type: string
'401':
$ref: '#/responses/401'
'500':
$ref: '#/responses/500'
head:
summary: Check if the project name user provided already exists.
description: This endpoint is used to check if the project name provided already exist.
tags:
- project
operationId: headProject
parameters:
- $ref: '#/parameters/requestId'
- name: project_name
in: query
description: Project name for checking exists.
required: true
type: string
responses:
'200':
$ref: '#/responses/200'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
post:
summary: Create a new project.
description: This endpoint is for user to create a new project.
tags:
- project
operationId: createProject
parameters:
- $ref: '#/parameters/requestId'
- name: project
in: body
description: New created project.
required: true
schema:
$ref: '#/definitions/ProjectReq'
responses:
'201':
$ref: '#/responses/201'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'409':
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
'/projects/{project_id}':
get:
summary: Return specific project detail information
description: This endpoint returns specific project information by project ID.
tags:
- project
operationId: getProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
responses:
'200':
description: Return matched project information.
schema:
$ref: '#/definitions/Project'
'401':
$ref: '#/responses/401'
'500':
$ref: '#/responses/500'
put:
summary: Update properties for a selected project.
description: This endpoint is aimed to update the properties of a project.
tags:
- project
operationId: updateProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
- name: project
in: body
required: true
schema:
$ref: '#/definitions/ProjectReq'
description: Updates of project.
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
delete:
summary: Delete project by projectID
description: This endpoint is aimed to delete project by project ID.
tags:
- project
operationId: deleteProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'412':
$ref: '#/responses/412'
'500':
$ref: '#/responses/500'
/projects/{project_id}/_deletable:
get:
summary: Get the deletable status of the project
description: Get the deletable status of the project
tags:
- project
operationId: getProjectDeletable
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
responses:
'200':
description: Return deletable status of the project.
schema:
$ref: '#/definitions/ProjectDeletable'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
'/projects/{project_id}/summary':
get:
summary: Get summary of the project.
description: Get summary of the project.
tags:
- project
operationId: getProjectSummary
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
responses:
'200':
description: Get summary of the project successfully.
schema:
$ref: '#/definitions/ProjectSummary'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories:
get:
summary: List repositories
@ -1252,7 +1459,8 @@ parameters:
in: path
description: The ID of the project
required: true
type: string
type: integer
format: int64
repositoryName:
name: repository_name
in: path
@ -1293,6 +1501,7 @@ parameters:
required: false
description: The size of per page
default: 10
maximum: 100
instanceName:
name: preheat_instance_name
in: path
@ -1387,6 +1596,14 @@ responses:
type: string
schema:
$ref: '#/definitions/Errors'
'412':
description: Precondition failed
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
schema:
$ref: '#/definitions/Errors'
'500':
description: Internal server error
headers:
@ -1958,3 +2175,243 @@ definitions:
content:
type: string
description: The base64 encoded content of the icon
ProjectReq:
type: object
properties:
project_name:
type: string
description: The name of the project.
public:
type: boolean
description: deprecated, reserved for project creation in replication
x-nullable: true
metadata:
description: The metadata of the project.
$ref: '#/definitions/ProjectMetadata'
cve_allowlist:
description: The CVE allowlist of the project.
$ref: '#/definitions/CVEAllowlist'
storage_limit:
type: integer
format: int64
description: The storage quota of the project.
x-nullable: true
registry_id:
type: integer
format: int64
description: The ID of referenced registry when creating the proxy cache project
x-nullable: true
Project:
type: object
properties:
project_id:
type: integer
format: int32
description: Project ID
owner_id:
type: integer
format: int32
description: The owner ID of the project always means the creator of the project.
name:
type: string
description: The name of the project.
registry_id:
type: integer
format: int64
description: The ID of referenced registry when the project is a proxy cache project.
creation_time:
type: string
format: date-time
description: The creation time of the project.
update_time:
type: string
format: date-time
description: The update time of the project.
deleted:
type: boolean
description: A deletion mark of the project.
owner_name:
type: string
description: The owner name of the project.
togglable:
type: boolean
description: Correspond to the UI about whether the project's publicity is updatable (for UI)
current_user_role_id:
type: integer
description: The role ID with highest permission of the current user who triggered the API (for UI). This attribute is deprecated and will be removed in future versions.
current_user_role_ids:
type: array
items:
type: integer
format: int32
description: The list of role ID of the current user who triggered the API (for UI)
repo_count:
type: integer
description: The number of the repositories under this project.
chart_count:
type: integer
description: The total number of charts under this project.
metadata:
description: The metadata of the project.
$ref: '#/definitions/ProjectMetadata'
cve_allowlist:
description: The CVE allowlist of this project.
$ref: '#/definitions/CVEAllowlist'
ProjectDeletable:
type: object
properties:
deletable:
type: boolean
description: Whether the project can be deleted.
message:
type: string
description: The detail message when the project can not be deleted.
ProjectMetadata:
type: object
properties:
public:
type: string
description: 'The public status of the project. The valid values are "true", "false".'
enable_content_trust:
type: string
description: 'Whether content trust is enabled or not. If it is enabled, user can''t pull unsigned images from this project. The valid values are "true", "false".'
x-nullable: true
prevent_vul:
type: string
description: 'Whether prevent the vulnerable images from running. The valid values are "true", "false".'
x-nullable: true
severity:
type: string
description: 'If the vulnerability is high than severity defined here, the images can''t be pulled. The valid values are "none", "low", "medium", "high", "critical".'
x-nullable: true
auto_scan:
type: string
description: 'Whether scan images automatically when pushing. The valid values are "true", "false".'
x-nullable: true
reuse_sys_cve_allowlist:
type: string
description: 'Whether this project reuse the system level CVE allowlist as the allowlist of its own. The valid values are "true", "false".
If it is set to "true" the actual allowlist associate with this project, if any, will be ignored.'
x-nullable: true
retention_id:
type: string
description: 'The ID of the tag retention policy for the project'
x-nullable: true
ProjectSummary:
type: object
properties:
repo_count:
type: integer
description: The number of the repositories under this project.
chart_count:
type: integer
description: The total number of charts under this project.
project_admin_count:
type: integer
description: The total number of project admin members.
maintainer_count:
type: integer
description: The total number of maintainer members.
developer_count:
type: integer
description: The total number of developer members.
guest_count:
type: integer
description: The total number of guest members.
limited_guest_count:
type: integer
description: The total number of limited guest members.
quota:
type: object
properties:
hard:
$ref: "#/definitions/ResourceList"
description: The hard limits of the quota
used:
$ref: "#/definitions/ResourceList"
description: The used status of the quota
registry:
$ref: "#/definitions/Registry"
CVEAllowlist:
type: object
description: The CVE Allowlist for system or project
properties:
id:
type: integer
description: ID of the allowlist
project_id:
type: integer
description: ID of the project which the allowlist belongs to. For system level allowlist this attribute is zero.
expires_at:
type: integer
description: the time for expiration of the allowlist, in the form of seconds since epoch. This is an optional attribute, if it's not set the CVE allowlist does not expire.
x-nullable: true
items:
type: array
items:
$ref: "#/definitions/CVEAllowlistItem"
creation_time:
type: string
format: date-time
description: The creation time of the allowlist.
update_time:
type: string
format: date-time
description: The update time of the allowlist.
CVEAllowlistItem:
type: object
description: The item in CVE allowlist
properties:
cve_id:
type: string
description: The ID of the CVE, such as "CVE-2019-10164"
RegistryCredential:
type: object
properties:
type:
type: string
description: Credential type, such as 'basic', 'oauth'.
access_key:
type: string
description: Access key, e.g. user name when credential type is 'basic'.
access_secret:
type: string
description: Access secret, e.g. password when credential type is 'basic'.
Registry:
type: object
properties:
id:
type: integer
format: int64
description: The registry ID.
url:
type: string
description: The registry URL string.
name:
type: string
description: The registry name.
credential:
$ref: '#/definitions/RegistryCredential'
type:
type: string
description: Type of the registry, e.g. 'harbor'.
insecure:
type: boolean
description: Whether or not the certificate will be verified when Harbor tries to access the server.
description:
type: string
description: Description of the registry.
status:
type: string
description: Health status of the registry.
creation_time:
type: string
description: The create time of the policy.
update_time:
type: string
description: The update time of the policy.
ResourceList:
type: object
additionalProperties:
type: integer
format: int64

View File

@ -17,6 +17,8 @@ package dao
import (
"encoding/json"
"fmt"
"time"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/log"
)
@ -24,6 +26,9 @@ import (
// CreateCVEAllowlist creates the CVE allowlist
func CreateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
o := GetOrmer()
now := time.Now()
l.CreationTime = now
l.UpdateTime = now
itemsBytes, _ := json.Marshal(l.Items)
l.ItemsText = string(itemsBytes)
return o.Insert(&l)
@ -32,6 +37,8 @@ func CreateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
// UpdateCVEAllowlist Updates the vulnerability white list to DB
func UpdateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
o := GetOrmer()
now := time.Now()
l.UpdateTime = now
itemsBytes, _ := json.Marshal(l.Items)
l.ItemsText = string(itemsBytes)
id, err := o.InsertOrUpdate(&l, "project_id")

View File

@ -22,7 +22,6 @@ import (
"time"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/replication/model"
)
@ -154,6 +153,10 @@ func (p *Project) FilterByMember(ctx context.Context, qs orm.QuerySeter, key str
subQuery = fmt.Sprintf("%s AND pm.role = %d", subQuery, query.Role)
}
if query.WithPublic {
subQuery = fmt.Sprintf("(%s) UNION (SELECT project_id FROM project_metadata WHERE name = 'public' AND value = 'true')", subQuery)
}
if len(query.GroupIDs) > 0 {
var elems []string
for _, groupID := range query.GroupIDs {
@ -210,41 +213,13 @@ type ProjectQueryParam struct {
ProjectIDs []int64 // project ID list
}
// ToQuery returns q.Query from param
func (param *ProjectQueryParam) ToQuery() *q.Query {
kw := q.KeyWords{}
if param.Name != "" {
kw["name"] = q.FuzzyMatchValue{Value: param.Name}
}
if param.Owner != "" {
kw["owner"] = param.Owner
}
if param.Public != nil {
kw["public"] = *param.Public
}
if param.RegistryID != 0 {
kw["registry_id"] = param.RegistryID
}
if len(param.ProjectIDs) > 0 {
kw["project_id__in"] = param.ProjectIDs
}
if param.Member != nil {
kw["member"] = param.Member
}
query := q.New(kw)
if param.Pagination != nil {
query.PageNumber = param.Pagination.Page
query.PageSize = param.Pagination.Size
}
return query
}
// MemberQuery filter by member's username and role
type MemberQuery struct {
Name string // the username of member
Role int // the role of the member has to the project
GroupIDs []int // the group ID of current user belongs to
WithPublic bool // include the public projects for the member
}
// Pagination ...

View File

@ -0,0 +1,41 @@
// 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 operator
import (
"context"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/user"
)
// FromContext return the event operator from context
func FromContext(ctx context.Context) string {
sc, ok := security.FromContext(ctx)
if !ok {
return ""
}
if sc.IsSolutionUser() {
user, err := user.Mgr.Get(ctx, 1)
if err == nil {
return user.Username
}
log.G(ctx).Errorf("failed to get operator for security %s, error: %v", sc.Name(), err)
}
return sc.GetUsername()
}

View File

@ -539,7 +539,7 @@ func (de *defaultEnforcer) toCandidates(ctx context.Context, p *models.Project,
// getProject gets the full metadata of the specified project
func (de *defaultEnforcer) getProject(ctx context.Context, id int64) (*models.Project, error) {
// Get project info with CVE allow list and metadata
return de.proCtl.Get(ctx, id, project.CVEAllowlist(true), project.Metadata(true))
return de.proCtl.Get(ctx, id, project.WithEffectCVEAllowlist())
}
// enforceError is a wrap error

View File

@ -17,10 +17,13 @@ package project
import (
"context"
event "github.com/goharbor/harbor/src/controller/event/metadata"
"github.com/goharbor/harbor/src/controller/event/operator"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/project/metadata"
"github.com/goharbor/harbor/src/pkg/project/models"
@ -33,6 +36,12 @@ var (
Ctl = NewController()
)
// Project alias to models.Project
type Project = models.Project
// MemberQuery alias to models.MemberQuery
type MemberQuery = models.MemberQuery
// Controller defines the operations related with blobs
type Controller interface {
// Create create project instance
@ -46,7 +55,9 @@ type Controller interface {
// GetByName get the project by project name
GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error)
// List list projects
List(ctx context.Context, query *models.ProjectQueryParam, options ...Option) ([]*models.Project, error)
List(ctx context.Context, query *q.Query, options ...Option) ([]*models.Project, error)
// Update update the project
Update(ctx context.Context, project *models.Project) error
}
// NewController creates an instance of the default project controller
@ -92,6 +103,14 @@ func (c *controller) Create(ctx context.Context, project *models.Project) (int64
return 0, err
}
// fire event
e := &event.CreateProjectEventMetadata{
ProjectID: projectID,
Project: project.Name,
Operator: operator.FromContext(ctx),
}
notification.AddEvent(ctx, e)
return projectID, nil
}
@ -100,7 +119,23 @@ func (c *controller) Count(ctx context.Context, query *q.Query) (int64, error) {
}
func (c *controller) Delete(ctx context.Context, id int64) error {
return c.projectMgr.Delete(ctx, id)
proj, err := c.Get(ctx, id)
if err != nil {
return err
}
if err := c.projectMgr.Delete(ctx, id); err != nil {
return err
}
e := &event.DeleteProjectEventMetadata{
ProjectID: proj.ProjectID,
Project: proj.Name,
Operator: operator.FromContext(ctx),
}
notification.AddEvent(ctx, e)
return nil
}
func (c *controller) Get(ctx context.Context, projectID int64, options ...Option) (*models.Project, error) {
@ -109,13 +144,11 @@ func (c *controller) Get(ctx context.Context, projectID int64, options ...Option
return nil, err
}
opts := newOptions(options...)
if opts.WithOwner {
if err := c.loadOwners(ctx, models.Projects{p}); err != nil {
if err := c.assembleProjects(ctx, models.Projects{p}, options...); err != nil {
return nil, err
}
}
return c.assembleProject(ctx, p, opts)
return p, nil
}
func (c *controller) GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error) {
@ -128,38 +161,164 @@ func (c *controller) GetByName(ctx context.Context, projectName string, options
return nil, err
}
opts := newOptions(options...)
if opts.WithOwner {
if err := c.loadOwners(ctx, models.Projects{p}); err != nil {
if err := c.assembleProjects(ctx, models.Projects{p}, options...); err != nil {
return nil, err
}
}
return c.assembleProject(ctx, p, newOptions(options...))
return p, nil
}
func (c *controller) List(ctx context.Context, query *models.ProjectQueryParam, options ...Option) ([]*models.Project, error) {
func (c *controller) List(ctx context.Context, query *q.Query, options ...Option) ([]*models.Project, error) {
projects, err := c.projectMgr.List(ctx, query)
if err != nil {
return nil, err
}
opts := newOptions(options...)
if opts.WithOwner {
if err := c.loadOwners(ctx, projects); err != nil {
return nil, err
}
if len(projects) == 0 {
return projects, nil
}
for _, p := range projects {
if _, err := c.assembleProject(ctx, p, opts); err != nil {
if err := c.assembleProjects(ctx, projects, options...); err != nil {
return nil, err
}
}
return projects, nil
}
func (c *controller) Update(ctx context.Context, p *models.Project) error {
// currently, allowlist manager not use the ormer from the context,
// the SQL executed in the allowlist manager will not be in the transaction with metadata manager,
// we will update the metadata of the project first so that we can be rollback the operations for the metadata
// when set allowlist for the project failed
if len(p.Metadata) > 0 {
meta, err := c.metaMgr.Get(ctx, p.ProjectID)
if err != nil {
return err
}
if meta == nil {
meta = map[string]string{}
}
metaNeedUpdated := map[string]string{}
metaNeedCreated := map[string]string{}
for key, value := range p.Metadata {
_, exist := meta[key]
if exist {
metaNeedUpdated[key] = value
} else {
metaNeedCreated[key] = value
}
}
if err = c.metaMgr.Add(ctx, p.ProjectID, metaNeedCreated); err != nil {
return err
}
if err = c.metaMgr.Update(ctx, p.ProjectID, metaNeedUpdated); err != nil {
return err
}
}
if p.CVEAllowlist.ProjectID == p.ProjectID {
if err := c.allowlistMgr.Set(p.ProjectID, p.CVEAllowlist); err != nil {
return err
}
}
return nil
}
func (c *controller) assembleProjects(ctx context.Context, projects models.Projects, options ...Option) error {
opts := newOptions(options...)
if opts.WithMetadata {
if err := c.loadMetadatas(ctx, projects); err != nil {
return err
}
}
if opts.WithEffectCVEAllowlist {
if err := c.loadEffectCVEAllowlists(ctx, projects); err != nil {
return err
}
} else if opts.WithCVEAllowlist {
if err := c.loadCVEAllowlists(ctx, projects); err != nil {
return err
}
}
if opts.WithOwner {
if err := c.loadOwners(ctx, projects); err != nil {
return err
}
}
return nil
}
func (c *controller) loadCVEAllowlists(ctx context.Context, projects models.Projects) error {
if len(projects) == 0 {
return nil
}
for _, p := range projects {
wl, err := c.allowlistMgr.Get(p.ProjectID)
if err != nil {
return err
}
p.CVEAllowlist = *wl
}
return nil
}
func (c *controller) loadEffectCVEAllowlists(ctx context.Context, projects models.Projects) error {
if len(projects) == 0 {
return nil
}
for _, p := range projects {
if p.ReuseSysCVEAllowlist() {
wl, err := c.allowlistMgr.GetSys()
if err != nil {
log.Errorf("get system CVE allowlist failed, error: %v", err)
return err
}
wl.ProjectID = p.ProjectID
p.CVEAllowlist = *wl
} else {
wl, err := c.allowlistMgr.Get(p.ProjectID)
if err != nil {
return err
}
p.CVEAllowlist = *wl
}
}
return nil
}
func (c *controller) loadMetadatas(ctx context.Context, projects models.Projects) error {
if len(projects) == 0 {
return nil
}
for _, p := range projects {
meta, err := c.metaMgr.Get(ctx, p.ProjectID)
if err != nil {
return err
}
p.Metadata = meta
}
return nil
}
func (c *controller) loadOwners(ctx context.Context, projects models.Projects) error {
if len(projects) == 0 {
return nil
}
owners, err := c.userMgr.List(ctx, q.New(q.KeyWords{"user_id__in": projects.OwnerIDs()}))
if err != nil {
return err
@ -177,42 +336,3 @@ func (c *controller) loadOwners(ctx context.Context, projects models.Projects) e
return nil
}
func (c *controller) assembleProject(ctx context.Context, p *models.Project, opts *Options) (*models.Project, error) {
if opts.Metadata {
meta, err := c.metaMgr.Get(ctx, p.ProjectID)
if err != nil {
return nil, err
}
if len(p.Metadata) == 0 {
p.Metadata = make(map[string]string)
}
for k, v := range meta {
p.Metadata[k] = v
}
}
if opts.CVEAllowlist {
if p.ReuseSysCVEAllowlist() {
wl, err := c.allowlistMgr.GetSys()
if err != nil {
log.Errorf("get system CVE allowlist failed, error: %v", err)
return nil, err
}
wl.ProjectID = p.ProjectID
p.CVEAllowlist = *wl
} else {
wl, err := c.allowlistMgr.Get(p.ProjectID)
if err != nil {
return nil, err
}
p.CVEAllowlist = *wl
}
}
return p, nil
}

View File

@ -22,6 +22,7 @@ import (
commonmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/project/models"
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
@ -102,8 +103,8 @@ func (suite *ControllerTestSuite) TestGetByName() {
}
{
allowlistMgr.On("GetSys").Return(&commonmodels.CVEAllowlist{}, nil)
p, err := c.GetByName(ctx, "library", CVEAllowlist(true))
allowlistMgr.On("Get", mock.Anything).Return(&commonmodels.CVEAllowlist{ProjectID: 1}, nil)
p, err := c.GetByName(ctx, "library", WithCVEAllowlist())
suite.Nil(err)
suite.Equal("library", p.Name)
suite.Equal(p.ProjectID, p.CVEAllowlist.ProjectID)
@ -140,8 +141,7 @@ func (suite *ControllerTestSuite) TestWithOwner() {
}
{
param := &models.ProjectQueryParam{ProjectIDs: []int64{1}}
projects, err := c.List(ctx, param, Metadata(false), WithOwner())
projects, err := c.List(ctx, q.New(q.KeyWords{"project_id__in": []int64{1}}), Metadata(false), WithOwner())
suite.Nil(err)
suite.Len(projects, 1)
suite.Equal("admin", projects[0].OwnerName)

View File

@ -19,22 +19,31 @@ type Option func(*Options)
// Options options used by `Get` method of `Controller`
type Options struct {
CVEAllowlist bool // get project with cve allowlist
Metadata bool // get project with metadata
WithOwner bool
WithCVEAllowlist bool // get project with cve allowlist
WithEffectCVEAllowlist bool // get project with effect cve allowlist
WithMetadata bool // get project with metadata
WithOwner bool // get project with owner name
}
// CVEAllowlist set CVEAllowlist for the Options
func CVEAllowlist(allowlist bool) Option {
// WithCVEAllowlist set WithCVEAllowlist for the Options
func WithCVEAllowlist() Option {
return func(opts *Options) {
opts.CVEAllowlist = allowlist
opts.WithCVEAllowlist = true
}
}
// Metadata set Metadata for the Options
// WithEffectCVEAllowlist set WithEffectCVEAllowlist for the Options
func WithEffectCVEAllowlist() Option {
return func(opts *Options) {
opts.WithMetadata = true // we need `reuse_sys_cve_allowlist` value in the metadata of project
opts.WithEffectCVEAllowlist = true
}
}
// Metadata set WithMetadata for the Options
func Metadata(metadata bool) Option {
return func(opts *Options) {
opts.Metadata = metadata
opts.WithMetadata = metadata
}
}
@ -47,7 +56,7 @@ func WithOwner() Option {
func newOptions(options ...Option) *Options {
opts := &Options{
Metadata: true, // default get project with metadata
WithMetadata: true, // default get project with metadata
}
for _, f := range options {

View File

@ -0,0 +1,64 @@
// 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 project
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/project/models"
)
// Result the result for ListAll func
type Result struct {
Data *models.Project
Error error
}
// ListAll returns all projects with chunk support
func ListAll(ctx context.Context, chunkSize int, query *q.Query, options ...Option) <-chan Result {
ch := make(chan Result, chunkSize)
go func() {
defer close(ch)
query = q.MustClone(query)
query.PageNumber = 1
query.PageSize = int64(chunkSize)
for {
projects, err := Ctl.List(ctx, query, options...)
if err != nil {
format := "failed to list projects at page %d with page size %d, error :%v"
ch <- Result{Error: fmt.Errorf(format, query.PageNumber, query.PageSize, err)}
return
}
for _, p := range projects {
ch <- Result{Data: p}
}
if len(projects) < chunkSize {
break
}
query.PageNumber++
}
}()
return ch
}

View File

@ -21,6 +21,7 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/graph-gophers/dataloader"
)
@ -43,7 +44,7 @@ func getProjectsBatchFn(ctx context.Context, keys dataloader.Keys) []*dataloader
projectIDs = append(projectIDs, id)
}
projects, err := project.Mgr.List(ctx, &models.ProjectQueryParam{ProjectIDs: projectIDs})
projects, err := project.Mgr.List(ctx, q.New(q.KeyWords{"project_id__in": projectIDs}))
if err != nil {
return handleError(err)
}

View File

@ -19,7 +19,6 @@ import (
"fmt"
"strconv"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
@ -55,40 +54,14 @@ func RefreshForProjects(ctx context.Context) error {
return err
}
projects := func(chunkSize int) <-chan *models.Project {
ch := make(chan *models.Project, chunkSize)
go func() {
defer close(ch)
params := &models.ProjectQueryParam{
Pagination: &models.Pagination{Page: 1, Size: int64(chunkSize)},
chunkSize := 50 // default chunk size is 50
for result := range project.ListAll(ctx, chunkSize, nil, project.Metadata(false)) {
if result.Error != nil {
log.Errorf("refresh quota for all projects got error: %v", result.Error)
continue
}
for {
results, err := project.Ctl.List(ctx, params, project.Metadata(false))
if err != nil {
log.Errorf("list projects failed, error: %v", err)
return
}
for _, p := range results {
ch <- p
}
if len(results) < chunkSize {
break
}
params.Pagination.Page++
}
}()
return ch
}(50) // default chunk size is 50
for p := range projects {
p := result.Data
referenceID := ReferenceID(p.ProjectID)
_, err := Ctl.GetByRef(ctx, ProjectReference, referenceID)

View File

@ -23,6 +23,7 @@ import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/quota"
"github.com/goharbor/harbor/src/pkg/quota/driver"
"github.com/goharbor/harbor/src/pkg/quota/types"
@ -89,7 +90,7 @@ func (suite *RefreshForProjectsTestSuite) TestRefreshForProjects() {
}
page := 1
mock.OnAnything(suite.projectCtl, "List").Return(func(context.Context, *models.ProjectQueryParam, ...project.Option) []*models.Project {
mock.OnAnything(suite.projectCtl, "List").Return(func(context.Context, *q.Query, ...project.Option) []*models.Project {
defer func() {
page++
}()

View File

@ -51,6 +51,11 @@ var (
retentionController retention.APIController
)
// GetRetentionController returns the retention API controller
func GetRetentionController() retention.APIController {
return retentionController
}
// BaseController ...
type BaseController struct {
api.BaseAPI

View File

@ -97,16 +97,12 @@ func init() {
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
beego.Router("/api/search/", &SearchAPI{})
beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id", &ProjectAPI{}, "delete:Delete;get:Get;put:Put")
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
beego.Router("/api/users/search", &UserAPI{}, "get:Search")
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/projects/:id([0-9]+)/summary", &ProjectAPI{}, "get:Summary")
beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable")
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get")
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post")
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete")

View File

@ -1,720 +0,0 @@
// Copyright 2018 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 (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
pro "github.com/goharbor/harbor/src/common/dao/project"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/common/utils"
errutil "github.com/goharbor/harbor/src/common/utils/error"
"github.com/goharbor/harbor/src/controller/event/metadata"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/goharbor/harbor/src/replication"
)
type deletableResp struct {
Deletable bool `json:"deletable"`
Message string `json:"message"`
}
// ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs
type ProjectAPI struct {
BaseController
project *models.Project
}
const projectNameMaxLen int = 255
const projectNameMinLen int = 1
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
const defaultDaysToRetention = 7
// Prepare validates the URL and the user
func (p *ProjectAPI) Prepare() {
p.BaseController.Prepare()
if len(p.GetStringFromPath(":id")) != 0 {
id, err := p.GetInt64FromPath(":id")
if err != nil || id <= 0 {
text := "invalid project ID: "
if err != nil {
text += err.Error()
} else {
text += fmt.Sprintf("%d", id)
}
p.SendBadRequestError(errors.New(text))
return
}
project, err := p.ProjectMgr.Get(id)
if err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to get project %d", id), err)
return
}
if project == nil {
p.handleProjectNotFound(id)
return
}
p.project = project
}
}
func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
if len(subresource) == 0 {
subresource = append(subresource, rbac.ResourceSelf)
}
return p.RequireProjectAccess(p.project.ProjectID, action, subresource...)
}
// Post ...
func (p *ProjectAPI) Post() {
if !p.SecurityCtx.IsAuthenticated() {
p.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
onlyAdmin, err := config.OnlyAdminCreateProject()
if err != nil {
log.Errorf("failed to determine whether only admin can create projects: %v", err)
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.SendForbiddenError(errors.New("Only system admin can create project"))
return
}
var pro *models.ProjectRequest
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.SendBadRequestError(fmt.Errorf("invalid request: %v", err))
return
}
// trying to create a proxy cache project
if pro.RegistryID > 0 {
// only system admin can create the proxy cache project
if !p.SecurityCtx.IsSysAdmin() {
p.SendForbiddenError(errors.New("Only system admin can create proxy cache project"))
return
}
registry, err := replication.RegistryMgr.Get(pro.RegistryID)
if err != nil {
p.SendInternalServerError(fmt.Errorf("failed to get the registry %d: %v", pro.RegistryID, err))
return
}
if registry == nil {
p.SendNotFoundError(fmt.Errorf("registry %d not found", pro.RegistryID))
return
}
permitted := false
for _, t := range config.GetPermittedRegistryTypesForProxyCache() {
if string(registry.Type) == t {
permitted = true
break
}
}
if !permitted {
p.SendBadRequestError(fmt.Errorf("unsupported registry type %s", string(registry.Type)))
return
}
}
var hardLimits types.ResourceList
if config.QuotaPerProjectEnable() {
setting, err := config.QuotaSetting()
if err != nil {
log.Errorf("failed to get quota setting: %v", err)
p.SendInternalServerError(fmt.Errorf("failed to get quota setting: %v", err))
return
}
if !p.SecurityCtx.IsSysAdmin() {
pro.StorageLimit = &setting.StoragePerProject
}
hardLimits, err = projectQuotaHardLimits(p.Ctx.Request.Context(), pro, setting)
if err != nil {
log.Errorf("Invalid project request, error: %v", err)
p.SendBadRequestError(fmt.Errorf("invalid request: %v", err))
return
}
}
exist, err := p.ProjectMgr.Exists(pro.Name)
if err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
pro.Name), err)
return
}
if exist {
p.SendConflictError(errors.New("conflict project"))
return
}
if pro.Metadata == nil {
pro.Metadata = map[string]string{}
}
// accept the "public" property to make replication work well with old versions(<=1.2.0)
if pro.Public != nil && len(pro.Metadata[models.ProMetaPublic]) == 0 {
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(*pro.Public == 1)
}
// populate public metadata as false if it isn't set
if _, ok := pro.Metadata[models.ProMetaPublic]; !ok {
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
}
// populate
owner := p.SecurityCtx.GetUsername()
// set the owner as the system admin when the API being called by replication
// it's a solution to workaround the restriction of project creation API:
// only normal users can create projects
if p.SecurityCtx.IsSolutionUser() {
user, err := dao.GetUser(models.User{
UserID: 1,
})
if err != nil {
p.SendInternalServerError(fmt.Errorf("failed to get the user 1: %v", err))
return
}
owner = user.Username
}
projectID, err := p.ProjectMgr.Create(&models.Project{
Name: pro.Name,
OwnerName: owner,
Metadata: pro.Metadata,
RegistryID: pro.RegistryID,
})
if err != nil {
if err == errutil.ErrDupProject {
log.Debugf("conflict %s", pro.Name)
p.SendConflictError(fmt.Errorf("conflict %s", pro.Name))
} else {
p.ParseAndHandleError("failed to add project", err)
}
return
}
if config.QuotaPerProjectEnable() {
ctx := p.Ctx.Request.Context()
referenceID := quota.ReferenceID(projectID)
if _, err := quota.Ctl.Create(ctx, quota.ProjectReference, referenceID, hardLimits); err != nil {
p.SendInternalServerError(fmt.Errorf("failed to create quota for project: %v", err))
return
}
}
// create a default retention policy for proxy project
if pro.RegistryID > 0 {
if err := p.addRetentionPolicyForProxy(projectID); err != nil {
p.SendInternalServerError(fmt.Errorf("failed to add tag retention policy for project: %v", err))
return
}
}
// fire event
evt.BuildAndPublish(&metadata.CreateProjectEventMetadata{
ProjectID: projectID,
Project: pro.Name,
Operator: owner,
})
p.Redirect(http.StatusCreated, strconv.FormatInt(projectID, 10))
}
func (p *ProjectAPI) addRetentionPolicyForProxy(projID int64) error {
plc := policy.WithNDaysSinceLastPull(projID, defaultDaysToRetention)
retID, err := retentionController.CreateRetention(plc)
if err != nil {
return err
}
if err := p.ProjectMgr.GetMetadataManager().Add(projID, map[string]string{"retention_id": strconv.FormatInt(retID, 10)}); err != nil {
return err
}
return nil
}
// Head ...
func (p *ProjectAPI) Head() {
if !p.SecurityCtx.IsAuthenticated() {
p.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
name := p.GetString("project_name")
if len(name) == 0 {
p.SendBadRequestError(errors.New("project_name is needed"))
return
}
project, err := p.ProjectMgr.Get(name)
if err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to get project %s", name), err)
return
}
if project == nil {
p.SendNotFoundError(fmt.Errorf("project %s not found", name))
return
}
}
// Get ...
func (p *ProjectAPI) Get() {
if !p.requireAccess(rbac.ActionRead) {
return
}
err := p.populateProperties(p.project)
if err != nil {
log.Errorf("populate project properties failed with : %+v", err)
}
p.Data["json"] = p.project
p.ServeJSON()
}
// Delete ...
func (p *ProjectAPI) Delete() {
if !p.requireAccess(rbac.ActionDelete) {
return
}
result, err := p.deletable(p.project.ProjectID)
if err != nil {
p.SendInternalServerError(fmt.Errorf(
"failed to check the deletable of project %d: %v", p.project.ProjectID, err))
return
}
if !result.Deletable {
p.SendPreconditionFailedError(errors.New(result.Message))
return
}
ctx := p.Ctx.Request.Context()
if err := project.Ctl.Delete(ctx, p.project.ProjectID); err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to delete project %d", p.project.ProjectID), err)
return
}
referenceID := quota.ReferenceID(p.project.ProjectID)
q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, referenceID)
if err != nil {
log.Warningf("failed to get quota for project %s, error: %v", p.project.Name, err)
} else {
if err := quota.Ctl.Delete(ctx, q.ID); err != nil {
p.SendInternalServerError(fmt.Errorf("failed to delete quota for project: %v", err))
return
}
}
// fire event
evt.BuildAndPublish(&metadata.DeleteProjectEventMetadata{
ProjectID: p.project.ProjectID,
Project: p.project.Name,
Operator: p.SecurityCtx.GetUsername(),
})
}
// Deletable ...
func (p *ProjectAPI) Deletable() {
if !p.requireAccess(rbac.ActionDelete) {
return
}
result, err := p.deletable(p.project.ProjectID)
if err != nil {
p.SendInternalServerError(fmt.Errorf(
"failed to check the deletable of project %d: %v", p.project.ProjectID, err))
return
}
p.Data["json"] = result
p.ServeJSON()
}
func (p *ProjectAPI) deletable(projectID int64) (*deletableResp, error) {
count, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{projectID},
})
if err != nil {
return nil, err
}
if count > 0 {
return &deletableResp{
Deletable: false,
Message: "the project contains repositories, can not be deleted",
}, nil
}
// Check helm charts number
if config.WithChartMuseum() {
charts, err := chartController.ListCharts(p.project.Name)
if err != nil {
return nil, err
}
if len(charts) > 0 {
return &deletableResp{
Deletable: false,
Message: "the project contains helm charts, can not be deleted",
}, nil
}
}
return &deletableResp{
Deletable: true,
}, nil
}
// List ...
func (p *ProjectAPI) List() {
// query strings
page, size, err := p.GetPaginationParams()
if err != nil {
p.SendBadRequestError(err)
return
}
query := &models.ProjectQueryParam{
Name: p.GetString("name"),
Owner: p.GetString("owner"),
Pagination: &models.Pagination{
Page: page,
Size: size,
},
}
public := p.GetString("public")
if len(public) > 0 {
pub, err := strconv.ParseBool(public)
if err != nil {
p.SendBadRequestError(fmt.Errorf("invalid public: %s", public))
return
}
query.Public = &pub
}
var projects []*models.Project
if !p.SecurityCtx.IsAuthenticated() {
// not login, only get public projects
pros, err := p.ProjectMgr.GetPublic()
if err != nil {
p.SendInternalServerError(fmt.Errorf("failed to get public projects: %v", err))
return
}
projects = []*models.Project{}
projects = append(projects, pros...)
} else {
if !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
projects = []*models.Project{}
// login, but not system admin or solution user, get public projects and
// projects that the user is member of
pros, err := p.ProjectMgr.GetPublic()
if err != nil {
p.SendInternalServerError(fmt.Errorf("failed to get public projects: %v", err))
return
}
projects = append(projects, pros...)
if sc, ok := p.SecurityCtx.(*local.SecurityContext); ok {
mps, err := p.ProjectMgr.GetAuthorized(sc.User())
if err != nil {
p.SendInternalServerError(fmt.Errorf("failed to list authorized projects: %v", err))
return
}
projects = append(projects, mps...)
}
}
}
// Query projects by user group
if projects != nil {
projectIDs := []int64{}
for _, project := range projects {
projectIDs = append(projectIDs, project.ProjectID)
}
query.ProjectIDs = projectIDs
}
result, err := p.ProjectMgr.List(query)
if err != nil {
p.ParseAndHandleError("failed to list projects", err)
return
}
for _, project := range result.Projects {
err = p.populateProperties(project)
if err != nil {
log.Errorf("populate project properties failed %v", err)
}
}
p.SetPaginationHeader(result.Total, page, size)
p.Data["json"] = result.Projects
p.ServeJSON()
}
func (p *ProjectAPI) populateProperties(project *models.Project) error {
// Transform the severity to severity of CVSS v3.0 Ratings
if severity, ok := project.GetMetadata(models.ProMetaSeverity); ok {
project.SetMetadata(models.ProMetaSeverity, strings.ToLower(vuln.ParseSeverityVersion3(severity).String()))
}
if sc, ok := p.SecurityCtx.(*local.SecurityContext); ok {
roles, err := pro.ListRoles(sc.User(), project.ProjectID)
if err != nil {
return err
}
project.RoleList = roles
project.Role = highestRole(roles)
}
total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{project.ProjectID},
})
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get repo count of project %d failed", project.ProjectID))
return err
}
project.RepoCount = total
// Populate chart count property
if config.WithChartMuseum() {
count, err := chartController.GetCountOfCharts([]string{project.Name})
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", project.ProjectID))
return err
}
project.ChartCount = count
}
return nil
}
// Put ...
func (p *ProjectAPI) Put() {
if !p.requireAccess(rbac.ActionUpdate) {
return
}
var req *models.ProjectRequest
if err := p.DecodeJSONReq(&req); err != nil {
p.SendBadRequestError(err)
return
}
if err := p.ProjectMgr.Update(p.project.ProjectID,
&models.Project{
Metadata: req.Metadata,
CVEAllowlist: req.CVEAllowlist,
}); err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to update project %d",
p.project.ProjectID), err)
return
}
}
// Summary returns the summary of the project
func (p *ProjectAPI) Summary() {
if !p.requireAccess(rbac.ActionRead) {
return
}
if err := p.populateProperties(p.project); err != nil {
log.Warningf("populate project properties failed with : %+v", err)
}
summary := &models.ProjectSummary{
RepoCount: p.project.RepoCount,
ChartCount: p.project.ChartCount,
}
var fetchSummaries []func(context.Context, int64, *models.ProjectSummary)
if hasPerm, _ := p.HasProjectPermission(p.project.ProjectID, rbac.ActionRead, rbac.ResourceQuota); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectQuotaSummary)
}
if hasPerm, _ := p.HasProjectPermission(p.project.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectMemberSummary)
}
ctx := p.Ctx.Request.Context()
var wg sync.WaitGroup
for _, fn := range fetchSummaries {
fn := fn
wg.Add(1)
go func() {
defer wg.Done()
fn(ctx, p.project.ProjectID, summary)
}()
}
wg.Wait()
if p.project.RegistryID > 0 {
registry, err := replication.RegistryMgr.Get(p.project.RegistryID)
if err != nil {
log.Warningf("failed to get registry %d: %v", p.project.RegistryID, err)
} else {
if registry != nil {
registry.Credential = nil
summary.Registry = registry
}
}
}
p.Data["json"] = summary
p.ServeJSON()
}
// TODO move this to pa ckage models
func validateProjectReq(req *models.ProjectRequest) error {
pn := req.Name
if utils.IsIllegalLength(pn, projectNameMinLen, projectNameMaxLen) {
return fmt.Errorf("Project name %s is illegal in length. (greater than %d or less than %d)", pn, projectNameMaxLen, projectNameMinLen)
}
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
legal := validProjectName.MatchString(pn)
if !legal {
return fmt.Errorf("project name is not in lower case or contains illegal characters")
}
metas, err := validateProjectMetadata(req.Metadata)
if err != nil {
return err
}
req.Metadata = metas
return nil
}
func projectQuotaHardLimits(ctx context.Context, req *models.ProjectRequest, setting *models.QuotaSetting) (types.ResourceList, error) {
hardLimits := types.ResourceList{}
if req.StorageLimit != nil {
hardLimits[types.ResourceStorage] = *req.StorageLimit
} else {
hardLimits[types.ResourceStorage] = setting.StoragePerProject
}
if err := quota.Validate(ctx, quota.ProjectReference, hardLimits); err != nil {
return nil, err
}
return hardLimits, nil
}
func getProjectQuotaSummary(ctx context.Context, projectID int64, summary *models.ProjectSummary) {
if !config.QuotaPerProjectEnable() {
log.Debug("Quota per project disabled")
return
}
q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, quota.ReferenceID(projectID))
if err != nil {
log.Debugf("failed to get quota for project: %d", projectID)
return
}
summary.Quota = &models.QuotaSummary{}
summary.Quota.Hard, _ = types.NewResourceList(q.Hard)
summary.Quota.Used, _ = types.NewResourceList(q.Used)
}
func getProjectMemberSummary(ctx context.Context, projectID int64, summary *models.ProjectSummary) {
var wg sync.WaitGroup
for _, e := range []struct {
role int
count *int64
}{
{common.RoleProjectAdmin, &summary.ProjectAdminCount},
{common.RoleMaintainer, &summary.MaintainerCount},
{common.RoleDeveloper, &summary.DeveloperCount},
{common.RoleGuest, &summary.GuestCount},
{common.RoleLimitedGuest, &summary.LimitedGuestCount},
} {
wg.Add(1)
go func(role int, count *int64) {
defer wg.Done()
total, err := pro.GetTotalOfProjectMembers(projectID, role)
if err != nil {
log.Debugf("failed to get total of project members of role %d", role)
return
}
*count = total
}(e.role, e.count)
}
wg.Wait()
}
// Returns the highest role in the role list.
// This func should be removed once we deprecate the "current_user_role_id" in project API
// A user can have multiple roles and they may not have a strict ranking relationship
func highestRole(roles []int) int {
if roles == nil {
return 0
}
rolePower := map[int]int{
common.RoleProjectAdmin: 50,
common.RoleMaintainer: 40,
common.RoleDeveloper: 30,
common.RoleGuest: 20,
common.RoleLimitedGuest: 10,
}
var highest, highestPower int
for _, role := range roles {
if p, ok := rolePower[role]; ok && p > highestPower {
highest = role
highestPower = p
}
}
return highest
}

View File

@ -1,535 +0,0 @@
// Copyright 2018 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 (
"fmt"
"net/http"
"strconv"
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/testing/apitests/apilib"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var addProject *apilib.ProjectReq
var addPID int
func addProjectByName(apiTest *testapi, projectName string) (int32, error) {
req := apilib.ProjectReq{ProjectName: projectName}
code, err := apiTest.ProjectsPost(*admin, req)
if err != nil {
return 0, err
}
if code != http.StatusCreated {
return 0, fmt.Errorf("created failed")
}
code, projects, err := apiTest.ProjectsGet(&apilib.ProjectQuery{Name: projectName}, *admin)
if err != nil {
return 0, err
}
if code != http.StatusOK {
return 0, fmt.Errorf("get failed")
}
if len(projects) == 0 {
return 0, fmt.Errorf("oops")
}
return projects[0].ProjectId, nil
}
func deleteProjectByIDs(apiTest *testapi, projectIDs ...int32) error {
for _, projectID := range projectIDs {
_, err := apiTest.ProjectsDelete(*admin, fmt.Sprintf("%d", projectID))
if err != nil {
return err
}
}
return nil
}
func InitAddPro() {
addProject = &apilib.ProjectReq{ProjectName: "add_project", Metadata: map[string]string{models.ProMetaPublic: "true"}}
}
func TestAddProject(t *testing.T) {
fmt.Println("\nTesting Add Project(ProjectsPost) API")
assert := assert.New(t)
apiTest := newHarborAPI()
// prepare for test
InitAddPro()
// case 1: admin not login, expect project creation fail.
result, err := apiTest.ProjectsPost(*unknownUsr, *addProject)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), result, "Case 1: Project creation status should be 401")
// t.Log(result)
}
// case 2: admin successful login, expect project creation success.
fmt.Println("case 2: admin successful login, expect project creation success.")
result, err = apiTest.ProjectsPost(*admin, *addProject)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), result, "Case 2: Project creation status should be 201")
// t.Log(result)
}
// case 3: duplicate project name, create project fail
fmt.Println("case 3: duplicate project name, create project fail")
result, err = apiTest.ProjectsPost(*admin, *addProject)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(409), result, "Case 3: Project creation status should be 409")
// t.Log(result)
}
// case 4: response code = 400 : Project name is illegal in length
fmt.Println("case 4 : response code = 400 : Project name is illegal in length ")
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{ProjectName: "", Metadata: map[string]string{models.ProMetaPublic: "true"}})
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), result, "case 4 : response code = 400 : Project name is illegal in length ")
}
// case 5: response code = 201 : expect project creation with quota success.
fmt.Println("case 5 : response code = 201 : expect project creation with quota success ")
var countLimit, storageLimit int64
countLimit, storageLimit = 100, 10
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{ProjectName: "with_quota", CountLimit: &countLimit, StorageLimit: &storageLimit})
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), result, "case 5 : response code = 201 : expect project creation with quota success ")
}
// case 6: response code = 400 : bad quota value, create project fail
fmt.Println("case 6: response code = 400 : bad quota value, create project fail")
countLimit, storageLimit = 100, -2
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{ProjectName: "with_quota", CountLimit: &countLimit, StorageLimit: &storageLimit})
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), result, "case 6: response code = 400 : bad quota value, create project fail")
}
fmt.Printf("\n")
}
func TestListProjects(t *testing.T) {
fmt.Println("\nTest for Project GET API by project name")
assert := assert.New(t)
apiTest := newHarborAPI()
var result []apilib.Project
cMockServer, oldCtrl, err := mockChartController()
if err != nil {
t.Fatal(err)
}
defer func() {
cMockServer.Close()
chartController = oldCtrl
}()
// ----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: response code:200")
httpStatusCode, result, err := apiTest.ProjectsGet(
&apilib.ProjectQuery{
Name: addProject.ProjectName,
Owner: admin.Name,
Public: true,
})
assert.Nil(err)
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
// find add projectID
addPID = int(result[0].ProjectId)
// -------------------case 3 : check admin project role------------------------//
httpStatusCode, result, err = apiTest.ProjectsGet(
&apilib.ProjectQuery{
Name: addProject.ProjectName,
Owner: admin.Name,
Public: true,
}, *admin)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
assert.Equal(int32(1), result[0].CurrentUserRoleId, "User project role is wrong")
}
// -------------------case 4 : add project member and check his role ------------------------//
CommonAddUser()
member := &models.MemberReq{
Role: 2,
MemberUser: models.User{
Username: TestUserName,
},
}
projectID := strconv.Itoa(addPID)
var memberID int
httpStatusCode, memberID, err = apiTest.AddProjectMember(*admin, projectID, member)
if err != nil {
t.Error("Error whihle add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
}
httpStatusCode, result, err = apiTest.ProjectsGet(
&apilib.ProjectQuery{
Name: addProject.ProjectName,
}, *testUser)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
assert.Equal(int32(2), result[0].CurrentUserRoleId, "User project role is wrong")
}
httpStatusCode, err = apiTest.DeleteProjectMember(*admin, projectID, strconv.Itoa(memberID))
if err != nil {
t.Error("Error whihle add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
CommonDelUser()
}
// Get project by proID
func TestProGetByID(t *testing.T) {
fmt.Println("\nTest for Project GET API by project id")
assert := assert.New(t)
apiTest := newHarborAPI()
var result apilib.Project
projectID := strconv.Itoa(addPID)
cMockServer, oldCtrl, err := mockChartController()
if err != nil {
t.Fatal(err)
}
defer func() {
cMockServer.Close()
chartController = oldCtrl
}()
// ----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: response code:200")
httpStatusCode, result, err := apiTest.ProjectsGetByPID(projectID)
if err != nil {
t.Error("Error while search project by proID", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result.ProjectName, "ProjectName is wrong")
assert.Equal("true", result.Metadata[models.ProMetaPublic], "Public is wrong")
}
fmt.Printf("\n")
}
func TestDeleteProject(t *testing.T) {
fmt.Println("\nTesting Delete Project(ProjectsPost) API")
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := strconv.Itoa(addPID)
// --------------------------case 1: Response Code=401,User need to log in first.-----------------------//
fmt.Println("case 1: Response Code=401,User need to log in first.")
httpStatusCode, err := apiTest.ProjectsDelete(*unknownUsr, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "Case 1: Project deletion status should be 401")
}
// --------------------------case 2: Response Code=200---------------------------------//
fmt.Println("case2: response code:200")
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "Case 2: Project deletion status should be 200")
}
// --------------------------case 3: Response Code=404,Project does not exist---------------------------------//
fmt.Println("case 3: Response Code=404,Project does not exist")
projectID = "11"
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "Case 3: Project deletion status should be 404")
}
// --------------------------case 4: Response Code=400,Invalid project id.---------------------------------//
fmt.Println("case 4: Response Code=400,Invalid project id.")
projectID = "cc"
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "Case 4: Project deletion status should be 400")
}
fmt.Printf("\n")
}
func TestProHead(t *testing.T) {
t.Log("\nTest for Project HEAD API")
assert := assert.New(t)
apiTest := newHarborAPI()
// ----------------------------case 1 : Response Code=200----------------------------//
t.Log("case 1: response code:200")
httpStatusCode, err := apiTest.ProjectsHead(*admin, "library")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
// ----------------------------case 2 : Response Code=404:Project name does not exist.----------------------------//
t.Log("case 2: response code:404,Project name does not exist.")
httpStatusCode, err = apiTest.ProjectsHead(*admin, "libra")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
t.Log("case 3: response code:401. Project exist with unauthenticated user")
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "library")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 404")
}
t.Log("case 4: response code:401. Project name does not exist with unauthenticated user")
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "libra")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 404")
}
fmt.Printf("\n")
}
func TestPut(t *testing.T) {
fmt.Println("\nTest for Project PUT API: Update properties for a selected project")
assert := assert.New(t)
apiTest := newHarborAPI()
project := &models.Project{
Metadata: map[string]string{
models.ProMetaPublic: "true",
},
}
fmt.Println("case 1: response code:200")
code, err := apiTest.ProjectsPut(*admin, "1", project)
require.Nil(t, err)
assert.Equal(int(200), code)
fmt.Println("case 2: response code:401, User need to log in first.")
code, err = apiTest.ProjectsPut(*unknownUsr, "1", project)
require.Nil(t, err)
assert.Equal(int(401), code)
fmt.Println("case 3: response code:400, Invalid project id")
code, err = apiTest.ProjectsPut(*admin, "cc", project)
require.Nil(t, err)
assert.Equal(int(400), code)
fmt.Println("case 4: response code:404, Not found the project")
code, err = apiTest.ProjectsPut(*admin, "1234", project)
require.Nil(t, err)
assert.Equal(int(404), code)
fmt.Printf("\n")
}
func TestDeletable(t *testing.T) {
apiTest := newHarborAPI()
chServer, oldController, err := mockChartController()
require.Nil(t, err)
require.NotNil(t, chServer)
defer chServer.Close()
defer func() {
chartController = oldController
}()
project := models.Project{
Name: "project_for_test_deletable",
OwnerID: 1,
}
id, err := dao.AddProject(project)
require.Nil(t, err)
// non-exist project
code, del, err := apiTest.ProjectDeletable(*admin, 1000)
assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, code)
// unauthorized
code, del, err = apiTest.ProjectDeletable(*unknownUsr, id)
assert.Nil(t, err)
assert.Equal(t, http.StatusUnauthorized, code)
// can be deleted
code, del, err = apiTest.ProjectDeletable(*admin, id)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, code)
assert.True(t, del)
err = dao.AddRepository(models.RepoRecord{
Name: project.Name + "/golang",
ProjectID: id,
})
require.Nil(t, err)
// can not be deleted as contains repository
code, del, err = apiTest.ProjectDeletable(*admin, id)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, code)
assert.False(t, del)
}
func TestProjectSummary(t *testing.T) {
fmt.Println("\nTest for Project Summary API")
assert := assert.New(t)
apiTest := newHarborAPI()
projectID, err := addProjectByName(apiTest, "project-summary")
assert.Nil(err)
defer func() {
deleteProjectByIDs(apiTest, projectID)
}()
// ----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: response code:200")
httpStatusCode, summary, err := apiTest.ProjectSummary(*admin, fmt.Sprintf("%d", projectID))
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(int64(1), summary.ProjectAdminCount)
assert.Equal(map[string]int64{"storage": -1}, summary.Quota.Hard)
}
fmt.Printf("\n")
}
func TestHighestRole(t *testing.T) {
cases := []struct {
input []int
expect int
}{
{
[]int{},
0,
},
{
[]int{
common.RoleDeveloper,
common.RoleMaintainer,
common.RoleLimitedGuest,
},
common.RoleMaintainer,
},
{
[]int{
common.RoleProjectAdmin,
common.RoleMaintainer,
common.RoleMaintainer,
},
common.RoleProjectAdmin,
},
{
[]int{
99,
33,
common.RoleLimitedGuest,
},
common.RoleLimitedGuest,
},
{
[]int{
99,
99,
99,
},
0,
},
{
nil,
0,
},
}
for _, c := range cases {
assert.Equal(t, c.expect, highestRole(c.input))
}
}

View File

@ -20,6 +20,7 @@ import (
"helm.sh/helm/v3/cmd/helm/search"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
pro "github.com/goharbor/harbor/src/common/dao/project"
"github.com/goharbor/harbor/src/common/models"
@ -205,3 +206,27 @@ func filterRepositories(projects []*models.Project, keyword string) (
}
return result, nil
}
// Returns the highest role in the role list.
// This func should be removed once we deprecate the "current_user_role_id" in project API
// A user can have multiple roles and they may not have a strict ranking relationship
func highestRole(roles []int) int {
if roles == nil {
return 0
}
rolePower := map[int]int{
common.RoleProjectAdmin: 50,
common.RoleMaintainer: 40,
common.RoleDeveloper: 30,
common.RoleGuest: 20,
common.RoleLimitedGuest: 10,
}
var highest, highestPower int
for _, role := range roles {
if p, ok := rolePower[role]; ok && p > highestPower {
highest = role
highestPower = p
}
}
return highest
}

View File

@ -18,20 +18,18 @@ import (
"os"
"time"
"github.com/goharbor/harbor/src/lib/errors"
redislib "github.com/goharbor/harbor/src/lib/redis"
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
blob_models "github.com/goharbor/harbor/src/pkg/blob/models"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/registryctl"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
redislib "github.com/goharbor/harbor/src/lib/redis"
"github.com/goharbor/harbor/src/pkg/artifactrash"
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
"github.com/goharbor/harbor/src/pkg/blob"
blob_models "github.com/goharbor/harbor/src/pkg/blob/models"
"github.com/goharbor/harbor/src/registryctl/client"
)
@ -52,7 +50,6 @@ type GarbageCollector struct {
artCtl artifact.Controller
artrashMgr artifactrash.Manager
blobMgr blob.Manager
projectCtl project.Controller
registryCtlClient client.Client
logger logger.Interface
redisURL string
@ -103,7 +100,6 @@ func (gc *GarbageCollector) init(ctx job.Context, params job.Parameters) error {
gc.artCtl = artifact.Ctl
gc.artrashMgr = artifactrash.NewManager()
gc.blobMgr = blob.NewManager()
gc.projectCtl = project.Ctl
}
if err := gc.registryCtlClient.Health(); err != nil {
gc.logger.Errorf("failed to start gc as registry controller is unreachable: %v", err)
@ -416,50 +412,21 @@ func (gc *GarbageCollector) deletedArt(ctx job.Context) (map[string][]model.Arti
// clean the untagged blobs in each project, these blobs are not referenced by any manifest and will be cleaned by GC
func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
// get all projects
projects := func(chunkSize int) <-chan *models.Project {
ch := make(chan *models.Project, chunkSize)
go func() {
defer close(ch)
params := &models.ProjectQueryParam{
Pagination: &models.Pagination{Page: 1, Size: int64(chunkSize)},
for result := range project.ListAll(ctx.SystemContext(), 50, nil, project.Metadata(false)) {
if result.Error != nil {
gc.logger.Errorf("remove untagged blobs for all projects got error: %v", result.Error)
continue
}
for {
results, err := gc.projectCtl.List(ctx.SystemContext(), params, project.Metadata(false))
if err != nil {
gc.logger.Errorf("list projects failed, error: %v", err)
return
}
for _, p := range results {
ch <- p
}
if len(results) < chunkSize {
break
}
params.Pagination.Page++
}
}()
return ch
}(50)
for project := range projects {
p := result.Data
all, err := gc.blobMgr.List(ctx.SystemContext(), blob.ListParams{
ProjectID: project.ProjectID,
ProjectID: p.ProjectID,
UpdateTime: time.Now().Add(-time.Duration(gc.timeWindowHours) * time.Hour),
})
if err != nil {
gc.logger.Errorf("failed to get blobs of project, %v", err)
continue
}
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), project.ProjectID, all); err != nil {
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), p.ProjectID, all); err != nil {
gc.logger.Errorf("failed to clean untagged blobs of project, %v", err)
continue
}

View File

@ -1,9 +1,27 @@
// 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 gc
import (
"os"
"testing"
"github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/common/models"
commom_regctl "github.com/goharbor/harbor/src/common/registryctl"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
@ -17,8 +35,6 @@ import (
"github.com/goharbor/harbor/src/testing/pkg/blob"
"github.com/goharbor/harbor/src/testing/registryctl"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
type gcTestSuite struct {
@ -29,6 +45,8 @@ type gcTestSuite struct {
projectCtl *projecttesting.Controller
blobMgr *blob.Manager
originalProjectCtl project.Controller
regCtlInit func()
}
@ -39,9 +57,16 @@ func (suite *gcTestSuite) SetupTest() {
suite.blobMgr = &blob.Manager{}
suite.projectCtl = &projecttesting.Controller{}
suite.originalProjectCtl = project.Ctl
project.Ctl = suite.projectCtl
regCtlInit = func() { commom_regctl.RegistryCtlClient = suite.registryCtlClient }
}
func (suite *gcTestSuite) TearDownTest() {
project.Ctl = suite.originalProjectCtl
}
func (suite *gcTestSuite) TestMaxFails() {
gc := &GarbageCollector{}
suite.Equal(uint(1), gc.MaxFails())
@ -110,7 +135,6 @@ func (suite *gcTestSuite) TestRemoveUntaggedBlobs() {
mock.OnAnything(suite.blobMgr, "CleanupAssociationsForProject").Return(nil)
gc := &GarbageCollector{
projectCtl: suite.projectCtl,
blobMgr: suite.blobMgr,
}
@ -244,7 +268,6 @@ func (suite *gcTestSuite) TestRun() {
gc := &GarbageCollector{
artCtl: suite.artifactCtl,
artrashMgr: suite.artrashMgr,
projectCtl: suite.projectCtl,
blobMgr: suite.blobMgr,
registryCtlClient: suite.registryCtlClient,
}
@ -318,7 +341,6 @@ func (suite *gcTestSuite) TestMark() {
gc := &GarbageCollector{
artCtl: suite.artifactCtl,
artrashMgr: suite.artrashMgr,
projectCtl: suite.projectCtl,
blobMgr: suite.blobMgr,
}
@ -336,7 +358,6 @@ func (suite *gcTestSuite) TestSweep() {
gc := &GarbageCollector{
artCtl: suite.artifactCtl,
artrashMgr: suite.artrashMgr,
projectCtl: suite.projectCtl,
blobMgr: suite.blobMgr,
registryCtlClient: suite.registryCtlClient,
deleteSet: []*pkg_blob.Blob{

View File

@ -16,22 +16,20 @@ package gcreadonly
import (
"fmt"
redislib "github.com/goharbor/harbor/src/lib/redis"
"os"
"time"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/artifactrash"
"github.com/goharbor/harbor/src/pkg/blob"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/registryctl"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/q"
redislib "github.com/goharbor/harbor/src/lib/redis"
"github.com/goharbor/harbor/src/pkg/artifactrash"
"github.com/goharbor/harbor/src/pkg/blob"
"github.com/goharbor/harbor/src/registryctl/client"
)
@ -160,7 +158,6 @@ func (gc *GarbageCollector) init(ctx job.Context, params job.Parameters) error {
gc.artCtl = artifact.Ctl
gc.artrashMgr = artifactrash.NewManager()
gc.blobMgr = blob.NewManager()
gc.projectCtl = project.Ctl
}
if err := gc.registryCtlClient.Health(); err != nil {
gc.logger.Errorf("failed to start gc as registry controller is unreachable: %v", err)
@ -285,49 +282,20 @@ func (gc *GarbageCollector) deleteCandidates(ctx job.Context) error {
// clean the untagged blobs in each project, these blobs are not referenced by any manifest and will be cleaned by GC
func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
// get all projects
projects := func(chunkSize int) <-chan *models.Project {
ch := make(chan *models.Project, chunkSize)
go func() {
defer close(ch)
params := &models.ProjectQueryParam{
Pagination: &models.Pagination{Page: 1, Size: int64(chunkSize)},
for result := range project.ListAll(ctx.SystemContext(), 50, nil, project.Metadata(false)) {
if result.Error != nil {
gc.logger.Errorf("remove untagged blobs for all projects got error: %v", result.Error)
continue
}
for {
results, err := gc.projectCtl.List(ctx.SystemContext(), params, project.Metadata(false))
if err != nil {
gc.logger.Errorf("list projects failed, error: %v", err)
return
}
for _, p := range results {
ch <- p
}
if len(results) < chunkSize {
break
}
params.Pagination.Page++
}
}()
return ch
}(50)
for project := range projects {
p := result.Data
all, err := gc.blobMgr.List(ctx.SystemContext(), blob.ListParams{
ProjectID: project.ProjectID,
ProjectID: p.ProjectID,
})
if err != nil {
gc.logger.Errorf("failed to get blobs of project, %v", err)
continue
}
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), project.ProjectID, all); err != nil {
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), p.ProjectID, all); err != nil {
gc.logger.Errorf("failed to clean untagged blobs of project, %v", err)
continue
}

View File

@ -1,9 +1,27 @@
// 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 gcreadonly
import (
"os"
"testing"
"github.com/goharbor/harbor/src/common/config"
"github.com/goharbor/harbor/src/common/models"
commom_regctl "github.com/goharbor/harbor/src/common/registryctl"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
@ -16,8 +34,6 @@ import (
"github.com/goharbor/harbor/src/testing/pkg/blob"
"github.com/goharbor/harbor/src/testing/registryctl"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
type gcTestSuite struct {
@ -28,6 +44,8 @@ type gcTestSuite struct {
projectCtl *projecttesting.Controller
blobMgr *blob.Manager
originalProjectCtl project.Controller
regCtlInit func()
setReadOnly func(cfgMgr *config.CfgManager, switcher bool) error
getReadOnly func(cfgMgr *config.CfgManager) (bool, error)
@ -40,11 +58,18 @@ func (suite *gcTestSuite) SetupTest() {
suite.blobMgr = &blob.Manager{}
suite.projectCtl = &projecttesting.Controller{}
suite.originalProjectCtl = project.Ctl
project.Ctl = suite.projectCtl
regCtlInit = func() { commom_regctl.RegistryCtlClient = suite.registryCtlClient }
setReadOnly = func(cfgMgr *config.CfgManager, switcher bool) error { return nil }
getReadOnly = func(cfgMgr *config.CfgManager) (bool, error) { return true, nil }
}
func (suite *gcTestSuite) TearDownTest() {
project.Ctl = suite.originalProjectCtl
}
func (suite *gcTestSuite) TestMaxFails() {
gc := &GarbageCollector{}
suite.Equal(uint(1), gc.MaxFails())
@ -105,7 +130,6 @@ func (suite *gcTestSuite) TestRemoveUntaggedBlobs() {
mock.OnAnything(suite.blobMgr, "CleanupAssociationsForProject").Return(nil)
gc := &GarbageCollector{
projectCtl: suite.projectCtl,
blobMgr: suite.blobMgr,
}
@ -218,7 +242,6 @@ func (suite *gcTestSuite) TestRun() {
artCtl: suite.artifactCtl,
artrashMgr: suite.artrashMgr,
cfgMgr: config.NewInMemoryManager(),
projectCtl: suite.projectCtl,
blobMgr: suite.blobMgr,
registryCtlClient: suite.registryCtlClient,
}

62
src/lib/convert_types.go Normal file
View File

@ -0,0 +1,62 @@
// 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 lib
import (
"strconv"
)
// BoolValue returns the value of the bool pointer or false if the pointer is nil
func BoolValue(v *bool) bool {
if v != nil {
return *v
}
return false
}
// Int64Value returns the value of the int64 pointer or 0 if the pointer is nil
func Int64Value(v *int64) int64 {
if v != nil {
return *v
}
return 0
}
// StringValue returns the value of the string pointer or "" if the pointer is nil
func StringValue(v *string) string {
if v != nil {
return *v
}
return ""
}
// ToBool convert interface to bool
func ToBool(v interface{}) bool {
switch b := v.(type) {
case bool:
return b
case nil:
return false
case int:
return v.(int) != 0
case int64:
return v.(int64) != 0
case string:
r, _ := strconv.ParseBool(v.(string))
return r
default:
return false
}
}

View File

@ -0,0 +1,51 @@
// 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 lib
import (
"testing"
)
func TestToBool(t *testing.T) {
type args struct {
v interface{}
}
tests := []struct {
name string
args args
want bool
}{
{"nil", args{nil}, false},
{"bool true", args{true}, true},
{"bool false", args{false}, false},
{"string true", args{"true"}, true},
{"string True", args{"True"}, true},
{"string 1", args{"1"}, true},
{"string false", args{"false"}, false},
{"string False", args{"False"}, false},
{"string 0", args{"0"}, false},
{"int 1", args{1}, true},
{"int 0", args{0}, false},
{"int64 1", args{int64(1)}, true},
{"int64 0", args{int64(0)}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ToBool(tt.args.v); got != tt.want {
t.Errorf("ToBool() = %v, want %v", got, tt.want)
}
})
}
}

31
src/lib/json_copy.go Normal file
View File

@ -0,0 +1,31 @@
// 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 lib
import (
"encoding/json"
)
// JSONCopy copy from src to dst with json marshal and unmarshal.
// NOTE: copy from one struct to another struct may miss some values depend on
// the json tag in the fields, you should know what will happened when call this function.
func JSONCopy(dst, src interface{}) error {
data, err := json.Marshal(src)
if err != nil {
return err
}
return json.Unmarshal(data, dst)
}

68
src/lib/json_copy_test.go Normal file
View File

@ -0,0 +1,68 @@
// 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 lib
import (
"testing"
"github.com/stretchr/testify/assert"
)
type jsonCopyFoo struct {
Name string `json:"name"`
Age int `json:"age"`
}
func TestJSONCopy(t *testing.T) {
assert := assert.New(t)
{
var m map[string]interface{}
foo := &jsonCopyFoo{
Name: "foo",
Age: 1,
}
assert.Nil(m)
assert.Nil(JSONCopy(&m, foo))
assert.NotNil(m)
assert.Len(m, 2)
}
{
var m map[string]interface{}
var foo *jsonCopyFoo
assert.Nil(m)
assert.Nil(JSONCopy(&m, foo))
assert.Nil(m)
}
{
m := map[string]interface{}{
"name": "foo",
"age": 1,
}
var foo *jsonCopyFoo
assert.Nil(JSONCopy(&foo, &m))
assert.NotNil(foo)
assert.Equal("foo", foo.Name)
assert.Equal(1, foo.Age)
}
{
assert.Error(JSONCopy(nil, JSONCopy))
}
}

View File

@ -51,6 +51,11 @@ func Context() context.Context {
return NewContext(context.Background(), orm.NewOrm())
}
// Clone returns new context with orm for ctx
func Clone(ctx context.Context) context.Context {
return NewContext(ctx, orm.NewOrm())
}
// WithTransaction a decorator which make f run in transaction
func WithTransaction(f func(ctx context.Context) error) func(ctx context.Context) error {
return func(ctx context.Context) error {

View File

@ -27,7 +27,7 @@ import (
func abstractArtData(ctx context.Context) error {
abstractor := art.NewAbstractor()
pros, err := project.Mgr.List(ctx)
pros, err := project.Mgr.List(ctx, nil)
if err != nil {
return err
}

View File

@ -94,6 +94,7 @@ func (d *dao) Create(ctx context.Context, project *models.Project) (int64, error
func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) {
query = q.MustClone(query)
query.Keywords["deleted"] = false
query.Sorting = ""
query.PageNumber = 0
query.PageSize = 0
@ -101,6 +102,7 @@ func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error
if err != nil {
return 0, err
}
return qs.Count()
}
@ -160,6 +162,10 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.Project, erro
return nil, err
}
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
projects := []*models.Project{}
if _, err := qs.All(&projects); err != nil {
return nil, err

View File

@ -196,10 +196,12 @@ func (suite *DaoTestSuite) TestList() {
}
}()
{
projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"project_id__in": projectIDs}))
suite.Nil(err)
suite.Len(projects, len(projectNames))
}
}
func (suite *DaoTestSuite) TestListByPublic() {
{
@ -259,6 +261,13 @@ func (suite *DaoTestSuite) TestListByMember() {
suite.Len(projects, 0)
}
{
// guest with public projects
projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"member": &models.MemberQuery{Name: "admin", Role: common.RoleGuest, WithPublic: true}}))
suite.Nil(err)
suite.Len(projects, 1)
}
{
suite.WithUser(func(userID int64, username string) {
project := &models.Project{

View File

@ -16,7 +16,9 @@ package project
import (
"context"
"regexp"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/project/dao"
@ -43,7 +45,7 @@ type Manager interface {
Get(ctx context.Context, idOrName interface{}) (*models.Project, error)
// List projects according to the query
List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error)
List(ctx context.Context, query *q.Query) ([]*models.Project, error)
}
// New returns a default implementation of Manager
@ -51,6 +53,14 @@ func New() Manager {
return &manager{dao: dao.New()}
}
const projectNameMaxLen int = 255
const projectNameMinLen int = 1
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
var (
validProjectName = regexp.MustCompile(`^` + restrictedNameChars + `$`)
)
type manager struct {
dao dao.DAO
}
@ -60,6 +70,17 @@ func (m *manager) Create(ctx context.Context, project *models.Project) (int64, e
if project.OwnerID <= 0 {
return 0, errors.BadRequestError(nil).WithMessage("Owner is missing when creating project %s", project.Name)
}
if utils.IsIllegalLength(project.Name, projectNameMinLen, projectNameMaxLen) {
format := "Project name %s is illegal in length. (greater than %d or less than %d)"
return 0, errors.BadRequestError(nil).WithMessage(format, project.Name, projectNameMaxLen, projectNameMinLen)
}
legal := validProjectName.MatchString(project.Name)
if !legal {
return 0, errors.BadRequestError(nil).WithMessage("project name is not in lower case or contains illegal characters")
}
return m.dao.Create(ctx, project)
}
@ -87,14 +108,6 @@ func (m *manager) Get(ctx context.Context, idOrName interface{}) (*models.Projec
}
// List projects according to the query
func (m *manager) List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) {
var param *models.ProjectQueryParam
if len(query) > 0 {
param = query[0]
}
if param == nil {
return m.dao.List(ctx, nil)
}
return m.dao.List(ctx, param.ToQuery())
func (m *manager) List(ctx context.Context, query *q.Query) ([]*models.Project, error) {
return m.dao.List(ctx, query)
}

View File

@ -36,8 +36,5 @@ func (projects Projects) OwnerIDs() []int {
// Member ...
type Member = models.Member
// ProjectQueryParam ...
type ProjectQueryParam = models.ProjectQueryParam
// MemberQuery ...
type MemberQuery = models.MemberQuery

View File

@ -326,7 +326,7 @@ func launcherError(err error) error {
}
func getProjects(projectMgr project.Manager) ([]*selector.Candidate, error) {
projects, err := projectMgr.List(orm.Context())
projects, err := projectMgr.List(orm.Context(), nil)
if err != nil {
return nil, err
}

View File

@ -16,7 +16,6 @@ package dao
import (
"context"
"strings"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
@ -39,34 +38,19 @@ type dao struct{}
// List list users
func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
query = q.MustClone(query)
if query.Sorting == "" {
query.Sorting = "username"
}
excludeAdmin := true
for key := range query.Keywords {
str := strings.ToLower(key)
if str == "user_id__in" {
excludeAdmin = false
break
} else if str == "user_id" {
excludeAdmin = false
break
}
}
if excludeAdmin {
// Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527
query.Keywords["user_id__gt"] = 1
}
query.Keywords["deleted"] = false
qs, err := orm.QuerySetter(ctx, &models.User{}, query)
if err != nil {
return nil, err
}
users := []*models.User{}
if _, err := qs.OrderBy(query.Sorting).All(&users); err != nil {
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
var users []*models.User
if _, err := qs.All(&users); err != nil {
return nil, err
}

View File

@ -39,6 +39,12 @@ func (suite *DaoTestSuite) TestList() {
suite.Nil(err)
suite.Len(users, 1)
}
{
users, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"username": "admin"}))
suite.Nil(err)
suite.Len(users, 1)
}
}
func TestDaoTestSuite(t *testing.T) {

View File

@ -16,7 +16,9 @@ package user
import (
"context"
"strings"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/user/dao"
"github.com/goharbor/harbor/src/pkg/user/models"
@ -29,6 +31,10 @@ var (
// Manager is used for user management
type Manager interface {
// Get get user by user id
Get(ctx context.Context, id int) (*models.User, error)
// Get get user by username
GetByName(ctx context.Context, username string) (*models.User, error)
// List users according to the query
List(ctx context.Context, query *q.Query) (models.Users, error)
}
@ -42,7 +48,57 @@ type manager struct {
dao dao.DAO
}
// Get get user by user id
func (m *manager) Get(ctx context.Context, id int) (*models.User, error) {
users, err := m.dao.List(ctx, q.New(q.KeyWords{"user_id": id}))
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, errors.NotFoundError(nil).WithMessage("user %d not found", id)
}
return users[0], nil
}
// Get get user by username
func (m *manager) GetByName(ctx context.Context, username string) (*models.User, error) {
users, err := m.dao.List(ctx, q.New(q.KeyWords{"username": username}))
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, errors.NotFoundError(nil).WithMessage("user %s not found", username)
}
return users[0], nil
}
// List users according to the query
func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
query = q.MustClone(query)
if query.Sorting == "" {
query.Sorting = "username"
}
excludeAdmin := true
for key := range query.Keywords {
str := strings.ToLower(key)
if str == "user_id__in" {
excludeAdmin = false
break
} else if str == "user_id" {
excludeAdmin = false
break
}
}
if excludeAdmin {
// Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527
query.Keywords["user_id__gt"] = 1
}
return m.dao.List(ctx, query)
}

View File

@ -59,7 +59,7 @@ func Middleware() func(http.Handler) http.Handler {
return err
}
proj, err := projectController.Get(ctx, art.ProjectID, project.CVEAllowlist(true))
proj, err := projectController.Get(ctx, art.ProjectID, project.WithEffectCVEAllowlist())
if err != nil {
logger.Errorf("get the project %d failed, error: %v", art.ProjectID, err)
return err

View File

@ -18,14 +18,15 @@ package handler
import (
"context"
"net/http"
"net/url"
"strconv"
"github.com/go-openapi/runtime"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
lib_http "github.com/goharbor/harbor/src/lib/http"
"github.com/goharbor/harbor/src/lib/q"
"net/http"
"net/url"
"strconv"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/common/rbac"
@ -115,6 +116,18 @@ func (b *BaseAPI) RequireSysAdmin(ctx context.Context) error {
return nil
}
// RequireAuthenticated checks it's authenticated according to the security context
func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error {
secCtx, ok := security.FromContext(ctx)
if !ok {
return errors.UnauthorizedError(errors.New("security context not found"))
}
if !secCtx.IsAuthenticated() {
return errors.UnauthorizedError(nil)
}
return nil
}
// BuildQuery builds the query model according to the query string
func (b *BaseAPI) BuildQuery(ctx context.Context, query *string, pageNumber, pageSize *int64) (*q.Query, error) {
var (

View File

@ -0,0 +1,79 @@
// 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 model
import (
"strings"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// Project model
type Project struct {
*project.Project
}
// ToSwagger converts the project to the swagger model
func (p *Project) ToSwagger() *models.Project {
var currentUserRoleIds []int32
for _, role := range p.RoleList {
currentUserRoleIds = append(currentUserRoleIds, int32(role))
}
var md *models.ProjectMetadata
if p.Metadata != nil {
var m models.ProjectMetadata
lib.JSONCopy(&m, p.Metadata)
// Transform the severity to severity of CVSS v3.0 Ratings
if m.Severity != nil {
severity := strings.ToLower(vuln.ParseSeverityVersion3(*m.Severity).String())
m.Severity = &severity
}
md = &m
}
var allowlist models.CVEAllowlist
if err := lib.JSONCopy(&allowlist, p.CVEAllowlist); err != nil {
log.Warningf("failed to copy CVEAllowlist form %T", p.CVEAllowlist)
}
return &models.Project{
ChartCount: int64(p.ChartCount),
CreationTime: strfmt.DateTime(p.CreationTime),
CurrentUserRoleID: int64(p.Role),
CurrentUserRoleIds: currentUserRoleIds,
CVEAllowlist: &allowlist,
Metadata: md,
Name: p.Name,
OwnerID: int32(p.OwnerID),
OwnerName: p.OwnerName,
ProjectID: int32(p.ProjectID),
RegistryID: p.RegistryID,
RepoCount: p.RepoCount,
UpdateTime: strfmt.DateTime(p.UpdateTime),
}
}
// NewProject ...
func NewProject(p *project.Project) *Project {
return &Project{p}
}

View File

@ -2,33 +2,216 @@ package handler
import (
"context"
"fmt"
"strconv"
"strings"
"sync"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common"
pro "github.com/goharbor/harbor/src/common/dao/project"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/controller/repository"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/audit"
"github.com/goharbor/harbor/src/pkg/project/metadata"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/user"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project"
)
// for the proxy cache type project, we will create a 7 days retention policy for it by default
const defaultDaysToRetentionForProxyCacheProject = 7
func newProjectAPI() *projectAPI {
return &projectAPI{
auditMgr: audit.Mgr,
proCtl: project.Ctl,
metadataMgr: metadata.Mgr,
userMgr: user.Mgr,
repositoryCtl: repository.Ctl,
projectCtl: project.Ctl,
quotaCtl: quota.Ctl,
}
}
type projectAPI struct {
BaseAPI
auditMgr audit.Manager
proCtl project.Controller
metadataMgr metadata.Manager
userMgr user.Manager
repositoryCtl repository.Controller
projectCtl project.Controller
quotaCtl quota.Controller
}
func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateProjectParams) middleware.Responder {
if err := a.RequireAuthenticated(ctx); err != nil {
return a.SendError(ctx, err)
}
onlyAdmin, err := config.OnlyAdminCreateProject()
if err != nil {
return a.SendError(ctx, fmt.Errorf("failed to determine whether only admin can create projects: %v", err))
}
secCtx, _ := security.FromContext(ctx)
if onlyAdmin && !(secCtx.IsSysAdmin() || secCtx.IsSolutionUser()) {
log.Errorf("Only sys admin can create project")
return a.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Only system admin can create project"))
}
req := params.Project
if req.RegistryID != nil && !secCtx.IsSysAdmin() {
// only system admin can create the proxy cache project
return a.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Only system admin can create proxy cache project"))
}
// populate storage limit
if config.QuotaPerProjectEnable() {
// the security context is not sys admin, set the StorageLimit the global StoragePerProject
if req.StorageLimit == nil || *req.StorageLimit == 0 || !secCtx.IsSysAdmin() {
setting, err := config.QuotaSetting()
if err != nil {
log.Errorf("failed to get quota setting: %v", err)
return a.SendError(ctx, fmt.Errorf("failed to get quota setting: %v", err))
}
defaultStorageLimit := setting.StoragePerProject
req.StorageLimit = &defaultStorageLimit
}
} else {
// ignore storage limit when quota per project disabled
req.StorageLimit = nil
}
if req.Metadata == nil {
req.Metadata = &models.ProjectMetadata{}
}
// accept the "public" property to make replication work well with old versions(<=1.2.0)
if req.Public != nil && req.Metadata.Public == "" {
req.Metadata.Public = strconv.FormatBool(*req.Public)
}
// populate public metadata as false if it isn't set
if req.Metadata.Public == "" {
req.Metadata.Public = strconv.FormatBool(false)
}
// validate the RegistryID and StorageLimit in the body of the request
if err := a.validateProjectReq(ctx, req); err != nil {
return a.SendError(ctx, err)
}
var ownerID int
// set the owner as the system admin when the API being called by replication
// it's a solution to workaround the restriction of project creation API:
// only normal users can create projects
if secCtx.IsSolutionUser() {
ownerID = 1
} else {
ownerName := secCtx.GetUsername()
user, err := a.userMgr.GetByName(ctx, ownerName)
if err != nil {
return a.SendError(ctx, err)
}
ownerID = user.UserID
}
p := &project.Project{
Name: req.ProjectName,
OwnerID: ownerID,
RegistryID: lib.Int64Value(req.RegistryID),
}
lib.JSONCopy(&p.Metadata, req.Metadata)
projectID, err := a.projectCtl.Create(ctx, p)
if err != nil {
return a.SendError(ctx, err)
}
// StorageLimit is provided in the request body and it's valid,
// create the quota for the project
if req.StorageLimit != nil {
referenceID := quota.ReferenceID(projectID)
hardLimits := types.ResourceList{types.ResourceStorage: *req.StorageLimit}
if _, err := a.quotaCtl.Create(ctx, quota.ProjectReference, referenceID, hardLimits); err != nil {
return a.SendError(ctx, fmt.Errorf("failed to create quota for project: %v", err))
}
}
// RegistryID is provided in the request body and it's valid,
// create a default retention policy for proxy project
if req.RegistryID != nil {
plc := policy.WithNDaysSinceLastPull(projectID, defaultDaysToRetentionForProxyCacheProject)
// TODO: move the retention controller to `src/controller/retention` and
// change to use the default retention controller in `src/controller/retention`
retentionID, err := api.GetRetentionController().CreateRetention(plc)
if err != nil {
return a.SendError(ctx, err)
}
md := map[string]string{"retention_id": strconv.FormatInt(retentionID, 10)}
if err := a.metadataMgr.Add(ctx, projectID, md); err != nil {
return a.SendError(ctx, err)
}
return nil
}
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), projectID)
return operation.NewCreateProjectCreated().WithLocation(location)
}
func (a *projectAPI) DeleteProject(ctx context.Context, params operation.DeleteProjectParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionDelete); err != nil {
return a.SendError(ctx, err)
}
result, err := a.deletable(ctx, params.ProjectID)
if err != nil {
return a.SendError(ctx, err)
}
if !result.Deletable {
return a.SendError(ctx, errors.PreconditionFailedError(errors.New(result.Message)))
}
if err := a.projectCtl.Delete(ctx, params.ProjectID); err != nil {
return a.SendError(ctx, err)
}
referenceID := quota.ReferenceID(params.ProjectID)
q, err := a.quotaCtl.GetByRef(ctx, quota.ProjectReference, referenceID)
if err != nil {
log.Warningf("failed to get quota for project %d, error: %v", params.ProjectID, err)
} else {
if err := a.quotaCtl.Delete(ctx, q.ID); err != nil {
return a.SendError(ctx, fmt.Errorf("failed to delete quota for project: %v", err))
}
}
return operation.NewDeleteProjectOK()
}
func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceLog); err != nil {
return a.SendError(ctx, err)
}
pro, err := a.proCtl.GetByName(ctx, params.ProjectName)
pro, err := a.projectCtl.GetByName(ctx, params.ProjectName)
if err != nil {
return a.SendError(ctx, err)
}
@ -63,3 +246,393 @@ func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(auditLogs)
}
func (a *projectAPI) GetProject(ctx context.Context, params operation.GetProjectParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionRead); err != nil {
return a.SendError(ctx, err)
}
p, err := a.getProject(ctx, params.ProjectID, project.WithCVEAllowlist(), project.WithOwner())
if err != nil {
return a.SendError(ctx, err)
}
return operation.NewGetProjectOK().WithPayload(model.NewProject(p).ToSwagger())
}
func (a *projectAPI) GetProjectDeletable(ctx context.Context, params operation.GetProjectDeletableParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionDelete); err != nil {
return a.SendError(ctx, err)
}
result, err := a.deletable(ctx, params.ProjectID)
if err != nil {
return a.SendError(ctx, err)
}
return operation.NewGetProjectDeletableOK().WithPayload(result)
}
func (a *projectAPI) GetProjectSummary(ctx context.Context, params operation.GetProjectSummaryParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionRead); err != nil {
return a.SendError(ctx, err)
}
p, err := a.getProject(ctx, params.ProjectID)
if err != nil {
return a.SendError(ctx, err)
}
summary := &models.ProjectSummary{
ChartCount: int64(p.ChartCount),
RepoCount: p.RepoCount,
}
var fetchSummaries []func(context.Context, *project.Project, *models.ProjectSummary)
if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionRead, rbac.ResourceQuota); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectQuotaSummary)
}
if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectMemberSummary)
}
if p.RegistryID > 0 {
fetchSummaries = append(fetchSummaries, getProjectRegistrySummary)
}
var wg sync.WaitGroup
for _, fn := range fetchSummaries {
fn := fn
wg.Add(1)
go func() {
defer wg.Done()
fn(ctx, p, summary)
}()
}
wg.Wait()
return operation.NewGetProjectSummaryOK().WithPayload(summary)
}
func (a *projectAPI) HeadProject(ctx context.Context, params operation.HeadProjectParams) middleware.Responder {
if err := a.RequireAuthenticated(ctx); err != nil {
return a.SendError(ctx, err)
}
if _, err := a.projectCtl.GetByName(ctx, params.ProjectName); err != nil {
return a.SendError(ctx, err)
}
return operation.NewHeadProjectOK()
}
func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProjectsParams) middleware.Responder {
query := q.New(q.KeyWords{})
query.Sorting = "name"
query.PageNumber = *params.Page
query.PageSize = *params.PageSize
if name := lib.StringValue(params.Name); name != "" {
query.Keywords["name"] = &q.FuzzyMatchValue{Value: name}
}
if owner := lib.StringValue(params.Owner); owner != "" {
query.Keywords["owner"] = owner
}
if params.Public != nil {
query.Keywords["public"] = lib.BoolValue(params.Public)
}
secCtx, ok := security.FromContext(ctx)
if ok && secCtx.IsAuthenticated() {
if !secCtx.IsSysAdmin() && !secCtx.IsSolutionUser() {
// authenticated but not system admin or solution user,
// return public projects and projects that the user is member of
if l, ok := secCtx.(*local.SecurityContext); ok {
currentUser := l.User()
member := &project.MemberQuery{
Name: currentUser.Username,
GroupIDs: currentUser.GroupIDs,
}
// not filter by public or filter by the public with true,
// so also return public projects for the member
if public, ok := query.Keywords["public"]; !ok || lib.ToBool(public) {
member.WithPublic = true
}
query.Keywords["member"] = member
} else {
// can't get the user info, force to return public projects
query.Keywords["public"] = true
}
}
} else {
if params.Public != nil && !*params.Public {
// anonymous want to query private projects return empty projects directly
return operation.NewListProjectsOK().WithXTotalCount(0).WithPayload([]*models.Project{})
}
// force to return public projects for anonymous
query.Keywords["public"] = true
}
total, err := a.projectCtl.Count(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
if total == 0 {
// no projects found for the query return directly
return operation.NewListProjectsOK().WithXTotalCount(0).WithPayload([]*models.Project{})
}
projects, err := a.projectCtl.List(ctx, query, project.WithCVEAllowlist(), project.WithOwner())
if err != nil {
return a.SendError(ctx, err)
}
var wg sync.WaitGroup
for _, p := range projects {
wg.Add(1)
go func(p *project.Project) {
defer wg.Done()
// due to the issue https://github.com/lib/pq/issues/81 of lib/pg or postgres,
// simultaneous queries in transaction may failed, so clone a ctx with new ormer here
if err := a.populateProperties(orm.Clone(ctx), p); err != nil {
log.G(ctx).Errorf("failed to populate propertites for project %s, error: %v", p.Name, err)
}
}(p)
}
wg.Wait()
var payload []*models.Project
for _, p := range projects {
payload = append(payload, model.NewProject(p).ToSwagger())
}
return operation.NewListProjectsOK().
WithXTotalCount(total).
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(payload)
}
func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateProjectParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionUpdate); err != nil {
return a.SendError(ctx, err)
}
p, err := a.projectCtl.Get(ctx, params.ProjectID, project.Metadata(false))
if err != nil {
return a.SendError(ctx, err)
}
if params.Project.CVEAllowlist != nil {
if params.Project.CVEAllowlist.ProjectID == 0 {
// project_id in cve_allowlist not provided or provided as 0, let it to be the id of the project which will be updating
params.Project.CVEAllowlist.ProjectID = params.ProjectID
} else if params.Project.CVEAllowlist.ProjectID != params.ProjectID {
return a.SendError(ctx, errors.BadRequestError(nil).
WithMessage("project_id in cve_allowlist must be %d but it's %d", params.ProjectID, params.Project.CVEAllowlist.ProjectID))
}
if err := lib.JSONCopy(&p.CVEAllowlist, params.Project.CVEAllowlist); err != nil {
return a.SendError(ctx, errors.UnknownError(nil).WithMessage("failed to process cve_allowlist, error: %v", err))
}
}
lib.JSONCopy(&p.Metadata, params.Project.Metadata)
if err := a.projectCtl.Update(ctx, p); err != nil {
return a.SendError(ctx, err)
}
return operation.NewUpdateProjectOK()
}
func (a *projectAPI) deletable(ctx context.Context, projectID int64) (*models.ProjectDeletable, error) {
proj, err := a.getProject(ctx, projectID)
if err != nil {
return nil, err
}
result := &models.ProjectDeletable{Deletable: true}
if proj.RepoCount > 0 {
result.Deletable = false
result.Message = "the project contains repositories, can not be deleted"
} else if proj.ChartCount > 0 {
result.Deletable = false
result.Message = "the project contains helm charts, can not be deleted"
}
return result, nil
}
func (a *projectAPI) getProject(ctx context.Context, projectID int64, options ...project.Option) (*project.Project, error) {
p, err := a.projectCtl.Get(ctx, projectID, options...)
if err != nil {
return nil, err
}
if err := a.populateProperties(ctx, p); err != nil {
return nil, err
}
return p, nil
}
func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.ProjectReq) error {
if req.RegistryID != nil {
if *req.RegistryID <= 0 {
return errors.BadRequestError(fmt.Errorf("%d is invalid value of registry_id, it should be geater than 0", *req.RegistryID))
}
registry, err := replication.RegistryMgr.Get(*req.RegistryID)
if err != nil {
return fmt.Errorf("failed to get the registry %d: %v", *req.RegistryID, err)
}
if registry == nil {
return errors.NotFoundError(fmt.Errorf("registry %d not found", *req.RegistryID))
}
permitted := false
for _, t := range config.GetPermittedRegistryTypesForProxyCache() {
if string(registry.Type) == t {
permitted = true
break
}
}
if !permitted {
return errors.BadRequestError(fmt.Errorf("unsupported registry type %s", string(registry.Type)))
}
}
if req.StorageLimit != nil {
hardLimits := types.ResourceList{types.ResourceStorage: *req.StorageLimit}
if err := quota.Validate(ctx, quota.ProjectReference, hardLimits); err != nil {
return errors.BadRequestError(err)
}
}
return nil
}
func (a *projectAPI) populateProperties(ctx context.Context, p *project.Project) error {
if secCtx, ok := security.FromContext(ctx); ok {
if sc, ok := secCtx.(*local.SecurityContext); ok {
roles, err := pro.ListRoles(sc.User(), p.ProjectID)
if err != nil {
return err
}
p.RoleList = roles
p.Role = highestRole(roles)
}
}
total, err := a.repositoryCtl.Count(ctx, q.New(q.KeyWords{"project_id": p.ProjectID}))
if err != nil {
return err
}
p.RepoCount = total
// Populate chart count property
if config.WithChartMuseum() {
count, err := api.GetChartController().GetCountOfCharts([]string{p.Name})
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", p.ProjectID))
return err
}
p.ChartCount = count
}
return nil
}
func getProjectQuotaSummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) {
if !config.QuotaPerProjectEnable() {
log.Debug("Quota per project disabled")
return
}
q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, quota.ReferenceID(p.ProjectID))
if err != nil {
log.Warningf("failed to get quota for project: %d", p.ProjectID)
return
}
summary.Quota = &models.ProjectSummaryQuota{}
if hard, err := q.GetHard(); err == nil {
lib.JSONCopy(&summary.Quota.Hard, hard)
}
if used, err := q.GetUsed(); err == nil {
lib.JSONCopy(&summary.Quota.Used, used)
}
}
func getProjectMemberSummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) {
var wg sync.WaitGroup
for _, e := range []struct {
role int
count *int64
}{
{common.RoleProjectAdmin, &summary.ProjectAdminCount},
{common.RoleMaintainer, &summary.MaintainerCount},
{common.RoleDeveloper, &summary.DeveloperCount},
{common.RoleGuest, &summary.GuestCount},
{common.RoleLimitedGuest, &summary.LimitedGuestCount},
} {
wg.Add(1)
go func(role int, count *int64) {
defer wg.Done()
total, err := pro.GetTotalOfProjectMembers(p.ProjectID, role)
if err != nil {
log.Warningf("failed to get total of project members of role %d", role)
return
}
*count = total
}(e.role, e.count)
}
wg.Wait()
}
func getProjectRegistrySummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) {
if p.RegistryID <= 0 {
return
}
registry, err := replication.RegistryMgr.Get(p.RegistryID)
if err != nil {
log.Warningf("failed to get registry %d: %v", p.RegistryID, err)
} else if registry != nil {
registry.Credential = nil
lib.JSONCopy(&summary.Registry, registry)
}
}
// Returns the highest role in the role list.
// This func should be removed once we deprecate the "current_user_role_id" in project API
// A user can have multiple roles and they may not have a strict ranking relationship
func highestRole(roles []int) int {
if roles == nil {
return 0
}
rolePower := map[int]int{
common.RoleProjectAdmin: 50,
common.RoleMaintainer: 40,
common.RoleDeveloper: 30,
common.RoleGuest: 20,
common.RoleLimitedGuest: 10,
}
var highest, highestPower int
for _, role := range roles {
if p, ok := rolePower[role]; ok && p > highestPower {
highest = role
highestPower = p
}
}
return highest
}

View File

@ -24,8 +24,6 @@ import (
func registerLegacyRoutes() {
version := APIVersion
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
beego.Router("/api/"+version+"/projects/", &api.ProjectAPI{}, "head:Head")
beego.Router("/api/"+version+"/projects/:id([0-9]+)", &api.ProjectAPI{})
beego.Router("/api/"+version+"/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
beego.Router("/api/"+version+"/users", &api.UserAPI{}, "get:List;post:Post")
beego.Router("/api/"+version+"/users/search", &api.UserAPI{}, "get:Search")
@ -42,9 +40,6 @@ func registerLegacyRoutes() {
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
beego.Router("/api/"+version+"/ping", &api.SystemInfoAPI{}, "get:Ping")
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
beego.Router("/api/"+version+"/projects/", &api.ProjectAPI{}, "get:List;post:Post")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/summary", &api.ProjectAPI{}, "get:Summary")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")

View File

@ -135,7 +135,7 @@ func (_m *Controller) GetByName(ctx context.Context, projectName string, options
}
// List provides a mock function with given fields: ctx, query, options
func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam, options ...project.Option) ([]*models.Project, error) {
func (_m *Controller) List(ctx context.Context, query *q.Query, options ...project.Option) ([]*models.Project, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
@ -146,7 +146,7 @@ func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam,
ret := _m.Called(_ca...)
var r0 []*models.Project
if rf, ok := ret.Get(0).(func(context.Context, *models.ProjectQueryParam, ...project.Option) []*models.Project); ok {
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, ...project.Option) []*models.Project); ok {
r0 = rf(ctx, query, options...)
} else {
if ret.Get(0) != nil {
@ -155,7 +155,7 @@ func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam,
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.ProjectQueryParam, ...project.Option) error); ok {
if rf, ok := ret.Get(1).(func(context.Context, *q.Query, ...project.Option) error); ok {
r1 = rf(ctx, query, options...)
} else {
r1 = ret.Error(1)
@ -163,3 +163,17 @@ func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam,
return r0, r1
}
// Update provides a mock function with given fields: ctx, _a1
func (_m *Controller) Update(ctx context.Context, _a1 *models.Project) error {
ret := _m.Called(ctx, _a1)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *models.Project) error); ok {
r0 = rf(ctx, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -96,19 +96,12 @@ func (_m *Manager) Get(ctx context.Context, idOrName interface{}) (*models.Proje
}
// List provides a mock function with given fields: ctx, query
func (_m *Manager) List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) {
_va := make([]interface{}, len(query))
for _i := range query {
_va[_i] = query[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*models.Project, error) {
ret := _m.Called(ctx, query)
var r0 []*models.Project
if rf, ok := ret.Get(0).(func(context.Context, ...*models.ProjectQueryParam) []*models.Project); ok {
r0 = rf(ctx, query...)
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.Project); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Project)
@ -116,8 +109,8 @@ func (_m *Manager) List(ctx context.Context, query ...*models.ProjectQueryParam)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, ...*models.ProjectQueryParam) error); ok {
r1 = rf(ctx, query...)
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}

View File

@ -5,10 +5,12 @@ package user
import (
context "context"
models "github.com/goharbor/harbor/src/pkg/user/models"
models "github.com/goharbor/harbor/src/common/models"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
)
// Manager is an autogenerated mock type for the Manager type
@ -16,16 +18,62 @@ type Manager struct {
mock.Mock
}
// Get provides a mock function with given fields: ctx, id
func (_m *Manager) Get(ctx context.Context, id int) (*models.User, error) {
ret := _m.Called(ctx, id)
var r0 *models.User
if rf, ok := ret.Get(0).(func(context.Context, int) *models.User); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: ctx, username
func (_m *Manager) GetByName(ctx context.Context, username string) (*models.User, error) {
ret := _m.Called(ctx, username)
var r0 *models.User
if rf, ok := ret.Get(0).(func(context.Context, string) *models.User); ok {
r0 = rf(ctx, username)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, username)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *Manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
func (_m *Manager) List(ctx context.Context, query *q.Query) (usermodels.Users, error) {
ret := _m.Called(ctx, query)
var r0 models.Users
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) models.Users); ok {
var r0 usermodels.Users
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) usermodels.Users); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(models.Users)
r0 = ret.Get(0).(usermodels.Users)
}
}

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import os
import sys
import time
import subprocess
@ -22,6 +22,10 @@ class Credential:
self.username = username
self.password = password
def get_endpoint():
harbor_server = os.environ.get("HARBOR_HOST", "localhost:8080")
return os.environ.get("HARBOR_HOST_SCHEMA", "https")+ "://"+harbor_server+"/api/v2.0"
def _create_client(server, credential, debug, api_type="products"):
cfg = None
if api_type in ('projectv2', 'artifact', 'repository', 'scan'):
@ -85,13 +89,16 @@ def run_command(command):
raise Exception('Error: Exited with error code: %s. Output:%s'% (e.returncode, e.output))
return output
class Base:
def __init__(self,
server = Server(endpoint="http://localhost:8080/api", verify_ssl=False),
credential = Credential(type="basic_auth", username="admin", password="Harbor12345"),
debug = True, api_type = "products"):
class Base(object):
def __init__(self, server=None, credential=None, debug=True, api_type="products"):
if server is None:
server = Server(endpoint=get_endpoint(), verify_ssl=False)
if not isinstance(server.verify_ssl, bool):
server.verify_ssl = server.verify_ssl == "True"
if credential is None:
credential = Credential(type="basic_auth", username="admin", password="Harbor12345")
self.server = server
self.credential = credential
self.debug = debug
@ -102,8 +109,6 @@ class Base:
if len(kwargs) == 0:
return self.client
server = self.server
if "api_type" in kwargs:
server.api_type = kwargs.get("api_type")
if "endpoint" in kwargs:
server.endpoint = kwargs.get("endpoint")
if "verify_ssl" in kwargs:
@ -118,4 +123,4 @@ class Base:
credential.username = kwargs.get("username")
if "password" in kwargs:
credential.password = kwargs.get("password")
return _create_client(server, credential, self.debug, self.api_type)
return _create_client(server, credential, self.debug, kwargs.get('api_type', self.api_type))

View File

@ -2,7 +2,8 @@
import base
import swagger_client
from swagger_client.rest import ApiException
import v2_swagger_client
from v2_swagger_client.rest import ApiException
def is_member_exist_in_project(members, member_user_name, expected_member_role_id = None):
result = False
@ -22,6 +23,12 @@ def get_member_id_by_name(members, member_user_name):
return None
class Project(base.Base):
def __init__(self, username=None, password=None):
kwargs = dict(api_type="projectv2")
if username and password:
kwargs["credential"] = base.Credential('basic_auth', username, password)
super(Project, self).__init__(**kwargs)
def create_project(self, name=None, metadata=None, expect_status_code = 201, expect_response_body = None, **kwargs):
if name is None:
name = base._random_name("project")
@ -30,7 +37,7 @@ class Project(base.Base):
client = self._get_client(**kwargs)
try:
_, status_code, header = client.projects_post_with_http_info(swagger_client.ProjectReq(name, metadata))
_, status_code, header = client.create_project_with_http_info(v2_swagger_client.ProjectReq(project_name=name, metadata=metadata))
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None:
@ -43,7 +50,7 @@ class Project(base.Base):
def get_projects(self, params, **kwargs):
client = self._get_client(**kwargs)
data = []
data, status_code, _ = client.projects_get_with_http_info(**params)
data, status_code, _ = client.list_projects_with_http_info(**params)
base._assert_status_code(200, status_code)
return data
@ -66,7 +73,7 @@ class Project(base.Base):
def check_project_name_exist(self, name=None, **kwargs):
client = self._get_client(**kwargs)
try:
_, status_code, _ = client.projects_head_with_http_info(name)
_, status_code, _ = client.head_project_with_http_info(name)
except ApiException as e:
status_code = -1
return {
@ -77,7 +84,7 @@ class Project(base.Base):
def get_project(self, project_id, expect_status_code = 200, expect_response_body = None, **kwargs):
client = self._get_client(**kwargs)
try:
data, status_code, _ = client.projects_project_id_get_with_http_info(project_id)
data, status_code, _ = client.get_project_with_http_info(project_id)
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None:
@ -90,9 +97,9 @@ class Project(base.Base):
def update_project(self, project_id, expect_status_code=200, metadata=None, cve_allowlist=None, **kwargs):
client = self._get_client(**kwargs)
project = swagger_client.ProjectReq(metadata=metadata, cve_allowlist=cve_allowlist)
project = v2_swagger_client.ProjectReq(metadata=metadata, cve_allowlist=cve_allowlist)
try:
_, sc, _ = client.projects_project_id_put_with_http_info(project_id, project)
_, sc, _ = client.update_project_with_http_info(project_id, project)
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
else:
@ -100,31 +107,34 @@ class Project(base.Base):
def delete_project(self, project_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
_, status_code, _ = client.projects_project_id_delete_with_http_info(project_id)
_, status_code, _ = client.delete_project_with_http_info(project_id)
base._assert_status_code(expect_status_code, status_code)
def get_project_log(self, project_id, expect_status_code = 200, **kwargs):
def get_project_log(self, project_name, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
body, status_code, _ = client.projects_project_id_logs_get_with_http_info(project_id)
body, status_code, _ = client.get_logs_with_http_info(project_name)
base._assert_status_code(expect_status_code, status_code)
return body
def filter_project_logs(self, project_id, operator, repository, tag, operation_type, **kwargs):
access_logs = self.get_project_log(project_id, **kwargs)
def filter_project_logs(self, project_name, operator, resource, resource_type, operation, **kwargs):
access_logs = self.get_project_log(project_name, **kwargs)
count = 0
for each_access_log in list(access_logs):
if each_access_log.username == operator and \
each_access_log.repo_name.strip(r'/') == repository and \
each_access_log.repo_tag == tag and \
each_access_log.operation == operation_type:
each_access_log.resource_type == resource_type and \
each_access_log.resource == resource and \
each_access_log.operation == operation:
count = count + 1
return count
def get_project_members(self, project_id, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
return client.projects_project_id_members_get(project_id)
def get_project_member(self, project_id, member_id, expect_status_code = 200, expect_response_body = None, **kwargs):
from swagger_client.rest import ApiException
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
data = []
try:
@ -140,6 +150,7 @@ class Project(base.Base):
return data
def get_project_member_id(self, project_id, member_user_name, **kwargs):
kwargs['api_type'] = 'products'
members = self.get_project_members(project_id, **kwargs)
result = get_member_id_by_name(list(members), member_user_name)
if result == None:
@ -148,18 +159,21 @@ class Project(base.Base):
return result
def check_project_member_not_exist(self, project_id, member_user_name, **kwargs):
kwargs['api_type'] = 'products'
members = self.get_project_members(project_id, **kwargs)
result = is_member_exist_in_project(list(members), member_user_name)
if result == True:
raise Exception(r"User {} should not be a member of project with ID {}.".format(member_user_name, project_id))
def check_project_members_exist(self, project_id, member_user_name, expected_member_role_id = None, **kwargs):
kwargs['api_type'] = 'products'
members = self.get_project_members(project_id, **kwargs)
result = is_member_exist_in_project(members, member_user_name, expected_member_role_id = expected_member_role_id)
if result == False:
raise Exception(r"User {} should be a member of project with ID {}.".format(member_user_name, project_id))
def update_project_member_role(self, project_id, member_id, member_role_id, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
role = swagger_client.Role(role_id = member_role_id)
data, status_code, _ = client.projects_project_id_members_mid_put_with_http_info(project_id, member_id, role = role)
@ -168,12 +182,14 @@ class Project(base.Base):
return data
def delete_project_member(self, project_id, member_id, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
_, status_code, _ = client.projects_project_id_members_mid_delete_with_http_info(project_id, member_id)
base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(200, status_code)
def add_project_members(self, project_id, user_id, member_role_id = None, expect_status_code = 201, **kwargs):
kwargs['api_type'] = 'products'
if member_role_id is None:
member_role_id = 1
_member_user = {"user_id": int(user_id)}
@ -185,6 +201,7 @@ class Project(base.Base):
return base._get_id_from_header(header)
def add_project_robot_account(self, project_id, project_name, expires_at, robot_name = None, robot_desc = None, has_pull_right = True, has_push_right = True, has_chart_read_right = True, has_chart_create_right = True, expect_status_code = 201, **kwargs):
kwargs['api_type'] = 'products'
if robot_name is None:
robot_name = base._random_name("robot")
if robot_desc is None:
@ -221,11 +238,13 @@ class Project(base.Base):
return base._get_id_from_header(header), data
def get_project_robot_account_by_id(self, project_id, robot_id, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
data, status_code, _ = client.projects_project_id_robots_robot_id_get_with_http_info(project_id, robot_id)
return data
def disable_project_robot_account(self, project_id, robot_id, disable, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
robotAccountUpdate = swagger_client.RobotAccountUpdate(disable)
_, status_code, _ = client.projects_project_id_robots_robot_id_put_with_http_info(project_id, robot_id, robotAccountUpdate)
@ -233,6 +252,7 @@ class Project(base.Base):
base._assert_status_code(200, status_code)
def delete_project_robot_account(self, project_id, robot_id, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products'
client = self._get_client(**kwargs)
_, status_code, _ = client.projects_project_id_robots_robot_id_delete_with_http_info(project_id, robot_id)
base._assert_status_code(expect_status_code, status_code)

View File

@ -14,7 +14,7 @@ def pull_harbor_image(registry, username, password, image, tag, expected_login_e
time.sleep(2)
ret = _docker_api.docker_image_pull(r'{}/{}'.format(registry, image), tag = tag, expected_error_message = expected_error_message)
def push_image_to_project(project_name, registry, username, password, image, tag, expected_login_error_message = None, expected_error_message = None, profix_for_image = None):
def push_image_to_project(project_name, registry, username, password, image, tag, expected_login_error_message = None, expected_error_message = None, profix_for_image = None, new_image=None):
_docker_api = DockerAPI()
_docker_api.docker_login(registry, username, password, expected_error_message = expected_login_error_message)
time.sleep(2)
@ -23,6 +23,8 @@ def push_image_to_project(project_name, registry, username, password, image, tag
_docker_api.docker_image_pull(image, tag = tag)
time.sleep(2)
image = new_image or image
if profix_for_image == None:
new_harbor_registry, new_tag = _docker_api.docker_image_tag(r'{}:{}'.format(image, tag), r'{}/{}/{}'.format(registry, project_name, image))
else:

View File

@ -44,7 +44,7 @@ class TestProjects(unittest.TestCase):
project_001_data = self.project.get_projects(dict(public=False), **USER_001_CLIENT)
#3.2 Check user-001 has no any private project
self.assertEqual(project_001_data, None, msg="user-001 should has no any private project, but we got {}".format(project_001_data))
self.assertEqual(len(project_001_data), 0, msg="user-001 should has no any private project, but we got {}".format(project_001_data))
#4. Add user-001 as a member of project-001
result = self.project.add_project_members(project_001_id, user_001_id, **ADMIN_CLIENT)

View File

@ -20,20 +20,13 @@ import unittest
import testutils
import docker
import swagger_client
from testutils import ADMIN_CLIENT
from swagger_client.models.project import Project
from swagger_client.models.project_req import ProjectReq
from swagger_client.models.project_metadata import ProjectMetadata
from swagger_client.models.project_member import ProjectMember
from swagger_client.models.user_group import UserGroup
from swagger_client.models.configurations import Configurations
from library.projectV2 import ProjectV2
from library.project import Project
from library.base import _assert_status_code
from library.base import _random_name
from v2_swagger_client.rest import ApiException
from pprint import pprint
@ -46,23 +39,20 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
repository_api = testutils.GetRepositoryApi("admin", "Harbor12345")
project_id = 0
docker_client = docker.from_env()
_project_name = _random_name("test_private")
_project_name = _random_name("test-ldap-group")
def setUp(self):
self.projectv2= ProjectV2()
self.project = Project()
#login with admin, create a project and assign role to ldap group
result = self.product_api.configurations_put(configurations=Configurations(ldap_filter="", ldap_group_attribute_name="cn", ldap_group_base_dn="ou=groups,dc=example,dc=com", ldap_group_search_filter="objectclass=groupOfNames", ldap_group_search_scope=2))
pprint(result)
cfgs = self.product_api.configurations_get()
pprint(cfgs)
req = ProjectReq()
req.project_name = self._project_name
req.metadata = ProjectMetadata(public="false")
result = self.product_api.projects_post(req)
result = self.project.create_project(self._project_name, dict(public="false"))
pprint(result)
projs = self.product_api.projects_get(name = self._project_name)
projs = self.project.get_projects(dict(name = self._project_name))
if len(projs)>0 :
project = projs[0]
self.project_id = project.project_id
@ -91,33 +81,30 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
result = self.product_api.projects_project_id_members_post( project_id=self.project_id, project_member=projectmember )
pprint(result)
pass
def tearDown(self):
if self.project_id > 0 :
# delete images in project
result = self.repository_api.delete_repository(self._project_name, "busybox")
pprint(result)
result = self.repository_api.delete_repository(self._project_name, "busyboxdev")
pprint(result)
if self.project_id > 0 :
self.product_api.projects_project_id_delete(self.project_id)
pass
self.project.delete_project(self.project_id)
def testAssignRoleToLdapGroup(self):
"""Test AssignRoleToLdapGroup"""
admin_product_api = testutils.GetProductApi(username="admin_user", password="zhu88jie")
projects = admin_product_api.projects_get(name=self._project_name)
admin_product_api = Project("admin_user", "zhu88jie")
projects = admin_product_api.get_projects(dict(name=self._project_name))
self.assertTrue(len(projects) == 1)
self.assertEqual(1, projects[0].current_user_role_id)
dev_product_api = testutils.GetProductApi("dev_user", "zhu88jie")
projects = dev_product_api.projects_get(name=self._project_name)
dev_product_api = Project("dev_user", "zhu88jie")
projects = dev_product_api.get_projects(dict(name=self._project_name))
self.assertTrue(len(projects) == 1)
self.assertEqual(2, projects[0].current_user_role_id)
guest_product_api = testutils.GetProductApi("guest_user", "zhu88jie")
projects = guest_product_api.projects_get(name=self._project_name)
guest_product_api = Project("guest_user", "zhu88jie")
projects = guest_product_api.get_projects(dict(name=self._project_name))
self.assertTrue(len(projects) == 1)
self.assertEqual(3, projects[0].current_user_role_id)
@ -130,8 +117,6 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
self.assertTrue(self.queryUserLogs(username="guest_user", password="zhu88jie")>0, "guest user can see logs")
self.assertTrue(self.queryUserLogs(username="test", password="123456", status_code=403)==0, "test user can not see any logs")
pass
# admin user can push, pull images
def dockerCmdLoginAdmin(self, username, password):
pprint(self.docker_client.info())
@ -143,7 +128,7 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
if output.find("error")>0 :
self.fail("Should not fail to push image for admin_user")
self.docker_client.images.pull(repository=self.harbor_host+"/"+self._project_name+"/busybox", tag="latest")
pass
# dev user can push, pull images
def dockerCmdLoginDev(self, username, password, harbor_server=harbor_host):
self.docker_client.login(username=username, password=password, registry=self.harbor_host)
@ -153,7 +138,7 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
output = self.docker_client.images.push(repository=self.harbor_host+"/"+self._project_name+"/busyboxdev", tag="latest")
if output.find("error") >0 :
self.fail("Should not fail to push images for dev_user")
pass
# guest user can pull images
def dockerCmdLoginGuest(self, username, password, harbor_server=harbor_host):
self.docker_client.login(username=username, password=password, registry=self.harbor_host)
@ -164,12 +149,12 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
if output.find("error")<0 :
self.fail("Should failed to push image for guest user")
self.docker_client.images.pull(repository=self.harbor_host+"/"+self._project_name+"/busybox", tag="latest")
pass
# check can see his log in current project
def queryUserLogs(self, username, password, status_code=200):
client=dict(endpoint = ADMIN_CLIENT["endpoint"], username = username, password = password)
try:
logs = self.projectv2.get_project_log(self._project_name, status_code, **client)
logs = self.project.get_project_log(self._project_name, status_code, **client)
count = 0
for log in list(logs):
count = count + 1

View File

@ -20,10 +20,10 @@ sys.path.append(os.environ["SWAGGER_CLIENT_PATH"])
import unittest
import testutils
import swagger_client
from testutils import TEARDOWN
from library.base import _random_name
from swagger_client.models.project_req import ProjectReq
from library.project import Project
from swagger_client.models.configurations import Configurations
from swagger_client.rest import ApiException
from pprint import pprint
@ -32,26 +32,30 @@ from pprint import pprint
class TestLdapAdminRole(unittest.TestCase):
"""AccessLog unit test stubs"""
product_api = testutils.GetProductApi("admin", "Harbor12345")
mike_product_api = testutils.GetProductApi("mike", "zhu88jie")
project_id = 0
def setUp(self):
pass
self.project= Project()
self.mike_product_api = Project("mike", "zhu88jie")
def tearDown(self):
print("Case completed")
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def test_ClearData(self):
if self.project_id > 0 :
self.mike_product_api.projects_project_id_delete(project_id=self.project_id)
pass
self.mike_product_api.delete_project(self.project_id)
def testLdapAdminRole(self):
"""Test LdapAdminRole"""
_project_name = _random_name("test_private")
_project_name = _random_name("test-ldap-admin-role")
result = self.product_api.configurations_put(configurations=Configurations(ldap_group_admin_dn="cn=harbor_users,ou=groups,dc=example,dc=com"))
# Create a private project
result = self.product_api.projects_post(project=ProjectReq(project_name= _project_name))
result = self.project.create_project(_project_name)
# query project with ldap user mike
projects = self.mike_product_api.projects_get(name=_project_name)
projects = self.mike_product_api.get_projects(dict(name=_project_name))
print("=================", projects)
self.assertTrue(len(projects) == 1)

View File

@ -1,10 +1,10 @@
from __future__ import absolute_import
import unittest
import swagger_client
import v2_swagger_client
import time
from testutils import ADMIN_CLIENT
from testutils import ADMIN_CLIENT, TEARDOWN
from library.project import Project
from library.user import User
@ -50,6 +50,10 @@ class TestProjectCVEAllowlist(unittest.TestCase):
self.member_id = int(m_id)
def tearDown(self):
print("Case completed")
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def test_ClearData(self):
print("Tearing down...")
self.project.delete_project_member(self.project_pa_id, self.member_id, **ADMIN_CLIENT)
self.project.delete_project(self.project_pa_id,**ADMIN_CLIENT)
@ -63,9 +67,9 @@ class TestProjectCVEAllowlist(unittest.TestCase):
self.assertEqual(0, len(p.cve_allowlist.items))
# User(RA) updates the project CVE allowlist, verify it fails with Forbidden error.
item_list = [swagger_client.CVEAllowlistItem(cve_id="CVE-2019-12310")]
item_list = [v2_swagger_client.CVEAllowlistItem(cve_id="CVE-2019-12310")]
exp = int(time.time()) + 1000
wl = swagger_client.CVEAllowlist(expires_at=exp, items=item_list)
wl = v2_swagger_client.CVEAllowlist(expires_at=exp, items=item_list)
self.project.update_project(self.project_pa_id, cve_allowlist=wl, expect_status_code=403, **self.USER_RA_CLIENT)
# Admin user updates User(RA) as project admin.
@ -78,14 +82,14 @@ class TestProjectCVEAllowlist(unittest.TestCase):
self.assertEqual(exp, p.cve_allowlist.expires_at)
# User(RA) updates the project CVE allowlist with empty items list
wl2 = swagger_client.CVEAllowlist(items=[])
wl2 = v2_swagger_client.CVEAllowlist(items=[])
self.project.update_project(self.project_pa_id, cve_allowlist=wl2, **self.USER_RA_CLIENT)
p = self.project.get_project(self.project_pa_id, **self.USER_RA_CLIENT)
self.assertEqual(0, len(p.cve_allowlist.items))
self.assertIsNone(p.cve_allowlist.expires_at)
# User(RA) updates the project metadata to set "reuse_sys_cve_allowlist" to true.
meta = swagger_client.ProjectMetadata(reuse_sys_cve_allowlist="true")
meta = v2_swagger_client.ProjectMetadata(reuse_sys_cve_allowlist="true")
self.project.update_project(self.project_pa_id, metadata=meta, **self.USER_RA_CLIENT)
p = self.project.get_project(self.project_pa_id, **self.USER_RA_CLIENT)
self.assertEqual("true", p.metadata.reuse_sys_cve_allowlist)