Merge pull request #14341 from heww/refactor-scanner-apis

refactor: generate scanner APIs by go-swagger
This commit is contained in:
Wenkai Yin(尹文开) 2021-03-04 17:48:04 +08:00 committed by GitHub
commit 4c2e698af8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1797 additions and 1575 deletions

View File

@ -2220,343 +2220,6 @@ paths:
description: User have no permission to delete immutable tags of the project.
'500':
description: Internal server errors.
'/scanners':
get:
summary: List scanner registrations
description: |
Returns a list of currently configured scanner registrations.
tags:
- Products
- Scanners
responses:
'200':
description: A list of scanner registrations.
schema:
type: array
items:
$ref: '#/definitions/ScannerRegistration'
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
'400':
description: Bad query paramters
'401':
description: Unauthorized request
'403':
description: Request is not allowed, system role required
'500':
description: Internal server error happened
parameters:
- name: page
in: query
type: integer
format: int32
required: false
description: The page number.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page.
post:
summary: Create a scanner registration
description: |
Creats a new scanner registration with the given data.
tags:
- Scanners
parameters:
- name: registration
in: body
description: A scanner registration to be created.
required: true
schema:
$ref: '#/definitions/ScannerRegistrationReq'
responses:
'201':
description: Created successfully
headers:
Location:
type: string
description: The URL of the created resource
'400':
description: Bad registration request
'401':
description: Unauthorized request
'403':
description: Request is not allowed, system role required
'500':
description: Internal server error happened
'/scanners/ping':
post:
summary: Tests scanner registration settings
description: |
Pings scanner adapter to test endpoint URL and authorization settings.
tags:
- Products
- Scanners
parameters:
- name: settings
in: body
description: A scanner registration settings to be tested.
required: true
schema:
$ref: '#/definitions/ScannerRegistrationSettings'
responses:
'200':
description: Test succeeded
'400':
description: Bad registration settings
'401':
description: Unauthorized request
'403':
description: Request is not allowed, system role required
'500':
description: Internal server error happened
'/scanners/{registration_id}':
get:
summary: Get a scanner registration details
description: |
Retruns the details of the specified scanner registration.
tags:
- Products
- Scanners
parameters:
- name: registration_id
in: path
description: The scanner registration identifer.
required: true
type: string
responses:
'200':
description: The details of the scanner registration.
schema:
$ref: '#/definitions/ScannerRegistration'
'401':
description: Unauthorized request
'403':
description: Request is not allowed, system role required
'404':
description: The requested object is not found
'500':
description: Internal server error happened
put:
summary: Update a scanner registration
description: |
Updates the specified scanner registration.
tags:
- Scanners
parameters:
- name: registration_id
in: path
description: The scanner registration identifier.
required: true
type: string
- name: registration
in: body
required: true
description: A scanner registraiton to be updated.
schema:
$ref: '#/definitions/ScannerRegistrationReq'
responses:
'200':
description: Updated successfully
'401':
description: Unauthorized request
'403':
description: Request is not allowed, system role required
'404':
description: The requested object is not found
'500':
description: Internal server error happened
delete:
summary: Delete a scanner registration
description: |
Deletes the specified scanner registration.
tags:
- Scanners
parameters:
- name: registration_id
in: path
description: The scanner registration identifier.
required: true
type: string
responses:
'200':
description: Deleted successfully and return the deleted registration
schema:
$ref: '#/definitions/ScannerRegistration'
'401':
description: Unauthorized request
'403':
description: Request is not allowed, system role required or registration is immutable
'404':
description: The requested object is not found
'500':
description: Internal server error happened
patch:
summary: Set system default scanner registration
description: |
Set the specified scanner registration as the system default one.
tags:
- Scanners
parameters:
- name: registration_id
in: path
description: The scanner registration identifier.
required: true
type: string
- name: payload
in: body
required: true
schema:
$ref: '#/definitions/IsDefault'
responses:
'200':
description: Successfully set the specified scanner registration as system default
'401':
description: Unauthorized request
'403':
description: Request is not allowed
'500':
description: Internal server error happened
'/scanners/{registration_id}/metadata':
get:
summary: Get the metadata of the specified scanner registration
description: |
Get the metadata of the specified scanner registration, including the capabilities and customzied properties.
tags:
- Products
- Scanners
parameters:
- name: registration_id
in: path
required: true
description: The scanner registration identifier.
type: string
responses:
'200':
description: The metadata of the specified scanner adapter
schema:
$ref: '#/definitions/ScannerAdapterMetadata'
'401':
description: Unauthorized request
'403':
description: Request is not allowed
'500':
description: Internal server error happened
'/projects/{project_id}/scanner':
get:
summary: Get project level scanner
description: Get the scanner registration of the specified project. If no scanner registration is configured for the specified project, the system default scanner registration will be returned.
tags:
- Products
- Scanners
parameters:
- name: project_id
in: path
required: true
description: The project identifier.
type: integer
format: int64
responses:
'200':
description: The details of the scanner registration.
schema:
$ref: '#/definitions/ScannerRegistration'
'400':
description: Bad project ID
'401':
description: Unauthorized request
'403':
description: Request is not allowed
'404':
description: The requested object is not found
'500':
description: Internal server error happened
put:
summary: Configure scanner for the specified project
description: Set one of the system configured scanner registration as the indepndent scanner of the specified project.
tags:
- Scanners
parameters:
- name: project_id
in: path
required: true
description: The project identifier.
type: integer
format: int64
- name: payload
in: body
required: true
schema:
$ref: '#/definitions/ProjectScanner'
responses:
'200':
description: Successfully set the project level scanner
'400':
description: Bad project ID
'401':
description: Unauthorized request
'403':
description: Request is not allowed
'404':
description: The requested object is not found
'500':
description: Internal server error happened
'/projects/{project_id}/scanner/candidates':
get:
summary: Get scanner registration candidates for configurating project level scanner
description: |
Retrieve the system configured scanner registrations as candidates of setting project level scanner.
tags:
- Products
- Scanners
parameters:
- name: page
in: query
type: integer
format: int32
required: false
description: The page number.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page.
- name: project_id
in: path
required: true
description: The project identifier.
type: integer
format: int64
responses:
'200':
description: A list of scanner registrations.
schema:
type: array
items:
$ref: '#/definitions/ScannerRegistration'
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
'400':
description: Bad project ID or query parameters
'401':
description: Unauthorized request
'403':
description: Request is not allowed
'500':
description: Internal server error happened
responses:
OK:
description: 'Success'
@ -3769,199 +3432,6 @@ definitions:
enabled:
type: boolean
description: The quota is enable or disable
ScannerRegistration:
type: object
description: |
Registration represents a named configuration for invoking a scanner via its adapter.
properties:
uuid:
type: string
description: The unique identifier of this registration.
name:
type: string
example: Clair
description: The name of this registration.
description:
type: string
description: An optional description of this registration.
example: |
A free-to-use tool that scans container images for package vulnerabilities.
url:
type: string
format: url
description: A base URL of the scanner adapter
example: http://harbor-scanner-clair:8080
disabled:
type: boolean
default: false
description: Indicate whether the registration is enabled or not
is_default:
type: boolean
default: false
description: Indicate if the registration is set as the system default one
health:
type: string
default: ""
description: Indicate the healthy of the registration
example: "healthy"
auth:
type: string
default: ""
description: |
Specify what authentication approach is adopted for the HTTP communications.
Supported types Basic", "Bearer" and api key header "X-ScannerAdapter-API-Key"
example: "Bearer"
access_credential:
type: string
description: |
An optional value of the HTTP Authorization header sent with each request to the Scanner Adapter API.
example: "Bearer: JWTTOKENGOESHERE"
skip_certVerify:
type: boolean
default: false
description: Indicate if skip the certificate verification when sending HTTP requests
use_internal_addr:
type: boolean
default: false
description: Indicate whether use internal registry addr for the scanner to pull content or not
adapter:
type: string
description: Optional property to describe the name of the scanner registration
example: "Clair"
vendor:
type: string
description: Optional property to describe the vendor of the scanner registration
example: "CentOS"
version:
type: string
description: Optional property to describe the version of the scanner registration
example: "1.0.1"
ScannerRegistrationReq:
type: object
properties:
name:
type: string
description: The name of this registration
example: Clair
description:
type: string
description: An optional description of this registration.
example: |
A free-to-use tool that scans container images for package vulnerabilities.
url:
type: string
format: url
description: A base URL of the scanner adapter.
example: http://harbor-scanner-clair:8080
auth:
type: string
default: ""
description: |
Specify what authentication approach is adopted for the HTTP communications.
Supported types Basic", "Bearer" and api key header "X-ScannerAdapter-API-Key"
example: "Bearer"
access_credential:
type: string
description: |
An optional value of the HTTP Authorization header sent with each request to the Scanner Adapter API.
example: "Bearer: JWTTOKENGOESHERE"
skip_certVerify:
type: boolean
default: false
description: Indicate if skip the certificate verification when sending HTTP requests
use_internal_addr:
type: boolean
default: false
description: Indicate whether use internal registry addr for the scanner to pull content or not
disabled:
type: boolean
default: false
description: Indicate whether the registration is enabled or not
ScannerRegistrationSettings:
type: object
properties:
name:
type: string
description: The name of this registration
example: Clair
url:
type: string
format: url
description: A base URL of the scanner adapter.
example: http://harbor-scanner-clair:8080
auth:
type: string
default: ""
description: |
Specify what authentication approach is adopted for the HTTP communications.
Supported types Basic", "Bearer" and api key header "X-ScannerAdapter-API-Key"
access_credential:
type: string
description: |
An optional value of the HTTP Authorization header sent with each request to the Scanner Adapter API.
example: "Bearer: JWTTOKENGOESHERE"
IsDefault:
type: object
properties:
is_default:
type: boolean
description: A flag indicating whether a scanner registration is default.
Scanner:
type: object
properties:
name:
type: string
description: Name of the scanner
example: "Clair"
vendor:
type: string
description: Name of the scanner provider
example: "CentOS"
version:
type: string
description: Version of the scanner adapter
example: "1.0.1"
ScannerCapability:
type: object
properties:
consumes_mime_types:
type: array
items:
type: string
example: "application/vnd.docker.distribution.manifest.v2+json"
produces_mime_types:
type: array
items:
type: string
example: "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
ScannerAdapterMetadata:
type: object
description: The metadata info of the scanner adapter
properties:
name:
$ref: '#/definitions/Scanner'
capabilities:
type: array
items:
$ref: '#/definitions/ScannerCapability'
properties:
type: object
additionalProperties:
type: string
example:
'harbor.scanner-adapter/registry-authorization-type': 'Bearer'
ProjectScanner:
type: object
properties:
uuid:
type: string
description: The identifier of the scanner registration
SupportedWebhookEventTypes:
type: object

