mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-26 10:38:00 +01:00
Merge pull request #14702 from ywk253100/210419_metadata
Refactor project metadata API
This commit is contained in:
commit
c54e690f69
@ -19,151 +19,6 @@ securityDefinitions:
|
||||
security:
|
||||
- basicAuth: []
|
||||
paths:
|
||||
'/projects/{project_id}/metadatas':
|
||||
get:
|
||||
summary: Get project metadata.
|
||||
description: |
|
||||
This endpoint returns metadata of the project specified by project ID.
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
description: The ID of project.
|
||||
required: true
|
||||
type: integer
|
||||
format: int64
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get metadata successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/ProjectMetadata'
|
||||
'401':
|
||||
description: User need to login first.
|
||||
'500':
|
||||
description: Internal server errors.
|
||||
post:
|
||||
summary: Add metadata for the project.
|
||||
description: |
|
||||
This endpoint is aimed to add metadata of a project.
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Selected project ID.
|
||||
- name: metadata
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ProjectMetadata'
|
||||
description: The metadata of project.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Add metadata successfully.
|
||||
'400':
|
||||
description: Invalid request.
|
||||
'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.
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
description: Internal server errors.
|
||||
'/projects/{project_id}/metadatas/{meta_name}':
|
||||
get:
|
||||
summary: Get project metadata
|
||||
description: |
|
||||
This endpoint returns specified metadata of a project.
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
description: Project ID for filtering results.
|
||||
required: true
|
||||
type: integer
|
||||
format: int64
|
||||
- name: meta_name
|
||||
in: path
|
||||
description: The name of metadat.
|
||||
required: true
|
||||
type: string
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get metadata successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/ProjectMetadata'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Internal server errors.
|
||||
put:
|
||||
summary: Update metadata of a project.
|
||||
description: |
|
||||
This endpoint is aimed to update the metadata of a project.
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The ID of project.
|
||||
- name: meta_name
|
||||
in: path
|
||||
description: The name of metadat.
|
||||
required: true
|
||||
type: string
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Updated metadata successfully.
|
||||
'400':
|
||||
description: Invalid request.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission to the project.
|
||||
'404':
|
||||
description: Project or metadata does not exist.
|
||||
'500':
|
||||
description: Internal server errors.
|
||||
delete:
|
||||
summary: Delete metadata of a project
|
||||
description: |
|
||||
This endpoint is aimed to delete metadata of a project.
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
description: The ID of project.
|
||||
required: true
|
||||
type: integer
|
||||
format: int64
|
||||
- name: meta_name
|
||||
in: path
|
||||
description: The name of metadat.
|
||||
required: true
|
||||
type: string
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Metadata is deleted successfully.
|
||||
'400':
|
||||
description: Invalid requst.
|
||||
'403':
|
||||
description: User need to log in first.
|
||||
'404':
|
||||
description: Project or metadata does not exist.
|
||||
'500':
|
||||
description: Internal server errors.
|
||||
/email/ping:
|
||||
post:
|
||||
summary: Test connection and authentication with email server.
|
||||
@ -376,28 +231,6 @@ responses:
|
||||
InternalServerError:
|
||||
description: 'Internal Server Error'
|
||||
definitions:
|
||||
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".'
|
||||
prevent_vul:
|
||||
type: string
|
||||
description: 'Whether prevent the vulnerable images from running. The valid values are "true", "false".'
|
||||
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".'
|
||||
auto_scan:
|
||||
type: string
|
||||
description: 'Whether scan images automatically when pushing. The valid values are "true", "false".'
|
||||
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.'
|
||||
Role:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -644,6 +644,164 @@ paths:
|
||||
$ref: '#/responses/403'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
'/projects/{project_name_or_id}/metadatas/':
|
||||
get:
|
||||
summary: Get the metadata of the specific project
|
||||
description: Get the metadata of the specific project
|
||||
operationId: listProjectMetadatas
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/isResourceName'
|
||||
- $ref: '#/parameters/projectNameOrId'
|
||||
tags:
|
||||
- projectMetadata
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
post:
|
||||
summary: Add metadata for the specific project
|
||||
operationId: addProjectMetadatas
|
||||
description: Add metadata for the specific project
|
||||
tags:
|
||||
- projectMetadata
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/isResourceName'
|
||||
- $ref: '#/parameters/projectNameOrId'
|
||||
- name: metadata
|
||||
in: body
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
'/projects/{project_name_or_id}/metadatas/{meta_name}':
|
||||
get:
|
||||
summary: Get the specific metadata of the specific project
|
||||
description: Get the specific metadata of the specific project
|
||||
operationId: getProjectMetadata
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/isResourceName'
|
||||
- $ref: '#/parameters/projectNameOrId'
|
||||
- name: meta_name
|
||||
in: path
|
||||
description: The name of metadata.
|
||||
required: true
|
||||
type: string
|
||||
tags:
|
||||
- projectMetadata
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
put:
|
||||
summary: Update the specific metadata for the specific project
|
||||
description: Update the specific metadata for the specific project
|
||||
operationId: updateProjectMetadata
|
||||
tags:
|
||||
- projectMetadata
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/isResourceName'
|
||||
- $ref: '#/parameters/projectNameOrId'
|
||||
- name: meta_name
|
||||
in: path
|
||||
description: The name of metadata.
|
||||
required: true
|
||||
type: string
|
||||
- name: metadata
|
||||
in: body
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
delete:
|
||||
summary: Delete the specific metadata for the specific project
|
||||
description: Delete the specific metadata for the specific project
|
||||
operationId: deleteProjectMetadata
|
||||
tags:
|
||||
- projectMetadata
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/isResourceName'
|
||||
- $ref: '#/parameters/projectNameOrId'
|
||||
- name: meta_name
|
||||
in: path
|
||||
description: The name of metadata.
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/repositories:
|
||||
get:
|
||||
summary: List all authorized repositories
|
||||
|
65
src/controller/project/metadata/controller.go
Normal file
65
src/controller/project/metadata/controller.go
Normal file
@ -0,0 +1,65 @@
|
||||
// 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 metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/pkg/project/metadata"
|
||||
)
|
||||
|
||||
// Ctl is the global controller instance
|
||||
var Ctl = NewController()
|
||||
|
||||
// Controller defines the operations that a project metadata controller should implement
|
||||
type Controller interface {
|
||||
// Add metadatas for project specified by projectID
|
||||
Add(ctx context.Context, projectID int64, meta map[string]string) error
|
||||
|
||||
// Delete metadatas whose keys are specified in parameter meta, if it is absent, delete all
|
||||
Delete(ctx context.Context, projectID int64, meta ...string) error
|
||||
|
||||
// Update metadatas
|
||||
Update(ctx context.Context, projectID int64, meta map[string]string) error
|
||||
|
||||
// Get metadatas whose keys are specified in parameter meta, if it is absent, get all
|
||||
Get(ctx context.Context, projectID int64, meta ...string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// NewController creates an instance of the default controller
|
||||
func NewController() Controller {
|
||||
return &controller{
|
||||
mgr: metadata.Mgr,
|
||||
}
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
mgr metadata.Manager
|
||||
}
|
||||
|
||||
func (c *controller) Add(ctx context.Context, projectID int64, meta map[string]string) error {
|
||||
return c.mgr.Add(ctx, projectID, meta)
|
||||
}
|
||||
|
||||
func (c *controller) Delete(ctx context.Context, projectID int64, meta ...string) error {
|
||||
return c.mgr.Delete(ctx, projectID, meta...)
|
||||
}
|
||||
|
||||
func (c *controller) Update(ctx context.Context, projectID int64, meta map[string]string) error {
|
||||
return c.mgr.Update(ctx, projectID, meta)
|
||||
}
|
||||
|
||||
func (c *controller) Get(ctx context.Context, projectID int64, meta ...string) (map[string]string, error) {
|
||||
return c.mgr.Get(ctx, projectID, meta...)
|
||||
}
|
@ -93,9 +93,6 @@ func init() {
|
||||
beego.BConfig.WebConfig.Session.SessionOn = true
|
||||
beego.TestBeegoInit(apppath)
|
||||
|
||||
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")
|
||||
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
||||
|
||||
// Charts are controlled under projects
|
||||
|
@ -1,233 +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"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/project/metadata"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||
)
|
||||
|
||||
// MetadataAPI ...
|
||||
type MetadataAPI struct {
|
||||
BaseController
|
||||
metaMgr metadata.Manager
|
||||
project *models.Project
|
||||
name string
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (m *MetadataAPI) Prepare() {
|
||||
m.BaseController.Prepare()
|
||||
|
||||
m.metaMgr = metadata.Mgr
|
||||
|
||||
id, err := m.GetInt64FromPath(":id")
|
||||
if err != nil || id <= 0 {
|
||||
text := "invalid project ID: "
|
||||
if err != nil {
|
||||
text += err.Error()
|
||||
} else {
|
||||
text += fmt.Sprintf("%d", id)
|
||||
}
|
||||
m.SendBadRequestError(errors.New(text))
|
||||
return
|
||||
}
|
||||
|
||||
project, err := m.ProjectCtl.Get(m.Context(), id)
|
||||
if err != nil {
|
||||
if errors.IsNotFoundErr(err) {
|
||||
m.handleProjectNotFound(id)
|
||||
} else {
|
||||
m.ParseAndHandleError(fmt.Sprintf("failed to get project %d", id), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
m.project = project
|
||||
|
||||
name := m.GetStringFromPath(":name")
|
||||
if len(name) > 0 {
|
||||
m.name = name
|
||||
metas, err := m.metaMgr.Get(m.Context(), project.ProjectID, name)
|
||||
if err != nil {
|
||||
m.SendInternalServerError(fmt.Errorf("failed to get metadata of project %d: %v", project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
if len(metas) == 0 {
|
||||
m.SendNotFoundError(fmt.Errorf("metadata %s of project %d not found", name, project.ProjectID))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MetadataAPI) requireAccess(action rbac.Action) bool {
|
||||
return m.RequireProjectAccess(m.project.ProjectID, action, rbac.ResourceMetadata)
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (m *MetadataAPI) Get() {
|
||||
if !m.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
var metas map[string]string
|
||||
var err error
|
||||
if len(m.name) > 0 {
|
||||
metas, err = m.metaMgr.Get(m.Context(), m.project.ProjectID, m.name)
|
||||
} else {
|
||||
metas, err = m.metaMgr.Get(m.Context(), m.project.ProjectID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
m.SendInternalServerError(fmt.Errorf("failed to get metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
m.Data["json"] = metas
|
||||
m.ServeJSON()
|
||||
}
|
||||
|
||||
// Post ...
|
||||
func (m *MetadataAPI) Post() {
|
||||
if !m.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
var metas map[string]string
|
||||
if err := m.DecodeJSONReq(&metas); err != nil {
|
||||
m.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ms, err := validateProjectMetadata(metas)
|
||||
if err != nil {
|
||||
m.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(ms) != 1 {
|
||||
m.SendBadRequestError(errors.New("invalid request: has no valid key/value pairs or has more than one valid key/value pairs"))
|
||||
return
|
||||
}
|
||||
|
||||
keys := reflect.ValueOf(ms).MapKeys()
|
||||
mts, err := m.metaMgr.Get(m.Context(), m.project.ProjectID, keys[0].String())
|
||||
if err != nil {
|
||||
m.SendInternalServerError(fmt.Errorf("failed to get metadata for project %d: %v", m.project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(mts) != 0 {
|
||||
m.SendConflictError(errors.New("conflict metadata"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.metaMgr.Add(m.Context(), m.project.ProjectID, ms); err != nil {
|
||||
m.SendInternalServerError(fmt.Errorf("failed to create metadata for project %d: %v", m.project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
|
||||
m.Ctx.ResponseWriter.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// Put ...
|
||||
func (m *MetadataAPI) Put() {
|
||||
if !m.requireAccess(rbac.ActionUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
var metas map[string]string
|
||||
if err := m.DecodeJSONReq(&metas); err != nil {
|
||||
m.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
meta, exist := metas[m.name]
|
||||
if !exist {
|
||||
m.SendBadRequestError(fmt.Errorf("must contains key %s", m.name))
|
||||
return
|
||||
}
|
||||
|
||||
ms, err := validateProjectMetadata(map[string]string{
|
||||
m.name: meta,
|
||||
})
|
||||
if err != nil {
|
||||
m.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.metaMgr.Update(m.Context(), m.project.ProjectID, map[string]string{
|
||||
m.name: ms[m.name],
|
||||
}); err != nil {
|
||||
m.SendInternalServerError(fmt.Errorf("failed to update metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (m *MetadataAPI) Delete() {
|
||||
if !m.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.metaMgr.Delete(m.Context(), m.project.ProjectID, m.name); err != nil {
|
||||
m.SendInternalServerError(fmt.Errorf("failed to delete metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// validate metas and return a new map which contains the valid key/value pairs only
|
||||
func validateProjectMetadata(metas map[string]string) (map[string]string, error) {
|
||||
if len(metas) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
boolMetas := []string{
|
||||
models.ProMetaPublic,
|
||||
models.ProMetaEnableContentTrust,
|
||||
models.ProMetaPreventVul,
|
||||
models.ProMetaAutoScan}
|
||||
|
||||
for _, boolMeta := range boolMetas {
|
||||
value, exist := metas[boolMeta]
|
||||
if exist {
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s to bool: %v", value, err)
|
||||
}
|
||||
metas[boolMeta] = strconv.FormatBool(b)
|
||||
}
|
||||
}
|
||||
|
||||
value, exist := metas[models.ProMetaSeverity]
|
||||
if exist {
|
||||
severity := vuln.ParseSeverityVersion3(strings.ToLower(value))
|
||||
if severity == vuln.Unknown {
|
||||
return nil, fmt.Errorf("invalid severity %s", value)
|
||||
}
|
||||
|
||||
metas[models.ProMetaSeverity] = strings.ToLower(severity.String())
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
@ -1,112 +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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateProjectMetadata(t *testing.T) {
|
||||
var metas map[string]string
|
||||
|
||||
// nil metas
|
||||
ms, err := validateProjectMetadata(metas)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, ms)
|
||||
|
||||
// valid key, invalid value(bool)
|
||||
metas = map[string]string{
|
||||
models.ProMetaPublic: "invalid_value",
|
||||
}
|
||||
ms, err = validateProjectMetadata(metas)
|
||||
require.NotNil(t, err)
|
||||
|
||||
// valid key/value(bool)
|
||||
metas = map[string]string{
|
||||
models.ProMetaPublic: "1",
|
||||
}
|
||||
ms, err = validateProjectMetadata(metas)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "true", ms[models.ProMetaPublic])
|
||||
|
||||
// valid key, invalid value(string)
|
||||
metas = map[string]string{
|
||||
models.ProMetaSeverity: "invalid_value",
|
||||
}
|
||||
ms, err = validateProjectMetadata(metas)
|
||||
require.NotNil(t, err)
|
||||
|
||||
// valid key, valid value(string)
|
||||
metas = map[string]string{
|
||||
models.ProMetaSeverity: "High",
|
||||
}
|
||||
ms, err = validateProjectMetadata(metas)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "high", ms[models.ProMetaSeverity])
|
||||
}
|
||||
|
||||
func TestMetaAPI(t *testing.T) {
|
||||
client := newHarborAPI()
|
||||
|
||||
// non-exist project, it should return 401 if user is not logged in.
|
||||
code, _, err := client.PostMeta(*unknownUsr, int64(1000), nil)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusUnauthorized, code)
|
||||
|
||||
// non-login
|
||||
code, _, err = client.PostMeta(*unknownUsr, int64(1), nil)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusUnauthorized, code)
|
||||
|
||||
// test post
|
||||
code, _, err = client.PostMeta(*admin, int64(1), map[string]string{
|
||||
models.ProMetaAutoScan: "true",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusCreated, code)
|
||||
|
||||
// test get
|
||||
code, metas, err := client.GetMeta(*admin, int64(1), models.ProMetaAutoScan)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
assert.Equal(t, "true", metas[models.ProMetaAutoScan])
|
||||
|
||||
// test put
|
||||
code, _, err = client.PutMeta(*admin, int64(1), models.ProMetaAutoScan,
|
||||
map[string]string{
|
||||
models.ProMetaAutoScan: "false",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
|
||||
code, metas, err = client.GetMeta(*admin, int64(1), models.ProMetaAutoScan)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
assert.Equal(t, "false", metas[models.ProMetaAutoScan])
|
||||
|
||||
// test delete
|
||||
code, _, err = client.DeleteMeta(*admin, int64(1), models.ProMetaAutoScan)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
|
||||
code, metas, err = client.GetMeta(*admin, int64(1), models.ProMetaAutoScan)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, code)
|
||||
}
|
@ -61,7 +61,14 @@ func (d *dao) Create(ctx context.Context, projectID int64, name, value string) (
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
return o.Insert(md)
|
||||
id, err := o.Insert(md)
|
||||
if err != nil {
|
||||
if e := orm.AsConflictError(err, "metadata %s already exists for project %d", name, projectID); e != nil {
|
||||
err = e
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Delete delete metadata interfaces filtered the query
|
||||
|
@ -63,6 +63,7 @@ func New() http.Handler {
|
||||
UserAPI: newUsersAPI(),
|
||||
HealthAPI: newHealthAPI(),
|
||||
StatisticAPI: newStatisticAPI(),
|
||||
ProjectMetadataAPI: newProjectMetadaAPI(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
160
src/server/v2.0/handler/project_metadata.go
Normal file
160
src/server/v2.0/handler/project_metadata.go
Normal file
@ -0,0 +1,160 @@
|
||||
// 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 handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/controller/project/metadata"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project_metadata"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func newProjectMetadaAPI() *projectMetadataAPI {
|
||||
return &projectMetadataAPI{
|
||||
ctl: metadata.Ctl,
|
||||
proCtl: project.Ctl,
|
||||
}
|
||||
}
|
||||
|
||||
type projectMetadataAPI struct {
|
||||
BaseAPI
|
||||
ctl metadata.Controller
|
||||
proCtl project.Controller
|
||||
}
|
||||
|
||||
func (p *projectMetadataAPI) AddProjectMetadatas(ctx context.Context, params operation.AddProjectMetadatasParams) middleware.Responder {
|
||||
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
|
||||
if err := p.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionCreate, rbac.ResourceMetadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
metadata := params.Metadata
|
||||
metadata, err := p.validate(metadata)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
project, err := p.proCtl.Get(ctx, projectNameOrID)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
if err = p.ctl.Add(ctx, project.ProjectID, metadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewAddProjectMetadatasOK()
|
||||
}
|
||||
|
||||
func (p *projectMetadataAPI) ListProjectMetadatas(ctx context.Context, params operation.ListProjectMetadatasParams) middleware.Responder {
|
||||
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
|
||||
if err := p.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionList, rbac.ResourceMetadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
project, err := p.proCtl.Get(ctx, projectNameOrID)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
metadata, err := p.ctl.Get(ctx, project.ProjectID)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewListProjectMetadatasOK().WithPayload(metadata)
|
||||
}
|
||||
|
||||
func (p *projectMetadataAPI) DeleteProjectMetadata(ctx context.Context, params operation.DeleteProjectMetadataParams) middleware.Responder {
|
||||
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
|
||||
if err := p.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionDelete, rbac.ResourceMetadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
project, err := p.proCtl.Get(ctx, projectNameOrID)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
if err = p.ctl.Delete(ctx, project.ProjectID, params.MetaName); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewDeleteProjectMetadataOK()
|
||||
}
|
||||
|
||||
func (p *projectMetadataAPI) GetProjectMetadata(ctx context.Context, params operation.GetProjectMetadataParams) middleware.Responder {
|
||||
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
|
||||
if err := p.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceMetadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
project, err := p.proCtl.Get(ctx, projectNameOrID)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
metadata, err := p.ctl.Get(ctx, project.ProjectID, params.MetaName)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewGetProjectMetadataOK().WithPayload(metadata)
|
||||
}
|
||||
|
||||
func (p *projectMetadataAPI) UpdateProjectMetadata(ctx context.Context, params operation.UpdateProjectMetadataParams) middleware.Responder {
|
||||
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
|
||||
if err := p.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionUpdate, rbac.ResourceMetadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
metadata := map[string]string{
|
||||
params.MetaName: params.Metadata[params.MetaName],
|
||||
}
|
||||
metadata, err := p.validate(metadata)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
project, err := p.proCtl.Get(ctx, projectNameOrID)
|
||||
if err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
if err = p.ctl.Update(ctx, project.ProjectID, metadata); err != nil {
|
||||
return p.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewUpdateProjectMetadataOK()
|
||||
}
|
||||
|
||||
func (p *projectMetadataAPI) validate(metas map[string]string) (map[string]string, error) {
|
||||
if len(metas) != 1 {
|
||||
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("only allow one key/value pair")
|
||||
}
|
||||
|
||||
key, value := "", ""
|
||||
for key, value = range metas {
|
||||
}
|
||||
|
||||
switch key {
|
||||
case models.ProMetaPublic, models.ProMetaEnableContentTrust,
|
||||
models.ProMetaPreventVul, models.ProMetaAutoScan:
|
||||
v, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid value: %s", value)
|
||||
}
|
||||
metas[key] = strconv.FormatBool(v)
|
||||
case models.ProMetaSeverity:
|
||||
severity := vuln.ParseSeverityVersion3(strings.ToLower(value))
|
||||
if severity == vuln.Unknown {
|
||||
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid value: %s", value)
|
||||
}
|
||||
metas[models.ProMetaSeverity] = strings.ToLower(severity.String())
|
||||
default:
|
||||
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid key: %s", key)
|
||||
}
|
||||
return metas, nil
|
||||
}
|
@ -24,8 +24,6 @@ import (
|
||||
func registerLegacyRoutes() {
|
||||
version := APIVersion
|
||||
beego.Router("/api/"+version+"/email/ping", &api.EmailAPI{}, "post:Ping")
|
||||
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")
|
||||
|
||||
// APIs for chart repository
|
||||
if config.WithChartMuseum() {
|
||||
|
Loading…
Reference in New Issue
Block a user