diff --git a/api/v2.0/legacy_swagger.yaml b/api/v2.0/legacy_swagger.yaml index 998b4c1bb..1cd9004f5 100644 --- a/api/v2.0/legacy_swagger.yaml +++ b/api/v2.0/legacy_swagger.yaml @@ -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 diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 747940ef7..21c9c20f7 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -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 diff --git a/src/controller/scanner/base_controller.go b/src/controller/scanner/base_controller.go index e2ace09a1..b402d7ba7 100644 --- a/src/controller/scanner/base_controller.go +++ b/src/controller/scanner/base_controller.go @@ -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) } diff --git a/src/controller/scanner/controller.go b/src/controller/scanner/controller.go index 190cf92a2..9df9a6d36 100644 --- a/src/controller/scanner/controller.go +++ b/src/controller/scanner/controller.go @@ -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. // diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index cf2745823..2abb70eab 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -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"} diff --git a/src/core/api/pro_scanner.go b/src/core/api/pro_scanner.go deleted file mode 100644 index 35cb4e1aa..000000000 --- a/src/core/api/pro_scanner.go +++ /dev/null @@ -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() -} diff --git a/src/core/api/pro_scanner_test.go b/src/core/api/pro_scanner_test.go deleted file mode 100644 index de0754af5..000000000 --- a/src/core/api/pro_scanner_test.go +++ /dev/null @@ -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) -} diff --git a/src/core/api/scanners.go b/src/core/api/scanners.go deleted file mode 100644 index 78798ab77..000000000 --- a/src/core/api/scanners.go +++ /dev/null @@ -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 -} diff --git a/src/core/api/scanners_test.go b/src/core/api/scanners_test.go deleted file mode 100644 index 7e275c593..000000000 --- a/src/core/api/scanners_test.go +++ /dev/null @@ -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) -} diff --git a/src/pkg/scan/dao/scanner/registration.go b/src/pkg/scan/dao/scanner/registration.go index 26ca60f43..13907bbee 100644 --- a/src/pkg/scan/dao/scanner/registration.go +++ b/src/pkg/scan/dao/scanner/registration.go @@ -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)) diff --git a/src/pkg/scan/dao/scanner/registration_test.go b/src/pkg/scan/dao/scanner/registration_test.go index eb554c1e0..f7e8fefac 100644 --- a/src/pkg/scan/dao/scanner/registration_test.go +++ b/src/pkg/scan/dao/scanner/registration_test.go @@ -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, }) diff --git a/src/pkg/scan/init.go b/src/pkg/scan/init.go index 0883ea3bc..243ccc87d 100644 --- a/src/pkg/scan/init.go +++ b/src/pkg/scan/init.go @@ -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) diff --git a/src/pkg/scan/init_test.go b/src/pkg/scan/init_test.go index fa2162fe0..ead2cf664 100644 --- a/src/pkg/scan/init_test.go +++ b/src/pkg/scan/init_test.go @@ -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", }, diff --git a/src/pkg/scan/scanner/manager.go b/src/pkg/scan/scanner/manager.go index afdb208d7..c9223e7fe 100644 --- a/src/pkg/scan/scanner/manager.go +++ b/src/pkg/scan/scanner/manager.go @@ -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 { diff --git a/src/server/v2.0/handler/base.go b/src/server/v2.0/handler/base.go index 607a6620f..7aed5d7c9 100644 --- a/src/server/v2.0/handler/base.go +++ b/src/server/v2.0/handler/base.go @@ -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 diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 62b58b542..97c119718 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -34,6 +34,7 @@ func New() http.Handler { ArtifactAPI: newArtifactAPI(), RepositoryAPI: newRepositoryAPI(), AuditlogAPI: newAuditLogAPI(), + ScannerAPI: newScannerAPI(), ScanAPI: newScanAPI(), ScanAllAPI: newScanAllAPI(), ProjectAPI: newProjectAPI(), diff --git a/src/server/v2.0/handler/model/scanner.go b/src/server/v2.0/handler/model/scanner.go new file mode 100644 index 000000000..99e861cb5 --- /dev/null +++ b/src/server/v2.0/handler/model/scanner.go @@ -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} +} diff --git a/src/server/v2.0/handler/project.go b/src/server/v2.0/handler/project.go index 1e7f18e7d..88c906a23 100644 --- a/src/server/v2.0/handler/project.go +++ b/src/server/v2.0/handler/project.go @@ -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 { diff --git a/src/server/v2.0/handler/project_test.go b/src/server/v2.0/handler/project_test.go new file mode 100644 index 000000000..fd37d1036 --- /dev/null +++ b/src/server/v2.0/handler/project_test.go @@ -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(`; rel="prev" , ; 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{}) +} diff --git a/src/server/v2.0/handler/scanner.go b/src/server/v2.0/handler/scanner.go new file mode 100644 index 000000000..57a63b914 --- /dev/null +++ b/src/server/v2.0/handler/scanner.go @@ -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 +} diff --git a/src/server/v2.0/handler/scanner_test.go b/src/server/v2.0/handler/scanner_test.go new file mode 100644 index 000000000..e4c002cc7 --- /dev/null +++ b/src/server/v2.0/handler/scanner_test.go @@ -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(`; rel="prev" , ; 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{}) +} diff --git a/src/server/v2.0/route/legacy.go b/src/server/v2.0/route/legacy.go index de0e7ad0d..9ddb79b38 100755 --- a/src/server/v2.0/route/legacy.go +++ b/src/server/v2.0/route/legacy.go @@ -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") } diff --git a/src/testing/controller/scanner/controller.go b/src/testing/controller/scanner/controller.go index d35bb450a..5934c6225 100644 --- a/src/testing/controller/scanner/controller.go +++ b/src/testing/controller/scanner/controller.go @@ -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) diff --git a/src/testing/pkg/scan/scanner/manager.go b/src/testing/pkg/scan/scanner/manager.go index e17138d6f..6f8fee63c 100644 --- a/src/testing/pkg/scan/scanner/manager.go +++ b/src/testing/pkg/scan/scanner/manager.go @@ -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) diff --git a/tests/apitests/python/library/base.py b/tests/apitests/python/library/base.py index 42a1c0221..5688aefac 100644 --- a/tests/apitests/python/library/base.py +++ b/tests/apitests/python/library/base.py @@ -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)), diff --git a/tests/apitests/python/library/scanner.py b/tests/apitests/python/library/scanner.py deleted file mode 100644 index 97a19ae49..000000000 --- a/tests/apitests/python/library/scanner.py +++ /dev/null @@ -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) - diff --git a/tests/apitests/python/test_assign_role_to_ldap_group.py b/tests/apitests/python/test_assign_role_to_ldap_group.py index 8c648dd8a..1a918bcfe 100644 --- a/tests/apitests/python/test_assign_role_to_ldap_group.py +++ b/tests/apitests/python/test_assign_role_to_ldap_group.py @@ -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() \ No newline at end of file + unittest.main() diff --git a/tests/apitests/python/test_registry_api.py b/tests/apitests/python/test_registry_api.py index 003afe545..179ad7580 100644 --- a/tests/apitests/python/test_registry_api.py +++ b/tests/apitests/python/test_registry_api.py @@ -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 diff --git a/tests/apitests/python/test_scan_image_artifact.py b/tests/apitests/python/test_scan_image_artifact.py index 2faa87fd6..0ba1ca683 100644 --- a/tests/apitests/python/test_scan_image_artifact.py +++ b/tests/apitests/python/test_scan_image_artifact.py @@ -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);