View File

@ -834,6 +834,97 @@ paths:
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/scanner':
get:
summary: Get project level scanner
description: Get the scanner registration of the specified project. If no scanner registration is configured for the specified project, the system default scanner registration will be returned.
tags:
- project
operationId: getScannerOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
description: The details of the scanner registration.
schema:
$ref: '#/definitions/ScannerRegistration'
'400':
description: Bad project ID
'401':
description: Unauthorized request
'403':
description: Request is not allowed
'404':
description: The requested object is not found
'500':
description: Internal server error happened
put:
summary: Configure scanner for the specified project
description: Set one of the system configured scanner registration as the indepndent scanner of the specified project.
tags:
- project
operationId: setScannerOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- name: payload
in: body
required: true
schema:
$ref: '#/definitions/ProjectScanner'
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'
'/projects/{project_name_or_id}/scanner/candidates':
get:
summary: Get scanner registration candidates for configurating project level scanner
description: Retrieve the system configured scanner registrations as candidates of setting project level scanner.
tags:
- project
operationId: listScannerCandidatesOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
responses:
'200':
description: A list of scanner registrations.
schema:
type: array
items:
$ref: '#/definitions/ScannerRegistration'
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
/audit-logs:
get:
summary: Get recent logs of the projects which the user is a member of
@ -2764,6 +2855,239 @@ paths:
'500':
$ref: '#/responses/500'
'/scanners':
get:
summary: List scanner registrations
description: |
Returns a list of currently configured scanner registrations.
tags:
- scanner
operationId: listScanners
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
responses:
'200':
description: A list of scanner registrations.
schema:
type: array
items:
$ref: '#/definitions/ScannerRegistration'
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
post:
summary: Create a scanner registration
description: |
Creats a new scanner registration with the given data.
tags:
- scanner
operationId: createScanner
parameters:
- $ref: '#/parameters/requestId'
- name: registration
in: body
description: A scanner registration to be created.
required: true
schema:
$ref: '#/definitions/ScannerRegistrationReq'
responses:
'201':
description: Created successfully
headers:
Location:
type: string
description: The URL of the created resource
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/scanners/ping':
post:
summary: Tests scanner registration settings
description: |
Pings scanner adapter to test endpoint URL and authorization settings.
tags:
- scanner
operationId: pingScanner
parameters:
- $ref: '#/parameters/requestId'
- name: settings
in: body
description: A scanner registration settings to be tested.
required: true
schema:
$ref: '#/definitions/ScannerRegistrationSettings'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/scanners/{registration_id}':
get:
summary: Get a scanner registration details
description: |
Retruns the details of the specified scanner registration.
tags:
- scanner
operationId: getScanner
parameters:
- $ref: '#/parameters/requestId'
- name: registration_id
in: path
description: The scanner registration identifer.
required: true
type: string
responses:
'200':
description: The details of the scanner registration.
schema:
$ref: '#/definitions/ScannerRegistration'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
put:
summary: Update a scanner registration
description: |
Updates the specified scanner registration.
tags:
- scanner
operationId: updateScanner
parameters:
- $ref: '#/parameters/requestId'
- name: registration_id
in: path
description: The scanner registration identifier.
required: true
type: string
- name: registration
in: body
required: true
description: A scanner registraiton to be updated.
schema:
$ref: '#/definitions/ScannerRegistrationReq'
responses:
'200':
$ref: '#/responses/200'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
delete:
summary: Delete a scanner registration
description: |
Deletes the specified scanner registration.
tags:
- scanner
operationId: deleteScanner
parameters:
- $ref: '#/parameters/requestId'
- name: registration_id
in: path
description: The scanner registration identifier.
required: true
type: string
responses:
'200':
description: Deleted successfully and return the deleted registration
schema:
$ref: '#/definitions/ScannerRegistration'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
patch:
summary: Set system default scanner registration
description: |
Set the specified scanner registration as the system default one.
tags:
- scanner
operationId: setScannerAsDefault
parameters:
- name: registration_id
in: path
description: The scanner registration identifier.
required: true
type: string
- name: payload
in: body
required: true
schema:
$ref: '#/definitions/IsDefault'
responses:
'200':
description: Successfully set the specified scanner registration as system default
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/scanners/{registration_id}/metadata':
get:
summary: Get the metadata of the specified scanner registration
description: |
Get the metadata of the specified scanner registration, including the capabilities and customized properties.
tags:
- scanner
operationId: getScannerMetadata
parameters:
- $ref: '#/parameters/requestId'
- name: registration_id
in: path
required: true
description: The scanner registration identifier.
type: string
responses:
'200':
description: The metadata of the specified scanner adapter
schema:
$ref: '#/definitions/ScannerAdapterMetadata'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
parameters:
query:
name: q
@ -2853,6 +3177,12 @@ parameters:
description: The size of per page
default: 10
maximum: 100
sort:
name: sort
in: query
type: string
required: false
description: The order by fields of the query, the format is '+field1,-field2'.
instanceName:
name: preheat_instance_name
in: path
@ -3705,6 +4035,14 @@ definitions:
used:
$ref: "#/definitions/ResourceList"
description: The used status of the quota
ProjectScanner:
type: object
required:
- uuid
properties:
uuid:
type: string
description: The identifier of the scanner registration
CVEAllowlist:
type: object
description: The CVE Allowlist for system or project
@ -4442,15 +4780,18 @@ definitions:
retained:
type: integer
x-omitempty: false
QuotaUpdateReq:
type: object
properties:
hard:
$ref: "#/definitions/ResourceList"
description: The new hard limits for the quota
QuotaRefObject:
type: object
additionalProperties: {}
Quota:
type: object
description: The quota object
@ -4477,3 +4818,200 @@ definitions:
type: string
format: date-time
description: the update time of the quota
ScannerRegistration:
type: object
description: |
Registration represents a named configuration for invoking a scanner via its adapter.
properties:
uuid:
type: string
description: The unique identifier of this registration.
name:
type: string
example: Trivy
description: The name of this registration.
description:
type: string
description: An optional description of this registration.
example: |
A free-to-use tool that scans container images for package vulnerabilities.
x-omitempty: false
url:
type: string
format: url
description: A base URL of the scanner adapter
example: http://harbor-scanner-trivy:8080
disabled:
type: boolean
default: false
description: Indicate whether the registration is enabled or not
x-omitempty: false
is_default:
type: boolean
default: false
description: Indicate if the registration is set as the system default one
x-omitempty: false
auth:
type: string
default: ""
description: |
Specify what authentication approach is adopted for the HTTP communications.
Supported types Basic", "Bearer" and api key header "X-ScannerAdapter-API-Key"
example: "Bearer"
x-omitempty: false
access_credential:
type: string
description: |
An optional value of the HTTP Authorization header sent with each request to the Scanner Adapter API.
example: "Bearer: JWTTOKENGOESHERE"
x-omitempty: false
skip_certVerify:
type: boolean
default: false
description: Indicate if skip the certificate verification when sending HTTP requests
x-omitempty: false
use_internal_addr:
type: boolean
default: false
description: Indicate whether use internal registry addr for the scanner to pull content or not
x-omitempty: false
create_time:
type: string
format: date-time
description: The creation time of this registration
update_time:
type: string
format: date-time
description: The update time of this registration
adapter:
type: string
description: Optional property to describe the name of the scanner registration
example: "Clair"
vendor:
type: string
description: Optional property to describe the vendor of the scanner registration
example: "CentOS"
version:
type: string
description: Optional property to describe the version of the scanner registration
example: "1.0.1"
health:
type: string
default: ""
description: Indicate the healthy of the registration
example: "healthy"
ScannerRegistrationReq:
type: object
required:
- name
- url
properties:
name:
type: string
description: The name of this registration
example: Trivy
description:
type: string
description: An optional description of this registration.
example: |
A free-to-use tool that scans container images for package vulnerabilities.
url:
type: string
format: uri
description: A base URL of the scanner adapter.
example: http://harbor-scanner-trivy:8080
auth:
type: string
description: |
Specify what authentication approach is adopted for the HTTP communications.
Supported types Basic", "Bearer" and api key header "X-ScannerAdapter-API-Key"
example: "Bearer"
access_credential:
type: string
description: |
An optional value of the HTTP Authorization header sent with each request to the Scanner Adapter API.
example: "Bearer: JWTTOKENGOESHERE"
skip_certVerify:
type: boolean
default: false
description: Indicate if skip the certificate verification when sending HTTP requests
use_internal_addr:
type: boolean
default: false
description: Indicate whether use internal registry addr for the scanner to pull content or not
disabled:
type: boolean
default: false
description: Indicate whether the registration is enabled or not
ScannerRegistrationSettings:
type: object
required:
- name
- url
properties:
name:
type: string
description: The name of this registration
example: Trivy
url:
type: string
format: uri
description: A base URL of the scanner adapter.
example: http://harbor-scanner-trivy:8080
auth:
type: string
default: ""
description: |
Specify what authentication approach is adopted for the HTTP communications.
Supported types Basic", "Bearer" and api key header "X-ScannerAdapter-API-Key"
access_credential:
type: string
description: |
An optional value of the HTTP Authorization header sent with each request to the Scanner Adapter API.
example: "Bearer: JWTTOKENGOESHERE"
IsDefault:
type: object
properties:
is_default:
type: boolean
description: A flag indicating whether a scanner registration is default.
ScannerCapability:
type: object
properties:
consumes_mime_types:
type: array
items:
type: string
example: "application/vnd.docker.distribution.manifest.v2+json"
produces_mime_types:
type: array
items:
type: string
example: "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
ScannerAdapterMetadata:
type: object
description: The metadata info of the scanner adapter
properties:
scanner:
$ref: '#/definitions/Scanner'
capabilities:
type: array
items:
$ref: '#/definitions/ScannerCapability'
properties:
type: object
additionalProperties:
type: string
example:
'harbor.scanner-adapter/registry-authorization-type': 'Bearer'
x-go-type:
type: ScannerAdapterMetadata
import:
package: "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
alias: v1

View File

@ -65,6 +65,11 @@ func (bc *basicController) ListRegistrations(ctx context.Context, query *q.Query
return l, nil
}
// Count returns the total count of scanner registrations according to the query.
func (bc *basicController) GetTotalOfRegistrations(ctx context.Context, query *q.Query) (int64, error) {
return bc.manager.Count(ctx, query)
}
// CreateRegistration ...
func (bc *basicController) CreateRegistration(ctx context.Context, registration *scanner.Registration) (string, error) {
if isReservedName(registration.Name) {
@ -324,6 +329,10 @@ func (bc *basicController) GetMetadata(ctx context.Context, registrationUUID str
return nil, errors.Wrap(err, "scanner controller: get metadata")
}
if r == nil {
return nil, errors.NotFoundError(nil).WithMessage("registration %s not found", registrationUUID)
}
return bc.Ping(ctx, r)
}

View File

@ -22,6 +22,9 @@ import (
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// Registration ...
type Registration = scanner.Registration
// Controller provides the related operations of scanner for the upper API.
// All the capabilities of the scanner are defined here.
type Controller interface {
@ -37,6 +40,9 @@ type Controller interface {
// error : non nil error if any errors occurred
ListRegistrations(ctx context.Context, query *q.Query) ([]*scanner.Registration, error)
// GetTotalOfRegistrations returns the total count of scanner registrations according to the query.
GetTotalOfRegistrations(ctx context.Context, query *q.Query) (int64, error)
// CreateRegistration creates a new scanner registration with the given data.
// Returns the scanner registration identifier.
//

View File

@ -160,18 +160,6 @@ func init() {
beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota")
beego.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota")
// Add routes for plugin scanner management
scannerAPI := &ScannerAPI{}
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/scanners/:uuid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
beego.Router("/api/scanners/:uuid/metadata", scannerAPI, "get:Metadata")
beego.Router("/api/scanners/ping", scannerAPI, "post:Ping")
// Add routes for project level scanner
proScannerAPI := &ProjectScannerAPI{}
beego.Router("/api/projects/:pid([0-9]+)/scanner", proScannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
beego.Router("/api/projects/:pid([0-9]+)/scanner/candidates", proScannerAPI, "get:GetProScannerCandidates")
// Init user Info
admin = &usrInfo{adminName, adminPwd}
unknownUsr = &usrInfo{"unknown", "unknown"}

View File

@ -1,143 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
)
// ProjectScannerAPI provides rest API for managing the project level scanner(s).
type ProjectScannerAPI struct {
// The base controller to provide common utilities
BaseController
// Scanner controller for operating scanner registrations.
c scanner.Controller
// ID of the project
pid int64
}
// Prepare sth. for the subsequent actions
func (sa *ProjectScannerAPI) Prepare() {
// Call super prepare method
sa.BaseController.Prepare()
// Check access permissions
if !sa.RequireAuthenticated() {
return
}
// Get ID of the project
pid, err := sa.GetInt64FromPath(":pid")
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "project scanner API"))
return
}
// Check if the project exists
exists, err := sa.ProjectCtl.Exists(sa.Context(), pid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "project scanner API"))
return
}
if !exists {
sa.SendNotFoundError(errors.Errorf("project with id %d", sa.pid))
return
}
sa.pid = pid
sa.c = scanner.DefaultController
}
// GetProjectScanner gets the project level scanner
func (sa *ProjectScannerAPI) GetProjectScanner() {
// Check access permissions
if !sa.RequireProjectAccess(sa.pid, rbac.ActionRead, rbac.ResourceScanner) {
return
}
r, err := sa.c.GetRegistrationByProject(sa.Context(), sa.pid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get project scanners"))
return
}
if r != nil {
sa.Data["json"] = r
} else {
sa.Data["json"] = make(map[string]interface{})
}
sa.ServeJSON()
}
// SetProjectScanner sets the project level scanner
func (sa *ProjectScannerAPI) SetProjectScanner() {
// Check access permissions
if !sa.RequireProjectAccess(sa.pid, rbac.ActionCreate, rbac.ResourceScanner) {
return
}
body := make(map[string]string)
if err := sa.DecodeJSONReq(&body); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
uuid, ok := body["uuid"]
if !ok || len(uuid) == 0 {
sa.SendBadRequestError(errors.New("missing scanner uuid when setting project scanner"))
return
}
if err := sa.c.SetRegistrationByProject(sa.Context(), sa.pid, uuid); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
}
// GetProScannerCandidates gets the candidates for setting project level scanner.
func (sa *ProjectScannerAPI) GetProScannerCandidates() {
// Check access permissions
// Same permission with project level scanner set action
if !sa.RequireProjectAccess(sa.pid, rbac.ActionCreate, rbac.ResourceScanner) {
return
}
p, pz, err := sa.GetPaginationParams()
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: get project scanner candidates"))
return
}
query := &q.Query{
PageSize: pz,
PageNumber: p,
}
all, err := sa.c.ListRegistrations(sa.Context(), query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get project scanner candidates"))
return
}
// Response to the client
sa.Data["json"] = all
sa.ServeJSON()
}

View File

@ -1,127 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"testing"
sc "github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ProScannerAPITestSuite is test suite for testing the project scanner API
type ProScannerAPITestSuite struct {
suite.Suite
originC sc.Controller
mockC *scannertesting.Controller
}
// TestProScannerAPI is the entry of ProScannerAPITestSuite
func TestProScannerAPI(t *testing.T) {
suite.Run(t, new(ProScannerAPITestSuite))
}
// SetupSuite prepares testing env
func (suite *ProScannerAPITestSuite) SetupTest() {
suite.originC = sc.DefaultController
m := &scannertesting.Controller{}
sc.DefaultController = m
suite.mockC = m
}
// TearDownTest clears test case env
func (suite *ProScannerAPITestSuite) TearDownTest() {
// Restore
sc.DefaultController = suite.originC
}
// TestScannerAPIProjectScanner tests the API of getting/setting project level scanner
func (suite *ProScannerAPITestSuite) TestScannerAPIProjectScanner() {
suite.mockC.On("SetRegistrationByProject", mock.Anything, int64(1), "uuid").Return(nil)
// Set
body := make(map[string]interface{}, 1)
body["uuid"] = "uuid"
runCodeCheckingCases(suite.T(), &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
method: http.MethodPut,
credential: projAdmin,
bodyJSON: body,
},
code: http.StatusOK,
})
r := &scanner.Registration{
ID: 1004,
UUID: "uuid",
Name: "TestScannerAPIProjectScanner",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}
suite.mockC.On("GetRegistrationByProject", mock.Anything, int64(1)).Return(r, nil)
// Get
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
method: http.MethodGet,
credential: projAdmin,
}, rr)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), r.Name, rr.Name)
assert.Equal(suite.T(), r.UUID, rr.UUID)
}
// TestScannerAPIGetScannerCandidates ...
func (suite *ProScannerAPITestSuite) TestScannerAPIGetScannerCandidates() {
query := &q.Query{
PageNumber: 1,
PageSize: 500,
}
ll := []*scanner.Registration{
{
ID: 1005,
UUID: "uuid",
Name: "TestScannerAPIGetScannerCandidates",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}}
suite.mockC.On("ListRegistrations", mock.Anything, query).Return(ll, nil)
// Get
l := make([]*scanner.Registration, 0)
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner/candidates", 1),
method: http.MethodGet,
credential: projAdmin,
}, &l)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), "uuid", l[0].UUID)
}

View File

@ -1,368 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/system"
"github.com/goharbor/harbor/src/pkg/permission/types"
s "github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
)
// ScannerAPI provides the API for managing the plugin scanners
type ScannerAPI struct {
// The base controller to provide common utilities
BaseController
// Controller for the plug scanners
c s.Controller
resource types.Resource
}
// Prepare sth. for the subsequent actions
func (sa *ScannerAPI) Prepare() {
// Call super prepare method
sa.BaseController.Prepare()
// Check access permissions
if !sa.SecurityCtx.IsAuthenticated() {
sa.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
sa.resource = system.NewNamespace().Resource(rbac.ResourceScanner)
// Use the default controller
sa.c = s.DefaultController
}
// Get the specified scanner
func (sa *ScannerAPI) Get() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionRead, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
if r := sa.get(); r != nil {
// Response to the client
sa.Data["json"] = r
sa.ServeJSON()
}
}
// Metadata returns the metadata of the given scanner.
func (sa *ScannerAPI) Metadata() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionRead, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
uuid := sa.GetStringFromPath(":uuid")
meta, err := sa.c.GetMetadata(sa.Context(), uuid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get metadata"))
return
}
// Response to the client
sa.Data["json"] = meta
sa.ServeJSON()
}
// List all the scanners
func (sa *ScannerAPI) List() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionList, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
p, pz, err := sa.GetPaginationParams()
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: list all"))
return
}
query := &q.Query{
PageSize: pz,
PageNumber: p,
}
// Get query key words
kws := make(map[string]interface{})
properties := []string{"name", "description", "url", "ex_name", "ex_url"}
for _, k := range properties {
kw := sa.GetString(k)
if len(kw) > 0 {
kws[k] = kw
}
}
if len(kws) > 0 {
query.Keywords = kws
}
all, err := sa.c.ListRegistrations(sa.Context(), query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: list all"))
return
}
// Response to the client
sa.Data["json"] = all
sa.ServeJSON()
}
// Create a new scanner
func (sa *ScannerAPI) Create() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionCreate, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
r := &scanner.Registration{}
if err := sa.DecodeJSONReq(r); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
return
}
if err := r.Validate(false); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
return
}
// Explicitly check if conflict
if !sa.checkDuplicated("name", r.Name) ||
!sa.checkDuplicated("url", r.URL) {
return
}
// All newly created should be non default one except the 1st one
r.IsDefault = false
uuid, err := sa.c.CreateRegistration(sa.Context(), r)
if err != nil {
sa.SendError(errors.Wrap(err, "scanner API: create"))
return
}
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, uuid)
sa.Ctx.ResponseWriter.Header().Add("Location", location)
resp := make(map[string]string, 1)
resp["uuid"] = uuid
// Response to the client
sa.Ctx.ResponseWriter.WriteHeader(http.StatusCreated)
sa.Data["json"] = resp
sa.ServeJSON()
}
// Update a scanner
func (sa *ScannerAPI) Update() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionUpdate, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
r := sa.get()
if r == nil {
// meet error
return
}
// Immutable registration is not allowed
if r.Immutable {
sa.SendForbiddenError(errors.Errorf("registration %s is not allowed to update as it is immutable: scanner API: update", r.Name))
return
}
// full dose updated
rr := &scanner.Registration{}
if err := sa.DecodeJSONReq(rr); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
return
}
if err := r.Validate(true); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
return
}
// Name changed?
if r.Name != rr.Name {
if !sa.checkDuplicated("name", rr.Name) {
return
}
}
// URL changed?
if r.URL != rr.URL {
if !sa.checkDuplicated("url", rr.URL) {
return
}
}
getChanges(r, rr)
if err := sa.c.UpdateRegistration(sa.Context(), r); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: update"))
return
}
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, r.UUID)
sa.Ctx.ResponseWriter.Header().Add("Location", location)
// Response to the client
sa.Data["json"] = r
sa.ServeJSON()
}
// Delete the scanner
func (sa *ScannerAPI) Delete() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionDelete, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
r := sa.get()
if r == nil {
// meet error
return
}
// Immutable registration is not allowed
if r.Immutable {
sa.SendForbiddenError(errors.Errorf("registration %s is not allowed to delete as it is immutable: scanner API: delete", r.Name))
return
}
deleted, err := sa.c.DeleteRegistration(sa.Context(), r.UUID)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: delete"))
return
}
sa.Data["json"] = deleted
sa.ServeJSON()
}
// SetAsDefault sets the given registration as default one
func (sa *ScannerAPI) SetAsDefault() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionCreate, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
uid := sa.GetStringFromPath(":uuid")
m := make(map[string]interface{})
if err := sa.DecodeJSONReq(&m); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set as default"))
return
}
if v, ok := m["is_default"]; ok {
if isDefault, y := v.(bool); y && isDefault {
if err := sa.c.SetDefaultRegistration(sa.Context(), uid); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set as default"))
}
return
}
}
// Not supported
sa.SendForbiddenError(errors.Errorf("not supported: %#v", m))
}
// Ping the registration.
func (sa *ScannerAPI) Ping() {
if !sa.SecurityCtx.Can(sa.Context(), rbac.ActionRead, sa.resource) {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
r := &scanner.Registration{}
if err := sa.DecodeJSONReq(r); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: ping"))
return
}
if err := r.Validate(false); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: ping"))
return
}
if _, err := sa.c.Ping(sa.Context(), r); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: ping"))
return
}
}
// get the specified scanner
func (sa *ScannerAPI) get() *scanner.Registration {
uid := sa.GetStringFromPath(":uuid")
r, err := sa.c.GetRegistration(sa.Context(), uid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get"))
return nil
}
if r == nil {
// NOT found
sa.SendNotFoundError(errors.Errorf("scanner: %s", uid))
return nil
}
return r
}
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
// Explicitly check if conflict
kw := make(map[string]interface{})
kw[property] = value
query := &q.Query{
Keywords: kw,
}
l, err := sa.c.ListRegistrations(sa.Context(), query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: check existence"))
return false
}
if len(l) > 0 {
sa.SendConflictError(errors.Errorf("duplicated entries: %s:%s", property, value))
return false
}
return true
}
func getChanges(e *scanner.Registration, eChange *scanner.Registration) {
e.Name = eChange.Name
e.Description = eChange.Description
e.URL = eChange.URL
e.Auth = eChange.Auth
e.AccessCredential = eChange.AccessCredential
e.Disabled = eChange.Disabled
e.SkipCertVerify = eChange.SkipCertVerify
e.UseInternalAddr = eChange.UseInternalAddr
}

View File

@ -1,276 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"testing"
sc "github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
const (
rootRoute = "/api/scanners"
)
// ScannerAPITestSuite is test suite for testing the scanner API
type ScannerAPITestSuite struct {
suite.Suite
originC sc.Controller
mockC *scannertesting.Controller
}
// TestScannerAPI is the entry of ScannerAPITestSuite
func TestScannerAPI(t *testing.T) {
suite.Run(t, new(ScannerAPITestSuite))
}
// SetupSuite prepares testing env
func (suite *ScannerAPITestSuite) SetupTest() {
suite.originC = sc.DefaultController
m := &scannertesting.Controller{}
sc.DefaultController = m
suite.mockC = m
}
// TearDownTest clears test case env
func (suite *ScannerAPITestSuite) TearDownTest() {
// Restore
sc.DefaultController = suite.originC
}
// TestScannerAPICreate tests the post request to create new one
func (suite *ScannerAPITestSuite) TestScannerAPIBase() {
// Including general cases
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 400
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: sysAdmin,
bodyJSON: &scanner.Registration{
URL: "http://a.b.c",
},
},
code: http.StatusBadRequest,
},
}
runCodeCheckingCases(suite.T(), cases...)
}
// TestScannerAPIGet tests api get
func (suite *ScannerAPITestSuite) TestScannerAPIGet() {
res := &scanner.Registration{
ID: 1000,
UUID: "uuid",
Name: "TestScannerAPIGet",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}
suite.mockC.On("GetRegistration", mock.Anything, "uuid").Return(res, nil)
// Get
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodGet,
credential: sysAdmin,
}, rr)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), rr)
assert.Equal(suite.T(), res.Name, rr.Name)
assert.Equal(suite.T(), res.UUID, rr.UUID)
}
// TestScannerAPICreate tests create.
func (suite *ScannerAPITestSuite) TestScannerAPICreate() {
r := &scanner.Registration{
Name: "TestScannerAPICreate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}
suite.mockQuery(r)
suite.mockC.On("CreateRegistration", mock.Anything, r).Return("uuid", nil)
// Create
res := make(map[string]string, 1)
err := handleAndParse(
&testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: sysAdmin,
bodyJSON: r,
}, &res)
require.NoError(suite.T(), err)
require.Condition(suite.T(), func() (success bool) {
success = res["uuid"] == "uuid"
return
})
}
// TestScannerAPIList tests list
func (suite *ScannerAPITestSuite) TestScannerAPIList() {
query := &q.Query{
PageNumber: 1,
PageSize: 500,
}
ll := []*scanner.Registration{
{
ID: 1001,
UUID: "uuid",
Name: "TestScannerAPIList",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}}
suite.mockC.On("ListRegistrations", mock.Anything, query).Return(ll, nil)
// List
l := make([]*scanner.Registration, 0)
err := handleAndParse(&testingRequest{
url: rootRoute,
method: http.MethodGet,
credential: sysAdmin,
}, &l)
require.NoError(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = len(l) > 0 && l[0].Name == ll[0].Name
return
})
}
// TestScannerAPIUpdate tests the update API
func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() {
before := &scanner.Registration{
ID: 1002,
UUID: "uuid",
Name: "TestScannerAPIUpdate_before",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}
updated := &scanner.Registration{
ID: 1002,
UUID: "uuid",
Name: "TestScannerAPIUpdate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}
suite.mockQuery(updated)
suite.mockC.On("UpdateRegistration", mock.Anything, updated).Return(nil)
suite.mockC.On("GetRegistration", mock.Anything, "uuid").Return(before, nil)
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodPut,
credential: sysAdmin,
bodyJSON: updated,
}, rr)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), rr)
assert.Equal(suite.T(), updated.Name, rr.Name)
assert.Equal(suite.T(), updated.UUID, rr.UUID)
}
//
func (suite *ScannerAPITestSuite) TestScannerAPIDelete() {
r := &scanner.Registration{
ID: 1003,
UUID: "uuid",
Name: "TestScannerAPIDelete",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
}
suite.mockC.On("GetRegistration", mock.Anything, "uuid").Return(r, nil)
suite.mockC.On("DeleteRegistration", mock.Anything, "uuid").Return(r, nil)
deleted := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodDelete,
credential: sysAdmin,
}, deleted)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), r.UUID, deleted.UUID)
assert.Equal(suite.T(), r.Name, deleted.Name)
}
// TestScannerAPISetDefault tests the set default
func (suite *ScannerAPITestSuite) TestScannerAPISetDefault() {
suite.mockC.On("SetDefaultRegistration", mock.Anything, "uuid").Return(nil)
body := make(map[string]interface{}, 1)
body["is_default"] = true
runCodeCheckingCases(suite.T(), &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodPatch,
credential: sysAdmin,
bodyJSON: body,
},
code: http.StatusOK,
})
}
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
kw := make(map[string]interface{}, 1)
kw["name"] = r.Name
query := &q.Query{
Keywords: kw,
}
emptyL := make([]*scanner.Registration, 0)
suite.mockC.On("ListRegistrations", mock.Anything, query).Return(emptyL, nil)
kw2 := make(map[string]interface{}, 1)
kw2["url"] = r.URL
query2 := &q.Query{
Keywords: kw2,
}
suite.mockC.On("ListRegistrations", mock.Anything, query2).Return(emptyL, nil)
}

View File

@ -16,8 +16,6 @@ package scanner
import (
"context"
"fmt"
"strings"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
@ -28,6 +26,20 @@ func init() {
orm.RegisterModel(new(Registration))
}
// GetTotalOfRegistrations returns the total count of scanner registrations according to the query.
func GetTotalOfRegistrations(ctx context.Context, query *q.Query) (int64, error) {
query = q.MustClone(query)
query.Sorting = ""
query.PageNumber = 0
query.PageSize = 0
qs, err := orm.QuerySetter(ctx, &Registration{}, query)
if err != nil {
return 0, err
}
return qs.Count()
}
// AddRegistration adds a new registration
func AddRegistration(ctx context.Context, r *Registration) (int64, error) {
o, err := orm.FromContext(ctx)
@ -108,39 +120,22 @@ func DeleteRegistration(ctx context.Context, UUID string) error {
// ListRegistrations lists all the existing registrations
func ListRegistrations(ctx context.Context, query *q.Query) ([]*Registration, error) {
o, err := orm.FromContext(ctx)
query = q.MustClone(query)
qs, err := orm.QuerySetter(ctx, &Registration{}, query)
if err != nil {
return nil, err
}
qt := o.QueryTable(new(Registration))
if query != nil {
if len(query.Keywords) > 0 {
for k, v := range query.Keywords {
if strings.HasPrefix(k, "ex_") {
kk := strings.TrimPrefix(k, "ex_")
qt = qt.Filter(kk, v)
continue
}
if s, ok := v.(string); ok {
v = orm.Escape(s)
}
qt = qt.Filter(fmt.Sprintf("%s__icontains", k), v)
}
}
if query.PageNumber > 0 && query.PageSize > 0 {
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
// Order the list
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
} else {
qs = qs.OrderBy("-is_default", "-create_time")
}
// Order the list
qt = qt.OrderBy("-is_default", "-create_time")
l := make([]*Registration, 0)
_, err = qt.All(&l)
_, err = qs.All(&l)
return l, err
}
@ -164,7 +159,7 @@ func SetDefaultRegistration(ctx context.Context, UUID string) error {
return err
}
if count == 0 {
return errors.Errorf("set default for %s failed", UUID)
return errors.NotFoundError(nil).WithMessage("registration %s not found", UUID)
}
qt2 := o.QueryTable(new(Registration))

View File

@ -108,7 +108,7 @@ func (suite *RegistrationDAOTestSuite) TestList() {
// with query and found items
keywords := make(map[string]interface{})
keywords["description"] = "sample"
keywords["description"] = &q.FuzzyMatchValue{Value: "sample"}
l, err = ListRegistrations(suite.Context(), &q.Query{
PageSize: 5,
PageNumber: 1,
@ -118,7 +118,7 @@ func (suite *RegistrationDAOTestSuite) TestList() {
require.Equal(suite.T(), 1, len(l))
// With query and not found items
keywords["description"] = "not_exist"
keywords["description"] = &q.FuzzyMatchValue{Value: "not_exist"}
l, err = ListRegistrations(suite.Context(), &q.Query{
Keywords: keywords,
})
@ -127,14 +127,14 @@ func (suite *RegistrationDAOTestSuite) TestList() {
// Exact match
exactKeywords := make(map[string]interface{})
exactKeywords["ex_name"] = "forUT"
exactKeywords["name"] = "forUT"
l, err = ListRegistrations(suite.Context(), &q.Query{
Keywords: exactKeywords,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
exactKeywords["ex_name"] = "forU"
exactKeywords["name"] = "forU"
l, err = ListRegistrations(suite.Context(), &q.Query{
Keywords: exactKeywords,
})

View File

@ -38,7 +38,7 @@ func EnsureScanners(ctx context.Context, wantedScanners []scanner.Registration)
names[i] = ws.Name
}
list, err := scannerManager.List(ctx, q.New(q.KeyWords{"ex_name__in": names}))
list, err := scannerManager.List(ctx, q.New(q.KeyWords{"name__in": names}))
if err != nil {
return errors.Errorf("listing scanners: %v", err)
}
@ -79,7 +79,7 @@ func EnsureDefaultScanner(ctx context.Context, scannerName string) (err error) {
log.Infof("Skipped setting %s as the default scanner. The default scanner is already set to %s", scannerName, defaultScanner.URL)
return
}
scanners, err := scannerManager.List(ctx, q.New(q.KeyWords{"ex_name": scannerName}))
scanners, err := scannerManager.List(ctx, q.New(q.KeyWords{"name": scannerName}))
if err != nil {
err = errors.Errorf("listing scanners: %v", err)
return
@ -99,7 +99,7 @@ func RemoveImmutableScanners(ctx context.Context, names []string) error {
if len(names) == 0 {
return nil
}
query := q.New(q.KeyWords{"ex_immutable": true, "ex_name__in": names})
query := q.New(q.KeyWords{"immutable": true, "name__in": names})
// TODO Instead of executing 1 to N SQL queries we might want to delete multiple rows with scannerManager.DeleteByImmutableAndURLIn(true, []string{})
registrations, err := scannerManager.List(ctx, query)

View File

@ -39,7 +39,7 @@ func TestEnsureScanners(t *testing.T) {
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{
"ex_name__in": []string{"scanner"},
"name__in": []string{"scanner"},
},
}).Return(nil, errors.New("DB error"))
@ -57,7 +57,7 @@ func TestEnsureScanners(t *testing.T) {
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{
"ex_name__in": []string{
"name__in": []string{
"trivy",
},
},
@ -81,7 +81,7 @@ func TestEnsureScanners(t *testing.T) {
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{
"ex_name__in": []string{
"name__in": []string{
"trivy",
},
},
@ -135,7 +135,7 @@ func TestEnsureDefaultScanner(t *testing.T) {
mgr.On("GetDefault", mock.Anything).Return(nil, nil)
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{"ex_name": "trivy"},
Keywords: map[string]interface{}{"name": "trivy"},
}).Return(nil, errors.New("DB error"))
err := EnsureDefaultScanner(context.TODO(), "trivy")
@ -149,7 +149,7 @@ func TestEnsureDefaultScanner(t *testing.T) {
mgr.On("GetDefault", mock.Anything).Return(nil, nil)
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{"ex_name": "trivy"},
Keywords: map[string]interface{}{"name": "trivy"},
}).Return([]*scanner.Registration{
{Name: "trivy"},
{Name: "trivy"},
@ -166,7 +166,7 @@ func TestEnsureDefaultScanner(t *testing.T) {
mgr.On("GetDefault", mock.Anything).Return(nil, nil)
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{"ex_name": "trivy"},
Keywords: map[string]interface{}{"name": "trivy"},
}).Return([]*scanner.Registration{
{
Name: "trivy",
@ -187,7 +187,7 @@ func TestEnsureDefaultScanner(t *testing.T) {
mgr.On("GetDefault", mock.Anything).Return(nil, nil)
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{"ex_name": "trivy"},
Keywords: map[string]interface{}{"name": "trivy"},
}).Return([]*scanner.Registration{
{
Name: "trivy",
@ -221,8 +221,8 @@ func TestRemoveImmutableScanners(t *testing.T) {
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{
"ex_immutable": true,
"ex_name__in": []string{"scanner"},
"immutable": true,
"name__in": []string{"scanner"},
},
}).Return(nil, errors.New("DB error"))
@ -249,8 +249,8 @@ func TestRemoveImmutableScanners(t *testing.T) {
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{
"ex_immutable": true,
"ex_name__in": []string{
"immutable": true,
"name__in": []string{
"scanner-1",
"scanner-2",
},
@ -285,8 +285,8 @@ func TestRemoveImmutableScanners(t *testing.T) {
mgr.On("List", mock.Anything, &q.Query{
Keywords: map[string]interface{}{
"ex_immutable": true,
"ex_name__in": []string{
"immutable": true,
"name__in": []string{
"scanner-1",
"scanner-2",
},

View File

@ -25,6 +25,9 @@ import (
// Manager defines the related scanner API endpoints
type Manager interface {
// Count returns the total count of scanner registrations according to the query.
Count(ctx context.Context, query *q.Query) (int64, error)
// List returns a list of currently configured scanner registrations.
// Query parameters are optional
List(ctx context.Context, query *q.Query) ([]*scanner.Registration, error)
@ -58,6 +61,10 @@ func New() Manager {
return &basicManager{}
}
func (bm *basicManager) Count(ctx context.Context, query *q.Query) (int64, error) {
return scanner.GetTotalOfRegistrations(ctx, query)
}
// Create ...
func (bm *basicManager) Create(ctx context.Context, registration *scanner.Registration) (string, error) {
if registration == nil {

View File

@ -18,12 +18,13 @@ package handler
import (
"context"
rbac_project "github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/rbac/system"
"net/http"
"net/url"
"strconv"
rbac_project "github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/rbac/system"
"github.com/go-openapi/runtime"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
@ -136,7 +137,7 @@ func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error {
}
// 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) {
func (b *BaseAPI) BuildQuery(ctx context.Context, query *string, pageNumber, pageSize *int64, sorts ...*string) (*q.Query, error) {
var (
qs string
pn int64
@ -151,7 +152,17 @@ func (b *BaseAPI) BuildQuery(ctx context.Context, query *string, pageNumber, pag
if pageSize != nil {
ps = *pageSize
}
return q.Build(qs, pn, ps)
r, err := q.Build(qs, pn, ps)
if err != nil {
return nil, err
}
if len(sorts) > 0 {
r.Sorting = lib.StringValue(sorts[0])
}
return r, nil
}
// Links return Links based on the provided pagination information

View File

@ -34,6 +34,7 @@ func New() http.Handler {
ArtifactAPI: newArtifactAPI(),
RepositoryAPI: newRepositoryAPI(),
AuditlogAPI: newAuditLogAPI(),
ScannerAPI: newScannerAPI(),
ScanAPI: newScanAPI(),
ScanAllAPI: newScanAllAPI(),
ProjectAPI: newProjectAPI(),

View File

@ -0,0 +1,59 @@
// 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 (
"context"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// ScannerRegistration ...
type ScannerRegistration struct {
*scanner.Registration
}
// ToSwagger ...
func (s *ScannerRegistration) ToSwagger(ctx context.Context) *models.ScannerRegistration {
if s.Registration == nil {
return nil
}
return &models.ScannerRegistration{
UUID: s.UUID,
Name: s.Name,
URL: s.URL,
Description: s.Description,
Auth: s.Auth,
AccessCredential: s.AccessCredential,
SkipCertVerify: &s.SkipCertVerify,
UseInternalAddr: &s.UseInternalAddr,
IsDefault: &s.IsDefault,
Disabled: &s.Disabled,
CreateTime: strfmt.DateTime(s.CreateTime),
UpdateTime: strfmt.DateTime(s.UpdateTime),
Adapter: s.Adapter,
Vendor: s.Vendor,
Version: s.Version,
Health: s.Health,
}
}
// NewScannerRegistration ...
func NewScannerRegistration(scanner *scanner.Registration) *ScannerRegistration {
return &ScannerRegistration{Registration: scanner}
}

View File

@ -33,6 +33,7 @@ import (
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/controller/repository"
"github.com/goharbor/harbor/src/controller/retention"
"github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib"
@ -66,6 +67,7 @@ func newProjectAPI() *projectAPI {
robotMgr: robot.Mgr,
preheatCtl: preheat.Ctl,
retentionCtl: retention.Ctl,
scannerCtl: scanner.DefaultController,
}
}
@ -80,6 +82,7 @@ type projectAPI struct {
robotMgr robot.Manager
preheatCtl preheat.Controller
retentionCtl retention.Controller
scannerCtl scanner.Controller
}
func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateProjectParams) middleware.Responder {
@ -502,6 +505,75 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP
return operation.NewUpdateProjectOK()
}
func (a *projectAPI) GetScannerOfProject(ctx context.Context, params operation.GetScannerOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceScanner); err != nil {
return a.SendError(ctx, err)
}
p, err := a.projectCtl.Get(ctx, projectNameOrID, project.Metadata(false))
if err != nil {
return a.SendError(ctx, err)
}
scanner, err := a.scannerCtl.GetRegistrationByProject(ctx, p.ProjectID)
if err != nil {
return a.SendError(ctx, err)
}
return operation.NewGetScannerOfProjectOK().WithPayload(model.NewScannerRegistration(scanner).ToSwagger(ctx))
}
func (a *projectAPI) ListScannerCandidatesOfProject(ctx context.Context, params operation.ListScannerCandidatesOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionCreate, rbac.ResourceScanner); err != nil {
return a.SendError(ctx, err)
}
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize, params.Sort)
if err != nil {
return a.SendError(ctx, err)
}
total, err := a.scannerCtl.GetTotalOfRegistrations(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
scanners, err := a.scannerCtl.ListRegistrations(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
payload := make([]*models.ScannerRegistration, len(scanners))
for i, scanner := range scanners {
payload[i] = model.NewScannerRegistration(scanner).ToSwagger(ctx)
}
return operation.NewListScannerCandidatesOfProjectOK().
WithXTotalCount(total).
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(payload)
}
func (a *projectAPI) SetScannerOfProject(ctx context.Context, params operation.SetScannerOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionCreate, rbac.ResourceScanner); err != nil {
return a.SendError(ctx, err)
}
p, err := a.projectCtl.Get(ctx, projectNameOrID, project.Metadata(false))
if err != nil {
return a.SendError(ctx, err)
}
if err := a.scannerCtl.SetRegistrationByProject(ctx, p.ProjectID, *params.Payload.UUID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewSetScannerOfProjectOK()
}
func (a *projectAPI) deletable(ctx context.Context, projectNameOrID interface{}) (*project.Project, *models.ProjectDeletable, error) {
p, err := a.getProject(ctx, projectNameOrID)
if err != nil {

View File

@ -0,0 +1,201 @@
// 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 (
"fmt"
"testing"
"github.com/goharbor/harbor/src/pkg/project/models"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/server/v2.0/restapi"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
"github.com/goharbor/harbor/src/testing/mock"
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
"github.com/stretchr/testify/suite"
)
type ProjectTestSuite struct {
htesting.Suite
projectCtl *projecttesting.Controller
scannerCtl *scannertesting.Controller
project *models.Project
reg *scanner.Registration
}
func (suite *ProjectTestSuite) SetupSuite() {
suite.project = &models.Project{
ProjectID: 1,
Name: "library",
}
suite.reg = &scanner.Registration{
Name: "reg",
URL: "http://reg:8080",
UUID: "uuid",
}
suite.projectCtl = &projecttesting.Controller{}
suite.scannerCtl = &scannertesting.Controller{}
suite.Config = &restapi.Config{
ProjectAPI: &projectAPI{
projectCtl: suite.projectCtl,
scannerCtl: suite.scannerCtl,
},
}
suite.Suite.SetupSuite()
}
func (suite *ProjectTestSuite) TestGetScannerOfProject() {
times := 4
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// get project failed
mock.OnAnything(suite.projectCtl, "Get").Return(nil, fmt.Errorf("failed to get project")).Once()
res, err := suite.Get("/projects/1/scanner")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// scanner not found
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(nil, nil).Once()
res, err := suite.Get("/projects/1/scanner")
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
{
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once()
var scanner scanner.Registration
res, err := suite.GetJSON("/projects/1/scanner", &scanner)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Equal(suite.reg.UUID, scanner.UUID)
}
{
mock.OnAnything(projectCtlMock, "GetByName").Return(suite.project, nil).Once()
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once()
var scanner scanner.Registration
res, err := suite.GetJSON("/projects/library/scanner", &scanner)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Equal(suite.reg.UUID, scanner.UUID)
}
}
func (suite *ProjectTestSuite) TestListScannerCandidatesOfProject() {
times := 4
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// list scanners failed
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(0), fmt.Errorf("failed to count scanners")).Once()
res, err := suite.Get("/projects/1/scanner/candidates")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// list scanners failed
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(1), nil).Once()
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return(nil, fmt.Errorf("failed to list scanners")).Once()
res, err := suite.Get("/projects/1/scanner/candidates")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// scanners not found
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(0), nil).Once()
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return(nil, nil).Once()
var scanners []interface{}
res, err := suite.GetJSON("/projects/1/scanner/candidates", &scanners)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Len(scanners, 0)
}
{
// scanners found
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(3), nil).Once()
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return([]*scanner.Registration{suite.reg}, nil).Once()
var scanners []interface{}
res, err := suite.GetJSON("/projects/1/scanner/candidates?page_size=1&page=2&name=n&description=d&url=u&ex_name=n&ex_url=u", &scanners)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Len(scanners, 1)
suite.Equal("3", res.Header.Get("X-Total-Count"))
suite.Contains(res.Header, "Link")
suite.Equal(`</api/v2.0/projects/1/scanner/candidates?description=d&ex_name=n&ex_url=u&name=n&page=1&page_size=1&url=u>; rel="prev" , </api/v2.0/projects/1/scanner/candidates?description=d&ex_name=n&ex_url=u&name=n&page=3&page_size=1&url=u>; rel="next"`, res.Header.Get("Link"))
}
}
func (suite *ProjectTestSuite) TestSetScannerOfProject() {
times := 3
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// get project failed
mock.OnAnything(suite.projectCtl, "Get").Return(nil, fmt.Errorf("failed to get project")).Once()
res, err := suite.PutJSON("/projects/1/scanner", map[string]interface{}{"uuid": "uuid"})
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "SetRegistrationByProject").Return(nil).Once()
res, err := suite.PutJSON("/projects/1/scanner", map[string]interface{}{"uuid": "uuid"})
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
{
mock.OnAnything(projectCtlMock, "GetByName").Return(suite.project, nil).Once()
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "SetRegistrationByProject").Return(nil).Once()
res, err := suite.PutJSON("/projects/library/scanner", map[string]interface{}{"uuid": "uuid"})
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
}
func TestProjectTestSuite(t *testing.T) {
suite.Run(t, &ProjectTestSuite{})
}

View File

@ -0,0 +1,247 @@
// 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"
"fmt"
"strings"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"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/scanner"
)
func newScannerAPI() *scannerAPI {
return &scannerAPI{
scannerCtl: scanner.DefaultController,
}
}
type scannerAPI struct {
BaseAPI
scannerCtl scanner.Controller
}
func (s *scannerAPI) CreateScanner(ctx context.Context, params operation.CreateScannerParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
r := &scanner.Registration{IsDefault: false}
copyToScannerRegistration(r, params.Registration)
if err := r.Validate(false); err != nil {
return s.SendError(ctx, errors.BadRequestError(nil).WithMessage(err.Error()))
}
uuid, err := s.scannerCtl.CreateRegistration(ctx, r)
if err != nil {
return s.SendError(ctx, err)
}
location := fmt.Sprintf("%s/%s", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), uuid)
return operation.NewCreateScannerCreated().WithLocation(location)
}
func (s *scannerAPI) DeleteScanner(ctx context.Context, params operation.DeleteScannerParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionDelete, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
r, err := s.scannerCtl.GetRegistration(ctx, params.RegistrationID)
if err != nil {
return s.SendError(ctx, err)
}
if r == nil {
return s.SendError(ctx, errors.NotFoundError(nil).WithMessage("scanner %s not found", params.RegistrationID))
}
// Immutable registration is not allowed
if r.Immutable {
format := "registration %s is not allowed to delete as it is immutable: scanner API: delete"
return s.SendError(ctx, errors.ForbiddenError(nil).WithMessage(format, r.Name))
}
deleted, err := s.scannerCtl.DeleteRegistration(ctx, r.UUID)
if err != nil {
return s.SendError(ctx, err)
}
return operation.NewDeleteScannerOK().WithPayload(model.NewScannerRegistration(deleted).ToSwagger(ctx))
}
func (s *scannerAPI) GetScanner(ctx context.Context, params operation.GetScannerParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
r, err := s.scannerCtl.GetRegistration(ctx, params.RegistrationID)
if err != nil {
return s.SendError(ctx, err)
}
if r == nil {
return s.SendError(ctx, errors.NotFoundError(nil).WithMessage("scanner %s not found", params.RegistrationID))
}
return operation.NewGetScannerOK().WithPayload(model.NewScannerRegistration(r).ToSwagger(ctx))
}
func (s *scannerAPI) GetScannerMetadata(ctx context.Context, params operation.GetScannerMetadataParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
meta, err := s.scannerCtl.GetMetadata(ctx, params.RegistrationID)
if err != nil {
return s.SendError(ctx, err)
}
return operation.NewGetScannerMetadataOK().WithPayload(meta)
}
func (s *scannerAPI) ListScanners(ctx context.Context, params operation.ListScannersParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
query, err := s.BuildQuery(ctx, params.Q, params.Page, params.PageSize, params.Sort)
if err != nil {
return s.SendError(ctx, err)
}
// compatible with previous version list scanners API
values := params.HTTPRequest.URL.Query()
for _, k := range []string{"name", "description", "url"} {
if v := values.Get(k); v != "" {
query.Keywords[k] = &q.FuzzyMatchValue{Value: v}
}
}
for _, k := range []string{"ex_name", "ex_url"} {
if v := values.Get(k); v != "" {
query.Keywords[strings.TrimPrefix(k, "ex_")] = v
}
}
total, err := s.scannerCtl.GetTotalOfRegistrations(ctx, query)
if err != nil {
return s.SendError(ctx, err)
}
scanners, err := s.scannerCtl.ListRegistrations(ctx, query)
if err != nil {
return s.SendError(ctx, err)
}
payload := make([]*models.ScannerRegistration, len(scanners))
for i, scanner := range scanners {
payload[i] = model.NewScannerRegistration(scanner).ToSwagger(ctx)
}
return operation.NewListScannersOK().
WithXTotalCount(total).
WithLink(s.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(payload)
}
func (s *scannerAPI) PingScanner(ctx context.Context, params operation.PingScannerParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
r := &scanner.Registration{
Name: lib.StringValue(params.Settings.Name),
URL: lib.StringValue((*string)(params.Settings.URL)),
Auth: params.Settings.Auth,
AccessCredential: params.Settings.AccessCredential,
}
if err := r.Validate(false); err != nil {
return s.SendError(ctx, errors.BadRequestError(nil).WithMessage(err.Error()))
}
if _, err := s.scannerCtl.Ping(ctx, r); err != nil {
return s.SendError(ctx, err)
}
return operation.NewPingScannerOK()
}
func (s *scannerAPI) SetScannerAsDefault(ctx context.Context, params operation.SetScannerAsDefaultParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
if params.Payload.IsDefault {
if err := s.scannerCtl.SetDefaultRegistration(ctx, params.RegistrationID); err != nil {
return s.SendError(ctx, err)
}
}
return operation.NewSetScannerAsDefaultOK()
}
func (s *scannerAPI) UpdateScanner(ctx context.Context, params operation.UpdateScannerParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceScanner); err != nil {
return s.SendError(ctx, err)
}
r, err := s.scannerCtl.GetRegistration(ctx, params.RegistrationID)
if err != nil {
return s.SendError(ctx, err)
}
if r == nil {
return s.SendError(ctx, errors.NotFoundError(nil).WithMessage("scanner %s not found", params.RegistrationID))
}
// Immutable registration is not allowed
if r.Immutable {
format := "registration %s is not allowed to update as it is immutable: scanner API: update"
return s.SendError(ctx, errors.ForbiddenError(nil).WithMessage(format, r.Name))
}
copyToScannerRegistration(r, params.Registration)
if err := r.Validate(true); err != nil {
return s.SendError(ctx, errors.BadRequestError(nil).WithMessage(err.Error()))
}
if err := s.scannerCtl.UpdateRegistration(ctx, r); err != nil {
return s.SendError(ctx, err)
}
return operation.NewUpdateScannerOK()
}
func copyToScannerRegistration(r *scanner.Registration, req *models.ScannerRegistrationReq) {
r.Name = lib.StringValue(req.Name)
r.URL = lib.StringValue((*string)(req.URL))
r.Description = req.Description
r.Disabled = lib.BoolValue(req.Disabled)
r.SkipCertVerify = lib.BoolValue(req.SkipCertVerify)
r.UseInternalAddr = lib.BoolValue(req.UseInternalAddr)
r.Auth = req.Auth
r.AccessCredential = req.AccessCredential
}

View File

@ -0,0 +1,532 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/server/v2.0/restapi"
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
"github.com/goharbor/harbor/src/testing/mock"
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
"github.com/stretchr/testify/suite"
)
type ScannerTestSuite struct {
htesting.Suite
scannerCtl *scannertesting.Controller
reg *scanner.Registration
metadata v1.ScannerAdapterMetadata
}
func (suite *ScannerTestSuite) SetupSuite() {
suite.reg = &scanner.Registration{
Name: "reg",
URL: "http://reg:8080",
UUID: "uuid",
}
suite.metadata = v1.ScannerAdapterMetadata{
Scanner: &v1.Scanner{
Name: "reg",
},
}
suite.scannerCtl = &scannertesting.Controller{}
suite.Config = &restapi.Config{
ScannerAPI: &scannerAPI{
scannerCtl: suite.scannerCtl,
},
}
suite.Suite.SetupSuite()
}
func (suite *ScannerTestSuite) TestAuthorization() {
newBody := func(body interface{}) io.Reader {
if body == nil {
return nil
}
buf, err := json.Marshal(body)
suite.Require().NoError(err)
return bytes.NewBuffer(buf)
}
reqs := []struct {
method string
url string
body interface{}
}{
{http.MethodGet, "/scanners", nil},
{http.MethodPost, "/scanners", suite.reg},
{http.MethodPost, "/scanners/ping", suite.reg},
{http.MethodGet, "/scanners/uuid1", nil},
{http.MethodPut, "/scanners/uuid1", suite.reg},
{http.MethodDelete, "/scanners/uuid1", nil},
{http.MethodPatch, "/scanners/uuid1", map[string]interface{}{"is_default": true}},
{http.MethodGet, "/scanners/uuid1/metadata", nil},
}
for _, req := range reqs {
{
// authorized required
suite.Security.On("IsAuthenticated").Return(false).Once()
res, err := suite.DoReq(req.method, req.url, newBody(req.body))
suite.NoError(err)
suite.Equal(401, res.StatusCode)
}
{
// permission required
suite.Security.On("IsAuthenticated").Return(true).Once()
suite.Security.On("GetUsername").Return("username").Once()
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Once()
res, err := suite.DoReq(req.method, req.url, newBody(req.body))
suite.NoError(err)
suite.Equal(403, res.StatusCode)
}
}
}
func (suite *ScannerTestSuite) TestCreateScannerWithInvalidBody() {
{
// empty body
res, err := suite.PostJSON("/scanners", nil)
suite.NoError(err)
suite.Equal(422, res.StatusCode)
}
{
// name missing
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(422, res.StatusCode)
}
{
// url missing
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"name": "reg",
})
suite.NoError(err)
suite.Equal(422, res.StatusCode)
}
{
// invalid url
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"name": "reg",
"url": "invalid url",
})
suite.NoError(err)
suite.Equal(422, res.StatusCode)
}
}
func (suite *ScannerTestSuite) TestCreateScanner() {
times := 4
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
mock.OnAnything(suite.scannerCtl, "CreateRegistration").Return("", fmt.Errorf("failed to create registration")).Once()
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
mock.OnAnything(suite.scannerCtl, "CreateRegistration").Return("uuid", nil).Once()
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(201, res.StatusCode)
suite.Equal("/api/v2.0/scanners/uuid", res.Header.Get("Location"))
}
{
// access_credential missing
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
"auth": "Basic",
})
suite.NoError(err)
suite.Equal(400, res.StatusCode)
}
{
mock.OnAnything(suite.scannerCtl, "CreateRegistration").Return("uuid", nil).Once()
res, err := suite.PostJSON("/scanners", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
"auth": "Basic",
"access_credential": "username:password",
})
suite.NoError(err)
suite.Equal(201, res.StatusCode)
suite.Equal("/api/v2.0/scanners/uuid", res.Header.Get("Location"))
}
}
func (suite *ScannerTestSuite) TestDeleteScanner() {
times := 5
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// get scanner failed
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(nil, fmt.Errorf("failed to get registration")).Once()
res, err := suite.Delete("/scanners/uuid")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// scanner not found
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(nil, nil).Once()
res, err := suite.Delete("/scanners/uuid")
suite.NoError(err)
suite.Equal(404, res.StatusCode)
}
{
// immutable scanner
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(&scanner.Registration{Immutable: true}, nil).Once()
res, err := suite.Delete("/scanners/uuid")
suite.NoError(err)
suite.Equal(403, res.StatusCode)
}
{
// delete scanner failed
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(suite.reg, nil).Once()
mock.OnAnything(suite.scannerCtl, "DeleteRegistration").Return(nil, fmt.Errorf("failed to delete registration")).Once()
res, err := suite.Delete("/scanners/uuid")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// delete scanner
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(suite.reg, nil).Once()
mock.OnAnything(suite.scannerCtl, "DeleteRegistration").Return(suite.reg, nil).Once()
res, err := suite.Delete("/scanners/uuid")
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
}
func (suite *ScannerTestSuite) TestGetScanner() {
times := 3
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// get scanner failed
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(nil, fmt.Errorf("failed to get registration")).Once()
res, err := suite.Get("/scanners/uuid")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// scanner not found
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(nil, nil).Once()
var scanner map[string]interface{}
res, err := suite.GetJSON("/scanners/uuid", &scanner)
suite.NoError(err)
suite.Equal(404, res.StatusCode)
}
{
// scanner found
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(suite.reg, nil).Once()
var scanner map[string]interface{}
res, err := suite.GetJSON("/scanners/uuid", &scanner)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Equal("uuid", scanner["uuid"])
}
}
func (suite *ScannerTestSuite) TestGetScannerMetadata() {
times := 3
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// get metadata failed
mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(nil, fmt.Errorf("failed to get metadata")).Once()
res, err := suite.Get("/scanners/uuid/metadata")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(&suite.metadata, nil).Once()
var md v1.ScannerAdapterMetadata
res, err := suite.GetJSON("/scanners/uuid/metadata", &md)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Equal(suite.metadata, md)
}
}
func (suite *ScannerTestSuite) TestListScanners() {
times := 4
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// list scanners failed
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(0), fmt.Errorf("failed to count scanners")).Once()
res, err := suite.Get("/scanners")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// list scanners failed
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(1), nil).Once()
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return(nil, fmt.Errorf("failed to list scanners")).Once()
res, err := suite.Get("/scanners")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// scanners not found
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(0), nil).Once()
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return(nil, nil).Once()
var scanners []interface{}
res, err := suite.GetJSON("/scanners", &scanners)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Len(scanners, 0)
}
{
// scanners found
mock.OnAnything(suite.scannerCtl, "GetTotalOfRegistrations").Return(int64(3), nil).Once()
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return([]*scanner.Registration{suite.reg}, nil).Once()
var scanners []interface{}
res, err := suite.GetJSON("/scanners?page_size=1&page=2", &scanners)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Len(scanners, 1)
suite.Equal("3", res.Header.Get("X-Total-Count"))
suite.Contains(res.Header, "Link")
suite.Equal(`</api/v2.0/scanners?page=1&page_size=1>; rel="prev" , </api/v2.0/scanners?page=3&page_size=1>; rel="next"`, res.Header.Get("Link"))
}
}
func (suite *ScannerTestSuite) TestPingScanner() {
times := 3
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// bad req
res, err := suite.PostJSON("/scanners/ping", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
"auth": "Basic",
})
suite.NoError(err)
suite.Equal(400, res.StatusCode)
}
{
// ping failed
mock.OnAnything(suite.scannerCtl, "Ping").Return(nil, fmt.Errorf("failed to ping scanner")).Once()
res, err := suite.PostJSON("/scanners/ping", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// ping
mock.OnAnything(suite.scannerCtl, "Ping").Return(&suite.metadata, nil).Once()
res, err := suite.PostJSON("/scanners/ping", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
}
func (suite *ScannerTestSuite) TestSetScannerAsDefault() {
times := 3
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
res, err := suite.PatchJSON("/scanners/uuid", map[string]interface{}{
"is_default": false,
})
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
{
// set default failed
mock.OnAnything(suite.scannerCtl, "SetDefaultRegistration").Return(fmt.Errorf("failed to set default")).Once()
res, err := suite.PatchJSON("/scanners/uuid", map[string]interface{}{
"is_default": true,
})
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// set default
mock.OnAnything(suite.scannerCtl, "SetDefaultRegistration").Return(nil).Once()
res, err := suite.PatchJSON("/scanners/uuid", map[string]interface{}{
"is_default": true,
})
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
}
func (suite *ScannerTestSuite) TestUpdateScanner() {
times := 7
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// update scanner no body
res, err := suite.Put("/scanners/uuid", nil)
suite.NoError(err)
suite.Equal(422, res.StatusCode)
}
{
// get scanner failed
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(nil, fmt.Errorf("failed to get registration")).Once()
res, err := suite.PutJSON("/scanners/uuid", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// scanner not found
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(nil, nil).Once()
res, err := suite.PutJSON("/scanners/uuid", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(404, res.StatusCode)
}
{
// immutable scanner
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(&scanner.Registration{Immutable: true}, nil).Once()
res, err := suite.PutJSON("/scanners/uuid", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(403, res.StatusCode)
}
{
// bad req
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(suite.reg, nil).Once()
res, err := suite.PutJSON("/scanners/uuid", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
"auth": "Basic",
})
suite.NoError(err)
suite.Equal(400, res.StatusCode)
}
{
// update scanner failed
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(suite.reg, nil).Once()
mock.OnAnything(suite.scannerCtl, "UpdateRegistration").Return(fmt.Errorf("failed to update the scanner")).Once()
res, err := suite.PutJSON("/scanners/uuid", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// update scanner
mock.OnAnything(suite.scannerCtl, "GetRegistration").Return(suite.reg, nil).Once()
mock.OnAnything(suite.scannerCtl, "UpdateRegistration").Return(nil).Once()
res, err := suite.PutJSON("/scanners/uuid", map[string]interface{}{
"name": "reg",
"url": "http://reg:8080",
})
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
}
func TestScannerTestSuite(t *testing.T) {
suite.Run(t, &ScannerTestSuite{})
}

View File

@ -81,16 +81,4 @@ func registerLegacyRoutes() {
beego.Router("/api/"+version+"/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
beego.Router("/api/"+version+"/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
}
// Add routes for plugin scanner management
scannerAPI := &api.ScannerAPI{}
beego.Router("/api/"+version+"/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/"+version+"/scanners/:uuid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
beego.Router("/api/"+version+"/scanners/:uuid/metadata", scannerAPI, "get:Metadata")
beego.Router("/api/"+version+"/scanners/ping", scannerAPI, "post:Ping")
// Add routes for project level scanner
proScannerAPI := &api.ProjectScannerAPI{}
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/scanner", proScannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/scanner/candidates", proScannerAPI, "get:GetProScannerCandidates")
}

View File

@ -140,6 +140,27 @@ func (_m *Controller) GetRegistrationByProject(ctx context.Context, projectID in
return r0, r1
}
// GetTotalOfRegistrations provides a mock function with given fields: ctx, query
func (_m *Controller) GetTotalOfRegistrations(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListRegistrations provides a mock function with given fields: ctx, query
func (_m *Controller) ListRegistrations(ctx context.Context, query *q.Query) ([]*scanner.Registration, error) {
ret := _m.Called(ctx, query)

View File

@ -5,10 +5,10 @@ package scanner
import (
context "context"
q "github.com/goharbor/harbor/src/lib/q"
daoscanner "github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
mock "github.com/stretchr/testify/mock"
scanner "github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
q "github.com/goharbor/harbor/src/lib/q"
)
// Manager is an autogenerated mock type for the Manager type
@ -16,19 +16,40 @@ type Manager struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, registration
func (_m *Manager) Create(ctx context.Context, registration *scanner.Registration) (string, error) {
func (_m *Manager) Create(ctx context.Context, registration *daoscanner.Registration) (string, error) {
ret := _m.Called(ctx, registration)
var r0 string
if rf, ok := ret.Get(0).(func(context.Context, *scanner.Registration) string); ok {
if rf, ok := ret.Get(0).(func(context.Context, *daoscanner.Registration) string); ok {
r0 = rf(ctx, registration)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *scanner.Registration) error); ok {
if rf, ok := ret.Get(1).(func(context.Context, *daoscanner.Registration) error); ok {
r1 = rf(ctx, registration)
} else {
r1 = ret.Error(1)
@ -52,15 +73,15 @@ func (_m *Manager) Delete(ctx context.Context, registrationUUID string) error {
}
// Get provides a mock function with given fields: ctx, registrationUUID
func (_m *Manager) Get(ctx context.Context, registrationUUID string) (*scanner.Registration, error) {
func (_m *Manager) Get(ctx context.Context, registrationUUID string) (*daoscanner.Registration, error) {
ret := _m.Called(ctx, registrationUUID)
var r0 *scanner.Registration
if rf, ok := ret.Get(0).(func(context.Context, string) *scanner.Registration); ok {
var r0 *daoscanner.Registration
if rf, ok := ret.Get(0).(func(context.Context, string) *daoscanner.Registration); ok {
r0 = rf(ctx, registrationUUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scanner.Registration)
r0 = ret.Get(0).(*daoscanner.Registration)
}
}
@ -75,15 +96,15 @@ func (_m *Manager) Get(ctx context.Context, registrationUUID string) (*scanner.R
}
// GetDefault provides a mock function with given fields: ctx
func (_m *Manager) GetDefault(ctx context.Context) (*scanner.Registration, error) {
func (_m *Manager) GetDefault(ctx context.Context) (*daoscanner.Registration, error) {
ret := _m.Called(ctx)
var r0 *scanner.Registration
if rf, ok := ret.Get(0).(func(context.Context) *scanner.Registration); ok {
var r0 *daoscanner.Registration
if rf, ok := ret.Get(0).(func(context.Context) *daoscanner.Registration); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scanner.Registration)
r0 = ret.Get(0).(*daoscanner.Registration)
}
}
@ -98,15 +119,15 @@ func (_m *Manager) GetDefault(ctx context.Context) (*scanner.Registration, error
}
// List provides a mock function with given fields: ctx, query
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*scanner.Registration, error) {
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*daoscanner.Registration, error) {
ret := _m.Called(ctx, query)
var r0 []*scanner.Registration
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*scanner.Registration); ok {
var r0 []*daoscanner.Registration
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*daoscanner.Registration); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scanner.Registration)
r0 = ret.Get(0).([]*daoscanner.Registration)
}
}
@ -135,11 +156,11 @@ func (_m *Manager) SetAsDefault(ctx context.Context, registrationUUID string) er
}
// Update provides a mock function with given fields: ctx, registration
func (_m *Manager) Update(ctx context.Context, registration *scanner.Registration) error {
func (_m *Manager) Update(ctx context.Context, registration *daoscanner.Registration) error {
ret := _m.Called(ctx, registration)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *scanner.Registration) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, *daoscanner.Registration) error); ok {
r0 = rf(ctx, registration)
} else {
r0 = ret.Error(0)

View File

@ -28,7 +28,7 @@ def get_endpoint():
def _create_client(server, credential, debug, api_type="products"):
cfg = None
if api_type in ('projectv2', 'artifact', 'repository', 'scan', 'scanall', 'preheat', 'quota', 'replication', 'robot', 'gc', 'retention'):
if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota', 'replication', 'robot', 'gc', 'retention'):
cfg = v2_swagger_client.Configuration()
else:
cfg = swagger_client.Configuration()
@ -60,7 +60,7 @@ def _create_client(server, credential, debug, api_type="products"):
"repository": v2_swagger_client.RepositoryApi(v2_swagger_client.ApiClient(cfg)),
"scan": v2_swagger_client.ScanApi(v2_swagger_client.ApiClient(cfg)),
"scanall": v2_swagger_client.ScanAllApi(v2_swagger_client.ApiClient(cfg)),
"scanner": swagger_client.ScannersApi(swagger_client.ApiClient(cfg)),
"scanner": v2_swagger_client.ScannerApi(v2_swagger_client.ApiClient(cfg)),
"replication": v2_swagger_client.ReplicationApi(v2_swagger_client.ApiClient(cfg)),
"robot": v2_swagger_client.RobotApi(v2_swagger_client.ApiClient(cfg)),
"gc": v2_swagger_client.GcApi(v2_swagger_client.ApiClient(cfg)),

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
import time
import base
import swagger_client
from swagger_client.rest import ApiException
class Scanner(base.Base, object):
def __init__(self):
super(Scanner,self).__init__(api_type = "scanner")
def scanners_get(self, **kwargs):
client = self._get_client(**kwargs)
return client.scanners_get()
def scanners_get_uuid(self, is_default = False, **kwargs):
scanners = self.scanners_get(**kwargs)
for scanner in scanners:
if scanner.is_default == is_default:
return scanner.uuid
def scanners_registration_id_patch(self, registration_id, is_default = True, **kwargs):
client = self._get_client(**kwargs)
isdefault = swagger_client.IsDefault(is_default)
client.scanners_registration_id_patch(registration_id, isdefault)

View File

@ -10,7 +10,6 @@ from library.user import User
from library.repository import Repository
from library.repository import push_image_to_project
from library.artifact import Artifact
from library.scanner import Scanner
from library.configurations import Configurations
from library.projectV2 import ProjectV2
@ -89,4 +88,4 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
self.repo.delete_repository(project_name, repo_name_dev.split('/')[1], **USER_ADMIN)
if __name__ == '__main__':
unittest.main()
unittest.main()

View File

@ -12,7 +12,6 @@ from library.user import User
from library.repository import Repository
from library.repository import push_self_build_image_to_project
from library.artifact import Artifact
from library.scanner import Scanner
from library.docker_api import list_image_tags
from library.docker_api import list_repositories
import os

View File

@ -57,8 +57,7 @@ class TestScan(unittest.TestCase):
4. Get private project of user(UA), user(UA) can see only one private project which is project(PA);
5. Create a new repository(RA) and tag(TA) in project(PA) by user(UA);
6. Send scan image command and get tag(TA) information to check scan result, it should be finished;
7. Swith Scanner;
8. Send scan another image command and get tag(TA) information to check scan result, it should be finished.
7. Send scan another image command and get tag(TA) information to check scan result, it should be finished.
Tear down:
1. Delete repository(RA) by user(UA);
2. Delete project(PA);
@ -93,8 +92,7 @@ class TestScan(unittest.TestCase):
4. Get private project of user(UA), user(UA) can see only one private project which is project(PA);
5. Create a new repository(RA) and tag(TA) in project(PA) by user(UA);
6. Send scan image command and get tag(TA) information to check scan result, it should be finished;
7. Swith Scanner;
8. Send scan another image command and get tag(TA) information to check scan result, it should be finished.
7. Send scan another image command and get tag(TA) information to check scan result, it should be finished.
Tear down:
1. Delete repository(RA) by user(UA);
2. Delete project(PA);